본문 바로가기

C++ 20 기능 소개

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

C++20은 C++11 이후 가장 큰 변화를 담은 표준으로, 템플릿의 조건을 선언에 적는 콘셉트, 알고리즘을 컨테이너에 바로 적용하고 이어 붙이는 범위 라이브러리, #include를 대신하는 모듈, 중단과 재개가 가능한 함수인 코루틴이 들어왔습니다. 이 네 가지는 하나의 기능이 아니라, 아래 그림처럼 각자 다른 문제를 다룹니다.

이 글은 네 기능과 세 방향 비교, 컴파일 시간에 결정하는 도구들, 그 밖의 C++20 기능을 차례로 개관하고, C++23에서 이어진 기능도 짧게 소개합니다. 예제는 C++20을 지원하는 GCC에서 -std=c++20으로, C++23 기능은 -std=c++23으로 빌드한다고 가정합니다.

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까지 보장하지 않는다.

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

콘셉트

함수 템플릿에서 본 것처럼, 템플릿은 넘긴 타입이 본문의 연산을 지원하지 않으면 본문을 인스턴스화하는 단계에서 길고 읽기 어려운 오류를 냅니다. 콘셉트(concept)는 템플릿 인수가 갖춰야 할 조건에 이름을 붙여 선언에 적게 해, 조건에 맞지 않는 타입을 호출 지점에서 바로 거부합니다.

concepts.cpp
#include <concepts>
#include <iostream>

template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::integral;
};

template <Addable T>
void printSum(T a, T b) {
    std::cout << "sum " << a + b << '\n';
}

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

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

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

int main() {
    printSum(10, 20);
    static_assert(SumHasValue<Item>);
    static_assert(!SumHasValue<int>);
    std::cout << sumValue(Item{4}, Item{5}) << '\n';
    // printSum(1.5, 2.5); // 컴파일 오류: double의 합은 정수 타입이 아님
}
sum 30
9

requires 식은 괄호 안의 매개변수로 적은 연산이 컴파일되는지와, -> 뒤의 콘셉트로 결과 타입을 검사합니다. Addable은 두 값의 합이 정수 타입이어야 하므로 int는 만족하고 double은 만족하지 않으며, printSum(1.5, 2.5)는 본문에 들어가기 전에 조건을 만족하지 않는다는 오류가 납니다. SumHasValue는 합의 value 멤버까지 검사하므로, int는 덧셈은 되지만 value가 없어 이 조건을 만족하지 않습니다.

콘셉트는 본문이 실제로 쓰는 연산과 맞춰 적어야 의미가 있습니다. 조건에는 덧셈만 적고 본문에서 .value를 쓰면, 조건은 통과했는데 본문에서 다시 오류가 날 수 있습니다. <concepts>에는 std::integral, std::floating_point, std::totally_ordered 같은 표준 콘셉트가 있고, void show(std::integral auto x)처럼 매개변수에 바로 콘셉트를 적는 축약 문법도 있습니다.

범위 라이브러리

알고리즘의 끝에서 본 std::ranges::sort처럼, 범위 라이브러리(ranges library)의 알고리즘은 반복자 쌍 대신 컨테이너를 바로 받습니다. 또 뷰(view)를 |로 이어 붙여, 원소를 거르고 바꾸는 과정을 파이프라인처럼 적을 수 있습니다.

ranges.cpp
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>

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

    std::ranges::sort(numbers);
    for (int n : numbers) { std::cout << n << ' '; }
    std::cout << '\n';

    auto evensPlusTen = numbers
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x + 10; });
    for (int n : evensPlusTen) { std::cout << n << ' '; }
    std::cout << '\n';

    for (int n : numbers) { std::cout << n << ' '; }
    std::cout << '\n';
}
1 2 3 4 5 6 7 8 9
12 14 16 18
1 2 3 4 5 6 7 8 9

std::ranges::sort는 원본을 정렬하지만, std::views::filter와 std::views::transform은 원본을 바꾸지 않고 새 컨테이너도 만들지 않습니다. 뷰는 순회할 때마다 원본에서 원소를 꺼내 그 자리에서 거르고 바꾸는 지연 평가(lazy evaluation)를 합니다. 그래서 결과를 오래 보관해야 한다면 별도의 컨테이너에 담아야 하고, 뷰를 쓰는 동안 원본이 살아 있어야 하며 원본을 바꾸면 뷰가 보는 값도 바뀝니다.

세 방향 비교

C++20의 세 방향 비교 연산자 <=>는 두 값을 한 번 비교해 작음, 같음, 큼을 알려 줍니다. 연산자 오버로딩 기초에서 본 것처럼 auto operator<=>(const T&) const = default;로 선언하면 컴파일러가 멤버를 선언 순서대로 비교하는 <=>를 만들고, 이를 바탕으로 <, <=, >, >=를 쓸 수 있습니다.

<=>를 = default로 선언하면 ==도 함께 만들어지지만, <=>를 직접 구현했다면 ==는 따로 정의해야 합니다. 같은지 확인하는 일은 크기를 비교하는 것보다 빠르게 할 수 있는 경우가 많아, 둘을 분리해 두었기 때문입니다.

모듈

#include는 헤더의 내용을 그대로 복사해 넣으므로, 같은 헤더가 많은 소스에 포함되면 매번 다시 컴파일되고 헤더의 매크로가 다른 코드에 새어 들어갑니다. 모듈(module)은 공개할 이름을 export로 밝히고, 쓰는 쪽은 import로 가져오게 합니다.

math_module.cppm
export module MathModule;

export namespace math {
    double add(double a, double b) { return a + b; }
}
main.cpp
import MathModule;

int main() {
    double sum = math::add(10.0, 5.0);
    (void)sum;
}

모듈은 한 번 컴파일한 인터페이스를 다른 소스가 재사용하므로 빌드 시간을 줄이고, export하지 않은 이름과 매크로는 밖으로 나가지 않습니다. 대신 모듈 인터페이스를 먼저 컴파일해야 그것을 import하는 소스를 컴파일할 수 있어, 빌드 순서를 지켜야 합니다. 모듈을 켜는 옵션과 인터페이스 파일의 확장자, 빌드 도구의 지원은 컴파일러마다 다르므로, 실제 프로젝트에 도입할 때는 쓰는 컴파일러와 빌드 도구의 문서를 확인합니다.

코루틴

코루틴(coroutine)은 실행을 중간에 멈추었다가 나중에 그 자리에서 다시 시작할 수 있는 함수입니다. 본문에 co_yield, co_return, co_await 가운데 하나가 있으면 코루틴이 되며, 멈추는 동안에도 지역 변수가 담긴 코루틴 프레임(coroutine frame)이 살아 있습니다. 언어는 중단과 재개의 틀만 주고, 스케줄러나 스레드는 제공하지 않습니다.

int_generator.cpp
#include <coroutine>
#include <exception>
#include <iostream>
#include <stdexcept>

struct IntGenerator {
    struct promise_type {
        int value = 0;
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        IntGenerator get_return_object() {
            return IntGenerator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        void unhandled_exception() { std::terminate(); }
        void return_value(int v) { value = v; }
        std::suspend_always yield_value(int v) {
            value = v;
            return {};
        }
    };

    std::coroutine_handle<promise_type> handle;

    explicit 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() const { return handle.done(); }
};

IntGenerator generateNumbers() {
    for (int i = 0; i < 5; ++i) {
        co_yield i;
    }
    co_return 100;
}

int main() {
    auto gen = generateNumbers();
    while (!gen.done()) {
        std::cout << "generated " << gen.next() << '\n';
    }
    std::cout << "done\n";
}
generated 0
generated 1
generated 2
generated 3
generated 4
generated 100
done

promise_type은 코루틴이 처음과 끝에서 멈출지, co_yield와 co_return의 값을 어디에 둘지를 정하며, 이 예제는 처음과 끝에서 모두 멈추도록 std::suspend_always를 돌려줍니다. 아래 그림처럼 generateNumbers()를 부르면 본문을 실행하지 않고 처음에 멈춘 채 gen을 돌려주고, next()가 resume()으로 재개할 때마다 다음 co_yield까지 실행해 0부터 4를 내놓습니다. 여섯 번째 next()는 반복문을 끝내고 co_return 100으로 마지막에 멈추므로 done()이 참이 되고, 프레임은 gen의 소멸자가 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는 별개이며 소유자가 마지막 해제를 담당한다.

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

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는 별개이며 소유자가 마지막 해제를 담당한다.

이 예제는 co_return의 값도 next()의 결과로 읽도록 만들었지만, 모든 생성기가 그렇게 동작하는 것은 아닙니다. 마지막에 멈춘 코루틴을 다시 resume()하는 것은 정의되지 않은 동작이어서, next()가 먼저 done()을 확인해 예외를 던집니다. 실제로 비동기 입출력에 코루틴을 쓸 때는 취소, 시간 제한, 오류 전달을 함께 설계해야 하며, C++23에는 이런 생성기를 대신하는 std::generator가 들어왔습니다.

컴파일 시간에 결정하는 도구

컴파일할 때 결정하는 일을 돕는 키워드도 늘었습니다. 이름이 비슷하지만 하는 일이 서로 다릅니다.

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_tools.cpp
#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
void printValue(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        std::cout << "[number] " << value << '\n';
    } else {
        std::cout << "[text] " << value << '\n';
    }
}

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

constinit int retryLimit = 3;

int main() {
    printValue(42);
    printValue(std::string("hi"));

    constexpr int area = square(5);
    retryLimit = 4;
    std::cout << area << ' ' << retryLimit << '\n';
}
[number] 42
[text] hi
25 4

C++17의 if constexpr는 템플릿 안에서 조건이 거짓인 갈래를 그 타입으로는 인스턴스화하지 않으므로, 타입에 따라 쓸 수 없는 코드를 한 함수에 함께 둘 수 있습니다. C++20의 consteval 함수는 반드시 컴파일할 때 계산되어야 하는 즉시 함수(immediate function)여서, square(5)처럼 상수 인자로만 부를 수 있습니다. constinit은 전역이나 정적 변수가 프로그램 시작 전에 상수로 초기화되도록 요구할 뿐, 변수를 const로 만들지는 않으므로 retryLimit에 4를 대입할 수 있습니다.

그 밖의 C++20 기능

기능하는 일
지정 초기화Point p{.x = 1, .y = 2};처럼 멤버 이름을 밝혀 초기화
std::formatstd::format("{} + {} = {}", 2, 3, 5)처럼 형식 문자열로 문자열을 만듦, <format>
std::span연속된 원소를 소유하지 않고 가리킴, 1차원 배열에서 본 함수 매개변수에 씀
constexpr 확장constexpr 함수 안에서 new, delete, try, 가상 함수를 쓸 수 있음
[[likely]], [[unlikely]]자주 또는 드물게 실행되는 갈래를 컴파일러에 알림
std::jthread소멸할 때 멈춤을 요청하고 자동으로 join하는 스레드, 스레드 생성과 관리에서 다룸

C++23에서 이어진 기능

C++23은 C++20을 다듬고 쓰기 편한 기능을 더했습니다. std::expected<T, E>는 값 또는 오류를 돌려주어, 예외 처리의 기본 개념에서 본 반환값 방식의 한계를 타입으로 해결합니다. std::print와 std::println은 std::format과 같은 형식 문자열로 바로 출력하고, std::ranges::to는 뷰의 결과를 컨테이너로 모읍니다.

그 밖에 멤버 함수의 대상 객체를 이름 있는 매개변수로 받는 명시적 객체 매개변수, 정렬한 vector 위에 만든 std::flat_map, 앞에서 언급한 std::generator도 C++23에 들어왔습니다. 새 기능은 쓰는 컴파일러와 표준 라이브러리가 지원하는지 먼저 확인하고, 작은 범위에서 하나씩 도입하는 편이 안전합니다.

더 깊이: 기능을 지원하는지 코드에서 확인하기

새 기능을 쓸 수 있는지는 컴파일러의 버전 번호보다 기능 검사 매크로(feature-test macro)로 확인하는 편이 정확합니다. 언어 기능의 매크로는 모든 번역 단위에 미리 정의되어 있어 #if defined(__cpp_concepts)처럼 바로 검사할 수 있고, 라이브러리 기능의 매크로는 C++20의 <version> 헤더와 그 기능을 담은 헤더가 정의하므로 <version>을 포함한 뒤 #ifdef __cpp_lib_format처럼 검사합니다. 매크로의 값은 201907L처럼 연도와 월을 이어 붙인 정수여서, 기능이 나중에 확장된 경우에는 값을 비교해 어느 단계까지 지원하는지도 알 수 있습니다. 반면 C++17의 __has_include(<format>)는 그 이름의 헤더 파일이 있는지만 알려 줄 뿐, 안의 기능이 완성되어 있는지는 알려 주지 않습니다.

표준 버전을 나타내는 __cplusplus는 C++20에서 202002L이지만, MSVC는 기본으로 이 값을 언제나 199711L로 둡니다. Microsoft 문서에 따르면 이 값에 기대는 기존 코드가 많기 때문이며, Visual Studio 2017 버전 15.7부터 있는 /Zc:__cplusplus 옵션을 켜야 /std 옵션에 맞는 값이 나옵니다. 그래서 여러 컴파일러에서 빌드할 코드는 __cplusplus보다 기능 검사 매크로를 기준으로 갈래를 나눕니다.

기능이 있다고 나와도 빌드 설정이 더 필요할 수 있습니다. 예를 들어 GCC 14의 변경 사항은 Windows에서 C++23의 std::print와 std::println을 쓰려면 -lstdc++exp를 함께 링크해야 한다고 적고 있으므로, 새 기능은 쓰는 환경에서 작은 예제로 먼저 빌드해 봅니다.

정리

  • C++20의 콘셉트는 템플릿 인수의 조건을 선언에 적어, 맞지 않는 타입을 호출 지점에서 거부하며, 조건은 본문이 쓰는 연산과 맞춰 적습니다.
  • 범위 라이브러리의 알고리즘은 컨테이너를 바로 받고, 뷰는 원본을 복사하지 않고 순회할 때 계산하므로 원본이 살아 있어야 합니다.
  • <=>를 = default로 선언하면 순서 비교와 ==를 함께 얻습니다.
  • 모듈은 export와 import로 공개 범위를 밝히고 빌드를 빠르게 하지만, 빌드 순서와 도구의 지원을 확인해야 합니다.
  • 코루틴은 멈추었다가 재개되는 함수이며, 프레임은 소유자가 destroy()할 때까지 살아 있고 끝난 코루틴은 재개하지 않습니다.
  • if constexpr는 갈래의 인스턴스화를, consteval은 컴파일 시간 계산을, constinit은 정적 초기화를 정하며 서로 다른 도구입니다.

연습 문제

1. 컴파일 시간이라고 모두 상수는 아니다

compile_time_tools.cpp의 선언을 그대로 두고 square(5), int n = 7; square(n);, retryLimit = 4;, constexpr int limit = retryLimit; 가운데 컴파일되는 것은 무엇인가요?

해설 보기

square(5)와 retryLimit = 4;가 컴파일됩니다. square(n)은 n이 상수가 아니어서 즉시 함수를 부를 수 없으므로 컴파일 오류입니다. retryLimit은 바꿀 수 있는 int이고 constinit은 상수로 만들지 않으므로, constexpr int limit = retryLimit;도 상수식이 아니어서 컴파일 오류입니다.

2. 생성기의 반복 횟수 바꾸기

int_generator.cpp의 generateNumbers에서 반복 조건만 i < 3으로, 마지막을 co_return 9;로 바꾸면 무엇이 출력되나요? 프레임은 언제 해제되나요?

해설 보기

generated 0, generated 1, generated 2, generated 9, done이 출력되고, next()는 네 번 호출됩니다. 네 번째 next()가 반복문을 끝내고 co_return 9로 마지막에 멈추므로 done()이 참이 되어 루프가 끝납니다. final_suspend가 멈추도록 했으므로 프레임은 그때 해제되지 않고, main이 끝나 gen의 소멸자가 destroy()할 때 해제됩니다.

3. 지연 평가되는 뷰

ranges.cpp에서 evensPlusTen을 만든 뒤, 순회하기 전에 numbers[1] = 100;을 실행하면 둘째 출력 줄은 무엇이 되나요? 정렬된 numbers의 인덱스 1은 원래 2였습니다.

해설 보기

뷰는 만들 때 결과를 계산해 두지 않고 순회할 때 원본을 읽으므로, 바뀐 100이 짝수로 걸러져 110이 됩니다. 원래 2에서 나왔을 12 자리에 110이 오므로 둘째 줄은 110 14 16 18입니다. 뷰의 결과를 원본과 상관없이 고정해 두려면 순회한 결과를 새 컨테이너에 담아야 합니다.

이것으로 14장의 현대 C++ 기능을 마칩니다. 다음 장의 스레드 생성과 관리에서는 여러 작업을 동시에 실행하는 스레드를 만들고, 스레드의 수명을 관리하는 방법을 다룹니다.