使用 std::make_signed_t 时概念解析为意外的函数模板

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

考虑以下片段

#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 和 Clang-10 都无法编译它,所以我想标准使它成为无效代码,那么这应该被认为是标准的疏忽吗?因为使用 SFINAE,代码按预期编译。

SFINAE 版本(这只适用于 double 的情况,因为 unsigned int 的情况会有歧义,但这不影响我问的问题)

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

编辑:显然,这与尾随 return 类型无关,因此我已将标题更改为适当的。

根据[meta]make_signed要求模板参数是整数类型:

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

所以 make_signed 对 SFINAE 不友好。

在模板参数替换之后执行约束满足检查。模板参数替换发生在建立重载候选集和约束满足检查后者时,建立哪些重载候选是可行的。

以您的案例为例:

  1. 编译器建立重载候选集合,这里不检查约束。那么编译器将要使用的相当于:

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

编译器推断 Tdouble,它在 make_signed_t 中替换 T => 错误:在 [= 的直接上下文中不会发生替换失败17=]声明。

编译器在这里停止,编译没有到达第二步选择可行的候选者,那里应该已经检查了约束。

这是 CWG 2369(遗憾的是,尽管多年前已提交,但不在 public 列表中)。我将在这里复制正文:

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.

基本符合OP中的例子。你认为你的约束阻止了 make_signed_t 的实例化(这需要一个整数类型),但实际上它在约束被检查之前被替换了。

方向似乎是将上述步骤的顺序更改为 [1, 2, 4, 6, 3, 5, 7],这将使 OP 示例有效(我们将删除 (1)考虑到一旦我们在代入 make_signed_t 之前未能通过相关约束,这肯定是针对 C++20 的缺陷。但这还没有发生。

在那之前,你最好的选择可能是制作一个 SFINAE 友好版本 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;