使用 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 不友好。
在模板参数替换之后执行约束满足检查。模板参数替换发生在建立重载候选集和约束满足检查后者时,建立哪些重载候选是可行的。
以您的案例为例:
编译器建立重载候选集合,这里不检查约束。那么编译器将要使用的相当于:
template <class T>
auto test(T) -> std::make_signed_t<T>; //(1)
template <typename T>
auto test(T) -> int; //(2)
编译器推断 T
为 double
,它在 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:
substitute explicitly-specified template arguments throughout the template parameter list and type;
deduce template arguments from the resulting function signature;
check that non-dependent parameters can be initialized from their arguments;
substitute deduced template arguments into the template parameter list and particularly into any needed default arguments to form a complete template argument list;;
substitute resulting template arguments throughout the type;
check that the associated constraints are satisfied;
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;
考虑以下片段
#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 不友好。
在模板参数替换之后执行约束满足检查。模板参数替换发生在建立重载候选集和约束满足检查后者时,建立哪些重载候选是可行的。
以您的案例为例:
编译器建立重载候选集合,这里不检查约束。那么编译器将要使用的相当于:
template <class T> auto test(T) -> std::make_signed_t<T>; //(1) template <typename T> auto test(T) -> int; //(2)
编译器推断 T
为 double
,它在 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:
substitute explicitly-specified template arguments throughout the template parameter list and type;
deduce template arguments from the resulting function signature;
check that non-dependent parameters can be initialized from their arguments;
substitute deduced template arguments into the template parameter list and particularly into any needed default arguments to form a complete template argument list;;
substitute resulting template arguments throughout the type;
check that the associated constraints are satisfied;
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;