본문 바로가기

c c++ mfc

C++ 20의 주목할만한 Feature

반응형

안녕하세요. 오늘도 역시 제 본업입니다.

지난 시간 C++17에 추가된 내용들을 먼저 정리해 보았습니다. 오늘은 이어서 C++20에 추가된 내용중에 제 개인적인 의견으로 유용할 것 같은 애들만 기술했습니다. 참고하시고, 시작합니다~~

 

1. 우주선 연산자(Spaceship Operator)

C++20에서는 새로운 비교 연산자인 "우주선 연산자"(Spaceship Operator)가 도입되었습니다. 이 우주선 연산자는 기존의 비교 연산자들을 한 번에 처리할 수 있는 강력한 도구입니다.

우주선 연산자는 <=> 기호로 표현되며, 공식적으로는 "세 방향 비교"(Three-Way Comparison) 또는 "총 등호"(Total Ordering)라고 불립니다.

 

이 연산자의 반환 값은 다음과 같습니다:

양수: 첫 번째 인수가 더 큼 (a > b)
0: 두 인수가 같음 (a == b)
음수: 첫 번째 인수가 더 작음 (a < b)
이를 통해, 우리는 한 줄의 코드로 ==, <, <=, >, >= 등의 관계를 모두 확인할 수 있습니다.

다음은 C++20 우주선 연산자 사용 예제입니다:

#include <compare>

struct MyType {
    int value;

    auto operator<=>(const MyType& other) const = default;
};

int main() {
    MyType a{5};
    MyType b{10};

    auto result = a <=> b;

    if (result < 0)
        std::cout << "a is less than b\n";
    else if (result == 0)
        std::cout << "a is equal to b\n";
    else
        std::cout << "a is greater than b\n";

    return 0;
}

이전의 표준에서 한 structure가 다른 자료형과 대소 비교 연산을 수행하기 위해서는 매우 많은 수의 연산자 재정의를 필요로 하였습니다. 아래와 같이...

struct MyType {
    int value;
    explicit MyType(int val): value{val} { }
    bool operator < (const MyType& rhs) const {                  
        return value < rhs.value;
    }
};
bool operator==(const MyType& rhs) const { 
    return value == rhs.value; 
}
bool operator!=(const MyType& rhs) const { 
    return !(*this == rhs);    
}
bool operator<=(const MyType& rhs) const { 
    return !(rhs < *this);     
}
bool operator>(const MyType& rhs)  const { 
    return rhs < *this;        
}
bool operator>=(const MyType& rhs) const {   
    return !(*this < rhs);     
}
friend bool operator < (const MyTypes& lhs, const MyType& rhs) {                  
    return lhs.value < rhs.value;
}

friend bool operator < (int lhs, const MyType& rhs) {                  
    return lhs < rhs.value;
}

friend bool operator < (const MyTypes& lhs, int rhs) {                  
    return lhs.value < rhs;
}
...

이렇게 보면 코드를 얼마나 줄일 수 있는지 실감이 납니다.

C++20에서는 compiler가 대소비교를 우주선 연산자로 자동 치환해 주기 때문에 가능합니다. 예를 들면,

a==b;

는 compiler에 의해,

(a<=>b) == 0;

으로 자동으로 치환됩니다.

 

2. 소멸자를 호출하지 않는 delete연산자

C++20에서는 객체의 소멸자를 호출하지 않고 메모리만 해제하는 새로운 delete 연산자가 도입되었습니다. 이 연산자는 "non-owning" 또는 "low-level" 리소스 관리에 유용하게 사용될 수 있습니다.

이 새로운 delete 연산자는 다음과 같은 형태를 가집니다.

delete std::destroying_delete_t{}, pointer;

여기서 std::destroying_delete_t은 특별한 타입으로, 이 타입의 인스턴스를 delete 연산자에 전달함으로써 소멸자 호출 없이 메모리만 해제하도록 할 수 있습니다.

다음은 이 기능을 사용하는 예시입니다.

struct MyType {
    ~MyType() {
        std::cout << "MyType destroyed!
";
    }
};

int main() {
    MyType* myObject = new MyType;

    // ... 

    delete std::destroying_delete_t{}, myObject;  // Only deallocates memory, does not call destructor

    return 0;
}

위 코드에서, myObject의 메모리는 해제되지만 소멸자는 호출되지 않습니다. 따라서 "MyType destroyed!"라는 메시지가 출력되지 않습니다.

이 기능은 주로 커스텀 메모리 관리나 특정 종류의 최적화에 유용합니다. 하지만 일반적인 경우에 대부분 객체의 생명주기 관리에 있어서 소멸자의 역할은 중요하기 때문에 이 기능을 사용할 때는 매우 주의해야 합니다. 특히, 리소스 누수나 예상치 못한 동작을 방지하기 위해서 해당 객체가 더 이상 필요 없어진 시점에서 안전하게 소멸될 수 있도록 관리해야 합니다.

 

3. Designated Initializers

C++20에서는 C 언어에서 이미 오랫동안 사용되어온 "Designated Initializers" 기능이 도입되었습니다. 이는 구조체나 배열을 초기화할 때 각 요소를 명시적으로 지정할 수 있게 해주는 문법입니다.

다음은 Designated Initializers를 사용하는 C++ 코드 예제입니다

struct MyStruct {
    int a;
    int b;
    int c;
};

int main() {
    MyStruct s = {.a = 1, .b = 2, .c = 3};
    
    return 0;
}

위 코드에서, MyStruct 타입의 객체 s를 초기화하는데 있어서 각 멤버 변수에 대해 명시적으로 값을 할당하였습니다. 이렇게 하면 코드가 더 읽기 쉬워지고, 잘못된 초기화로 인한 버그를 방지할 수 있습니다.

그러나 C++의 Designated Initializers는 C와 약간 다릅니다. C++에서는 다음과 같은 제약사항이 존재합니다:

 

1. 초기화 순서: 멤버들은 선언된 순서대로 초기화되어야 합니다.
2. 모든 멤버를 명시해야 함: 비공개(private) 멤버가 있는 경우에도 모든 멤버를 명시해야 합니다.
3. 배열과 유니온: 배열과 유니온에 대해서는 Designated Initializers가 지원되지 않습니다.


이런 제약사항들 때문에, 실제로 C++에서 Designated Initializers를 사용할 때에는 주의가 필요합니다. 

특히 기존의 C 코드를 C++로 포팅하는 과정에서 문제가 발생할 수 있으므로 주의 깊게 확인해야 합니다.

 

4. Lambda capture에서 [=, this] 허용

C++20에서는 람다 캡처 목록에서 [=, this] 형태를 허용하게 되었습니다. 이전의 C++ 표준에서는 이런 형태의 캡처가 금지되었습니다.

이 변경은 코드의 명확성과 일관성을 위한 것입니다. C++17까지는 [=] 캡처가 암묵적으로 this 포인터를 캡처합니다. 그러나 개발자들 사이에는 이 동작이 직관적이지 않고 혼란스러울 수 있다는 의견이 있었습니다.

따라서 C++20에서는 [=, this]를 허용함으로써 this 포인터가 캡처되고 있다는 것을 명시적으로 나타낼 수 있게 되었습니다. 이로써 코드의 가독성과 명확성이 향상되었습니다.

다음은 C++20에서 가능해진 람다 캡처 예제입니다

class MyClass {
public:
    void MyMethod() {
        auto lambda = [=, this]() { /*...*/ };
        // ...
    }
};

위 코드에서 lambda 함수 객체 내부에서 클래스 멤버 변수에 접근하려면 this 포인터가 필요합니다. 이때 [=, this]를 사용하여 this 포인터를 명시적으로 캡처할 수 있습니다.

그러나 실제로 [=, this]와 단순히 [=] 사이에 기능상 차이점은 없습니다. 둘 다 같은 동작을 합니다. 모든 자유 변수를 값으로 캡처하고, this 포인터도 함께 캡처가 됩니다. 따라서 이 변경은 주로 가독성과 코드 스타일에 영향을 줍니다.

 

다만 =, this등으로 전체를 capture해서 lambda함수를 사용하는 것은 자제하는 것이 좋을거 같습니다.

 

5. Coroutine(코루틴)

C++20에서는 코루틴(Coroutine)이라는 중요한 기능이 도입되었습니다. 코루틴은 비동기 프로그래밍을 더 간결하고 직관적으로 작성할 수 있게 돕는 기능입니다.

코루틴은 함수처럼 동작하지만, 실행을 중간에 일시정지하고 나중에 다시 재개할 수 있는 특징을 가지고 있습니다. 이를 통해 비동기 연산이나 지연된 계산 등을 간단하게 표현할 수 있습니다.

coroutine



C++20의 코루틴은 세 가지 새로운 키워드를 사용합니다: co_await, co_yield, co_return.

1. co_await: 비동기 연산의 완료를 기다립니다. 이 키워드를 만나면 코루틴은 일시정지 상태가 되며, 대기 중인 연산이 완료될 때까지 제어권을 호출자에게 넘깁니다.
2. co_yield: 값을 생성(emit)하고, 현재 위치에서 실행을 일시정지합니다.
3. co_return: 값을 반환하고 코루틴의 실행을 종료합니다.


다음은 C++20에서 가능한 간단한 코루틴 예제입니다.

#include <experimental/coroutine>
using namespace std::experimental;

generator<int> range(int start, int end) {
    while (start < end) {
        co_yield start++;
    }
}

int main() {
    for (int value : range(0, 5)) {
        std::cout << value << '\n';
    }

    return 0;
}

위 코드에서, range 함수는 주어진 범위의 정수들을 하나씩 생성하는 generator라는 타입의 코루틴입니다. 이때 'generator'는 C++ 표준 라이브러리에 포함되어 있지 않으며 예제 코드를 위해 사용된 것으로 실제로 사용하기 위해서는 별도로 구현해야 합니다.

main 함수에서 for-each loop를 사용하여 range가 생성하는 모든 값을 출력합니다. 이때 'range' 함수 내부에서 'co_yield' 문장이 실행될 때마다 해당 위치에서 실행이 일시정지되며 생성된 값이 for-each loop로 전달됩니다.

C++20의 코루틴 기능은 비동기 프로그래밍, 반복자 구현, 지연 계산 등 다양한 상황에서 유용하게 사용될 수 있습니다. 하지만 아직 코루틴에 대한 라이브러리 지원이 완전하지 않으므로 실제 사용 시에는 해당 컴파일러와 라이브러리의 코루틴 지원 상황을 확인해야 합니다.

 

6. 모듈(Module)

C++20에서는 코드의 재사용성과 캡슐화를 향상시키기 위해 모듈(Module)이라는 새로운 기능이 도입되었습니다. 이전까지 C++에서는 헤더 파일을 사용하여 코드를 공유하고 재사용했습니다. 그러나 이 방식은 컴파일 시간을 늘리고, 이름 충돌 문제 등 여러 단점을 가지고 있습니다.

모듈은 이런 문제들을 해결하려는 시도입니다. 모듈은 컴파일 단위를 나타내며, 각 모듈은 고유한 이름공간을 가지고 있어서 이름 충돌 없이 다른 모듈과 독립적으로 동작할 수 있습니다.

모듈의 선언 및 정의는 다음과 같습니다.

// math.ixx
export module math;

export int add(int a, int b) {
    return a + b;
}

여기서 math.ixx라는 파일에 math라는 이름의 모듈을 정의하였습니다. export module math;라인은 이 코드가 math 모듈임을 선언합니다. 그리고 add 함수 앞에 있는 export 키워드로 해당 함수가 외부에서 사용 가능함을 명시합니다.

모듈의 사용은 다음과 같습니다.

// main.cpp
import math;

int main() {
    int result = add(3, 4);
    
    return 0;
}

여기서 'import' 키워드를 사용하여 'math' 모듈을 가져왔으며, 그 결과로 'add' 함수를 호출할 수 있게 되었습니다.

.ixx 확장자는 일반적으로 C++20 모듈 구현체에 사용되며, 일부 컴파일러에서 지원하는 규칙입니다. 하지만 C++20 표준 자체에 .ixx 확장자가 명시되어 있는 것은 아닙니다.

.ixx 추가

 

여기까지 C++ 20에 추가된 내용들 중 유용한 것들을 살펴보았습니다.

이 글을 참고하시는 분들에게 유용한 정보가 되었으면 좋겠습니다.

 

오늘도 좋은 하루 되세요~~

 

감사합니다.

반응형