#066 – Implementing custom deduction guides for CTAD

C++17’s class template argument deduction (CTAD) lets you write std::pair p{1, 2} instead of std::pair<int, int> p{1, 2} — the compiler figures out the template arguments from the constructor call. For most class templates this works automatically, because the compiler synthesises an implicit deduction guide from each constructor signature. But “most” is not “all”: some constructors don’t carry enough information to deduce the right template arguments, and some carry information that points at the wrong ones. User-defined deduction guides are the tool for those cases — a small piece of out-of-class syntax that tells the compiler how to map a constructor call to a specialisation.

Three things to take away:

  • CTAD generates implicit guides from constructors automatically; user-defined guides exist for the cases where the implicit ones are missing or wrong.
  • The syntax is template <...> ClassName(params) -> ClassName<args>;, declared in the same scope as the class template.
  • Reach for a custom guide when constructor parameters don’t match template parameters one-to-one, or when you want decay (const char* → std::string, array → pointer, and so on).

What CTAD gives you for free

For a class template with ordinary constructors, the compiler generates an implicit deduction guide for each one:

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

Box b{ 42 };          // deduces Box<int>
Box s{ "hello" };     // deduces Box<const char*>

Behind the scenes, the compiler treats Box(T) as if you had also written template <typename T> Box(T) -> Box<T>;. The same mechanism makes std::vector v{1, 2, 3} deduce std::vector<int> and std::pair p{1, 2.0} deduce std::pair<int, double>. For most class templates with straightforward constructors, no further work is needed.

When the implicit guide is wrong: the iterator-pair case

The textbook case for a user-defined guide is a constructor whose parameter types don’t directly correspond to the template parameters. std::vector has a constructor that takes a pair of iterators:

template <typename T>
class MyVector {
public:
    template <typename Iter>
    MyVector(Iter first, Iter last);
    // ...
};

The implicit guide derived from this constructor would be template <typename Iter> MyVector(Iter, Iter) -> MyVector<Iter> — which is nonsense. A vector built from two int* iterators should be MyVector<int>, not MyVector<int*>. The constructor takes iterators; the template is parameterised on the value type the iterators dereference to. Without intervention, CTAD either picks the wrong specialisation or refuses to deduce at all.

A user-defined guide closes the gap:

template <typename Iter>
MyVector(Iter, Iter) -> MyVector<typename std::iterator_traits<Iter>::value_type>;

This declares the rule explicitly: when CTAD sees MyVector(it1, it2), deduce Iter as the iterator type, then construct MyVector<value_type_of_Iter>. The Standard Library’s std::vector ships with exactly this kind of guide; it’s why std::vector v(arr, arr + 5) deduces a vector of the array’s element type rather than a vector of pointers.

The syntax

A user-defined deduction guide looks like a function template declaration with a trailing return type, but the “function name” is the name of the class template, and there is no body:

template <typename T>
struct Pair {
    T first{};
    T second{};
};

template <typename T>
Pair(T, T) -> Pair<T>;          // user-defined guide

Pair p{ 1, 2 };                 // deduces Pair<int>

Three things to note about the syntax:

  1. The guide must be declared in the same namespace as the class template — at namespace scope if the class is at namespace scope, or as a friend if it needs access to private types.
  2. There is no return statement and no body — the trailing return type is the deduction result.
  3. The guide can have its own template parameters, independent of the class template’s.

The aggregate Pair above doesn’t have a constructor at all, so in C++17 there were no implicit guides for it and CTAD failed without the user-defined one. C++20 made aggregate CTAD work without a guide in many cases, but the pattern of writing one explicitly is still useful when you want to constrain or shape the deduction.

Forcing decay: const char* → std::string

Another common reason to write a guide is to force the deduced type to decay to something more useful. By default, a string literal deduces as const char*:

template <typename T>
class Wrapper {
public:
    Wrapper(T value) : m_value{ std::move(value) } {}
private:
    T m_value;
};

Wrapper w{ "hello" };   // deduces Wrapper<const char*>

If the wrapper is meant to own a string, that deduction is the wrong default — the wrapper now stores a pointer to a string literal instead of a copy of it. A user-defined guide can redirect the deduction:

Wrapper(const char*) -> Wrapper<std::string>;

Wrapper w{ "hello" };   // now deduces Wrapper<std::string>

The guide is non-templated — it specifies that whenever CTAD sees a Wrapper constructed from a const char*, the specialisation to pick is Wrapper<std::string>. Overload resolution then handles the actual construction (the std::string is constructed from the pointer in the constructor call). The same trick works for T[N] → T*std::reference_wrapper<T> → T&, and other “the obvious type is not the useful type” cases.

Non-type template parameters: std::arraystyle size deduction

Deduction guides can also compute non-type template parameters from the call. The Standard Library uses this for std::array:

template <typename T, std::size_t N>
struct Array {
    T data[N];
};

template <typename T, typename... U>
Array(T, U...) -> Array<T, 1 + sizeof...(U)>;

Array a{ 1, 2, 3, 4 };   // deduces Array<int, 4>

The guide pattern-matches a first argument of type T followed by a parameter pack U..., then computes the size as 1 + sizeof...(U). The result is std::array-style brace-list initialisation that figures out its own length — a feature that simply could not exist without a user-defined guide, because the size is not a constructor parameter at all.

Takeaway

Most class templates need no help from you to participate in CTAD — the implicit guides the compiler generates from constructors are correct by default. Reach for a user-defined deduction guide when the constructor’s parameter types don’t map one-to-one onto the template parameters (the iterator-pair case), when you want the deduction to decay to a more useful type (const char* → std::string), or when a non-type template parameter has to be computed from the call (the array-size case). The syntax is a near-function-declaration with a trailing return type and no body, declared in the same scope as the class template. The rule is simple: if the obvious deduction is wrong and the call site has the information needed to fix it, write a guide.