Document #: | P2998R0 |
Date: | 2024-10-15 |
Project: | Programming Language C++ |
Audience: |
Evolution |
Reply-to: |
James Touton <bekenn@gmail.com> |
This paper proposes to extend template argument deduction from a function call to include candidates determined via class template argument deduction when the type of a function parameter cannot otherwise be deduced. This will allow some function templates to match a much wider range of invocations involving conversions to the deduced type rather than requiring an exact match. The feature works by leveraging existing wording involving class template argument deduction for alias templates, treating a dependent template-id as the aliased type of a hypothetical alias template definition. Template arguments for the hypothetical alias template are deduced using the existing rules for class template argument deduction, and the results are substituted back into the template-id of the function parameter type. Template argument deduction for the function template then proceeds as usual from the substituted parameter type.
#include <span>
#include <vector>
template <typename T> void f(std::span<T>);
template <typename T> void g(const std::vector<T>&);
void example()
{
int x[] = { 1, 2, 3, 4, 5 };
(x); // previously ill-formed, now OK; T deduced as int
f({ 1, 2, 3, 4, 5 }); // previously ill-formed, now OK; T deduced as int
g}
This paper also proposes to allow users to declare deduction guides for alias templates.
Function templates are intended to behave very much like ordinary functions in that they can be called like ordinary functions and participate in overload resolution with ordinary functions. One way to think of this is that a function template behaves as a collection of overloads consisting of all possible substitutions of its template parameters. At the point of a function call, the implementation compares the list of function arguments against each available overload and selects the overload that most closely matches the function arguments. If the overload originated as a function template, then the template is instantiated using the substitutions that generated the overload.
Of course, that’s not how it actually works. The set of substitutions for a template is effectively unbounded; the implementation cannot simply generate a list of all possible substitutions for a template. Instead, the implementation follows a set of rules intended to determine the most appropriate substitution for a template based on the information available in the call and in the structure of the template declaration. This keeps the search space manageable and, importantly, avoids incidental instantiations of other participating templates.
The existing rules are generally very good and have withstood the test of time; function templates are one of the most important bedrock features in the language today, and have been since their inception. The success of the language very much rests on the success of function templates. However, there are limitations to the existing rules for template argument deduction that exclude some desirable use cases.
For the most part, template argument deduction is limited to finding
a more-or-less exact match for the type of an argument expression.
Conversions are generally not considered. This is usually fine; if,
given a type template parameter
T
, a function parameter’s type
is simply T
(or some other
simple modification, such as
const T&
), then it doesn’t
make sense to consider substitutions other than the type of the function
argument, because any such substitution will be a worse match.
Similarly, if a function parameter’s type is some variation on
vector<T>
and the argument
is a specialization of vector
,
then it is easy (and correct) to say that the substitution for
T
is the substitution that makes
vector<T>
equal to the
argument type.
But there are cases where conversions may be desired, and the rules don’t currently provide a way to handle that:
#include <span>
#include <vector>
template <typename T> void f(const T&);
template <typename T> void g(const std::vector<T>&);
template <typename T> void h(std::span<T>);
void example()
{
::vector<int> v;
std
(v); // OK, T deduced as std::vector<int>
f(v); // OK, T deduced as int
g(v); // error: unable to deduce T
h}
In the above example, based on our knowledge of
std::span
, it is easy to see
that a match for the call to h
is possible; if T
were to
somehow be deduced as int
, then
the call to h
would be valid.
And, in fact, there is an existing context in the language where that
deduction succeeds:
#include <span>
#include <vector>
::vector<int> v;
std::span s = v; // OK, s is std::span<int, std::dynamic_extent> std
By applying this mechanism in the context of a function call, we can reproduce the successful deduction, and thereby make the language more useful and more self-consistent.
The mechanism is class template argument deduction (CTAD, [P0091R3]), initially introduced in C++17
with support for class templates, and later expanded in C++20 with
support for alias templates ([P1814R0]). With CTAD, an unspecialized
template name is used in a declaration as a placeholder for a
specialization of that template, which is inferred from the
declaration’s initializer. A placeholder may appear in variable
declarations, new
expressions,
function-style casts, and non-type template parameter declarations, but
not in function parameter declarations.
Note that this paper does not propose allowing placeholders in function parameter declarations:
void f(std::pair p); // NOT proposed
Such a feature is certainly achievable, and may be useful in its own right, but this approach has limitations because the placeholder syntax disallows template arguments on the placeholder:
It is impossible to express relationships between function parameters making use of the same template parameters from the function template. For instance, given the hypothetical declaration
void copy(std::span from, std::span to);
there is no straightforward way to establish that the value types of
from
and
to
are the same, or that the
element type of from
is
const
-qualified while the
element type of to
is
not.
CTAD using placeholders deduces arguments for all parameters of the template; there is no way to provide explicit arguments for some template parameters and deduce the others. Given a declaration
template <typename V> void f(std::tuple<std::string_view, V>);
we would like to be able to deduce
V
given an argument of type
std::pair<std::string, int>
.
Given these shortcomings and a desire for the feature to work with existing templates, the proposal is to make no changes to the existing syntax for function parameters. Instead, the approach is to generate an alias template from the function parameter type, and then perform template argument deduction for the initialization of a variable declared with the name of the alias template as its type and the function argument as its initializer.
For example, given the function declaration and call argument from the second bullet point above:
template <typename V>
void f(std::tuple<std::string_view, V>);
void example()
{
::pair<std::string, int> p = { "hello", 5 };
std(p);
f}
…an alias template and corresponding variable declaration are considered. Template arguments for the type of the variable are determined according to the rules for CTAD for alias templates ([P1814R0]):
// Hypothetical alias template
template <typename V>
using A = std::tuple<std::string_view, V>;
= p; // A deduced as A<int> (i.e. std::tuple<std::string_view, int>) A x
and the result is substituted back into the type of the function parameter:
(p); // OK, V deduced as int f
Consider this simple function template for copying data between two contiguous ranges:
template <typename T>
void copy(std::span<const T> from, std::span<T> to);
With the approach outlined above, this function template is now
directly callable using span
s,
vector
s, arrays (both built-in
and std::
), or even braced
pointer/size pairs. (Of course, a more generic range concept could also
have handled all of those cases and more, but this approach has the
advantage of generating fewer template instantiations; everything
converts to some flavor of span
before the function call.) All of that would have required additional
overloads without this feature.
But there’s still a surprising gap in functionality here:
void example(std::span<const int> src1, std::span<int> src2)
{
int dst[5];
(src1, dst); // OK
copy(src2, dst); // error: deduction failed for T from src2
copy}
This stems from an existing limitation with deduction guides; the same problem arises in existing code today:
#include <span>
template <typename ElementType, std::size_t Extent = std::dynamic_extent>
using const_span = std::span<const ElementType, Extent>;
::span<int> x;
std<int> y = x; // OK
const_span= x; // error: No guide for const_span from std::span<int, std::dynamic_extent> const_span z
To understand why this fails, we have to examine the set of guides
that are generated for
const_span
. The guides for
const_span
are generated from
the guides for std::span
, of
which there are two that could conceivably be relevant to this
situation:
namespace std
{
// Guide generated from the copy constructor of std::span
template <typename ElementType, size_t Extent>
(const span<ElementType, Extent>&) -> span<ElementType, Extent>;
span
// User-declared deduction guide for adapting contiguous ranges
template <ranges::contiguous_range R>
(R&&) -> span<remove_reference_t<ranges::range_reference_t<R>>>;
span}
These are the corresponding guides generated for
const_span
:
template <typename ElementType, std::size_t Extent>
(const std::span<const ElementType, Extent>&) -> std::span<const ElementType, Extent>
const_spanrequires deduces-const-span<std::span<const ElementType, Extent>>;
template <std::ranges::contiguous_range R>
(R&&) -> std::span<std::remove_reference_t<std::ranges::range_reference_t<R>>>
const_spanrequires deduces-const-span<std::span<std::remove_reference_t<std::ranges::range_reference_t<R>>>>;
Here, deduces-const-span is a constraint that is satisfied
if and only if template arguments for
const_span
are deducible from
the type argument. This validates that, for a given set of template
arguments, the return type of the guide can be expressed as a
specialization of
const_span
.
The first guide fails because it’s expecting a span of
const ElementType
, while
x
is a span of
(non-const
)
int
.
The second guide fails because the return type fails the deducibility
constraint. Substituting the type of
x
for
R
, we get:
(std::span<int>&) -> std::span<int>
const_spanrequires deduces-const-span<std::span<int>>;
where std::span<int>
cannot be expressed as a specialization of
const_span
.
We could solve this by providing our own user-declared guides for
const_span
:
template <typename ElementType, std::size_t Extent>
(const std::span<ElementType, Extent>&) -> const_span<ElementType, Extent>;
const_span// etc
…but the language does not currently allow us to do that. Users can only declare deduction guides for class templates, not alias templates.
This restriction is artificial. Language rules already require implementations to keep track of a set of guides for each alias template; allowing users to extend this set should pose no technical challenge. (Indeed, with Clang, this change required only removing a single check explicitly rejecting deduction guide declarations for alias templates.)
With the addition of user-declared deduction guides, we can rewrite
the copy
example in a form that
works:
#include <span>
template <typename ElementType, std::size_t Extent = std::dynamic_extent>
using const_span = std::span<const ElementType, Extent>;
template <typename ElementType, std::size_t Extent>
(const std::span<ElementType, Extent>&) -> const_span<ElementType, Extent>;
const_spantemplate <std::ranges::contiguous_range R>
(R&&) -> const_span<std::remove_reference_t<std::ranges::range_reference_t<R>>>;
const_span// etc
template <typename T>
void copy(const_span<T> from, std::span<T> to);
void example(std::span<const int> src1, std::span<int> src2)
{
int dst[5];
(src1, dst); // OK
copy(src2, dst); // OK
copy}
There is another way in which the
copy
example might be made to
work without the need for a rewrite or a custom alias template with
accompanying user-declared guides. Fundamentally, the problem is that a
deduction guide can only map a given set of inputs to a single
specialization of the template. Extending deduction guides to allow a
list of return types would allow us to express deeper relationships
between types and permit the implementation to choose the most
appropriate alternative for a given context.
Continuing with the span
example, we could add this user-declared deduction guide to show that
spans of T
are adaptable to
spans of const T
:
namespace std
{
template <typename ElementType, size_t Extent>
(const span<ElementType, Extent>&)
span-> span<ElementType, Extent>,
<ElementType, dynamic_extent>,
span<const ElementType, Extent>
span<const ElementType, dynamic_extent>;
span}
Each return type is an alternative deduction, expressed in decreasing order of preference. Variable declarations using the name of a class template would be unaffected by this change; the first alternative would always be chosen. These alternatives would come into play for alias templates; the compiler would try each alternative in the order given, and the first to pass the deducibility constraint would be selected.
That would make the
const_span
example work without
the need for additional user-declared deduction guides:
#include <span>
template <typename ElementType, std::size_t Extent = std::dynamic_extent>
using const_span = std::span<const ElementType, Extent>;
// No new deduction guides here!
::span<int> x;
std<int> y = x; // OK
const_span= x; // OK, implementation selects std::span<const int,
std::dynamic_extent> const_span z
This idea has not yet been fully examined and may be revisited in a future revision of this paper.
This feature makes some overloads viable that were not viable before, potentially expanding the set of viable functions for a given function call. This has the potential to make ambiguous a function call that was previously well-formed:
template <typename T>
struct X {};
template <typename T>
struct Y
{
(X<T>);
Y};
template <typename T>
void f(X<T>, float); // #1
template <typename T>
void f(Y<T>, int); // #2
(X<short>(), 5); // OK before, ambiguous with this proposal f
Without this proposal, the call to
f
selects #1; #2 is non-viable
because T
cannot be deduced from
the call. With this proposal, the call is ambiguous; #2 is now viable
with T
deduced as
short
, but #1 is a better match
for the first argument, while #2 is a better match for the second
argument.
It is currently unknown how much code in the wild would be broken by
adopting this feature. To avoid breaks, we could introduce a rule that
makes #2 a worse match than #1 by virtue of its use of this feature.
Such a rule would be decidedly unprincipled, and may lead to some
surprising results; arguably the call to
f
should be ambiguous.
Still, even with the compatibility work-around, the feature should prove
useful.
All changes are presented relative to [N4986].
Modify §12.2.2.9 [over.match.class.deduct] paragraph 1:
1 When resolving a placeholder for a deduced class type (9.2.9.8 [dcl.type.class.deduct]) where the template-name names a primary class template
C
, a set of functions and function templates, called the guides ofC
, is formed comprising:
[…]
(1.4) For each deduction-guide declared for
C
(13.7.2.3 [temp.deduct.guide]), a function or function templatewith the following properties:as described below.[…]
Modify §12.2.2.9 [over.match.class.deduct] paragraph 3:
3 When resolving a placeholder for a deduced class type (9.2.9.3 [dcl.type.simple]) where the template-name names an alias template
A
, the defining-type-id ofA
must be of the formtypename
opt nested-name-specifieropttemplate
opt simple-template-idas specified in 9.2.9.3 [dcl.type.simple]. Let
B
be the template named by the simple-template-id of the defining-type-id ofA
. The guides ofA
are the set of functions or function templatesformed as follows.comprising:
(3.1) For each function or function template
f
in the guides ofthe template named by the simple-template-id of the defining-type-idB
, a corresponding function or function templatef
′ formed as follows:the templateTemplate argumentsoffor the template parameters used in the return type off
are deduced from the defining-type-id ofA
according to the process in 13.10.3.6 [temp.deduct.type] with the exception that deduction does not fail if not all template arguments are deduced. If deduction fails for another reason, proceed with an empty set of deduced template arguments. Letg
denote the result of substituting these deductions intof
. If substitutionsucceedsfails, there is nof
′ corresponding to thisf
; otherwise,form a function or function templatef
′withhas the following propertiesand add it to the set of guides of:A
- […]
- (3.2) For each deduction-guide declared for
A
(13.7.2.3 [temp.deduct.guide]), a function or function template as described below.If a function or function template in the guides of
A
formed from the guides ofB
would potentially conflict (6.4.1 [basic.scope.scope]) with a function or function template formed from a deduction-guide declared for A if given the same name, then the former is discarded.
Insert a new paragraph immediately following the above modified paragraph:
4 A guide formed from a deduction-guide has the following properties:
Modify §13.7.2.3 [temp.deduct.guide] paragraph 1:
deduction-guide:
explicit-specifieropt template-name(
parameter-declaration-clause)
requires-clauseopt->
simple-template-id;
1 Deduction guides are used when a template-name appears as a type specifier for a deduced class type (9.2.9.8 [dcl.type.class.deduct]). A deduction guide is declared for the template named by its template-name. Deduction guides are not found by name lookup. Instead, when performing class template argument deduction (12.2.2.9 [over.match.class.deduct]), all reachable deduction guides declared for the
classtemplate are considered.deduction-guide:
explicit-specifieropt template-name(
parameter-declaration-clause)
requires-clauseopt->
simple-template-id;
Modify §13.7.2.3 [temp.deduct.guide] paragraph 3:
3 The same restrictions apply to the parameter-declaration-clause of a deduction guide as in a function declaration (9.3.4.6 [dcl.fct]), except that a generic parameter type placeholder (9.2.9.7 [dcl.spec.auto]) shall not appear in the parameter-declaration-clause of a deduction guide. The
simple-template-idtemplate-name shall name aclass template specializationdeducible template (9.2.9.3 [dcl.type.simple]) and the simple-template-id shall name a specialization of that template. The template-name shall be the same identifier as the template-name of the simple-template-id. A deduction-guide shall inhabit the scope to which the correspondingclasstemplate belongs and, for a memberclasstemplate, have the same access. Two deduction guide declarations for the sameclasstemplate shall not have equivalent parameter-declaration-clauses if either is reachable from the other.
Split §13.10.3.2 [temp.deduct.call] paragraph 1 after the first sentence, with modifications as shown:
1 Template argument deduction is done by comparing each function template parameter type
(call itP
)that contains template-parameters that participate in template argument deduction with the typeA
of the corresponding argument of the call(call itas described in 13.10.3.6 [temp.deduct.type], with potential modifications toA
)P
andA
as described below.
Form a new paragraph from the remainder, with modifications as shown, splitting it again after the first example:
2 If removing references and cv-qualifiers from
P
givesstd::initializer_list<P
′>
orP
′[N]
for someP
′ andN
and the argument is a non-empty initializer list (9.4.5 [dcl.init.list]), then deduction is performed instead for each element of the initializer list independently, takingP
′ as separate function template parameter typesP
′i and the ith initializer element as the corresponding argument. In theP
′[N]
case, ifN
is a non-type template parameter,N
is deduced from the length of the initializer list.Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context (13.10.3.6 [temp.deduct.type]).[ Example:— end example ]template<class T> void f(std::initializer_list<T>); ({1,2,3}); // T deduced as int f({1,"asdf"}); // error: T deduced as both int and const char* f
template<class T> void g(T);g({1,2,3}); // error: no argument deduced for Ttemplate<class T, int N> void h(T const(&)[N]); ({1,2,3}); // T deduced as int; N deduced as 3 h template<class T> void j(T const(&)[3]); ({42}); // T deduced as int; array bound not considered j struct Aggr { int i; int j; }; template<int N> void k(Aggr const(&)[N]); ({1,2,3}); // error: deduction fails, no conversion from int to Aggr k({{1},{2},{3}}); // OK, N deduced as 3 k template<int M, int N> void m(int const(&)[M][N]); ({{1,2},{3,4}}); // M and N both deduced as 2 m template<class T, int N> void n(T const(&)[N], T); ({{1},{2},{3}},Aggr()); // OK, T is Aggr, N is 3 n template<typename T, int N> void o(T (* const (&)[N])(T)) { } int f1(int); int f4(int); char f4(char); ({ &f1, &f4 }); // OK, T deduced as int from first element, nothing o// deduced from second element, N deduced as 2 ({ &f1, static_cast<char(*)(char)>(&f4) }); // error: conflicting deductions for T o
Insert a new paragraph immediately following the above modified paragraph:
3 If removing references and cv-qualifiers from
P
yields a specializationP
′ of a deducible template (9.2.9.3 [dcl.type.simple]) other thanstd::initializer_list
, and either
- the argument is an initializer list, or
- the deducible template is a class template and
A
is neither a specialization of the same class template nor a class derived from such,deduction is performed as described in 12.2.2.9 [over.match.class.deduct] for a declaration of the form
PA x = a;
where
PA
is the name of a hypothetical alias template whose defining-type-id isP
′ and whose template parameter list consists of all the template parameters of the enclosing function template that appear inP
′, anda
is the function argument corresponding toP
. If the deduction succeeds, then the type ofx
is used in place ofA
for type deduction.[ Example:— end example ]template<class T> struct P { P(T*); }; template<class T> void p(P<T>); float x; (&x); // OK, T deduced as float p template<class T> struct V { V(std::initializer_list<T>); }; template<class T> void q(V<T>); ({ 1, 2, 3 }); // OK, T deduced as int q template<class T1, class T2> struct Pair { T1 a; T2 b; }; template<class T1, class T2> r(const Pair<T1, T2>&); ({ "pi", 3.14159 }); // T1 deduced as const char*; T2 deduced as double r template<class T, std::size_t N> struct A { A(T (&)[N]); }; template<std::size_t N> s(A<int, N>&&); int arr[] = { 1, 2, 3, 4, 5 }; (arr); // OK, N deduced as 5 s
Insert a new paragraph immediately following the above insterted paragraph:
4 In all other cases, an initializer list argument causes the parameter to be considered a non-deduced context (13.10.3.6 [temp.deduct.type]).
[ Example:— end example ]template<class T> void g(T); ({1,2,3}); // error: no argument deduced for T g
Retain the remainder of the original paragraph 1 unchanged as a new paragraph immediately following the above inserted paragraph:
5 For a function parameter pack that occurs at the end of the parameter-declaration-list, deduction is performed for each remaining argument of the call, taking the type
P
of the declarator-id of the function parameter pack as the corresponding function template parameter type. Each deduction deduces template arguments for subsequent positions in the template parameter packs expanded by the function parameter pack. When a function parameter pack appears in a non-deduced context (13.10.3.6 [temp.deduct.type]), the type of that pack is never deduced.[ Example:— end example ]template<class ... Types> void f(Types& ...); template<class T1, class ... Types> void g(T1, Types ...); template<class T1, class ... Types> void g1(Types ..., T1); void h(int x, float& y) { const int z = x; (x, y, z); // Types deduced as int, float, const int f(x, y, z); // T1 deduced as int; Types deduced as float, int g(x, y, z); // error: Types is not deduced g1<int, int, int>(x, y, z); // OK, no deduction occurs g1}
Thanks to Mike Spertus for some early feedback on the proposal.