본문 바로가기

C++ 20 기능 소개

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

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

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

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

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

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

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

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

C++ · FEATURE BOUNDARIES

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

빌드·제어 흐름·데이터 처리·타입 요구를 구분하고, 각 기능의 본문 예제와 지원 조건을 연결한다.

Modules · 공개 빌드 경계

선언을 export하고 사용자는 import한다.

export module MathModule;
import MathModule;

모듈 의존성의 빌드 순서와 컴파일러·빌드 도구 지원을 확인한다. 이름 선언 규칙은 계속 적용된다.

Coroutines · 중단과 재개 경계

프레임을 보존해 순차 코드로 중단·재개를 표현한다.

co_yield i;
co_return 100;

본문은 동기 제너레이터이다. 스케줄러·취소·타임아웃은 별도 설계가 필요하다.

Ranges · 데이터 처리 경계

알고리즘에 범위를 주거나 지연 평가 뷰를 연결한다.

std::ranges::sort(numbers);
// filter → transform → 순회

본문 sort는 원본을 바꾼다. filter/transform 뷰가 항상 원본을 소유하거나 수정을 막지는 않는다.

Concepts · 타입 요구 경계

템플릿 후보가 만족할 연산과 결과 타입을 명시한다.

template<Addable T>
void print_sum_and_type(T,T);

본문 SimpleAddable은 덧셈만 검사한다. 함수 본문이 요구하는 .value까지 보장하지 않는다.

기능 이름만 적용하는 데서 끝내지 않고 공개 경계, 프레임 수명, 원본 범위, 연산의 의미 계약을 확인한다.


모듈 (Modules)

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

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

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

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

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

export namespace Math { // export를 통해 외부에 노출
    double add(double a, double b) {
        return a + b;
    }
    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);
    // ...
}

따라 할 때 첫 코드 블록은 주석의 파일 이름에 맞춰 인터페이스와 구현 파일로 나눕니다. main.cpp도 별도 파일입니다. 모듈 인터페이스의 컴파일 결과를 import 때 찾는 과정과 객체 파일을 링크하는 과정이 모두 필요합니다. 파일 확장자·컴파일 옵션·의존성 자동 처리는 도구마다 달라서, 일반적인 단일 main.cpp용 명령만으로 이 세 파일이 빌드된다고 가정하지 않습니다. 출력 코드가 없는 현재 main은 sum=15, diff=5를 계산하는 예입니다.

모듈은 헤더 의존성을 줄이고 번역 단위의 공개 경계를 명시하는 방향으로 C++ 빌드 구조를 바꿉니다.

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

코루틴 (Coroutines)

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

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

IntGenerator의 초기 중단→next/resume→yield 0..4→co_return 100 최종 중단→소유자 destroy가 구체 상태 변화다.

C++ · COROUTINE LIFETIME

본문 제너레이터의 프레임은 중단 사이에도 살아 있다

이 그림은 initial_suspend와 final_suspend가 모두 suspend_always인 본문 IntGenerator의 수명이다. 출력은 0, 1, 2, 3, 4, 100이고 소유자가 frame을 정리한다.

현재 IntGenerator의 초기·yield·최종 중단초기중단에서next가재개하며yield값0부터4는donefalse,다음next로재개한다.return100은최종중단donetrue로가며재개하지않는다.소유자소멸때destroy로프레임을해제한다.nextco_yield 0..4다음 nextco_return100소유자 소멸초기 중단아직 value 읽지 않음next → resume이 예제의 동기 재개yield 중단value 0..4 · done false최종 중단value 100 · done true프레임 해제소멸자가 destroy 호출
현재 IntGenerator의 초기·yield·최종 중단초기중단에서next가재개하며yield값0부터4는donefalse,다음next로재개한다.return100은최종중단donetrue로가며재개하지않는다.소유자소멸때destroy로프레임을해제한다.nextco_yield0..4다음 nextco_return100소유자 소멸초기 중단value 아직 안 읽음next → resume이 예제의 동기 재개yield 중단0..4 · done false최종 중단100 · done true프레임 해제destroy 호출

보조 설명 · 코드와 수명 조건

  1. 생성 · initial_suspend

    generate_numbers()는 promise와 frame을 만들고 suspend_always로 초기 중단한다.

  2. next() · 실행과 co_yield

    handle.resume()으로 실행해 0부터 4까지 값을 하나씩 만들고 co_yield에서 상태를 보존한다.

  3. 다음 next() · 다시 실행

    다음 호출은 중단 이후에서 재개한다. 실행과 yield를 반복하며 i와 promise.value를 유지한다.

  4. co_return 100 · final_suspend

    마지막 next는 100을 저장하고 suspend_always로 최종 중단한다. done()은 true가 되어 재개하지 않는다.

  5. 소유자 소멸 · destroy

    본문 IntGenerator 소멸자가 handle.destroy()로 중단된 프레임을 정확히 한 번 파괴한다.

코루틴 자체는 새 스레드나 스케줄러를 만들지 않는다. 다른 promise의 중단·파괴 정책은 다를 수 있으며 최종 중단된 코루틴을 resume하면 안 된다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명초기 중단에서 next가 재개하며 yield 값 0부터 4에서는 done이 false이고 다음 next로 재개한다. return 100은 최종 중단에서 done이 true가 되며 재개하지 않는다. 소유자 소멸 때 destroy로 프레임을 해제한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
초기 중단

generator 소유자가 coroutine frame을 보유한다.

  • frame: 보유됨
  • 상태: initial suspend
  • 본문 실행 전에 초기 중단 지점에서 제어를 돌려준다.
resume과 yield

next가 frame을 재개해 0부터 4까지 내놓는다.

  • 값: 0,1,2,3,4
  • 상태: 각 yield에서 중단
  • 다음 resume이 올 때까지 중단된 실행 상태가 frame에 남는다.
최종 중단·해제

co_return 100 뒤 소유자가 frame을 destroy한다.

  • 반환 기록: 100
  • 최종 상태: frame 해제
  • final suspend와 destroy는 별개이며 소유자가 마지막 해제를 담당한다.

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

IntGenerator의 초기 중단→next/resume→yield 0..4→co_return 100 최종 중단→소유자 destroy가 구체 상태 변화다.

C++ · COROUTINE LIFETIME

본문 제너레이터의 프레임은 중단 사이에도 살아 있다

이 그림은 initial_suspend와 final_suspend가 모두 suspend_always인 본문 IntGenerator의 수명이다. 출력은 0, 1, 2, 3, 4, 100이고 소유자가 frame을 정리한다.

현재 IntGenerator의 초기·yield·최종 중단초기중단에서next가재개하며yield값0부터4는donefalse,다음next로재개한다.return100은최종중단donetrue로가며재개하지않는다.소유자소멸때destroy로프레임을해제한다.nextco_yield 0..4다음 nextco_return100소유자 소멸초기 중단아직 value 읽지 않음next → resume이 예제의 동기 재개yield 중단value 0..4 · done false최종 중단value 100 · done true프레임 해제소멸자가 destroy 호출
현재 IntGenerator의 초기·yield·최종 중단초기중단에서next가재개하며yield값0부터4는donefalse,다음next로재개한다.return100은최종중단donetrue로가며재개하지않는다.소유자소멸때destroy로프레임을해제한다.nextco_yield0..4다음 nextco_return100소유자 소멸초기 중단value 아직 안 읽음next → resume이 예제의 동기 재개yield 중단0..4 · done false최종 중단100 · done true프레임 해제destroy 호출

보조 설명 · 코드와 수명 조건

  1. 생성 · initial_suspend

    generate_numbers()는 promise와 frame을 만들고 suspend_always로 초기 중단한다.

  2. next() · 실행과 co_yield

    handle.resume()으로 실행해 0부터 4까지 값을 하나씩 만들고 co_yield에서 상태를 보존한다.

  3. 다음 next() · 다시 실행

    다음 호출은 중단 이후에서 재개한다. 실행과 yield를 반복하며 i와 promise.value를 유지한다.

  4. co_return 100 · final_suspend

    마지막 next는 100을 저장하고 suspend_always로 최종 중단한다. done()은 true가 되어 재개하지 않는다.

  5. 소유자 소멸 · destroy

    본문 IntGenerator 소멸자가 handle.destroy()로 중단된 프레임을 정확히 한 번 파괴한다.

코루틴 자체는 새 스레드나 스케줄러를 만들지 않는다. 다른 promise의 중단·파괴 정책은 다를 수 있으며 최종 중단된 코루틴을 resume하면 안 된다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명초기 중단에서 next가 재개하며 yield 값 0부터 4에서는 done이 false이고 다음 next로 재개한다. return 100은 최종 중단에서 done이 true가 되며 재개하지 않는다. 소유자 소멸 때 destroy로 프레임을 해제한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
초기 중단

generator 소유자가 coroutine frame을 보유한다.

  • frame: 보유됨
  • 상태: initial suspend
  • 본문 실행 전에 초기 중단 지점에서 제어를 돌려준다.
resume과 yield

next가 frame을 재개해 0부터 4까지 내놓는다.

  • 값: 0,1,2,3,4
  • 상태: 각 yield에서 중단
  • 다음 resume이 올 때까지 중단된 실행 상태가 frame에 남는다.
최종 중단·해제

co_return 100 뒤 소유자가 frame을 destroy한다.

  • 반환 기록: 100
  • 최종 상태: frame 해제
  • final suspend와 destroy는 별개이며 소유자가 마지막 해제를 담당한다.

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

언어 자체가 스케줄러나 새 스레드를 제공하는 것은 아닙니다. 이는 비동기 프로그래밍, 이벤트 기반 프로그래밍, 제너레이터(Generator) 구현 등에 활용될 수 있습니다.

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

특징
  • 함수 일시 중지 및 재개: co_await, co_yield, co_return 키워드를 사용하여 함수 실행을 제어합니다.
  • 비동기 I/O: 복잡한 콜백(callback) 지옥을 피하고 동기 코드처럼 자연스럽게 비동기 작업을 작성할 수 있습니다.
  • 제너레이터 구현: 값의 시퀀스를 생성하고 필요할 때마다 다음 값을 제공하는 함수를 쉽게 구현할 수 있습니다.
기본 키워드
  • co_await: 비동기 작업의 완료를 기다립니다.
  • co_yield: 값을 반환하고 실행을 일시 중지합니다 (제너레이터).
  • co_return: 코루틴의 종료 값을 반환합니다.
코루틴을 이용한 제너레이터 예시
// C++20 coroutine 지원이 필요한 동기식 제너레이터 예시입니다.
// 비동기 I/O나 별도 스케줄러는 구현하지 않습니다.
#include <iostream>
#include <coroutine>
#include <exception>
#include <stdexcept>

// 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;
    IntGenerator(std::coroutine_handle<promise_type> h) : handle(h) {}
    IntGenerator(const IntGenerator&) = delete;
    IntGenerator& operator=(const IntGenerator&) = delete;
    ~IntGenerator() { if (handle) handle.destroy(); }
    int next() {
        if (!handle || handle.done()) throw std::logic_error("generator finished");
        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;
}

이 코드에서는 co_await를 이용한 외부 I/O 대기가 없습니다. 일반적인 co_await도 awaiter의 await_ready가 참이면 중단 없이 결과를 얻을 수 있습니다. 실제 중단 여부와 재개 방법은 awaiter의 규칙에 달려 있습니다.

따라 하기: 생성한 직후와 여섯 번째 호출의 차이

원래 완전한 예제를 C++20 지원 환경에서 실행하면 Generated: 0부터 Generated: 4까지 다섯 줄, 이어 Generated: 100, 마지막 Done generating.이 나옵니다. 이 IntGenerator는 초기 중단 상태에서 시작하므로 객체를 만든 것만으로 promise의 value를 읽어서는 안 됩니다. 첫 next는 본문을 재개해 co_yield 0에 도달한 뒤 값을 읽습니다.

시점이번 next가 반환한 값done()다음 행동
생성 직후아직 반환 없음false첫 next로 재개
첫째부터 다섯째 next 뒤0, 1, 2, 3, 4false다음 next 가능
여섯째 next 뒤100true재개 종료, 소유자 소멸 때 프레임 해제

이 예제는 return_value가 기록한 100도 next의 반환값으로 읽도록 설계했습니다. 모든 제너레이터가 co_return 값을 데이터 항목으로 내보내는 것은 아닙니다. 이 상태에서 일곱째 next를 직접 부르면 함수의 보호 조건이 logic_error를 던지고 resume를 실행하지 않습니다. 보호 조건을 없애고 최종 중단 상태를 resume하는 것은 코루틴 재개 전제를 위반합니다.

독립 연습

IntGenerator 자체와 main의 while 조건은 그대로 두고 generate_numbers의 반복 조건만 i < 3, 마지막 반환만 co_return 9;로 바꿉니다. Generated 줄의 값, next 호출 횟수, 마지막 호출 후 done 값을 예상하세요. 프레임 해제가 마지막 반환 직후인지, 소유자 소멸 때인지도 구분하세요.

yield와 final_suspend를 나누는 풀이

값은 0, 1, 2, 9이고 next는 네 번입니다. 처음 세 호출은 각각 yield에서 중단하고 done은 false입니다. 네 번째 재개는 반복문을 끝내고 return_value(9)를 거쳐 final_suspend에 도달하므로 done은 true입니다. final_suspend가 suspend_always인 이 구현에서는 프레임이 남아 있고, gen의 소멸자가 destroy를 호출할 때 해제됩니다. while 조건이 true였던 마지막 진입에서 이미 끝난 프레임을 재개한 것은 아닙니다.

코루틴은 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

    // sort는 원본을 변경했고, 이후 filter/transform 순회는 추가로 변경하지 않았음을 확인
    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에는 원본을 바꾸는 ranges::sort 같은 알고리즘과 지연 평가하는 뷰가 함께 있습니다. 뷰가 원본의 수명을 항상 소유하거나 원본 수정을 막는 것은 아닙니다.


컨셉 (Concepts)

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

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

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

특징
  • 개선된 컴파일 오류 메시지: 템플릿 사용 시 요구 사항을 충족하지 못하면 명확하고 이해하기 쉬운 오류 메시지를 제공합니다.
  • 더 나은 인터페이스: 템플릿이 어떤 타입의 인자를 기대하는지 명확하게 명시할 수 있습니다.
  • 오버로드 해결 개선: 컨셉을 통해 템플릿 함수의 오버로드를 보다 정교하게 제어할 수 있습니다.
컨셉 기본 형식
#include <concepts>
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;
}

이 예시의 SimpleAddable은 덧셈만 검사합니다. print_sum_simple 본문은 결과의 .value도 요구하므로 int처럼 컨셉만 만족하는 타입은 본문 인스턴스화에서 실패할 수 있습니다.

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

실무 적용 판단 기준 (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)

정적 또는 스레드 저장 기간 객체가 정적 초기화되도록 요구합니다.

초기화 뒤 변수 값을 변경할 수 있으며, 변수 자체가 const가 되거나 상수 식에서 사용 가능해지는 것은 아닙니다.

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

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

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

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

C++ · COMPILE TIME CONTRACTS

컴파일 시간 도구를 제약·분기·호출·초기화로 구분한다

비슷한 키워드여도 보장하는 계약은 다르다. 본문의 선언과 사용 예로 각 도구가 검사하는 경계를 확인한다.

concepts · 템플릿 후보 제약

요구하는 연산과 결과 타입을 인터페이스에 적는다.

template<Addable T>
void print_sum_and_type(T,T);

본문 Addable은 덧셈 결과가 정수 타입인지 검사한다. 본문의 추가 요구까지 모든 것이 자동 보장되지는 않는다.

if constexpr · 인스턴스화 분기

C++17부터 타입별 템플릿에서 선택되지 않은 분기를 버린다.

if constexpr (
  std::is_arithmetic_v<T>) {
  // number 처리
}

버린 분기도 구문 검사는 받는다. 모든 잘못된 코드를 숨기는 장치는 아니다.

consteval · 즉시 함수

C++20 즉시 함수의 즉시 호출은 상수식이어야 한다.

consteval int square(int x) {
  return x * x;
}

본문 square(5)는 가능하고 일반 런타임 변수로 즉시 호출하면 실패한다.

constinit · 정적 초기화

C++20 정적·스레드 저장 기간 변수의 동적 초기화를 막는다.

constinit int g_retry_limit = 3;

변수 자체를 const로 만들거나 상수식에서 사용할 수 있게 하지는 않는다. 초기화 뒤 변경할 수 있다.

Concepts는 후보 제약, if constexpr는 템플릿 분기, consteval은 즉시 호출, constinit는 변수 초기화에 적용한다.


연습: compile time이라고 해서 모두 상수는 아니다

현재 square와 g_retry_limit 선언을 각각 유지한 상태에서 square(5), int n = 7; square(n);, main 안의 g_retry_limit = 4;, constexpr int limit = g_retry_limit; 중 허용되는 것을 고르세요. n은 constexpr로 바꾸지 않고, g_retry_limit은 constinit int로 그대로 둡니다.

요구하는 시점을 비교하는 풀이

square(5)는 허용됩니다. 일반 변수 n을 즉시 함수에 넘기는 호출은 상수 식 요구를 만족하지 못해 컴파일 오류입니다. g_retry_limit은 수정 가능한 int이므로 4를 대입할 수 있지만, constinit이 변수를 상수 식에서 읽을 수 있는 constexpr 변수로 바꾼 것은 아니므로 마지막 constexpr 초기화는 실패합니다. if constexpr의 제외된 분기도 문법 검사를 전부 면제받는 것은 아닙니다. 템플릿에서 인스턴스화하지 않을 분기를 선택하는 것과 코드를 파싱하지 않는 것은 다릅니다.

예시: 컨셉과 함수 본문에 필요한 연산 맞추기

앞의 SimpleAddable은 +만 검사하므로 .value를 읽는 본문과 조건이 다를 수 있었습니다. 다음 C++20 프로그램은 덧셈 결과의 value를 int로 변환할 수 있다는 조건까지 검사합니다.

덧셈 결과의 value까지 제약하는 완전한 예제
#include <concepts>
#include <iostream>

struct Item {
    int value;
    friend Item operator+(Item a, Item b) { return {a.value + b.value}; }
};

template<class T>
concept SumHasValue = requires(T a, T b) {
    { (a + b).value } -> std::convertible_to<int>;
};

template<SumHasValue T>
int sum_value(T a, T b) { return (a + b).value; }

int main() {
    static_assert(SumHasValue<Item>);
    static_assert(!SumHasValue<int>);
    std::cout << sum_value(Item{4}, Item{5}) << '\n';
}

출력은 9입니다. int의 덧셈 자체는 가능하지만 결과에 value가 없으므로 이 조건은 만족하지 않습니다. 원래 SimpleAddable에 필요한 연산을 무조건 늘리기보다, 각 공개 함수 본문이 실제로 사용하는 연산과 조건을 맞춥니다. ranges의 앞 예제에서도 sort는 원본을 바꾸고, filter·transform 뷰는 결과 전체를 새 컨테이너로 복사하지 않습니다. 계산 결과를 영구히 보관하려면 별도 컨테이너를 만들 필요가 있습니다.


기타 C++20 주요 기능들

  • <=> (Three-way comparison operator / Spaceship operator)
    • 두 객체의 순서 비교 결과(작음·같음·큼, 비교 방식에 따라 순서를 정할 수 없음)를 반환하는 연산자입니다.
    • <, <=, >, >= 비교식은 적절한 <=>를 후보로 삼아 처리할 수 있습니다. 직접 구현한 <=>만으로 operator==가 자동 선언되지는 않습니다. 클래스 안에서 <=>를 = default로 선언하고 명시적인 멤버·friend operator==가 없으면 operator==도 암시적으로 선언됩니다.
  • constexpr 확장
    • C++11부터 컴파일 시간 계산을 위한 constexpr 키워드가 도입되었지만, C++20에서는 더욱 강력해져 new, delete, try-catch, 가상 함수 등도 constexpr 함수 내에서 사용할 수 있게 되었습니다.
    • 이를 통해 컴파일 타임에 더 많은 로직을 실행하여 런타임 성능을 개선할 수 있습니다.
  • [[likely]], [[unlikely]] 속성
    • 코드에서 특정 분기(branch)가 발생할 확률이 높거나 낮다는 힌트를 컴파일러에게 줍니다.
    • 컴파일러가 이 힌트를 사용하여 CPU의 분기 예측(branch prediction)을 최적화하고 성능을 향상시키는 데 도움을 줄 수 있습니다.
  • 즉시 호출 람다 (IILE, C++11부터 가능한 사용 패턴)
    • 람다를 정의와 동시에 호출하는 문법입니다.
    • 특정 스코프에서만 필요한 일회성 로직을 간결하게 작성할 때 유용합니다.
  • std::jthread
    • std::thread의 개선 버전으로, 소멸 시 joinable이면 request_stop() 후 join()을 호출합니다. 중지 요청은 협력적이므로 작업이 이를 확인하지 않으면 완료까지 기다릴 수 있습니다.
    • 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의 경계 정리 위에 얹을 때 효과가 크므로, 도입 순서를 작은 검증 단위로 나누는 편이 안전합니다.