Patrick's Devlog

[Modern C++] 시작하기 본문

Study/C++

[Modern C++] 시작하기

Patrick_ 2022. 5. 16. 16:37

개요

시간이 지남에 따라 Modern C++가 발전하고 있는 상태이다. 내가 제대로 배운 언어는 일반 C++였으며, 조금 더 C++에 대해 깊게 공부하기 위해 간략하게 준비를 하고자 한다. 본 게시글은 계속해서 정리하는 글이 아닌, 개요만 살짝 맛보는 시간이다. Modern C++의 자세한 공부는 Effective C++의 공부가 끝나는대로 바로 진행할 예정이다. 


C++는 다른 언어보다 유연성이 높다. 높은 추상화(abstract)는 물론 회로 설계에도 사용가능하다. C++는 최적화된 표쥰 라이브러리를 제공한다. 이를 통해 낮은 하드웨어 기능에 액세스해 속도를 최대한으로 높이고 메모리 요구 사항을 최소화할 수 있다. 

 

이처럼 C++은 다른 언어에 비해 메모리 접근성에 대해도 좋으며, 그만큼 좋은 성능 대비 낮은 하드웨어를 사용할 수 있다. 그래서 게임, 데스크톱, 임베디드 및 모바일 등 모든 종류의 프로그램을 제작할 수 있다. 본 글에서는 기존의 C++에서 발전된 Modern C++에 대해 작성한다.

 

여기서 작성한 모든 기능은 별도의 언급이 없는한 C++11 이상에서 사용할 수 있다. 

리소스 및 스마트 포인터

C 프로그래밍의 버그 중 하나는 메모리 누수이다. 원인은 new를 사용해 할당된 메모리의 delete 호출 오류이다. Modern C++는 Resource Acquisition Is Initialization(RAII) 원칙을 강조한다. 리소스(힙 메모리, 파일 핸들, 소켓 등)는 개체에 의해 소유되어야 하는 개념이다. 이 개체는 해당 생성자에서 새로 할당된 리소스를 만들거나 받아 해당 소멸자에서 삭제한다. RAII 원칙은 소유하는 개체가 범위를 벗어나면 모든 리소스가 운영체제에 제대로 반환되도록 보장한다.

 

RAII 원칙을 채택할 수 있도록 C++ 표준 라이브러리는 std::unique_ptr, std::shared_ptr, std::weak_ptr의 세 가지 스마트 포인터 형식을 제공한다. 스마트포인터는 소유하고 있는 메모리의 할당, 삭제 처리를 한다. 아래의 예제를 살펴보자

#include <memory>
class widget {
private:
    std::unique_ptr<int> data;
public:
    widget(const int size) { data = std::make_unique<int>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);
    w.do_something();
}

make_unique()에 대한 호출에서 힙에 할당된 배열(array) 멤버가 있는 클래스를 보여준다. new와 delete에 대한 호출은 unique_ptr에 의해 캡슐화된다. widget 개체가 범위를 벗어나면 unique_ptr 소멸자가 호출되어 배열을 위해 할당된 메모리를 알아서 해제한다. 가능한 경우 우리는 스마트 포인터를 사용해 힙 메모리를 관리한다. new와 delete를 명시적으로 사용해야 하는경우 RAII의 원칙을 따른다.

std::string 및 std::string_view

C 스타일 문자열은 버그의 주요 원인이 된다. std::string과 std::wstring을 사용하면 C 문자열과 관련된 모든 오류를 제거할 수 있다. 또한 검색, 추가 등에서 멤버 함수의 이점을 얻을 수 있다. 그리고 속도에 고도로 최적화되어 있다. 읽기 전용 액세스만 필요한 함수에 문자열을 전달하는 경우 C++17에서는 std::string_view를 사용해 성능상 이점을 얻을 수 있다.

std::vector 및 기타 표준 라이브러리 컨테이너

표준 라이브러리 컨테이너는 모두 RAII 원칙을 따르며 요소의 안전한 탐색을 위한 반복기(iterator)를 제공한다. 그리고 성능에 최적화 되어있으며, 정확성을 철저히 테스트했다고 한다. 이와 같은 컨테이너를 사용하면 custom data structure에 유입될 수 있는 버그 또는 비효율성을 없앨 수 있다. C++에서 원시 배열 대신 vector를 순차 컨테이너로 사용한다.

vector<string> apples;
apple.push_back("Granny Smith");

 

또한 map(unordered_map X)을 기본 연관 컨테이너로 사용한다. 중복 제거 및 다중 케이스에는 set, multimap, multiset을 사용한다.

map<string, string> apple_color;
apple_color["Granny Smith"] = "Green";

성능 최적화가 필요할 경우 다음을 사용하는 것이 좋다.

 

 - 예를 들어, 클래스 멤버로서 임베딩할 시 array 유형이 중요

 - unordered_map과 같이 순서가 지정되지 않은 연관 컨테이너(요소 당 오버헤드가 낮고 시간이 일정하나, 정확하고 효율적으로 사용하기 힘듦)

 - 정렬된 vector

표준 라이브러리 알고리즘(STL)

표준 라이브러리에는 검색(searching), 정렬(sorting), 필터링(filtering), 무작위화(randomizing) 등 여러 일반적 작업을 위해 계속 늘어나는 알고리즘 모음이 포함되어 있다. 수식 또한 광범위 하다. C++17부터는 많은 알고리즘의 병렬 버전이 제공된다. 아래의 몇가지 예를 확인하자

 

 - 기본 탐색 알고리즘인 for_each( 범위기반 for 루프와 함께 사용)

 - 컨테이너 요소의 not-in-place 수정을 위한 transform

 - 기본 검색 알고리즘 find_if

 - sort, lower_bound, 기타 기본 정렬 및 검색 알고리즘

 

비교 연산자를 사용하려면 strict <를 사용하고 가능한 경우 named lambda를 사용한다.

auto comp = [](const widget& w1, const widget& w2)
    { return w1.weiget() < w2.weiget(); }

sort(v.begin(), v.end(), comp);

auto i = lower_bound(v.begin(), v.end(), comp);

명시적 형식 이름 대신 auto

C++11에서는 auto 키워드가 돌입되었다. auto가 개체의 형식을 추론하도록 컴파일러에 지시하므로 명시적으로 입력할 필요가 없다. auto는 추론된 형식이 중첩된 템플릿인 경우 유용하다.

map<int, list<string>>::iterator i = m.begin(); // C style
auto i = m.begin(); // Moder C++

범위 기반 for 루프

배열과 컨테이너에 대한 C 스타일 반복은 인덱싱 오류가 발생하기 쉽고 입력하기도 번거롭다. 이러한 오류를 제거하고 코드를 더 읽기 쉽게하려면 표준 라이브러리 컨테이너 및 원시 배열과 함께 범위 기반 for 루프를 사용한다.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v {1, 2, 3};
    
    for (int i = 0; i < v.size(); ++i) { // C style
        std::cout << v[i];
    }
    
    for (auto& num : v) { // Modern C++
        std::cout << num;
    }
}

매크로 대신 constexper 표현

C와 C++ 매크로는 컴파일 전 전처리기에 의해 처리된다. 매크로 토큰의 각 인스턴스는 파일이 컴파일되기 전 정의된 값 또는 식으로 교체된다. 매크로는 일반적으로 C 스타일 프로그래밍에서 컴파일 시간 상수 값을 정의하는데 사용된다. 그러나 매크로는 오류가 발생하기 쉬우며 디버그하기 어렵다. Modern C++에서는 컴파일 시간 상수에 constexpr 변수를 사용하는 것이 좋다

#define SIZE 10 // C style
constexpr int size = 10; // Modern C++

Uniform initialization

Modern C++에서는 모든 형식에 중괄호 초기화 사용이 가능하다. 이러한 형태의 초기화는 백터, 베열 또는 기타 컨테이너를 초기화할 때 편리하다.

#include <vector>

struct S {
    std::string name;
    float num;
    S(std::string s, float f) : name(s}, num(f) {}
};

int main() {
    // C style
    std::vector<S> v;
    S s1("Norah", 2.7);
    S s2("Frank", 3.5);
    S s3("Jeri", 85.9);
    
    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);
    
    // Modern C++
    std::vector<S> v2 {s1, s2, s3};
    // or
    std::vector<S> v3 { {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };
}

Move Semantics

Modern C++는 불필요한 메모리 복사본을 제거할 수 있도록 이동 의미 체계(move semantics)를 제공한다. 이동 작업은 복사를 수행하지 않고 한 개체에서 다음 개체로 리소스 소유권을 전송한다. 일부 클래스는 힙 메모리, 파일 핸들 등의 리소스를 소유한다. 리소스 소유 클래스를 구현할 때 이동 생성자와 해당 이동 대입 연산자를 정의할 수 있다. 컴파일러는 복사본이 필요하지 않는 상황에서 오버로드 확인중에 이러한 특수 멤버를 선택한다. 표준 라이브러리 컨테이너 형식은 개체에 대해 이동 생성자를 호출한다. 

Lambda 표현

C 프로그래밍에서는 함수 포인터를 사용해 함수를 다른 함수에 전달할 수 있다. 함수 포인터는 유지관리하고 이해하기 힘들다. 함수 포인터가 참조하는 함수는 호출되는 지점과 멀리 떨어진 소스코드 다른 곳에서 정의될 수 있다. 형식 또한 안전하지 않다. Modern C++에서는 연산자를 재정의하는 function object(함수 개체)를 제공한다. 또한 이를 함수처럼 호출할 수 있게 한다. 함수 개체를 만드는 가장 간단한 방법은 inline lambda 표현을 사용한다. 

std::vector<int> v {1,2,3,4,5};
int x = 2;
int y = 4;
auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });

람다 표현[=](int i) { .. }은 "integer 형식의 단일 인수를 사용하고 x보다 크며 y보다 작은지 여부를 나타내는 bool을 반환하는 함수"로 해석할 수 있다. 주변 컨텍스트의 변수x와 y는 람다에서 사용할 수 있다. [=]는 해당 변수가 값에 의해 캡처되도록 지정한다. 즉, 람다 표현은 해당 값의 고유한 복사본을 가진다. 

std::atomic

Thread간 통신 메커니즘에 C++ 표준 라이브러리 std::atomic 구조체와 관련 형식을 사용한다.

std::variant(C++17)

C 프로그래밍에서는 일반적으로 공용 구조체를 사용해 서로 다른 형식의 멤버가 동일한 메모리 위치를 점유할 수 있도록 함으로써 메모리를 보존한다. 하지만 공용 구조체는 안전하지 않고 오류가 발생하기 쉽다. 이에 대안으로 std::variant가 도입되었다. std::visit 함수를 사용하면 variant 형식의 멤버에 형식이 안전한 방식으로 액세스 할 수 있다. 


참고 문헌

https://docs.microsoft.com/ko-kr/cpp/cpp/welcome-back-to-cpp-modern-cpp?view=msvc-170 

 

C++ 시작하기 - 최신 C++

최신 C++의 새로운 프로그래밍 관용구와 그 근거를 설명합니다.

docs.microsoft.com

 

'Study > C++' 카테고리의 다른 글

[Effective C++] Chapter 2 정리 - 1  (0) 2022.05.26
[Modern C++] Lvalue 및 Rvalue  (0) 2022.05.20
[Modern C++] Smart Pointer  (0) 2022.05.17
[Effective C++] Chapter 1 정리 - 2  (0) 2022.05.11
[Effective C++] Chapter 1 정리 - 1  (0) 2022.05.09