콘텐츠로 이동

C++17 클래스 템플릿 인수 추론(CTAD)과 Deduction Guides

C++17 이전에는 클래스 템플릿을 사용할 때 항상 타입 인수를 명시해야 했습니다. std::pair<int, double>처럼 길고 중복적인 코드가 필요했습니다. C++17의 **CTAD(Class Template Argument Deduction)**는 생성자 인수에서 템플릿 인수를 자동으로 추론하여 이 문제를 해결합니다.


// C++14 이전: 명시적 타입 지정 필요
std::pair<int, double> p1(42, 3.14);
auto p2 = std::make_pair(42, 3.14); // 헬퍼 함수 필요
// C++17: 생성자에서 자동 추론
std::pair p3(42, 3.14); // pair<int, double>로 추론
std::vector v{1, 2, 3, 4}; // vector<int>로 추론
std::array arr{1, 2, 3}; // array<int, 3>으로 추론

컴파일러는 생성자를 분석하여 추론 가이드를 자동으로 생성합니다. 가장 적합한 오버로드를 선택하는 방식입니다.

template<typename T>
struct Box {
T value;
Box(T v) : value(v) {}
};
Box b1(42); // Box<int>
Box b2(3.14); // Box<double>
Box b3("hello"); // Box<const char*>

기본 추론이 원하는 타입을 유추하지 못할 때 명시적 deduction guide를 작성합니다.

template<typename T>
struct Stack {
std::vector<T> data;
};
// 초기화 리스트로부터 추론하는 가이드
template<typename T>
Stack(std::initializer_list<T>) -> Stack<T>;
Stack s{1, 2, 3}; // Stack<int>
template<typename T>
struct Wrapper {
T value;
Wrapper(T v) : value(v) {}
};
// const char* 대신 std::string으로 추론하도록 가이드
Wrapper(const char*) -> Wrapper<std::string>;
Wrapper w("hello"); // Wrapper<std::string> (const char* 아님)
template<typename Iter>
struct Range {
Iter begin_, end_;
Range(Iter b, Iter e) : begin_(b), end_(e) {}
};
// 이터레이터 쌍에서 원소 타입 추론
template<typename Iter>
Range(Iter, Iter) -> Range<Iter>;
std::vector<int> v{1, 2, 3};
Range r(v.begin(), v.end()); // Range<std::vector<int>::iterator>

// 이전 방식
auto p1 = std::make_pair(1, std::string("hello"));
std::pair<int, std::string> p2{1, "hello"};
// C++17
std::pair p3{1, std::string("hello")}; // pair<int, string>
std::pair p4{1, "hello"}; // pair<int, const char*>
// 이전
auto t1 = std::make_tuple(1, 2.0, "three");
// C++17
std::tuple t2{1, 2.0, "three"}; // tuple<int, double, const char*>
std::optional opt1{42}; // optional<int>
std::optional opt2{"hello"}; // optional<const char*>
// function은 CTAD 지원이 제한적 — 람다로 직접 초기화 불가
// std::function f = [](int x) { return x; }; // C++17에서 여전히 불가
// 명시적 타입 필요
std::function<int(int)> f = [](int x) { return x; };

C++20에서는 집합체(aggregate) 타입도 CTAD를 지원합니다.

template<typename T, typename U>
struct Pair {
T first;
U second;
};
// C++20: aggregate deduction guide 자동 생성
Pair p{1, 2.0}; // Pair<int, double>

6. 실전 활용 — 타입 안전 빌더 패턴

섹션 제목: “6. 실전 활용 — 타입 안전 빌더 패턴”
template<typename... Ts>
struct TypeList {
using types = std::tuple<Ts...>;
};
template<typename T>
struct Builder {
std::vector<T> items;
Builder& add(T item) {
items.push_back(std::move(item));
return *this;
}
};
// 첫 인수에서 T를 추론
template<typename T>
Builder(T) -> Builder<T>;
auto b = Builder(42).add(10).add(20); // Builder<int>

// 1. 부분 특수화: 전체 인수 추론만 가능
template<typename T, typename U>
struct Pair {};
// 2. 별칭 템플릿: C++20 이전에는 불가
template<typename T>
using Vec = std::vector<T>;
// Vec v{1,2,3}; // C++20 이전 오류
// 3. 상속: 기본 클래스 생성자로만 추론
template<typename T>
struct MyVector : std::vector<T> {
using std::vector<T>::vector;
};
// MyVector v{1,2,3}; // 추론 실패 (C++20 이전)

// 주의1: 이니셜라이저 리스트 vs 생성자 인수
std::vector v1(5, 0); // vector<int>: 5개의 0
std::vector v2{5, 0}; // vector<int>: {5, 0}
// 주의2: 예상과 다른 추론
std::pair p{"hello", "world"}; // pair<const char*, const char*>
// std::string으로 추론되지 않음 — 명시적 가이드 필요
// 주의3: 중괄호 초기화와 initializer_list 우선순위
std::vector v3{1, 2, 3}; // initializer_list<int> — vector<int>
std::vector v4(3, 1); // 생성자(count, value) — vector<int> 3개의 1

기능C++14C++17C++20
함수 템플릿 추론OOO
클래스 템플릿 추론XOO (aggregate 포함)
명시적 deduction guideXOO
별칭 템플릿 CTADXXO

CTAD는 코드의 중복을 줄이고 make_* 헬퍼 함수의 필요성을 낮춥니다. 다만, 예상치 못한 타입으로 추론될 수 있으므로 명시적 deduction guide와 함께 활용하는 것이 안전합니다.