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++의 흐름을 이해하고 앞으로 심화 학습을 위한 흥미를 유발하는 데 도움이 될 것입니다.
모듈 (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)
코루틴은 함수 실행을 일시 중지하고 나중에 다시 시작할 수 있게 해주는 기능입니다.
이는 비동기 프로그래밍, 이벤트 기반 프로그래밍, 제너레이터(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;
}범위 라이브러리는 데이터를 처리하는 방식에 대한 우리의 사고방식을 바꿀 수 있는 강력한 도구이며, 훨씬 선언적이고 간결한 코드를 작성할 수 있게 합니다.
컨셉 (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)컴파일 타임 조건 분기입니다. 조건이 거짓인 분기 코드는 인스턴스화 대상에서 제외되므로, 템플릿 분기 코드를 훨씬 안전하게 작성할 수 있습니다.
#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 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처럼 값이 상수 식이어야 한다까지 요구하지는 않지만, 초기화 시점 안전성을 보장하는 데 유용합니다.
#include <iostream>
constinit int g_retry_limit = 3; // 정적 초기화 보장
int main() {
std::cout << g_retry_limit << "\n";
}- 템플릿 분기:
if constexpr - 반드시 컴파일 타임 계산 함수:
consteval - 전역/정적 객체 초기화 안정성:
constinit
기타 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함수 내에서 사용할 수 있게 되었습니다. - 이를 통해 컴파일 타임에 더 많은 로직을 실행하여 런타임 성능을 개선할 수 있습니다.
- C++11부터 컴파일 시간 계산을 위한
-
[[likely]],[[unlikely]]속성- 코드에서 특정 분기(branch)가 발생할 확률이 높거나 낮음을 컴파일러에게 힌트를 줍니다.
- 컴파일러가 이 힌트를 사용하여 CPU의 분기 예측(branch prediction)을 최적화하고 성능을 향상시키는 데 도움을 줄 수 있습니다.
-
즉시 초기화 람다 (Immediate Invoked Lambda Expressions, IILE)
- 람다를 정의와 동시에 호출하는 문법입니다.
- 특정 스코프에서만 필요한 일회성 로직을 간결하게 작성할 때 유용합니다.
-
std::jthreadstd::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로 오류 경로 표준화.