본문으로 건너뛰기
Previous
Next
C++ Deduction Guides: Customizing CTAD in C++17

C++ Deduction Guides: Customizing CTAD in C++17

C++ Deduction Guides: Customizing CTAD in C++17

이 글의 핵심

Write deduction guides to customize C++17 CTAD: transform types at deduction time, resolve ambiguous constructors, match STL patterns, and avoid common pitfalls with initializer_list and explicit guides.

What Is Class Template Argument Deduction?

Before C++17, you always had to spell out template arguments explicitly when constructing a class template:

// C++14 and earlier — must write <int>
std::pair<int, double> p(42, 3.14);
std::vector<std::string> v{"a", "b", "c"};

// Or use factory functions that could deduce for you
auto p = std::make_pair(42, 3.14);    // deduces pair<int, double>

C++17 introduced Class Template Argument Deduction (CTAD): the compiler deduces template arguments from constructor call arguments, just as it deduces function template arguments:

// C++17 — compiler deduces template arguments
std::pair   p(42, 3.14);       // pair<int, double>
std::vector v{"a", "b", "c"};  // vector<const char*>

CTAD uses the class’s constructors as a set of “implicit deduction guides.” When the implicit guides don’t produce the right type, you write an explicit deduction guide.


The Basic Syntax

A deduction guide is a top-level declaration in the class template’s namespace:

template<typename T>
class Box {
public:
    Box(T value) : value_(value) {}
private:
    T value_;
};

// Deduction guide: when constructed from T, deduce Box<T>
// This matches what the constructor already does — so it's redundant here
// but shows the syntax
template<typename T>
Box(T) -> Box<T>;

Box b(42);       // Box<int>
Box s("hello");  // Box<const char*>

The syntax is: template-header class-name(parameters) -> class-name<deduced-args> ;


Why the Implicit Guides Aren’t Always Enough

Type Transformation

The most common reason to write a guide: transform a constructor argument type into a better template argument:

template<typename T>
class Buffer {
public:
    Buffer(const char* s) : data_(s) {}   // constructor takes const char*
private:
    T data_;
};

// Without a guide: Buffer("hello") would deduce Buffer<???> — no T to deduce from
// With a guide: convert const char* to std::string
Buffer(const char*) -> Buffer<std::string>;

Buffer b("hello");   // Buffer<std::string>, not Buffer<const char*>

Without the guide, the constructor Buffer(const char*) provides no way for the compiler to know what T should be — it cannot appear in the constructor signature at all. The guide fills that gap.

Iterator Pair Pattern

The STL containers use this for range construction:

template<typename T>
class MyVector {
public:
    template<typename Iter>
    MyVector(Iter first, Iter last) {
        while (first != last) data_.push_back(*first++);
    }
private:
    std::vector<T> data_;
};

// Guide: deduce T from the iterator's value_type
template<typename Iter>
MyVector(Iter, Iter) -> MyVector<typename std::iterator_traits<Iter>::value_type>;

std::vector<int> source = {1, 2, 3, 4, 5};
MyVector v(source.begin(), source.end());   // MyVector<int>

Without the guide, T cannot be deduced from Iter, Iter — the compiler only knows about Iter, not what *Iter produces. The guide extracts value_type from the iterator traits.


Complete Working Example

A Pair class that deduces element types and converts string literals to std::string:

#include <string>
#include <iostream>
#include <type_traits>

template<typename A, typename B>
class Pair {
public:
    A first;
    B second;

    Pair(A a, B b) : first(std::move(a)), second(std::move(b)) {}

    void print() const {
        std::cout << "(" << first << ", " << second << ")\n";
    }
};

// Guide 1: standard deduction from constructor types
template<typename A, typename B>
Pair(A, B) -> Pair<A, B>;

// Guide 2: convert const char* arguments to std::string
Pair(const char*, const char*) -> Pair<std::string, std::string>;

// Guide 3: mixed — first is const char*, second is some type T
template<typename T>
Pair(const char*, T) -> Pair<std::string, T>;

int main() {
    Pair p1(42, 3.14);                 // Pair<int, double> via Guide 1
    Pair p2("hello", "world");         // Pair<string, string> via Guide 2
    Pair p3("name", 42);               // Pair<string, int> via Guide 3

    p1.print();   // (42, 3.14)
    p2.print();   // (hello, world)
    p3.print();   // (name, 42)

    // Copy construction — must not loop
    Pair p4 = p1;   // Pair<int, double>
    p4.print();     // (42, 3.14)
}

Explicit Deduction Guides

The explicit keyword on a deduction guide prevents implicit conversion — the guide only fires when the constructor arguments are provided directly, not in copy-initialization:

template<typename T>
class Wrapper {
public:
    explicit Wrapper(T val) : val_(val) {}
    T val_;
};

// explicit guide: only fires for direct initialization
template<typename T>
explicit Wrapper(T) -> Wrapper<T>;

Wrapper w1(42);            // OK — direct initialization
// Wrapper w2 = 42;        // ERROR — explicit guide prevents this
// auto w3 = Wrapper{42};  // OK — brace direct init works

// Without explicit, both forms would work

Use explicit guides when implicit construction from that argument type would be surprising or unsafe.


Common Patterns

Array + Size

template<typename T>
class ArrayView {
    T*     data_;
    size_t size_;
public:
    ArrayView(T* data, size_t size) : data_(data), size_(size) {}
};

// Deduce T from the pointer type
template<typename T>
ArrayView(T*, size_t) -> ArrayView<T>;

int arr[] = {1, 2, 3, 4, 5};
ArrayView view(arr, 5);   // ArrayView<int>

Smart Pointer Style

template<typename T>
class Handle {
    T* ptr_;
public:
    explicit Handle(T* p) : ptr_(p) {}
    ~Handle() { delete ptr_; }
};

template<typename T>
Handle(T*) -> Handle<T>;

Handle h(new int(42));   // Handle<int>

Conditional Element Type (C++17 if constexpr)

template<typename T>
class NumericBox {
public:
    // Store float for integral types (promotes precision)
    using StoredType = std::conditional_t<std::is_integral_v<T>, float, T>;
    StoredType value;
    NumericBox(T v) : value(static_cast<StoredType>(v)) {}
};

// Guide: integral types become float, others pass through
template<typename T>
NumericBox(T) -> NumericBox<T>;

NumericBox nb(42);     // NumericBox<int>, but stored as float
NumericBox nd(3.14);   // NumericBox<double>

Pitfalls

Ambiguous Guides

When two guides match equally well, CTAD fails:

template<typename T>
class Box {
public:
    Box(T a, T b) {}
    Box(T a, int b) {}
};

template<typename T> Box(T, T) -> Box<T>;     // Guide 1
template<typename T> Box(T, int) -> Box<T>;   // Guide 2

// Box b(1, 2);   // ERROR: ambiguous — both guides match with T=int

Fix: make one guide more specific, or use explicit template arguments: Box<int> b(1, 2).

Copy Constructor Conflict

When you define guides, CTAD may confuse copy construction with other construction:

template<typename T>
class Container {
public:
    Container(T value) {}
    Container(const Container&) = default;   // copy constructor
};

template<typename T>
Container(T) -> Container<T>;

Container<int> c1(42);
Container c2 = c1;   // should copy-construct Container<int>
                     // guide would give Container<Container<int>> — wrong!

Fix: add a guide that preserves the type when constructing from the same template:

template<typename T>
Container(Container<T>) -> Container<T>;   // copies stay the same type

initializer_list Priority

Brace initialization preferentially binds to initializer_list constructors, which can suppress CTAD:

template<typename T>
class MyList {
public:
    MyList(std::initializer_list<T> items) {}
    MyList(T a, T b) {}
};

MyList ml1{1, 2};      // initializer_list<int> — clear
MyList ml2(1, 2);      // (int, int) constructor
// MyList ml3{1, 2.0}; // may be ambiguous — prefer explicit types

When brace initialization might be ambiguous, use parentheses or explicit template arguments.


Standard Library Deduction Guides

The C++17 standard added deduction guides to common containers so CTAD works naturally:

// std::vector guide (iterator pair)
std::vector v(arr.begin(), arr.end());   // deduces vector<int>

// std::array guide
std::array a{1, 2, 3, 4, 5};            // deduces array<int, 5>

// std::pair guide
std::pair p(42, "hello");               // pair<int, const char*>

// std::optional guide
std::optional o(42);                    // optional<int>

// std::tuple guide
std::tuple t(1, 2.0, "three");          // tuple<int, double, const char*>

When NOT to Use Deduction Guides

If the default CTAD already works: adding a guide that does the same thing as the implicit one is noise.

// Redundant — compiler already deduces this from the constructor
template<typename T>
Box(T) -> Box<T>;   // unnecessary if Box has Box(T val) constructor

If the transformation is surprising: a guide that converts int to long silently would confuse users. Make surprising conversions explicit (either explicit guides or factory functions).

Pre-C++17 compatibility: deduction guides require C++17. If you need to support older standards, stick to factory functions:

// make_pair style factory — works in C++11/14
template<typename A, typename B>
Pair<A, B> make_pair(A a, B b) { return Pair<A, B>(std::move(a), std::move(b)); }

Key Takeaways

  • Deduction guides customize how CTAD maps constructor arguments to template arguments
  • Syntax: template<...> ClassName(params) -> ClassName<deduced-args>; in the enclosing namespace
  • Common uses: type transformation (const char*std::string), iterator-range deduction (extract value_type), pointer-to-pointee deduction
  • explicit guides prevent implicit construction — use for guides that would otherwise allow surprising conversions
  • Copy constructor conflict: add a ClassName(ClassName<T>) -> ClassName<T> guide when the generic guide would wrap instead of copy
  • initializer_list priority: brace init prefers initializer_list constructors — use parentheses when you need the other constructor
  • Compile-time only: deduction guides have zero runtime cost
  • Standard library: std::vector, std::array, std::pair, std::tuple, etc. all have C++17 guides — rely on them

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Deduction guides for CTAD: syntax, iterator pairs, const char* to string conversions, explicit guides, and pitfalls with… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, Deduction Guides, CTAD, C++17, Templates 등으로 검색하시면 이 글이 도움이 됩니다.