C++은 끊임없이 진화하고 있습니다. 과거의 복잡하고 위험했던 코드들은 이제 우아하고 안전한 현대적 문법으로 대체되고 있죠. 오늘은 C++ 엔지니어라면 반드시 알아야 할, 실무의 질을 바꾸는 핵심 문법 6가지를 소개합니다.

 

기능 (Feature) 도입 표준 주요 역할
구조화된 바인딩 C++17 데이터 컨테이너의 직관적 해체
std::views / Ranges C++20 선언적 데이터 처리 및 파이프라인
std::format C++20 타입 안전한 고속 포맷팅
std::jthread C++20 RAII 기반의 안전한 스레드 제어
consteval C++20 컴파일 타임 연산 강제 (Immediate Functions)
std::is_constant_evaluated C++20 컴파일/런타임 문맥 판별
std::expected C++23 모나딕 기반의 우아한 에러 핸들링

 

1. std::views와 Ranges (C++20) — "반복문 코드 다이어트"

복잡한 필터링과 변환 로직, 이제 for문 대신 파이프라인 연산자(|)로 한 줄에 해결하세요. 원본 컨테이너를 복사하지 않는 'View' 방식이라 성능 오버헤드도 없습니다.

 

기본 예제

#include <vector>
#include <ranges>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6};

    // 짝수 추출 -> 10 곱하기
    auto result = nums | std::views::filter([](int n) { return n % 2 == 0; })
                       | std::views::transform([](int n) { return n * 10; });

    for (int n : result) std::cout << n << " "; // 결과: 20 40 60
}

 

view를 이용한 이미지의 이진화 예제

#include <vector>
#include <ranges>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>

#pragma comment(lib, "opencv_world341d.lib")


int main() {

    cv::Mat mat, matGray, matGrayView, matGrayColor, matGrayViewColor;
    mat = cv::imread("D:\\capture.bmp");

    cv::cvtColor(mat, matGray, cv::COLOR_BGR2GRAY);
    matGrayView = matGray.clone();

    std::span<uchar> data_span(matGrayView.data, matGrayView.total());
    // 짝수만 골라서 10을 곱한 뒤 출력하기
    auto result = data_span | std::views::transform([](uchar& pixel) { return pixel = (pixel >= 50) ? 255 : 0; });

    // 4. 파이프라인 구동 (평가 수행)
        // Views는 지연 평가(Lazy Evaluation)되므로 루프를 돌 때 실제로 연산이 일어납니다.
    for (uchar pixel : result) {
        // 이 루프를 돌고 나면 원본 img.data가 직접 수정됩니다.
    }

    cv::cvtColor(matGray, matGrayColor, cv::COLOR_GRAY2BGR);
    cv::cvtColor(matGrayView, matGrayViewColor, cv::COLOR_GRAY2BGR);
    std::vector<cv::Mat> space_synchronized_images = { mat, matGrayColor, matGrayViewColor };

    // 6. 나란히 가로로 붙이기
    cv::Mat combined;
    cv::hconcat(space_synchronized_images, combined);

    cv::imshow("TEST", combined);
    cv::waitKey(10000);

}


 

2. 구조화된 바인딩 (C++17) — "가독성 깡패"

std::pair나 struct의 멤버를 하나씩 꺼내 쓰느라 지저분했던 코드, 이제는 구조화된 바인딩으로 직관적으로 이름을 붙여 사용하세요.

 
#include <vector>
#include <ranges>
#include <iostream>
#include <map>
#include <string>
#include <print>


int main() {

    std::map<int, std::string> users = { {10, "Test"}, {11, "My"}, };

    // 옛날엔 이터레이터 돌려서 it->first, it->second로 접근했지?
    for (const auto& [id, name] : users) {
        // id와 name을 일반 변수처럼 바로 사용 가능!
        std::println("Map {}->{}", id, name);
    }
}

3. std::format (C++20) — "형식 안정성이 보장된 printf"

std::cout의 스트림 연산자 << 지옥은 이제 그만. 파이썬이나 C#처럼 간결하고 안전한 포맷팅을 지원합니다.

 

 

#include <format>
#include <iostream>

std::string s = std::format("안녕 오빠, 여기는 Visual Studio {}이야! By {}.", 2026, "제니");
std::cout << s;

 

이 format 기능을 이용하기 위해서는 프로젝트 속성에서 C++ 표준을 바꿔줘야 한다.

 

4. std::jthread (C++20) — "자원 해제 스트레스 해소"

std::thread의 고질적인 문제였던 .join() 누락 문제를 소멸자에서 자동으로 해결해 줍니다. 멀티스레드 설계 시 발생하던 크래시를 근본적으로 차단하세요.

#include <iostream>
#include <thread>
#include <chrono>

// 인자에 stop_token을 넣어두면 컴파일러가 알아서 매칭해 줌
void infinite_loop(std::stop_token token) {
    while (!token.stop_requested()) { // 외부에서 멈추라고 했는지 체크
        std::cout << "하드웨어 데이터 수집 중...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    std::cout << "스레드 안전하게 종료!\n";
}

int main() {
    std::jthread jt(infinite_loop);

    std::this_thread::sleep_for(std::chrono::seconds(2));

    // 외부에서 "이제 그만!" 신호 보내기
    // 호출 즉시 token.stop_requested()가 true가 됨
    jt.request_stop(); 
}

5. consteval & is_constant_evaluated() (C++20) — "컴파일 타임의 끝판왕"

런타임 비용을 0으로 만드는 마법입니다. consteval은 함수 실행을 컴파일 타임으로 강제하여, 연산 결과를 상수처럼 즉시 박아버립니다.

 
consteval int square(int n) { return n * n; }
constexpr int val = square(10); // 컴파일 타임에 100으로 계산 완료!

6. std::expected (C++23) — "에러 핸들링의 혁신"

예외(Exception)와 에러 코드 사이에서 갈팡질팡하던 시대는 끝났습니다. 성공하면 '값'을, 실패하면 '에러 객체'를 반환하는 안전한 컨테이너를 사용하세요.

 
#include <iostream>
#include <expected>
#include <string>

// 에러 종류 정의
enum class PrintError { OutOfMaterial, FileCorrupted, HardwareFailure };

void handlePrintError(const PrintError& e) {
    if (e == PrintError::OutOfMaterial) std::cerr << "[LOG] 에러: 실리콘 원료 부족!\n";
    else if (e == PrintError::FileCorrupted) std::cerr << "[LOG] 에러: 파일 데이터 손상!\n";
    else std::cerr << "[LOG] 에러: 하드웨어 손상!\n";
}

// 템플릿 헬퍼: 에러 처리의 공통 로직을 캡슐화
template <typename T, typename E, typename SuccessFunc>
void executeTask(std::expected<T, E>&& res, SuccessFunc&& onSuccess) {
    // 마지막 체이닝 결과를 void로 캐스팅하여 '의도적으로 무시함'을 알림
    static_cast<void>(
        res.and_then([&](const T& val) -> std::expected<void, E> {
        onSuccess(val);
        return {};
    })
        .or_else([&](const E& err) -> std::expected<void, E> {
        handlePrintError(err);
        return std::unexpected(err);
    })
    );
}

// 3D 프린터의 상태를 나타내는 구조체
struct Sample { std::string id; int quality; };

// 샘플 제작 함수: 성공 시 Sample, 실패 시 PrintError 반환
std::expected<Sample, PrintError> createSample(int materialAmount, bool isFileValid) {
    if (materialAmount < 10) return std::unexpected(PrintError::OutOfMaterial);
    if (!isFileValid) return std::unexpected(PrintError::FileCorrupted);

    return Sample{ "Jenny_v0.1", 100 }; // 성공!
}

int main() {
    // 1. 샘플 제작 시도
    executeTask(createSample(20, true), [&](const Sample& s)
    {
        std::cout << "성공: " << s.id << " 제작 시작!\n";
    });

    return 0;
}

 

마치며

이 문법들은 단순히 코드를 짧게 만드는 것을 넘어, 더 안전하고 예측 가능한 프로그램을 만드는 데 목적이 있습니다. 10년 전의 C++과 지금의 C++은 완전히 다른 언어라 할 만큼 발전했습니다. 오늘 당장 프로젝트에 하나씩 적용해 보세요!

+ Recent posts