for*_*818 12 c++ sfinae language-lawyer template-instantiation
这个问题中的代码基于这个答案。我有点困惑它是如何产生输出的,以及它是否都定义良好
#include <type_traits>
#include <iostream>
#include <vector>
struct bar {};
void foo(bar) {}
struct moo {};
template<class T>
struct is_fooable {
static std::false_type test(...);
template<class U>
static auto test(const U& u) -> decltype(foo(u), std::true_type{});
static constexpr bool value = decltype(test(std::declval<T>()))::value;
};
template<class T> inline constexpr bool is_fooable_v = is_fooable<T>::value;
template <typename T>
std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}
int main() {
std::cout << is_fooable_v<bar>;
std::cout << is_fooable_v<moo>;
foo(bar{});
foo(moo{});
}
Run Code Online (Sandbox Code Playgroud)
使用 gcc 输出(与 clang 和 msvc 相同):
10
Run Code Online (Sandbox Code Playgroud)
如果is_fooable_v<moo>是false,那么 SFINAE 不会丢弃foo模板,然后moo是“fooable”,is_fooable_v<moo>尽管如此false。
我发现令人困惑的是,该特征的用途有限,因为它moo在用于定义后无法判断是否是“fooable foo<T>” T==moo。不管潜在的混乱如何,代码是否定义良好?
可以根据测试函数是否存在的特征来定义函数吗?
Tur*_*ght 12
std::enable_if_t<!is_fooable_v<T>,void> foo(T)在初始化期间可见is_fooable<T>::value\nis_fooable<T>::value会falsestruct is_really_fooable具有与 相同的定义is_fooable)本文仅考虑 C++20 标准。
\n我没有检查以前的标准是否符合。
foo函数的可见性模板化的 foo 函数 ( template <typename T> std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}) 从内部可见is_fooable并参与重载决策。
这是由于test(std::declval<T>())依赖于T- 所以名称查找需要考虑模板定义的上下文和实例化点的上下文:
\n\n13.8.2 从属名称 [temp.dep] (2)
\n
\n如果运算符的操作数是类型相关表达式,则该运算符还表示从属名称。
\n[注意:此类名称是未绑定的,并且会在模板定义的上下文和实例化点的上下文 ( [temp.dep.candidate ) 的模板实例化点 ( [temp.point ] ) 处查找])。\xe2\x80\x94尾注]
// [...]\n\ntemplate<class T>\nstruct is_fooable { // <-- Template definition\n static std::false_type test(...);\n \n template<class U>\n static auto test(const U& u) -> decltype(foo(u), std::true_type{});\n static constexpr bool value = decltype(test(std::declval<T>()))::value;\n};\n\n// is_fooable is dependent on T in this case,\n// so the point of instantiation will be the point where is_fooable_v<T> is itself instantiated\ntemplate<class T> inline constexpr bool is_fooable_v = is_fooable<T>::value;\n\ntemplate <typename T>\n// same as for is_fooable_v - \nstd::enable_if_t<!is_fooable_v<T>,void> foo(T) {}\n\nint main() {\n std::cout << is_fooable_v<bar>; // <-- Point of instantiation for is_fooable<bar>\n std::cout << is_fooable_v<moo>; // <-- Point of instantiation for is_fooable<moo>\n foo(bar{});\n foo(moo{});\n}\nRun Code Online (Sandbox Code Playgroud)\n因此,模板化foo函数从模板定义中不可见,但从实例化点来看它是可见的 - 并且由于我们需要查看两者,因此将考虑在is_fooable.
注意:如果表达式不依赖于模板参数,例如foo(12),那么我们只需要考虑模板定义的上下文:
\n\n13.8 名称解析 [temp.res] (10)
\n
\n如果名称不依赖于模板参数(如[temp.dep]中定义),则该名称的声明(或声明集)应位于以下范围内名称出现在模板定义中的位置;该名称绑定到在该点找到的声明(或多个声明),并且此绑定不受实例化点可见的声明的影响。
13.8.5.1 实例化点 [temp.point] (7)在这种情况下不适用 - 我们只有一个翻译单元,并且每个单元只有一个实例化点is_fooable<T>- 因此不会违反 odr 规则。
注意:如果您在多个翻译单元中使用它,您仍然需要小心(但这基本上适用于任何类似特征的模板)。这将违反 ODR 规则(格式错误,ndr):
\n// Translation unit 1\nstruct bar{};\nvoid foo(bar) {}\n\ntemplate<class T> struct is_fooable { /* ... */ };\n\n// would print 1\nvoid test1() { std::cout << is_fooable<bar>::value << std::endl; }\n\n// Translation unit 2\nstruct bar{};\n// foo(bar) not defined before test2\n\ntemplate<class T> struct is_fooable { /* ... */ };\n\n// would print 0\nvoid test2() { std::cout << is_fooable<bar>::value << std::endl; }\n\n// -> different definitions of is_fooable<bar>::value in different translation units\n// -> ill-formed, ndr\nRun Code Online (Sandbox Code Playgroud)\nis_fooable<moo>::value最终结果如何false本质上,这是常量表达式与 SFINAE 结合的有趣应用。
\n首先我们需要了解一些基本规则:
\n在变量自身初始化期间访问变量是未定义的行为。(例如int x = x;)
\n这是由于以下两个规则:(强调我的)
\n\n6.7.3 生命周期 [basic.life] (1)
\n
\n[...] 类型 T 的对象的生命周期开始于:\n
\n- 获得类型 T 具有适当对齐和大小的存储,并且
\n- 其初始化(如果有)已完成[...]
\n
\n\n6.7.3 生命周期 [basic.life] (7)
\n
\n[...] 在对象的生命周期开始之前,但在分配该对象将占用的存储空间之后 [...],引用的任何泛左值原始对象可以被使用,但只能以有限的方式使用。[...] 如果出现以下情况,则程序具有未定义的行为:\n
\n- 左值用于访问对象[...]
\n
非类型模板参数必须转换为常量表达式
\n\n\n13.4.2 模板非类型参数[temp.arg.nontype] (2)
\n
\n非类型模板参数的模板参数应为模板类型的转换常量表达式 ( [expr.const] ) -范围。
\n\n7.7 常量表达式 [expr.const] (10)
\n
\nT类型的转换常量表达式是隐式转换为 T 类型的表达式,其中转换后的表达式是常量表达式 [...]
\n\n7.7 常量表达式 [expr.const] (11)
\n
\n常量表达式可以是泛左值核心常量表达式,它引用作为常量表达式(如下定义)允许的结果的实体,也可以是纯右值核心常量表达式 [。 ..]
现在我们可以把它拼凑起来:
\nstd::cout << is_fooable_v<moo>;: 这将实例化is_fooable_v<moo>,进而实例化is_fooable<moo>::value。is_fooable<moo>::value开始了。\ntest()以两个函数作为候选函数进行test\ntest(...)很简单,并且始终是一个可行的功能(优先级较低)test(const U& u)将是可行的并将被实例化\nfoo(u),它也有 2 个潜在的候选函数:foo(bar)和foo(T)\nfoo(bar)不可行,因为moo不能转换为barfoo(T)将是可行的并将被实例化\nfoo(T)我们会遇到一个问题:foo(T)访问is_fooable<moo>::value- 仍未初始化(我们当前正在尝试初始化它)std::enable_if_t转换的常量表达式),因此适用特殊规则:(强调我的)\n\n\n7.7 常量表达式 [expr.const] (5)
\n
\n表达式 E 是核心常量表达式,除非对 E 的求值遵循抽象机 ( [intro.execution] )的规则,将求值下列之一:
\n [...]\n
\n- 具有未定义行为的操作,如本文档的[intro]到[cpp]中指定的那样[注意:包括,例如,有符号整数溢出 ( [expr.prop] )、某些指针算术 ( [expr.add] ),除以零,或某些移位运算\xe2\x80\x94尾注] ;\n[...]
\n
is_fooable_v<T>insidestd::enable_if_t<!is_fooable_v<T>,void>不是核心常量表达式,这是标准对非类型模板参数的要求foo(T)将是不正确的(而不是未定义的行为)foo(T)失败并且不会是一个可行的函数foo(u)可以匹配Uin 的模板参数替换失败test(const U& u)foo(u)test(const U& u)由于foo(u)格式不正确而不再可行 - 但test(...)仍然可行test(...)将是最好的可行功能(并且错误test(const U& u)将被 SFINAE 吞没)test(...)在重载决策期间选择的,因此is_fooable<moo>::value将被初始化为falseis_fooable<moo>::value完成因此,这是完全符合标准的,因为常量表达式中不允许未定义的行为(因此foo(T)在初始化期间总是会导致替换失败is_fooable<T>::value)
这全部包含在is_fooable结构中,因此即使您第一次调用foo(moo{});也会得到相同的结果,例如:
int main() {\n foo(moo{});\n std::cout << is_fooable_v<moo>; // will still be false\n}\nRun Code Online (Sandbox Code Playgroud)\n它本质上与上面的顺序相同,只是您从函数 开始foo(T),然后导致 的实例化is_fooable_v<T>。
is_fooable_v<T>被初始化为falsefoo(T)成功\n->foo<moo>(moo{})将被调用注意:如果您注释掉该test(...)函数(因此 SFINAE 将无法抑制 的替换失败test(const U& u)),那么您的编译器应该报告此替换错误(它的格式不正确,因此应该有一条诊断消息)。
\n这是 gcc 12.1 的结果:(仅有趣的部分)
\n godbolt
In instantiation of \'constexpr const bool is_fooable<moo>::value\':\nerror: no matching function for call to \'is_fooable<moo>::test(moo)\'\nerror: no matching function for call to \'foo(const moo&)\'\nnote: candidate: \'template<class T> std::enable_if_t<(! is_fooable_v<T>), void> foo(T)\'\nnote: template argument deduction/substitution failed:\nerror: the value of \'is_fooable_v<moo>\' is not usable in a constant expression\nnote: \'is_fooable_v<moo>\' used in its own initializer\nnote: in template argument for type \'bool\'\nRun Code Online (Sandbox Code Playgroud)\nis_fooable如果您使用 C++20 require 子句,则可以缩短您的特征,例如:
template<class T>\nconstexpr bool is_fooable_v = requires(T const& t) { foo(t); };\nRun Code Online (Sandbox Code Playgroud)\n请注意,您不能使用概念,因为概念永远不会被实例化。
\n如果您还想能够检测到,foo(T)可以通过定义第二个特征来实现。
\n第二个特征不会参与is_fooable使用的初始化恶作剧,因此能够检测到foo(T)过载:\n godbolt
struct bar {};\nvoid foo(bar) {}\nstruct moo {};\n\ntemplate<class T>\nconstexpr bool is_fooable_v = requires(T const& t) { foo(t); };\n\ntemplate<class T>\nconstexpr bool is_really_fooable_v = requires(T const& t) { foo(t); };\n\ntemplate <typename T>\nstd::enable_if_t<!is_fooable_v<T>,void> foo(T) {}\n\nint main() {\n foo(moo{});\n std::cout << is_fooable_v<moo>; // 0\n std::cout << is_really_fooable_v<moo>; // 1\n}\nRun Code Online (Sandbox Code Playgroud)\n是的,如果您愿意,您可以将这些特征叠加在一起,例如:
\n godbolt
struct a {};\nstruct b {};\nstruct c {};\n\n\nvoid foo(a) { std::cout << "foo1" << std::endl; }\n\ntemplate<class T> inline constexpr bool is_fooable_v = requires(T const& t) { foo(t); };\ntemplate<class T> inline constexpr bool is_really_fooable_v = requires(T const& t) { foo(t); };\ntemplate<class T> inline constexpr bool is_really_really_fooable_v = requires(T const& t) { foo(t); };\n\n\ntemplate <class T, class = std::enable_if_t<std::is_same_v<T, b>>>\nstd::enable_if_t<!is_fooable_v<T>,void> foo(T) { std::cout << "foo2" << std::endl; }\n\ntemplate <class T>\nstd::enable_if_t<!is_really_fooable_v<T>,void> foo(T) { std::cout << "foo3" << std::endl; }\n\nint main() {\n foo(a{});\n foo(b{});\n foo(c{});\n std::cout << "a: "\n << is_fooable_v<a> << " "\n << is_really_fooable_v<a> << " " \n << is_really_really_fooable_v<a> << std::endl;\n std::cout << "b: "\n << is_fooable_v<b> << " "\n << is_really_fooable_v<b> << " " \n << is_really_really_fooable_v<b> << std::endl;\n std::cout << "c: "\n << is_fooable_v<c> << " "\n << is_really_fooable_v<c> << " " \n << is_really_really_fooable_v<c> << std::endl;\n /* Output:\n foo1\n foo2\n foo3\n a: 1 1 1\n b: 0 1 1\n c: 1 0 1\n */\n}\nRun Code Online (Sandbox Code Playgroud)\n但这会变得非常非常混乱,所以我不会推荐它。
\nFel*_*leg -2
在给出答案之前,让我回顾一下代码为什么会这样工作:
“问题”就出在这两行:
template<class T> inline constexpr bool is_fooable_v = is_fooable<T>::value;
template <typename T> std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}
Run Code Online (Sandbox Code Playgroud)
现在想象一下foo(moo)第二行正在创建。它必须执行,std::enable_if<!is_fooable_v<T>, void>而它又必须执行!is_fooable_v<T>。这样的表达式有什么价值T = moo?当然,为了找出答案,我们必须实例化is_fooable_v<moo>。经过一些计算后,编译器将此布尔值实例化为false.
就是这样:现在,is_fooable_v<moo> 因为该点false 在该表达式的所有其他计算中都相等(temp.inst#7)。所以当你在main()函数中使用它时:
std::cout << is_fooable_v<moo>;
Run Code Online (Sandbox Code Playgroud)
它变得“固定”为该值。
为了证明这一点,如果您在这两行中添加额外的一行,会发生什么:
template<> inline constexpr bool is_fooable_v<moo> = true;
Run Code Online (Sandbox Code Playgroud)
这当然会使该foo(moo{});行无法编译,但它也会使两行都带有std::cout打印行。
至于问题:
可以根据测试函数是否存在的特征来定义函数吗?
好吧,答案取决于你是否真的能找到这个“技巧”的用处。
例如,如果您正在创建一个库,那么使用这种技术,您可以根据您的库的用户是否实际提供他/她自己的库来创建函数的默认实现。当然,有一个问题,我想在上面的回顾中展示:您不能is_fooable_v<>在代码中使用模板来测试此类用户提供的函数是否存在。
| 归档时间: |
|
| 查看次数: |
614 次 |
| 最近记录: |