본문으로 건너뛰기

안동민 개발노트

본문 시작

C++ 20 기능 소개

모듈·코루틴·범위·컨셉과 컴파일 시간 기능을 훑으며 C++20 이후 언어가 해결하려는 설계 문제를 파악합니다.

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

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

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

C++23에서는 이 흐름을 이어 std::expected, std::print 같은 실무 친화 기능이 더해져 도입 장벽이 한층 낮아졌습니다.

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

이 내용들은 당장 모든 프로젝트에 쓰이지는 않지만, 현대 C++의 변화 방향과 이후 심화 학습 주제를 파악하는 데 기준이 됩니다.

C++20의 변화는 문법 자체보다 빌드 경계, 비동기 흐름, 데이터 처리, 타입 제약을 어떻게 분리해서 다루는지로 먼저 잡으면 이해가 쉽습니다.

C++20 기능은 서로 다른 경계를 다룬다

C++ · ARCHITECTURE

C++20 기능은 서로 다른 경계를 다룬다

모듈, 코루틴, ranges, concepts를 빌드·제어 흐름·알고리즘·타입 제약 문제에 맞춰 배치한다.

C++20 기능은 서로 다른 경계를 다룬다모듈, 코루틴, ranges, concepts를 빌드·제어 흐름·알고리즘·타입 제약 문제에 맞춰 배치한다. LANGUAGECONTRACT문제 축빌드 · 흐름 · 데이터LANGUAGE기능 선택modules · coroutinesranges · conceptsC++20지원 경계build · lifetime · dataCONTRACT
  1. 문제 축

    빌드 · 제어 흐름 · 데이터 · 타입 제약

  2. 기능 선택

    modules · coroutines · ranges · concepts

  3. 지원 경계

    build tool · lifetime · 원본 범위 · 의미 계약을 확인한다.

모듈, 코루틴, ranges, concepts를 빌드·제어 흐름·알고리즘·타입 제약 문제에 맞춰 배치한다.


모듈 (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++ 빌드 구조를 바꿉니다.

실무 적용 판단 기준 (Modules)
  • 도입 가치가 큰 경우: 공통 라이브러리가 크고 전체 빌드 시간이 병목일 때.
  • 권장 도입 순서: 변경이 잦지 않은 코어 라이브러리부터 모듈화하고, 애플리케이션 경계는 #include와 혼용해 점진 전환.
  • 사전 점검: 컴파일러/IDE/CMake의 모듈 지원 수준과 CI 캐시 전략을 함께 검증해야 합니다.

코루틴 (Coroutines)

코루틴은 함수 호출이 한 번에 끝난다는 가정을 깨고, 중단과 재개 사이에 프레임과 수명 경계를 남깁니다.

코루틴 프레임은 중단과 재개를 넘어 살아간다

C++ · STATE

코루틴 프레임은 중단과 재개를 넘어 살아간다

호출 시 프레임이 만들어지고 실행과 중단을 반복한 뒤 final suspend와 파괴를 거친다.

코루틴 프레임은 중단과 재개를 넘어 살아간다호출 시 프레임이 만들어지고 실행과 중단을 반복한 뒤 final suspend와 파괴를 거친다. 다시 resume하면 실행으로완료CREATE프레임 생성promise · 인자 저장RUN본문 실행resume 진입SUSPEND중단프레임 상태 보존RESUME재개 요청중단 지점 다음 실행FINAL SUSPEND최종 중단 정책suspend 여부 결정DESTROY프레임 파괴소유자 또는 런타임이 정확히 한 번
  1. 프레임 생성

    promise와 중단 사이에 살아야 할 인자·지역 상태를 보관한다.

  2. 실행과 중단

    본문이 suspend 지점에 도달하면 프레임 상태를 보존한다.

  3. 재개

    resume은 중단 지점 다음부터 실행하며 다시 중단할 수 있다.

  4. final suspend

    본문 완료 뒤 promise 정책이 최종 중단 여부를 정한다.

  5. 프레임 파괴

    소유자 또는 런타임이 정책에 맞춰 정확히 한 번 정리한다.

호출 시 프레임이 만들어지고 실행과 중단을 반복한 뒤 final suspend와 파괴를 거친다.

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

이는 비동기 프로그래밍, 이벤트 기반 프로그래밍, 제너레이터(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++에서 비동기 처리와 이벤트 기반 흐름을 함수 단위로 표현할 수 있게 하는 기능입니다.

실무 적용 판단 기준 (Coroutines)
  • 도입 가치가 큰 경우: 네트워크/파일 I/O 중심 서비스처럼 대기 시간이 긴 작업이 많은 경우.
  • 설계 우선 항목: 취소(cancellation), 타임아웃, 에러 전파 정책을 먼저 정하고 co_await 체인에 일관되게 반영.
  • 주의점: 코루틴 자체가 성능을 자동 보장하지 않으므로, 런타임 스케줄링/컨텍스트 전환 비용을 프로파일링으로 검증해야 합니다.

범위 (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;
}

범위 라이브러리는 뷰, 파이프라인, 지연 평가를 조합해 데이터 처리 흐름을 선언적으로 표현합니다.

Ranges는 원본을 바로 바꾸는 알고리즘 호출보다, 필요한 변환을 파이프라인으로 겹쳐 읽는 데 초점이 있습니다.


컨셉 (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;
}

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

실무 적용 판단 기준 (Concepts)
  • 공개 API 우선: 라이브러리 public 템플릿 함수/클래스부터 적용하면 오용을 빠르게 차단할 수 있습니다.
  • 과도한 제약 회피: 내부 구현 템플릿은 최소 제약으로 두고, 외부 경계면에서만 강한 제약을 둡니다.
  • 팀 생산성: SFINAE 중심 오류보다 메시지가 명확해 코드 리뷰/온보딩 비용이 줄어듭니다.

컴파일 타임 분기와 초기화 제어 (if constexpr, consteval, constinit)

C++17/20에서는 컴파일 타임에 더 많은 결정을 내리는 도구가 크게 강화되었습니다.

특히 템플릿 코드 품질을 높일 때 아래 세 기능의 역할을 구분해 이해하는 것이 중요합니다.

if constexpr (C++17)

컴파일 타임 조건 분기입니다.

조건이 거짓인 분기 코드는 인스턴스화 대상에서 제외되므로, 템플릿 분기 코드를 훨씬 안전하게 작성할 수 있습니다.

if constexpr로 타입별 분기
#include <iostream>
#include <type_traits>
#include <string>

template<typename T>
void print_value(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        std::cout << "[number] " << value << "\n";
    } else if constexpr (std::is_same_v<T, std::string>) {
        std::cout << "[string] " << value << "\n";
    } else {
        // 사용자 정의 타입은 보통 operator<<가 필요
        std::cout << "[custom] " << value << "\n";
    }
}

위 예시의 마지막 분기는 사용자 정의 타입의 출력 연산자 설계와 연결됩니다.

관련 시그니처/패턴은 ‘연산자 오버로딩 기초’, ‘프렌드 함수와 프렌드 클래스’에서 함께 확인하세요.

consteval (C++20)

즉시 함수(immediate function)입니다.

호출 결과가 반드시 컴파일 타임에 계산되어야 합니다.

런타임 값으로 호출하려고 하면 컴파일 오류가 발생합니다.

consteval 즉시 함수
consteval int square(int x) {
    return x * x;
}

constexpr int a = square(5); // OK (컴파일 타임)
// int n = 7;
// int b = square(n);         // 컴파일 오류: n은 런타임 값
constinit (C++20)

정적 저장 기간 객체가 상수 초기화되도록 강제합니다.

constexpr처럼 값이 상수 식이어야 한다까지 요구하지는 않지만, 초기화 시점 안전성을 보장하는 데 유용합니다.

constinit으로 정적 초기화 보장
#include <iostream>

constinit int g_retry_limit = 3; // 정적 초기화 보장

int main() {
    std::cout << g_retry_limit << "\n";
}
선택 기준 요약
  • 템플릿 분기: if constexpr
  • 반드시 컴파일 타임 계산 함수: consteval
  • 전역/정적 객체 초기화 안정성: constinit

아래 그림은 C++20 기능을 문제 종류별로 나눠 어떤 도구를 먼저 선택할지 정리합니다.

컴파일 타임 도구는 요구 계약으로 고른다

C++ · FLOWCHART

컴파일 타임 도구는 요구 계약으로 고른다

템플릿 제약, 분기 제거, 즉시 함수, 정적 초기화 요구를 구분한다.

컴파일 타임 도구는 요구 계약으로 고른다템플릿 제약, 분기 제거, 즉시 함수, 정적 초기화 요구를 구분한다. TYPE REQUIREMENT템플릿 후보 제약요구 연산을 이름 붙임INSTANTIATION타입별 분기선택하지 않은 분기 제외IMMEDIATE모든 호출을 상수 평가런타임 인자 거부STATIC STORAGE정적 초기화 요구const를 뜻하지 않음concepts후보와 진단 경계if constexprinstantiation 분기consteval즉시 함수constinit초기화 시점 계약
  1. concepts

    템플릿 후보가 만족해야 할 타입 요구를 이름 붙인다.

  2. if constexpr

    타입별 instantiation에서 선택하지 않은 분기를 제외한다.

  3. consteval

    함수의 모든 잠재적 평가 호출을 상수 평가로 강제한다.

  4. constinit

    정적·thread 저장 기간 변수의 정적 초기화를 요구하며 const를 뜻하지 않는다.

템플릿 제약, 분기 제거, 즉시 함수, 정적 초기화 요구를 구분한다.


기타 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을 호출하여 자원 누수를 방지합니다.

C++23로 이어지는 실전 확장

C++20 기능을 도입한 코드베이스는 C++23 기능을 결합할 때 운영 안정성이 더 좋아집니다.

  • std::expected: 예외 대신 명시적인 성공/실패 타입을 사용해 API 오류 흐름을 타입으로 표현할 수 있습니다.
  • std::print: 포맷 기반 출력으로 로그/디버깅 코드를 간결하게 유지할 수 있습니다.
  • 권장 결합 순서: Concepts로 템플릿 경계 고정 → Coroutines로 I/O 흐름 단순화 → expected로 오류 경로 표준화.

C++20 기능은 modules, coroutines, ranges, concepts가 빌드·비동기·제약 표현을 어떻게 바꾸는지로 묶어 볼 수 있습니다.

C++23 기능은 C++20의 경계 정리 위에 얹을 때 효과가 크므로, 도입 순서를 작은 검증 단위로 나누는 편이 안전합니다.