본문으로 건너뛰기 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

Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Deduction guides for CTAD: syntax, iterator pairs, const char* to string conversions, explicit guides, and pitfalls.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, Deduction Guides, CTAD, C++17, Templates.