Leveraging non-deduced contexts for template argument deduction

Tags// , ,

What is an example of a non-deduced context, and why can it be useful to know about these?

The following snippet

template<typename T>
struct Foo { T t; };

template<typename T>
void addToFoo(Foo<T>& foo, T val) { foo.t += val; }

int main() {
    Foo<long> f{42};
    addToFoo(f, 13); // error: no matching function for call to 'addToFoo'
    return 0;
}

is ill-formed, as (function) template argument deduction for the dependent function parameters foo and val of the addToFoo function template resolves to different, conflicting types in the addToFoo(f, 13) function call.

error: no matching function for call to addToFoo.

note: candidate template ignored: deduced conflicting types for parameter T (long vs. int).

Whilst we could make the program well-formed by invoking addToFoo as addToFoo(f, 13L), this would keep the restrictions of the addToFoo API to limit the two dependent function parameters to deduce the same type template parameter T. This may not be our intent and, if indeed not, probably not an intentional limitation we want to place on the user of addToFoo.

Instead, we could redesign addToFoo to explicitly ensure that the second function parameter val (and the argument(s) passed to it) will not participate in (function) template argument deduction of T for a given addToFoo(...) call.

How? A dependent parameter name in a function template can only be used to deduce the associated template parameter if the latter is to the right of the right-most scope resolution operator :: in the type of the dependent parameter; otherwise it is not deducable; formally, placing the dependent name in a non-deduced context. This can leveraged to, with intent, place a template parameter of a given function parameter in a non-deduced context, such that the given template parameter, say T, can only be deduced from elsewhere (say other function parameters dependent on T).

We may apply this to the example above, using an identity transformation trait on the dependent type of val in addToFoo:

#include <type_traits>

template<typename T>
struct Foo { T t; };

template<typename T>
using identity_t = std::common_type_t<T>;

template<typename T>
void addToFoo(Foo<T>& foo,  identity_t<T> val) { foo.t += val; }
                         // ^^^^^^^^^^^^^ T in non-deduced context.
int main() {
    Foo<long> f{42};
    addToFoo(f, 13);  // Ok! T unambiguously deduced to 'long', and 13 promoted.

    return 0;
}

As val is now in a non-deduced context, template argument deduction falls back entirely on deduction via the argument supplied for the parameter foo.

Note that the identity_t transformation trait above is just an alias, and, for clarity, we could likewise specify the type of val as typename std::common_type<T>::type, in which case it may be more apparent as to why T is non-deducable.

Finally, note that as of C++20, specifically through its implementation of P0887R1 (The identity metafunction), this very identity transformation trait has been added to the to the type_traits header, as the the library utility metafunction std::type_identity along with its helper alias template std::type_identity_t.


More Reading
comments powered by Disqus