Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Concept resolve to the unexpected function template when using std::make_signed_t

Consider the following snippet

#include <type_traits>

template <typename T>
concept unsigned_integral = std::is_integral_v<T> &&std::is_unsigned_v<T>;

template <unsigned_integral T>
auto test(T) -> std::make_signed_t<T>; //(1)
template <typename T>
auto test(T) -> int; //(2)

int sandbox() {
  test(1u); // Call to (1) as expected
  test(1.0); // Expected to call (2), compilers choose (1) and fail to compile
}

MSVC 14.26, GCC-10, and Clang-10 all failed to compile this, so I suppose the standard makes this an invalid code, so should this be considered an oversight from the standard? because using SFINAE, the code is compiled as expected.

SFINAE version (this only works with the double case, because there will be ambiguity for the unsigned int case, but that doesn't affect the question I'm asking)

template <typename T, typename = std::enable_if_t<unsigned_integral<T>>>
auto test(T) -> std::make_signed_t<T>;

edit: apparently, this does not relate to trailing return types, so I have changed the title to the appropriate one.

like image 585
Uy Hà Avatar asked Jun 14 '20 09:06

Uy Hà


Video Answer


2 Answers

This is CWG 2369 (unfortunately not on a public list despite having been submitted years ago). I'll just copy the main text here:

The specification of template argument deduction in 13.9.2 [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.

This basically matches the example in OP. You think that your constraints are preventing the instantiation of make_signed_t (which requires an integral type), but actually it's substituted into before the constraints are checked.

The direction seems to be to change the order of steps above to [1, 2, 4, 6, 3, 5, 7], which would make the OP example valid (we would remove (1) from consideration once we fail the associated constraints before substituting into make_signed_t), and this would certainly be a defect against C++20. But it just hasn't happened yet.

Until then, your best bet might be to just make a SFINAE-friendly version of make_signed:

template <typename T> struct my_make_signed { };
template <std::integral T> struct my_make_signed<T> { using type = std::make_signed_t<T>; };
template <typename T> using my_make_signed_t = /* no typename necessary */ my_make_signed<T>::type;
like image 194
Barry Avatar answered Oct 16 '22 08:10

Barry


According to [meta], make_signed mandate that the template argument is an integral type:

Mandates: T is an integral or enumeration type other than cv bool.

So make_signed is not SFINAE friendly.

Constraint fullfilment checks are performed after template argument substitution. Template argument substitution happens when establishing the set of overload candidates and constraint fullfilment check latter, when establishing which overload candidates are viable.

Taking your case as an exemple:

  1. The compiler establish the set of overload candidate, constraint are not checked here. So what is going to be used by the compiler is equivalent to:

    template <class T>
    auto test(T) -> std::make_signed_t<T>; //(1)
    template <typename T>
    auto test(T) -> int; //(2)
    

The compiler deduce T to be double, it substitute T in make_signed_t => Error: substitution failure does not happen in the direct context of test declaration.

The compiler stop here, compilation does not reach the second step of selection of viable candidates where the constraint would have been checked.

like image 23
Oliv Avatar answered Oct 16 '22 06:10

Oliv