Skip to main content

C++20/23 주요 기능 개관

Views 8약 11분생성형 AI 활용
Summary

표준을 배운다는 것

C++을 공부하다 보면 C++11, C++17, C++20, C++23 같은 이름을 자주 만나게 됩니다. 처음에는 이것이 마치 게임의 확장팩 이름처럼 보일 수 있습니다. 하지만 C++ 표준은 단순한 기능 목록이 아닙니다. 어떤 문법을 쓸 수 있는지, 어떤 표준 라이브러리를 믿고 사용할 수 있는지, 컴파일러와 빌드 시스템을 어떻게 설정해야 하는지를 정하는 약속입니다.

예를 들어 std::optional을 사용하고 싶은데 프로젝트가 C++14로 빌드되고 있다면 코드는 컴파일되지 않습니다. concept를 써서 템플릿 조건을 명확히 하고 싶은데 -std=c++17로 컴파일하고 있다면 역시 실패합니다. 그래서 C++에서 "어떤 표준을 쓴다"는 말은 "이 프로젝트에서 허용하는 언어의 경계선을 정한다"는 뜻에 가깝습니다.

우리가 2장에서 보았던 컴파일 옵션을 떠올려 봅시다.

g++ -std=c++20 main.cpp -o app

이 한 줄에서 -std=c++20은 컴파일러에게 이렇게 말하는 것과 같습니다.

"이 코드는 C++20 규칙에 맞춰 해석해 주세요."

CMake를 쓴다면 보통 다음처럼 프로젝트의 표준을 지정합니다.

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

여기서 중요한 점은 표준을 높인다고 무조건 좋은 코드가 되는 것은 아니라는 사실입니다. C++23로 컴파일한다고 해서 코드가 자동으로 안전해지지는 않습니다. 반대로 C++17로 작성된 코드라도 RAII, 스마트 포인터, std::vector, 알고리즘 라이브러리를 잘 활용하면 충분히 현대적인 코드가 될 수 있습니다. 표준은 도구 상자이고, 좋은 코드는 그 도구를 언제 어떻게 쓰는지 아는 데서 나옵니다.

C++20/23 주요 기능 개관

C++ 표준의 큰 흐름

C++은 오래된 언어입니다. 그만큼 시대별로 코드 스타일이 꽤 다릅니다. C++98/03 시절의 코드는 직접 포인터를 관리하고, 반복자를 길게 쓰고, 템플릿 오류 메시지와 싸우는 일이 많았습니다. C++11 이후의 코드는 auto, 범위 기반 for, 람다, 스마트 포인터, 이동 의미론을 적극적으로 사용합니다. C++20 이후의 코드는 Concepts, ranges, coroutines, modules 같은 더 큰 구조 변화를 받아들이기 시작했습니다.

대략적인 감각은 다음과 같습니다.

표준성격입문자 관점
C++98/03고전 C++의 기반오래된 코드베이스를 읽을 때 만난다
C++11현대 C++의 출발점반드시 알아야 하는 전환점이다
C++14C++11 다듬기편의 기능이 많지만 독립적인 큰 장벽은 아니다
C++17실무 코드의 강한 기반현재 코드베이스를 읽기 위해 매우 중요하다
C++20큰 구조 변화Concepts, ranges, coroutines, modules가 등장한다
C++23C++20 보강라이브러리 편의성과 표현력이 좋아진다
C++26다음 세대 표준반사, 안전성, 계약, 비동기 실행 모델이 큰 흐름이다

이 표를 외울 필요는 없습니다. 대신 감각을 잡아야 합니다. C++11은 "현대 C++이 시작된 지점", C++17은 "요즘 실무 코드를 읽기 위한 안정적인 기반", C++20은 "C++이 더 큰 추상화로 넘어간 지점", C++23은 "그 방향을 실용적으로 보강한 표준"이라고 보면 됩니다.

C++11: 현대 C++의 출발점

현대 C++이라는 말은 보통 C++11 이후의 C++을 가리킵니다. C++11은 auto, 범위 기반 for, 람다, 이동 의미론, rvalue 참조, nullptr, constexpr, 강한 enum, 스마트 포인터, std::thread, std::chrono, std::array, unordered_map 같은 기능을 도입하며 C++ 코드의 생김새를 크게 바꾸었습니다.1

C++11 이전 스타일에서는 컨테이너를 순회할 때 반복자 타입을 길게 적어야 했습니다.

std::vector<std::string> names = {"Mina", "Joon", "Ara"};

for (std::vector<std::string>::const_iterator it = names.begin();
     it != names.end();
     ++it) {
    std::cout << *it << 'n';
}

C++11 이후에는 같은 코드를 훨씬 자연스럽게 쓸 수 있습니다.

std::vector<std::string> names = {"Mina", "Joon", "Ara"};

for (const auto& name : names) {
    std::cout << name << 'n';
}

여기서 auto는 타입이 사라졌다는 뜻이 아닙니다. 타입은 여전히 컴파일 시간에 정확히 결정됩니다. 다만 프로그래머가 길고 뻔한 타입 이름을 반복해서 적지 않아도 됩니다. const auto&는 "원소를 복사하지 않고 읽기 전용 참조로 받겠다"는 뜻입니다. 앞에서 반복문과 참조를 배울 때 익힌 감각이 그대로 이어집니다.

C++11의 변화는 단순히 문법이 짧아진 데서 끝나지 않습니다. 더 중요한 변화는 "자원 소유권을 코드로 표현한다"는 사고방식입니다. 예전 C++에서는 new로 얻은 메모리를 어디선가 delete해야 했습니다. 문제는 "어디선가"라는 말이 너무 위험하다는 데 있습니다. 함수가 중간에 return하면? 예외가 발생하면? 누군가 포인터를 복사해서 같은 객체를 두 번 delete하면?

C++11 이후에는 이런 소유권을 스마트 포인터로 표현할 수 있습니다.

std::unique_ptr<User> user(new User{"Mina"});

C++14 이후에는 보통 다음처럼 씁니다.

auto user = std::make_unique<User>("Mina");

std::unique_ptr는 "이 객체는 오직 하나의 주인만 가진다"는 뜻을 타입으로 드러냅니다. 이 객체가 스코프를 벗어나면 소멸자가 자동으로 메모리를 반납합니다. 이것이 앞에서 배운 RAII와 스마트 포인터의 핵심입니다.

이동 의미론도 C++11의 큰 전환점입니다. 큰 문자열, 벡터, 파일 핸들 같은 자원을 매번 복사하지 않고 "소유권을 옮기는" 방식으로 성능과 안전성을 함께 얻을 수 있게 되었습니다. 우리가 이동 생성자와 std::move를 배웠던 이유도 바로 여기에 있습니다. C++11 이후의 C++은 "필요하면 직접 제어하되, 기본적으로는 객체와 타입이 안전한 관리를 맡게 하자"는 방향으로 움직입니다.

C++14: 작은 보강이지만 자주 쓰인다

C++14는 C++11만큼 극적인 표준은 아니지만, C++11의 거친 부분을 많이 다듬었습니다. 대표적으로 제네릭 람다, std::make_unique, 더 유연한 constexpr, 함수 반환 타입 추론 등이 추가되었습니다.

제네릭 람다는 매개변수에 auto를 쓸 수 있게 해 줍니다.

auto print = [](const auto& value) {
    std::cout << value << 'n';
};

print(10);
print("hello");
print(3.14);

이 코드는 람다 하나로 여러 타입을 받을 수 있습니다. 물론 이것이 모든 곳에서 좋은 것은 아닙니다. 너무 넓은 타입을 받아 버리면 함수의 의도가 흐려질 수 있습니다. 그래도 작은 출력 함수나 알고리즘의 조건자처럼 타입에 크게 의존하지 않는 코드를 작성할 때 매우 편리합니다.

std::make_unique도 실무에서 중요합니다. new를 직접 쓰지 않고 unique_ptr를 만들 수 있기 때문입니다. 이 작은 기능 하나가 "직접 new를 줄이는 습관"을 훨씬 자연스럽게 만들어 줍니다.

C++17: 현재 코드베이스를 읽기 위한 필수 기반

C++17은 입문자가 반드시 익혀야 할 현대 C++의 안정적인 기반입니다. 이 표준에는 structured binding, if와 switch의 초기화문, std::optional, std::variant, std::any, std::string_view, std::filesystem, 병렬 알고리즘 같은 기능이 들어왔습니다.1

structured binding은 여러 값을 자연스럽게 풀어 쓰게 해 줍니다.

std::map<std::string, int> scores{
    {"Mina", 95},
    {"Joon", 87}
};

for (const auto& [name, score] : scores) {
    std::cout << name << ": " << score << 'n';
}

std::map을 순회하면 각 원소는 키와 값의 쌍입니다. 예전에는 entry.first, entry.second처럼 써야 했습니다. C++17의 structured binding을 쓰면 name, score라는 이름으로 바로 꺼낼 수 있습니다. 코드가 짧아졌을 뿐 아니라 의미도 더 선명해졌습니다.

if 초기화문도 실무에서 자주 보입니다.

if (auto it = scores.find("Mina"); it != scores.end()) {
    std::cout << "score: " << it->second << 'n';
}

여기서 it는 if 문 안에서만 필요합니다. C++17 이전에는 if 바깥에 변수를 선언해야 했고, 그러면 불필요하게 변수의 유효 범위가 넓어졌습니다. C++17 방식은 "필요한 곳에서 만들고, 필요한 범위 안에서만 사용한다"는 좋은 습관을 문법으로 도와줍니다.

라이브러리 쪽에서는 std::optional과 std::variant가 특히 중요합니다. 앞 장에서 살펴본 것처럼 std::optional<T>는 "값이 있을 수도 있고 없을 수도 있음"을 타입으로 표현합니다. std::variant<A, B, C>는 "여러 타입 중 하나일 수 있음"을 표현합니다. 이것들은 단순한 편의 기능이 아닙니다. 실패, 상태, 선택지를 타입 시스템 안으로 끌어들이는 도구입니다.

std::string_view도 자주 등장합니다. 이것은 문자열을 소유하지 않고 바라보는 얇은 뷰입니다. 문자열을 복사하지 않고 함수에 전달하고 싶을 때 유용합니다.

void printTitle(std::string_view title) {
    std::cout << title << 'n';
}

단, string_view는 문자열을 소유하지 않습니다. 바라보던 문자열이 먼저 사라지면 위험합니다. 그래서 string_view는 성능 도구이면서 동시에 생명주기 감각을 요구하는 도구입니다. C++의 많은 최신 기능은 이런 성격을 가집니다. 편리하지만, 타입과 생명주기를 이해하고 써야 합니다.

std::filesystem은 파일과 디렉터리를 다루는 표준 라이브러리입니다. 예전에는 운영체제별 API나 외부 라이브러리에 의존해야 했던 일을 표준 방식으로 처리할 수 있게 되었습니다.

#include <filesystem>

namespace fs = std::filesystem;

for (const auto& entry : fs::directory_iterator{"."}) {
    std::cout << entry.path() << 'n';
}

이런 기능 때문에 C++17은 "현대 C++ 코드를 읽기 위한 최소 실전 표준"에 가깝습니다. 새 프로젝트는 C++20 이상을 선택할 수 있더라도, 현장에서 만나는 많은 코드가 C++17 기반이라는 점은 기억해 두는 것이 좋습니다.

C++20: C++이 크게 방향을 튼 표준

C++20은 C++11 이후 가장 큰 변화로 자주 언급됩니다. 특히 Concepts, ranges, coroutines, modules는 C++20의 큰 네 축으로 불립니다.2 이 기능들은 입문 단계에서 모두 깊이 파고들 필요는 없지만, 이름과 의도를 알고 있으면 앞으로 만날 코드가 덜 낯설어집니다.

Concepts: 템플릿의 입구에 표지판 세우기

C++ 템플릿은 강력하지만, 오류 메시지가 어렵기로 유명합니다. 템플릿 함수 안에서 a + b를 썼는데 어떤 타입은 + 연산을 지원하지 않는다면, 컴파일러는 아주 긴 오류 메시지를 쏟아낼 수 있습니다.

Concepts는 템플릿 매개변수에 조건을 붙이는 기능입니다.

#include <concepts>

template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;

template <Number T>
T add(T a, T b) {
    return a + b;
}

이 코드는 add가 정수나 부동소수점 타입에 대해 동작한다는 의도를 드러냅니다. Concepts는 "템플릿 함수 안에 들어와서 실패하지 말고, 애초에 들어올 수 있는 타입을 명확히 하자"는 방향입니다.

앞에서 Concepts를 맛볼 때 보았듯이, concept는 만능이 아닙니다. 조건을 지나치게 강하게 걸면 쓸 수 있는 타입의 범위가 불필요하게 좁아집니다. 반대로 너무 느슨하면 이름만 그럴듯한 장식이 됩니다. 좋은 concept는 함수가 실제로 요구하는 최소 능력을 잘 표현해야 합니다.

Ranges: 알고리즘을 데이터 흐름처럼 쓰기

C++20 ranges는 컨테이너와 알고리즘을 더 자연스럽게 연결합니다. 기존 알고리즘은 보통 begin, end 반복자를 넘겼습니다.

std::sort(numbers.begin(), numbers.end());

ranges를 사용하면 컨테이너 자체를 알고리즘에 넘기거나, 뷰를 파이프라인처럼 연결할 수 있습니다.

#include <ranges>
#include <vector>

std::vector<int> numbers{1, 2, 3, 4, 5, 6};

auto processed = numbers
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * 10; });

for (int value : processed) {
    std::cout << value << 'n';
}

이 코드는 "짝수만 골라서 10배로 바꾼다"는 흐름이 그대로 읽힙니다. 여기서 filter와 transform은 즉시 새 벡터를 만드는 것이 아니라, 필요한 순간에 값을 계산하는 뷰를 만듭니다. 이런 방식을 지연 평가라고 합니다. 쉽게 말해 "미리 다 해 놓지 않고, 필요할 때 계산한다"는 뜻입니다.

ranges는 멋진 문법이지만, 처음부터 모든 반복문을 ranges로 바꾸려고 할 필요는 없습니다. 단순한 순회는 범위 기반 for가 더 읽기 쉬울 수 있습니다. 데이터 변환이 여러 단계로 이어질 때 ranges의 장점이 커집니다.

Coroutines: 멈췄다가 다시 이어지는 함수

코루틴은 함수를 중간에 멈추고 나중에 다시 이어서 실행할 수 있게 해 주는 언어 기능입니다. 비동기 I/O, 이벤트 루프, 제너레이터 같은 흐름을 표현할 때 유용합니다.

개념적으로는 다음과 같은 코드를 떠올릴 수 있습니다.

Task loadUser() {
    auto text = co_await readFileAsync("user.txt");
    co_return parseUser(text);
}

이 코드는 일반 함수처럼 위에서 아래로 읽힙니다. 하지만 co_await 지점에서 작업이 끝날 때까지 현재 흐름을 잠시 멈추고, 다른 작업을 진행하게 할 수 있습니다. 콜백 함수를 여러 겹 중첩하는 방식보다 의도가 더 선명해질 수 있습니다.

다만 C++20 코루틴은 입문자에게 조금 까다롭습니다. 언어는 co_await, co_yield, co_return 같은 재료를 제공하지만, 실제로 어떤 Task 타입을 쓸지, 스케줄러를 어떻게 만들지, 이벤트 루프와 어떻게 연결할지는 라이브러리와 프로젝트 설계에 달려 있습니다. 그래서 처음에는 "코루틴은 비동기 흐름을 함수처럼 표현하기 위한 기반 기능" 정도로 이해하면 충분합니다.

Modules: 헤더 포함 모델을 넘어서기

전통적인 C++ 프로젝트는 헤더 파일과 소스 파일로 구성됩니다. #include는 단순히 파일 내용을 끼워 넣는 방식에 가깝기 때문에, 큰 프로젝트에서는 컴파일 시간이 길어지고 의존성이 복잡해질 수 있습니다.

C++20 modules는 이런 문제를 줄이기 위한 새로운 코드 구성 방식입니다.

// math.cppm
export module math;

export int square(int x) {
    return x * x;
}
// main.cpp
import math;

int main() {
    return square(5);
}

import math;는 #include와 다르게 모듈 단위로 컴파일된 인터페이스를 가져오는 방식입니다. 이론적으로는 빌드 시간을 줄이고, 매크로 오염을 줄이며, 라이브러리 경계를 더 명확히 할 수 있습니다.

하지만 모듈은 컴파일러와 빌드 시스템의 지원 상태를 함께 봐야 합니다. 문법을 안다고 바로 모든 프로젝트에서 쓸 수 있는 기능은 아닙니다. 특히 기존 헤더 기반 프로젝트를 모듈로 바꾸는 작업은 설계와 빌드 환경을 함께 손봐야 합니다.

C++20에는 이 네 가지 외에도 std::span, std::format, std::jthread, 문자열의 starts_with와 ends_with, 세 방향 비교 연산자 <=>, constinit, std::bit_cast 같은 기능이 들어왔습니다. 입문자에게 가장 중요한 것은 "C++20은 단순 문법 추가가 아니라 템플릿, 알고리즘, 비동기, 프로젝트 구조를 모두 건드린 표준"이라는 감각입니다.

C++23: C++20의 방향을 실용적으로 보강하다

C++23은 C++20만큼 거대한 전환점은 아니지만, 언어와 표준 라이브러리 곳곳을 다듬고 보강한 표준입니다. C++23에는 if consteval, explicit object parameter, 다차원 operator[], range 기반 for의 임시 객체 생명주기 개선 같은 언어 기능과 std::expected, std::optional의 모나딕 연산, 문자열 contains, std::to_underlying, std::stacktrace, spanstream, ranges 확장 같은 라이브러리 기능이 포함되었습니다.3

문자열 contains는 아주 작은 기능처럼 보이지만, 코드를 읽기 쉽게 만듭니다.

std::string text = "hello modern c++";

if (text.contains("modern")) {
    std::cout << "foundn";
}

이전에는 보통 다음처럼 썼습니다.

if (text.find("modern") != std::string::npos) {
    std::cout << "foundn";
}

두 코드는 같은 일을 하지만, contains 쪽이 의도를 바로 드러냅니다. "찾은 위치가 npos가 아닌가?"보다 "포함하는가?"가 사람의 생각에 더 가깝습니다. 현대 C++의 많은 개선은 이렇게 작은 표현력 차이에서 시작됩니다.

std::to_underlying은 enum class 값을 기반 정수 타입으로 바꿔 줍니다.

#include <utility>

enum class Role : int {
    user = 1,
    admin = 2
};

int code = std::to_underlying(Role::admin);

C 스타일 캐스팅이나 static_cast<int>를 매번 쓰는 것보다 의도가 분명합니다. "이 enum의 underlying type 값을 얻겠다"는 뜻이 함수 이름에 들어 있기 때문입니다.

std::expected<T, E>는 실패할 수 있는 함수의 결과를 표현하는 데 유용합니다. std::optional<T>가 "값이 있거나 없다"를 표현한다면, std::expected<T, E>는 "성공하면 T, 실패하면 E"를 표현합니다.

#include <expected>
#include <string>

std::expected<int, std::string> divide(int a, int b) {
    if (b == 0) {
        return std::unexpected("division by zero");
    }

    return a / b;
}

이 함수는 예외를 던지지 않아도 실패 이유를 호출자에게 돌려줄 수 있습니다. 예외를 쓰지 않는 프로젝트, 임베디드 환경, 성능에 민감한 코드, 실패를 명시적으로 처리하고 싶은 API에서 유용합니다. 물론 이것도 예외를 완전히 대체하는 만능 도구는 아닙니다. 프로젝트의 오류 처리 정책에 따라 예외, optional, expected, 에러 코드 중 무엇을 쓸지 결정해야 합니다.

C++23에서는 std::optional에도 and_then, transform, or_else 같은 연산이 추가되었습니다. 이런 연산은 값이 있을 때만 다음 단계를 실행하는 체인을 만들 때 편리합니다.

auto displayName = findUser(id)
    .and_then(loadProfile)
    .transform([](const Profile& profile) {
        return profile.name;
    })
    .value_or("unknown");

이 코드는 "사용자를 찾고, 있으면 프로필을 불러오고, 있으면 이름으로 바꾸고, 실패하면 unknown을 쓴다"는 흐름을 보여 줍니다. 중간마다 if (!value)를 반복하는 방식보다 데이터 흐름이 선명할 수 있습니다.

if consteval은 컴파일 시간 평가 중인지에 따라 분기할 수 있게 합니다.

constexpr int adjust(int x) {
    if consteval {
        return x * 2;
    } else {
        return x;
    }
}

이 기능은 일반 입문 코드에서 자주 쓰이지는 않습니다. 하지만 템플릿, constexpr, 메타프로그래밍을 깊게 다루는 라이브러리 코드에서는 컴파일 시간과 실행 시간 동작을 구분하는 데 도움이 됩니다.

explicit object parameter, 흔히 "deducing this"라고 부르는 기능은 멤버 함수의 this를 더 명시적으로 다룰 수 있게 해 줍니다. 특히 const 객체와 비 const 객체에 대해 비슷한 멤버 함수를 중복 작성해야 하는 상황을 줄이는 데 도움이 됩니다. 입문 단계에서는 직접 쓰기보다 "C++23부터는 멤버 함수의 자기 자신을 더 유연하게 표현할 수 있다" 정도로 이해하면 됩니다.

또 하나 흥미로운 변화는 range 기반 for에서 임시 객체의 생명주기 문제가 개선되었다는 점입니다. C++에서는 임시 객체가 언제 사라지는지가 매우 중요합니다. C++23은 range 기반 for의 일부 위험한 패턴을 더 안전하게 만들었습니다. 이것은 문법이 예뻐지는 변화라기보다, 기존에 실수하기 쉬웠던 구석을 표준이 줄여 주는 변화입니다.

C++26: 다음 세대 C++은 어디로 가는가

이 책을 쓰는 2026년 5월 14일 기준으로, C++26은 ISO C++ 위원회의 기술 작업이 완료되었고 최종 승인·출판 절차와 구현 확산 단계에 있는 다음 세대 표준입니다. Herb Sutter는 2026년 3월 회의 보고에서 C++26의 기술 작업 완료를 알리며, C++26의 큰 축으로 compile-time reflection, 메모리 안전성 개선, contracts, std::execution을 강조했습니다.4

입문자에게 C++26의 세부 문법을 모두 외우는 것은 적절하지 않습니다. 하지만 방향은 알아둘 가치가 있습니다. C++이 어디로 움직이는지 알면, 지금 배우는 RAII, 타입 설계, Concepts, ranges, 예외 처리, 스마트 포인터가 왜 중요한지 더 잘 보입니다.

첫 번째 큰 방향은 컴파일 타임 능력 강화입니다. C++은 이미 constexpr, 템플릿, Concepts를 통해 많은 일을 컴파일 시간에 처리할 수 있습니다. C++26의 compile-time reflection은 프로그램이 자기 자신의 구조, 예를 들어 타입의 멤버나 이름 같은 정보를 컴파일 시간에 살펴볼 수 있게 하는 방향입니다. 쉽게 말해, 코드가 코드를 더 잘 이해하고 생성할 수 있는 길이 열리는 것입니다.

예를 들어 지금은 구조체를 JSON으로 저장하려면 멤버 이름을 직접 나열하거나 외부 매크로, 코드 생성기, 라이브러리 규칙에 의존하는 경우가 많습니다. reflection이 발전하면 라이브러리가 타입의 멤버 정보를 컴파일 시간에 읽고, 직렬화 코드를 더 자연스럽게 만들 수 있습니다. 물론 이것이 곧 "마법처럼 모든 코드가 자동으로 좋아진다"는 뜻은 아닙니다. 강력한 도구일수록 남용하면 코드가 더 어려워질 수 있습니다.

두 번째 방향은 안전성입니다. C++은 성능과 제어를 중시하는 언어라서 undefined behavior, 즉 정의되지 않은 동작을 피하는 것이 중요합니다. C++26은 초기화되지 않은 지역 변수 읽기와 관련된 위험을 줄이고, 표준 라이브러리의 bounds safety를 강화하는 방향으로 나아갑니다. 여기서 bounds safety는 벡터나 문자열 같은 범위가 있는 객체에 대해 잘못된 위치에 접근하는 문제를 줄이려는 흐름입니다.

세 번째 방향은 contracts입니다. 계약은 함수의 전제조건과 후조건을 코드에 명시하는 기능입니다. 전제조건은 "이 함수를 호출하기 전에 반드시 참이어야 하는 조건"이고, 후조건은 "함수가 끝난 뒤 보장해야 하는 조건"입니다. 예를 들어 "나이는 0 이상이어야 한다", "정렬 함수가 끝나면 결과는 정렬되어 있어야 한다" 같은 규칙을 함수 선언 가까이에 표현할 수 있습니다. 이것은 단순한 주석보다 강합니다. 도구와 컴파일러가 이해할 수 있는 형태로 의도를 드러내기 때문입니다.

네 번째 방향은 비동기와 병렬 실행 모델입니다. std::execution은 sender/receiver 모델을 바탕으로 비동기 작업을 표현하고 조합하기 위한 표준 프레임워크입니다. 지금까지 C++의 비동기 코드는 스레드, future, 콜백, 코루틴, 플랫폼 API, 외부 라이브러리가 뒤섞이기 쉬웠습니다. C++26은 이런 흐름을 더 표준적인 방식으로 정리하려는 방향을 보여 줍니다.

이 외에도 C++26은 constexpr 확장, 진단 기능 개선, 구조적 바인딩 개선, pack indexing, 새로운 라이브러리 헤더와 컨테이너, SIMD와 선형대수 지원 같은 여러 변화를 포함합니다. 그러나 입문자에게 중요한 결론은 하나입니다. C++은 더 안전하게, 더 많이 컴파일 시간에 검증하고, 더 명확하게 비동기와 제네릭 코드를 표현하는 방향으로 가고 있습니다.

최신 기능을 대하는 현실적인 태도

최신 표준을 보면 마음이 급해질 수 있습니다. "C++23도 모르는데 C++26이 나온다고?"라는 생각이 들 수도 있습니다. 하지만 C++ 학습은 모든 기능을 연도별로 외우는 시험이 아닙니다. 좋은 학습 전략은 중심축을 먼저 잡고, 필요한 기능을 점진적으로 추가하는 것입니다.

입문자에게 추천하는 기준은 다음과 같습니다.

상황추천 기준
C++을 처음 제대로 배우는 중C++17/20을 기본 감각으로 삼는다
기존 실무 코드 읽기C++11, C++14, C++17 기능을 확실히 익힌다
새 프로젝트 시작컴파일러와 라이브러리가 지원한다면 C++20을 적극 고려한다
템플릿이 많은 코드Concepts를 배워 오류와 의도를 다룬다
데이터 처리 코드ranges와 알고리즘 라이브러리를 함께 익힌다
C++23 기능 사용프로젝트 표준과 컴파일러 지원 여부를 먼저 확인한다
C++26 기능 관심방향을 이해하고, 실제 사용은 도구 지원을 확인한 뒤 결정한다

특히 표준 기능과 컴파일러 지원은 구분해야 합니다. 표준에 들어간 기능이라고 해서 모든 컴파일러와 표준 라이브러리에서 즉시 완벽히 사용할 수 있는 것은 아닙니다. 언어 기능은 컴파일러가 지원해야 하고, 라이브러리 기능은 표준 라이브러리 구현이 지원해야 합니다. 예를 들어 GCC, Clang, MSVC가 같은 시점에 같은 수준으로 모든 기능을 제공하지 않을 수 있습니다.

그래서 실전에서는 다음 질문을 먼저 던지는 습관이 좋습니다.

  • 우리 프로젝트는 어떤 C++ 표준으로 빌드되는가?

  • 사용하는 컴파일러 버전은 해당 기능을 지원하는가?

  • 표준 라이브러리 구현도 그 기능을 제공하는가?

  • CI, 배포 환경, 고객 환경에서도 같은 설정이 가능한가?

  • 팀원들이 그 기능을 읽고 유지보수할 준비가 되어 있는가?

새 문법을 아는 것은 멋진 일입니다. 하지만 팀 프로젝트에서는 "내가 쓸 수 있는가"보다 "팀이 안정적으로 유지할 수 있는가"가 더 중요할 때가 많습니다.

기능보다 중요한 축: 소유권, 타입, 범위, 의도

표준이 바뀌어도 변하지 않는 핵심이 있습니다. 이 책에서 계속 강조한 것들입니다.

소유권을 명확히 해야 합니다. 누가 자원을 소유하는지, 언제 반납되는지, 복사 가능한지, 이동만 가능한지를 타입으로 표현해야 합니다. 이것이 RAII, 스마트 포인터, 이동 의미론의 바탕입니다.

타입으로 상태를 표현해야 합니다. 값이 없을 수 있다면 std::optional, 성공과 실패가 함께 있다면 std::expected, 여러 상태 중 하나라면 std::variant, 정해진 선택지라면 enum class를 떠올릴 수 있습니다. 이것은 "주석으로 설명하는 코드"에서 "타입이 설명하는 코드"로 넘어가는 길입니다.

범위를 좁게 유지해야 합니다. C++17의 if 초기화문, 범위 기반 for, RAII 객체, 람다 캡처 규칙은 모두 변수와 자원의 생명주기를 좁고 분명하게 만드는 데 도움이 됩니다.

의도를 드러내야 합니다. Concepts는 템플릿의 요구사항을 드러내고, ranges는 데이터 처리 흐름을 드러내며, contains, to_underlying, expected 같은 라이브러리 기능은 코드가 하는 일을 더 직접적으로 말하게 해 줍니다.

이 네 가지 축을 잡고 있으면 새 표준이 나와도 덜 흔들립니다. 기능 이름은 바뀌고 늘어나지만, C++이 좋은 코드를 향해 움직이는 방향은 꽤 일관됩니다.

작은 예제로 보는 표준의 변화

마지막으로 같은 문제를 여러 표준 감각으로 바라보겠습니다. 문자열 목록에서 길이가 5 이상인 이름만 골라 대문자로 바꿔 출력한다고 해 봅시다.

전통적인 방식은 반복문을 직접 씁니다.

std::vector<std::string> names{"mina", "joon", "alexander", "sora"};

for (const auto& name : names) {
    if (name.size() >= 5) {
        std::string upper = name;
        for (char& ch : upper) {
            ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
        }
        std::cout << upper << 'n';
    }
}

이 코드는 나쁘지 않습니다. 오히려 입문 단계에서는 매우 좋은 코드입니다. 흐름이 명확하고, 어디서 무엇을 하는지 보입니다.

C++20 ranges 감각을 섞으면 데이터 흐름을 더 앞에 드러낼 수 있습니다.

auto longNames = names
    | std::views::filter([](const std::string& name) {
        return name.size() >= 5;
    });

for (const auto& name : longNames) {
    std::string upper = name;
    for (char& ch : upper) {
        ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
    }
    std::cout << upper << 'n';
}

여기서는 "길이가 5 이상인 이름"이라는 선택 조건을 뷰로 분리했습니다. 더 복잡한 데이터 처리라면 이 방식이 코드를 읽기 쉽게 만들 수 있습니다.

하지만 이 예제에서 중요한 결론은 "ranges가 항상 더 낫다"가 아닙니다. 단순한 코드는 단순하게 쓰는 것이 좋습니다. 최신 기능은 복잡성을 줄일 때 써야지, 실력을 과시하기 위해 쓰는 것이 아닙니다.

어떤 표준부터 배우면 좋을까?

이 책의 독자라면 C++17과 C++20을 중심축으로 삼는 것이 좋습니다. C++17은 현재 많은 코드베이스를 읽기 위한 실용적 기반이고, C++20은 현대 C++의 방향을 이해하기 위한 중요한 표준입니다. C++23은 그 위에 필요한 기능부터 추가하면 됩니다. C++26은 당장 모든 세부 기능을 익히기보다, C++이 어디로 가는지 조망하는 정도가 적절합니다.

학습 순서를 현실적으로 잡아 보면 다음과 같습니다.

  1. C++11의 auto, 범위 기반 for, 람다, 스마트 포인터, 이동 의미론을 확실히 이해합니다.

  2. C++17의 structured binding, if 초기화문, optional, variant, string_view, filesystem을 익힙니다.

  3. C++20의 Concepts와 ranges를 먼저 배우고, coroutines와 modules는 개념을 잡은 뒤 필요할 때 깊게 들어갑니다.

  4. C++23의 expected, contains, to_underlying, optional 체이닝 같은 실용 기능을 프로젝트 지원 여부에 맞춰 받아들입니다.

  5. C++26의 reflection, contracts, 안전성 강화, std::execution은 방향을 이해하고 도구가 성숙해지는 과정을 지켜봅니다.

C++은 한 번에 정복하는 언어가 아닙니다. 표준도 마찬가지입니다. 오래된 기능 위에 새 기능이 쌓이고, 새 기능은 다시 기존 코드를 더 안전하고 읽기 쉽게 만드는 방향으로 사용됩니다.

좋은 C++ 개발자는 최신 표준의 모든 기능을 외우는 사람이 아닙니다. 문제를 보고 적절한 표준 도구를 고를 수 있는 사람입니다. 어떤 때는 for문이 가장 좋고, 어떤 때는 알고리즘 라이브러리가 좋으며, 어떤 때는 ranges가 좋습니다. 어떤 실패는 예외로 처리하는 것이 자연스럽고, 어떤 실패는 std::optional이나 std::expected로 표현하는 것이 더 분명합니다.

이제 우리는 C++의 주요 문법, 객체지향, STL, 자원 관리, 템플릿, 예외, 파일, 프로젝트 구조, 현대 C++ 기능의 큰 흐름을 지나왔습니다. 다음부터는 이 재료들을 실제 프로그램으로 묶어 볼 차례입니다. 표준 기능은 그 자체가 목적지가 아닙니다. 우리가 만들 프로그램을 더 안전하고, 더 읽기 쉽고, 더 오래 버티게 만드는 도구입니다.

참고

1Modern C++ Features cheatsheet

2C++26: The Next C++ Standard

3CppCon 2023 C++23: An Overview of Almost All New and Updated Features

4C++26 is done! — Trip report: March 2026 ISO C++ standards meeting

책 목록

처음 만나는 C++: 객체지향에서 현대 C++까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기