The language feature in C++17 known as class template argument deduction was intended to supersede factory functions like make_pair , make_tuple , make_optional , as described in p0091r2. This goal has not been fully achieved and we may still need to stick to make_ functions. In this post we will briefly describe what class template argument deduction is, and why it works differently than what people often expect.

The original problem was that people had to write code like this:

f(std::pair<int, std::vector::iterator>(0, v.begin()));

Being required to spell out the types of 0 and v.begin() is inconvenient, so the library came with the convenience function:

f(std::make_pair(0, v.begin()));

This works because while template parameters of class templates (prior to C++17) could not be deduced from constructor arguments, they can be deduced for function templates from function arguments. This is an improvement, although sometimes you want to go with the former construct: when you want to adjust the types of the arguments:

g(std::pair<short, std::string>(0, "literal"));

Next improvement, in C++17, was to actually allow deduction of class template parameters from arguments passed in construction, so that the workaround with make_pair should not be needed:

// C++17 f(std::pair(0, v.begin()));

And it works for make_pair , and most of the time it also works for other factories, like make_tuple , except when it does not.

Consider the following example:

auto o = std::make_optional(int{}); // o is optional<int> auto p = std::make_optional(Threshold{}); // p is optional<Threshold> auto q = std::make_optional(std::optional<int>{}); // q is optional<optional<int>>

Function make_optional just wraps the argument into a std::optional . This is obvious for the first two cases with o and p . Regarding the third option someone might have doubts, expecting that nested optional s would collapse into one, but that would not be a good idea: sometimes we want them to nest. For instance optional<int> could model a “threshold”, where no-value means threshold is infinity; whereas optional<optional<int>> could mean a “threshold that may not be known”. Actually, Threshold in the second example could be an alias for

optional<int> . Also, consider a general case inside a template:

template <typename T> void f(T v) { auto o = std::make_optional(v); static_assert(std::is_same_v<decltype(o), std::optional<T>>); }

We want the assertion to hold regardless of what type the template is instantiated with.

Now, let’s try to replace make_optional in our examples with class template argument deduction:

std::optional o (int{}); // o is optional<int>

This works as expected.

std::optional q (std::optional<int>{}); // q is optional<int> !

This works different than make_optional ! This is because the class template argument deduction logic rather than working uniformly, tries to guess what your intention is differently in different context. This logic assumes that in line 2 below:

std::optional o (1); // intention: wrap std::optional q (o); // intention: copy

our intention was more likely to make a copy. In some cases this guess works, but it does not work when our intention is always to wrap inside an make_optional . Now, this case:

std::optional p (Threshold{}); // p may be optional<Threshold> // p may be Threshold

We do not know if the type of p is optional<Threshold> or Threshold , because we do not know if type Threshold is an instance of std::optional or not. This means that inside a template, when we want to be sure that we are always wrapping a T into an optional<T> , we have to use make_optional and cannot rely on class template argument deduction. This is one of these cases where compiler can deduce something else than what you expect. We have a similar situation with make_tuple :

std::tuple t (1); // tuple<int> std::tuple u (t); // tuple<int> std::tuple v (Threshold{}); // ???

This problem occurs because we have two contradictory expectations of a deduction like this. One is that it should wrap, the other is that it should make an exact same type:

std::optional o (1); // intention: wrap std::optional q (o); // intention: copy

These expectations are contradictory in the cases like line 2 above.

std::optional has a second, similar problem. Consider this case:

optional<int> o = 1; assert (o != nullopt);

This works as expected because T can be converted to optional<T> . The following also works as expected:

optional<int> a = 1; optional<long> b = a; assert (b != nullopt);

This works because optional<U> can be converted to optional<T> whenever U is convertible to T , a valueless optional<U> being converted into a valueless optional<T> . This is intuitive. But consider this case:

optional<int> a {}; optional<optional<int>> b = a; assert (b == nullopt); // correct?

Should the initialization in line 2 be treated as a conversion from T to optional<T> ? (In that case the assertion will fire.) Or should it be treated as a conversion from optional<U> to optional<T> ? (In that case the assertion will pass.) The compiler will try to guess what we intended, but it will likely guess wrong. Note that this problem will be more difficult to spot if the code looks like this:

Threshold a {}; optional<Threshold> b = a; assert (b == nullopt); // correct?

Or like this:

template <typename T> void f(T v) { optional<T> o = v; assert (o == nullopt); // correct? }

For this reason, in generic context (and even in non-generic context, when you cannot be sure about the properties of the type), in order to avoid bugs resulting from ambiguity described above, you cannot rely on the “intelligent” logic in opitonal ’s constructors. You had better state your intentions directly:

template <typename T> void f(T v) { std::optional<T> o {std::in_place, v}; assert (o != nullopt); // correct }

The bottom line of this post is: intelligent deductions usually work as expected, but sometimes they do something else.