CWG Issue 2369 (original) (raw)

This is an unofficial snapshot of the ISO/IEC JTC1 SC22 WG21 Core Issues List revision 117a. See http://www.open-std.org/jtc1/sc22/wg21/ for the official list.

2025-04-13


2369. Ordering between constraints and substitution

Section: 13.10.3 [temp.deduct]Status: CD6Submitter: Agustin BergéDate: 2017-10-09

[Accepted at the November, 2020 meeting.]

The specification of template argument deduction in 13.10.3 [temp.deduct] paragraph 5 specifies the order of processing as:

  1. substitute explicitly-specified template arguments throughout the template parameter list and type;
  2. deduce template arguments from the resulting function signature;
  3. check that non-dependent parameters can be initialized from their arguments;
  4. substitute deduced template arguments into the template parameter list and particularly into any needed default arguments to form a complete template argument list;;
  5. substitute resulting template arguments throughout the type;
  6. check that the associated constraints are satisfied;
  7. check that remaining parameters can be initialized from their arguments.

This ordering yields unexpected differences between concept and SFINAE implementations. For example:

template struct static_assert_integral { static_assert(std::is_integral_v); using type = T; };

struct fun { template <typename T, typename Requires = std::enable_if_t<std::is_integral_v>> typename static_assert_integral::type operator()(T) {} };

Here the substitution ordering guarantees are leveraged to preventstatic_assert_integral from being instantiated when the constraints are not satisfied. As a result, the following assertion holds:

static_assert(!std::is_invocable_v<fun, float>);

A version of this code written using constraints unexpectedly behaves differently:

struct fun { template requires std::is_integral_v typename static_assert_integral::type operator()(T) {} };

or

struct fun { template typename static_assert_integral::type operator()(T) requires std::is_integral_v {} };

static_assert(!std::is_invocable_v<fun, float>); // error: static assertion failed: std::is_integral_v

Perhaps steps 5 and 6 should be interchanged.

Proposed resolution (August, 2020):

  1. Delete paragraph 10 of 13.10.3.2 [temp.deduct.call]:

If deduction succeeds for all parameters that contain _template-parameter_s that participate in template argument deduction, and all template arguments are explicitly specified, deduced, or obtained from default template arguments, remaining parameters are then compared with the corresponding arguments. For each remaining parameter P with a type that was non-dependent before substitution of any explicitly-specified template arguments, if the corresponding argument A cannot be implicitly converted to P, deduction fails. [_Note 2:_Parameters with dependent types in which no _template-parameter_s participate in template argument deduction, and parameters that became non-dependent due to substitution of explicitly-specified template arguments, will be checked during overload resolution. —_end note_]

[Example 9:

template struct Z { typedef typename T::x xx; }; template typename Z::xx f(void *, T); // #1 template void f(int, T); // #2 struct A {} a; int main() { f(1, a); // OK, deduction fails for #1 because there is no conversion from int to void* }

—_end example_]

  1. Change 13.10.3.1 [temp.deduct.general] paragraph 5 as follows:

...When all template arguments have been deduced or obtained from default template arguments, all uses of template parameters in the template parameter list of the templateand the function type are replaced with the corresponding deduced or default argument values. If the substitution results in an invalid type, as described above, type deduction fails. If the function template has associated constraints (13.5.3 [temp.constr.decl]), those constraints are checked for satisfaction (13.5.2 [temp.constr.constr]). If the constraints are not satisfied, type deduction fails. In the context of a function call, if type deduction has not yet failed, then for those function parameters for which the function call has arguments, each function parameter with a type that was non-dependent before substitution of any explicitly-specified template arguments is checked against its corresponding argument; if the corresponding argument cannot be implicitly converted to the parameter type, type deduction fails. [Note: Overload resolution will check the other parameters, including parameters with dependent types in which no template parameters participate in template argument deduction and parameters that became non-dependent due to substitution of explicitly-specified template arguments. —_end note_] If type deduction has not yet failed, then all uses of template parameters in the function type are replaced with the corresponding deduced or default argument values. If the substitution results in an invalid type, as described above, type deduction fails. [Example:

template struct Z { typedef typename T::x xx; }; template concept C = requires { typename T::A; }; template typename Z::xx f(void *, T); // #1 template void f(int, T); // #2 struct A {} a; struct ZZ { template <class T, class = typename Z::xx> operator T (); operator int(); }; int main() { ZZ zz; f(1, a); // OK, deduction fails for #1 because there is no conversion from int to void f(zz, 42); // OK, deduction fails for #1 because C is not satisfied }

—_end example_]