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

2023-01-15


2369. Ordering between constraints and substitution

Section: 13.10.3  [temp.deduct]     Status: CD6     Submitter: 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 <typename T>
   struct static_assert_integral {
     static_assert(std::is_integral_v<T>);
     using type = T;
   };

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

Here the substitution ordering guarantees are leveraged to prevent static_assert_integral<T> 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 <typename T>
       requires std::is_integral_v<T>
     typename static_assert_integral<T>::type
     operator()(T) {}
   };

or

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

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

Perhaps steps 5 and 6 should be interchanged.

Proposed resolution (August, 2020):

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

  2. If deduction succeeds for all parameters that contain template-parameters 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-parameters 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 <class T> struct Z {
        typedef typename T::x xx;
      };
      template <class T> typename Z<T>::xx f(void *, T); // #1
      template <class T> 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]

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

  4. ...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 template and 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 <class T> struct Z {
        typedef typename T::x xx;
      };
      template <class T> concept C = requires { typename T::A; };
      template <C T> typename Z<T>::xx f(void *, T); // #1
      template <class T> void f(int, T);             // #2
      struct A {} a;
      struct ZZ {
        template <class T, class = typename Z<T>::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<int> is not satisfied
      }
    

    end example]