icon
14장 : 현대 C++ 기능

C++ 20 기능 소개

C++는 3년마다 새로운 표준을 발표하며 끊임없이 발전하고 있습니다.

C++11, C++14, C++17에 이어, 2020년에는 또 한 번 언어와 표준 라이브러리에 대대적인 변화를 가져온 C++20 표준이 발표되었습니다.

C++20은 C++의 "빅 3"라 불리는 모듈(Modules), 코루틴(Coroutines), 범위(Ranges)를 포함하며, 동시성(Concurrency)과 메타프로그래밍(Metaprogramming)에도 많은 개선을 가져왔습니다.

이 장에서는 C++20의 주요 기능들을 간략하게 소개하고, 각 기능이 C++ 프로그래밍에 어떤 영향을 미치는지 맛보기로 살펴보겠습니다.

이 내용들은 당장 여러분의 주력 코딩에 사용되지 않을 수도 있지만, 현대 C++의 흐름을 이해하고 앞으로 심화 학습을 위한 흥미를 유발하는 데 도움이 될 것입니다.


모듈 (Modules)

C++ 컴파일 시간의 가장 큰 걸림돌 중 하나는 #include 전처리기 지시문이었습니다.

#include는 단순히 파일 내용을 복사-붙여넣기 하는 방식으로 동작하며, 이는 대규모 프로젝트에서 중복 컴파일을 야기하고 컴파일 시간을 매우 길게 만들었습니다.

모듈은 이러한 #include 시스템을 대체하기 위해 도입된 기능입니다.

모듈은 컴파일된 바이너리 형식으로 제공되어 컴파일러가 소스 코드를 매번 파싱할 필요 없이 빠르게 가져다 쓸 수 있도록 합니다.

특징

  • 빠른 컴파일: 소스 파일을 한 번만 컴파일하고 그 결과를 재사용하여 전체 빌드 시간을 단축합니다.
  • 매크로 문제 해결: 매크로 오염(macro pollution) 문제로부터 자유롭습니다. 모듈은 매크로를 외부에 노출하지 않습니다.
  • 선언 순서 독립성: 헤더 파일처럼 선언 순서에 제약을 받지 않습니다.
  • 더 나은 격리: 모듈 내에서 선언된 이름들은 명시적으로 export하지 않는 한 외부에 노출되지 않습니다.
모듈 인터페이스 기본 형식
// math_module.ixx (모듈 인터페이스 파일)
export module MathModule; // 모듈 선언

export namespace Math { // export를 통해 외부에 노출
    export double add(double a, double b) {
        return a + b;
    }
    export double subtract(double a, double b); // 선언만 export
}

// math_module.cpp (모듈 구현 파일)
module MathModule; // 해당 파일이 MathModule의 일부임을 선언

namespace Math {
    double subtract(double a, double b) { // 위에서 선언된 함수 구현
        return a - b;
    }
}
모듈 사용 예시
// main.cpp
import MathModule; // 모듈 가져오기

int main() {
    double sum = Math::add(10.0, 5.0);
    double diff = Math::subtract(10.0, 5.0);
    // ...
}

모듈은 C++ 빌드 시스템에 혁신적인 변화를 가져올 것으로 기대됩니다.


코루틴 (Coroutines)

코루틴은 함수 실행을 일시 중지하고 나중에 다시 시작할 수 있게 해주는 기능입니다.

이는 비동기 프로그래밍, 이벤트 기반 프로그래밍, 제너레이터(Generator) 구현 등에 매우 강력하게 활용될 수 있습니다.

기존의 스레드(Thread) 기반 동시성과는 달리, 코루틴은 협동적(cooperative) 멀티태스킹 방식으로 동작하며, 컨텍스트 스위칭 비용이 매우 낮습니다.

특징

  • 함수 일시 중지 및 재개: co_await, co_yield, co_return 키워드를 사용하여 함수 실행을 제어합니다.
  • 비동기 I/O: 복잡한 콜백(callback) 지옥을 피하고 동기 코드처럼 자연스럽게 비동기 작업을 작성할 수 있습니다.
  • 제너레이터 구현: 값의 시퀀스를 생성하고 필요할 때마다 다음 값을 제공하는 함수를 쉽게 구현할 수 있습니다.

기본 키워드

  • co_await: 비동기 작업의 완료를 기다립니다.
  • co_yield: 값을 반환하고 실행을 일시 중지합니다 (제너레이터).
  • co_return: 코루틴의 종료 값을 반환합니다.
코루틴을 이용한 제너레이터 예시
// 이 예시를 직접 컴파일하려면 코루틴 라이브러리 및 복잡한 설정이 필요합니다.
// 여기서는 개념 이해를 돕기 위한 유사 코드입니다.
#include <iostream>
#include <coroutine> // 실제 사용시 필요한 헤더

// int를 생성하는 간단한 제너레이터 (코루틴)
struct IntGenerator {
    struct promise_type {
        int value;
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        IntGenerator get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; }
        void unhandled_exception() { std::terminate(); }
        void return_value(int v) { value = v; } // co_return int;
        std::suspend_always yield_value(int v) { // co_yield int;
            value = v;
            return {};
        }
    };
    std::coroutine_handle<promise_type> handle;
    int next() {
        handle.resume();
        return handle.promise().value;
    }
    bool done() { return handle.done(); }
};

IntGenerator generate_numbers() {
    for (int i = 0; i < 5; ++i) {
        co_yield i; // 값을 반환하고 일시 중지
    }
    co_return 100; // 최종 반환 값
}

int main() {
    auto gen = generate_numbers();
    while (!gen.done()) {
        std::cout << "Generated: " << gen.next() << std::endl;
    }
    std::cout << "Done generating.\n";
    return 0;
}

코루틴은 C++에서 비동기 및 이벤트 기반 시스템을 구축하는 방식을 근본적으로 변화시킬 잠재력을 가지고 있습니다.


범위 (Ranges)

STL 알고리즘은 강력하지만, begin()end() 반복자를 항상 명시해야 하고, 여러 알고리즘을 체인처럼 연결할 때 중간 결과가 복잡해지며, 컨테이너를 변형하는 알고리즘은 원본 데이터를 손상시킬 수 있다는 단점이 있었습니다.

범위(Ranges) 라이브러리는 이러한 STL 알고리즘의 문제점을 해결하고, 더욱 함수형 프로그래밍 스타일에 가까운 방식으로 데이터를 처리할 수 있게 해줍니다.

특징

  • begin()/end() 쌍 불필요: 알고리즘에 직접 컨테이너를 전달할 수 있습니다.
  • 파이프 연산자 |: 여러 알고리즘을 파이프라인 형태로 연결하여 데이터 흐름을 명확하게 표현할 수 있습니다.
  • 뷰(Views): 원본 데이터를 복사하지 않고, 데이터를 필터링하거나 변환하는 '뷰'를 생성하여 효율성을 높입니다.
범위 라이브러리 사용 예시
#include <iostream>
#include <vector>
#include <algorithm> // for std::sort, std::for_each (traditional)
#include <ranges>    // C++20 Ranges (requires C++20 compiler)

int main() {
    std::vector<int> numbers = {1, 8, 2, 9, 3, 7, 4, 6, 5};

    // 전통적인 방식:
    // std::sort(numbers.begin(), numbers.end());
    // for (int n : numbers) { std::cout << n << " "; }

    std::cout << "--- Ranges: Sorting and Printing ---\n";
    // 범위 기반 정렬 (numbers 전체에 적용)
    std::ranges::sort(numbers);
    // 범위 기반 for 루프 (numbers 전체 출력)
    for (int n : numbers) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // 출력: 1 2 3 4 5 6 7 8 9

    std::cout << "\n--- Ranges: Filtering and Transforming with Views ---\n";
    // 파이프 연산자 '|'를 사용하여 체인 형태로 표현
    // 짝수만 필터링하고, 각 요소에 10을 더한 후 출력
    for (int n : numbers | std::views::filter([](int x){ return x % 2 == 0; })
                         | std::views::transform([](int x){ return x + 10; })) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // 출력: 12 14 16 18

    // 원본 numbers 벡터는 변경되지 않았음을 확인
    std::cout << "Original numbers (unchanged): ";
    for (int n : numbers) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // 출력: 1 2 3 4 5 6 7 8 9

    return 0;
}

범위 라이브러리는 데이터를 처리하는 방식에 대한 우리의 사고방식을 바꿀 수 있는 강력한 도구이며, 훨씬 선언적이고 간결한 코드를 작성할 수 있게 합니다.


컨셉 (Concepts)

컨셉 (Concepts) 은 템플릿 매개변수에 대한 제약 조건(constraints)을 명시적으로 표현할 수 있게 해주는 기능입니다.

기존에는 템플릿 인자의 요구 사항을 주석으로 설명하거나 컴파일 오류 메시지를 통해서만 알 수 있었습니다.

컨셉은 이러한 요구 사항을 언어 레벨에서 정의하고 검증할 수 있게 하여, 템플릿 메타프로그래밍의 가독성과 유용성을 크게 향상시킵니다.

특징

  • 개선된 컴파일 오류 메시지: 템플릿 사용 시 요구 사항을 충족하지 못하면 명확하고 이해하기 쉬운 오류 메시지를 제공합니다.
  • 더 나은 인터페이스: 템플릿이 어떤 타입의 인자를 기대하는지 명확하게 명시할 수 있습니다.
  • 오버로드 해결 개선: 컨셉을 통해 템플릿 함수의 오버로드를 보다 정교하게 제어할 수 있습니다.
컨셉 기본 형식
template<typename T>
concept MyConcept = requires(T val) {
    { val.some_method() } -> std::same_as<int>; // val.some_method()가 int를 반환해야 함
    { val + val } -> std::integral; // val + val 결과가 정수 타입이어야 함
    // ...
};

template<MyConcept T> // MyConcept을 만족하는 타입 T만 허용
void func(T arg) {
    // ...
}
컨셉을 이용한 템플릿 함수 예시
#include <iostream>
#include <string>
#include <vector>
#include <concepts> // C++20 concepts

// '정수' 컨셉: std::integral은 C++ 표준 컨셉 중 하나
// template<std::integral T>
// void print_square(T n) {
//     std::cout << n * n << std::endl;
// }

// 'Addable' 컨셉: 두 개의 같은 타입이 더해질 수 있고, 그 결과가 정수여야 함
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::integral; // a + b 의 결과가 std::integral 컨셉을 만족해야 함
};

template<Addable T> // Addable 컨셉을 만족하는 타입 T만 허용
void print_sum_and_type(T a, T b) {
    std::cout << "Sum: " << a + b << std::endl;
    // std::cout << "Type of sum: " << typeid(a + b).name() << std::endl; // typeid는 런타임 정보
}

struct MyStruct {
    int value;
    MyStruct operator+(const MyStruct& other) const {
        return {value + other.value};
    }
};

// MyStruct에 대한 Addable 컨셉의 요구사항을 충족시키기 위해
// MyStruct + MyStruct의 결과가 std::integral이 되도록 특수화하거나
// MyStruct가 std::integral을 만족하도록 만들 수는 없음 (객체이므로)
// 즉, MyStruct는 현재 Addable 개념을 만족하지 않음.
// 만약 Addable을 '더해질 수 있는' 정도로만 정의한다면:
template<typename T>
concept SimpleAddable = requires(T a, T b) {
    { a + b }; // 더하기 연산이 유효하기만 하면 됨
};

template<SimpleAddable T>
void print_sum_simple(T a, T b) {
    std::cout << "Simple Sum: " << (a + b).value << std::endl;
}


int main() {
    print_sum_and_type(10, 20); // int는 Addable
    print_sum_and_type(10LL, 20LL); // long long도 Addable

    // print_sum_and_type(10.5, 20.5); // 컴파일 오류: double은 Addable을 만족하지 않음 (std::integral이 아님)
    // print_sum_and_type("hello", "world"); // 컴파일 오류: 문자열 + 문자열은 정수 결과가 아님

    MyStruct s1{10}, s2{20};
    // print_sum_and_type(s1, s2); // 컴파일 오류: MyStruct는 Addable을 만족하지 않음

    print_sum_simple(s1, s2); // MyStruct는 SimpleAddable을 만족
    
    return 0;
}

컨셉은 템플릿 메타프로그래밍을 훨씬 더 안전하고, 읽기 쉽고, 디버그하기 쉽게 만듭니다.


기타 C++20 주요 기능들

  • <=> (Three-way comparison operator / Spaceship operator)

    • 객체 간의 관계(= =, <>, >, <)를 한 번의 연산으로 반환하는 연산자입니다.
    • 복잡한 operator==, operator!=, operator<, operator>, operator<=, operator>=를 한 번의 구현으로 자동 생성할 수 있게 합니다.
  • constexpr 확장

    • C++11부터 컴파일 시간 계산을 위한 constexpr 키워드가 도입되었지만, C++20에서는 더욱 강력해져 new, delete, try-catch, 가상 함수 등도 constexpr 함수 내에서 사용할 수 있게 되었습니다.
    • 이를 통해 컴파일 타임에 더 많은 로직을 실행하여 런타임 성능을 개선할 수 있습니다.
  • [[likely]], [[unlikely]] 속성

    • 코드에서 특정 분기(branch)가 발생할 확률이 높거나 낮음을 컴파일러에게 힌트를 줍니다.
    • 컴파일러가 이 힌트를 사용하여 CPU의 분기 예측(branch prediction)을 최적화하고 성능을 향상시키는 데 도움을 줄 수 있습니다.
  • 즉시 초기화 람다 (Immediate Invoked Lambda Expressions, IILE)

    • 람다를 정의와 동시에 호출하는 문법입니다.
    • 특정 스코프에서만 필요한 일회성 로직을 간결하게 작성할 때 유용합니다.
  • std::jthread

    • std::thread의 개선 버전으로, 스레드가 생성될 때 자동으로 join 또는 detach를 설정할 수 있는 join_on_destruction 속성을 가집니다.
    • std::jthread 객체가 스코프를 벗어나 소멸될 때 자동으로 join을 호출하여 자원 누수를 방지합니다.