Leveraging non-deduced contexts for template argument deduction
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(longvs.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.