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
(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
.