tl;dr
- 这个模式是明确定义的
-
std::enable_if_t<!is_fooable_v<T>,void> foo(T)
is visible during the initialization of is_fooable<T>::value
- 但模板参数替换会失败,所以
is_fooable<T>::value
将false
- 您可以使用第二个特征类来检测这两个函数(例如
struct is_really_fooable
具有相同的定义is_fooable
)
1. 免责声明
本文仅考虑 C++20 标准。
我没有检查以前的标准是否符合。
2. 模板的可见性foo
功能
模板化 foo 函数 (template <typename T> std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}
) 从内部可见is_fooable
并参与重载决策。
这是因为test(std::declval<T>())
依赖于T
- 所以名称查找需要考虑模板定义的上下文and实例化点的上下文:
13.8.2 从属名称 [temp.dep] (2) https://timsong-cpp.github.io/cppwp/n4861/temp.res#temp.dep-2
如果运算符的操作数是依赖于类型的表达式,则该运算符还表示依赖名称。
[ Note:这些名称是未绑定的,并在模板实例化时查找([温度点] https://timsong-cpp.github.io/cppwp/n4861/temp.res#temp.point)在模板定义的上下文和实例化点的上下文中([临时候选人] https://timsong-cpp.github.io/cppwp/n4861/temp.res#temp.dep.candidate). — end note ]
// [...]
template<class T>
struct is_fooable { // <-- Template definition
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;
};
// is_fooable is dependent on T in this case,
// so the point of instantiation will be the point where is_fooable_v<T> is itself instantiated
template<class T> inline constexpr bool is_fooable_v = is_fooable<T>::value;
template <typename T>
// same as for is_fooable_v -
std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}
int main() {
std::cout << is_fooable_v<bar>; // <-- Point of instantiation for is_fooable<bar>
std::cout << is_fooable_v<moo>; // <-- Point of instantiation for is_fooable<moo>
foo(bar{});
foo(moo{});
}
所以模板化的foo
函数从模板定义中不可见,但从实例化点来看它是可见的 - 并且由于我们需要同时查看两者,因此将考虑在其中进行重载解析is_fooable
.
注意:如果表达式不依赖于模板参数,例如foo(12)
,那么我们只需要考虑模板定义的上下文:
13.8 名称解析[temp.res] (10) https://timsong-cpp.github.io/cppwp/n4861/temp.res#10
如果一个名字不依赖于模板参数 https://timsong-cpp.github.io/cppwp/n4861/temp.param#nt:template-parameter(如定义[温度差异] https://timsong-cpp.github.io/cppwp/n4861/temp.res#temp.dep),该名称的声明(或声明集)应位于该名称出现在模板定义中的范围内;该名称绑定到在该点找到的声明(或多个声明),并且此绑定不受实例化点可见的声明的影响。
13.8.5.1 实例化点 [temp.point] (7) https://timsong-cpp.github.io/cppwp/n4868/temp.point#7不适用于这种情况 - 我们只有一个翻译单元,并且每个翻译单元只有一个实例化点is_fooable<T>
- 这样就不会违反网上解决规则。
注意:如果您在多个翻译单元中使用它,您仍然需要小心(但这基本上适用于任何类似特征的模板)。
例如这将违反 ODR 规则(格式错误,ndr):
// Translation unit 1
struct bar{};
void foo(bar) {}
template<class T> struct is_fooable { /* ... */ };
// would print 1
void test1() { std::cout << is_fooable<bar>::value << std::endl; }
// Translation unit 2
struct bar{};
// foo(bar) not defined before test2
template<class T> struct is_fooable { /* ... */ };
// would print 0
void test2() { std::cout << is_fooable<bar>::value << std::endl; }
// -> different definitions of is_fooable<bar>::value in different translation units
// -> ill-formed, ndr
3. How is_fooable<moo>::value
最终成为false
本质上,这是常量表达式与 SFINAE 结合的有趣应用。
首先我们需要介绍一些基本规则:
现在我们可以把它拼凑起来:
- 让我们从
std::cout << is_fooable_v<moo>;
:这将实例化is_fooable_v<moo>
,这又会实例化is_fooable<moo>::value
.
- So the initialization of
is_fooable<moo>::value
begins.
- The overload resolution for
test()
takes place with both test
functions as candidates
-
test(...)
很简单,并且始终是一个可行的功能(优先级较低)
-
test(const U& u)
would be viable and will be instanciated
- this in turn will result in the overload resolution of
foo(u)
, which also has 2 potential candidate functions: foo(bar)
and foo(T)
-
foo(bar)
是不可行的,因为moo
不可转换为bar
-
foo(T)
would be viable and will be instanciated
- 在参数替换为
foo(T)
我们会遇到一个问题:foo(T)
访问is_fooable<moo>::value
- 尚未初始化(我们当前正在尝试初始化它)
- this would be undefined behaviour normally - but because we're in a constantly evaluated context (non-type template arguments like the one of
std::enable_if_t
need to be converted constant expressions) a special rule applies: (emphasis mine)
7.7 常量表达式[expr.const] (5) https://timsong-cpp.github.io/cppwp/n4861/expr.const#5
表达式 E 是核心常量表达式除非 E 的求值遵循抽象机的规则([执行简介] https://timsong-cpp.github.io/cppwp/n4861/intro.execution),将评估以下其中一项:
[...]
-
具有未定义行为的操作,如[intro] https://timsong-cpp.github.io/cppwp/n4861/intro通过[cpp] https://timsong-cpp.github.io/cppwp/n4861/cpp本文件的 [ Note:例如,包括有符号整数溢出([expr.prop] https://timsong-cpp.github.io/cppwp/n4861/expr.prop),某些指针算术([表达式.添加] https://timsong-cpp.github.io/cppwp/n4861/expr.add), 被零除 https://timsong-cpp.github.io/cppwp/n4861/expr.mul,或某些轮班操作 https://timsong-cpp.github.io/cppwp/n4861/expr.shift — end note];
[...]
-
6.7.3 生命周期[basic.life] https://timsong-cpp.github.io/cppwp/n4861/basic.life#1在。。。之间4 简介[简介] https://timsong-cpp.github.io/cppwp/n4861/intro and 15 预处理指令 [cpp] https://timsong-cpp.github.io/cppwp/n4861/cpp因此该规则适用于访问其生命周期之外的变量。
- 因此
is_fooable_v<T>
within std::enable_if_t<!is_fooable_v<T>,void>
is 不是核心常量表达式,该标准要求非类型模板参数
- 所以这个实例化
foo(T)
将是格式不正确的(并且不是未定义的行为)
- 所以模板参数替换为
foo(T)
失败并且不会成为可行的功能
- 没有可行的功能
foo(u)
可以匹配
- 模板参数替换为
U
in test(const U& u)
由于没有可以调用的可行函数而失败foo(u)
-
test(const U& u)
不再可行,因为foo(u)
格式不正确 - 但是test(...)
仍然可行
-
test(...)
将是最好的可行函数(并且错误来自test(const U& u)
将因 SFINAE 被吞食)
-
test(...)
在重载决策期间选择的,所以is_fooable<moo>::value
将被初始化为false
- 的初始化
is_fooable<moo>::value
做完了
因此,这是完全符合标准的,因为常量表达式中不允许未定义的行为(因此foo(T)
在初始化期间总是会导致替换失败is_fooable<T>::value
)
这全部包含在is_fooable
struct,所以即使你第一次调用foo(moo{});
你会得到相同的结果,例如:
int main() {
foo(moo{});
std::cout << is_fooable_v<moo>; // will still be false
}
它本质上与上面的顺序相同,只是你从函数开始foo(T)
,然后导致实例化is_fooable_v<T>
.
- (有关事件发生的顺序,请参阅上文)
-
is_fooable_v<T>
被初始化为false
- 的参数替换
foo(T)
成功
->foo<moo>(moo{})
将被称为
注意:如果您注释掉test(...)
函数(因此 SFINAE 将无法抑制替换失败test(const U& u)
)那么你的编译器应该报告这个替换错误(它的格式不正确,因此应该有一条诊断消息)。
这是 gcc 12.1 的结果:(仅有趣的部分)
godbolt https://godbolt.org/z/3rz9d4s7j
In instantiation of 'constexpr const bool is_fooable<moo>::value':
error: no matching function for call to 'is_fooable<moo>::test(moo)'
error: no matching function for call to 'foo(const moo&)'
note: candidate: 'template<class T> std::enable_if_t<(! is_fooable_v<T>), void> foo(T)'
note: template argument deduction/substitution failed:
error: the value of 'is_fooable_v<moo>' is not usable in a constant expression
note: 'is_fooable_v<moo>' used in its own initializer
note: in template argument for type 'bool'
四、备注
你可以缩短你的is_fooable
如果您使用 C++20 需要子句,则具有特征,例如:
template<class T>
constexpr bool is_fooable_v = requires(T const& t) { foo(t); };
请注意,您can't使用概念,因为概念永远不会被实例化。
如果您还想能够检测到foo(T)
您可以通过定义第二个特征来做到这一点。
第二个特征不会参与初始化恶作剧is_fooable
使用,因此能够检测到foo(T)
超载:godbolt https://godbolt.org/z/nshzvP4fj
struct bar {};
void foo(bar) {}
struct moo {};
template<class T>
constexpr bool is_fooable_v = requires(T const& t) { foo(t); };
template<class T>
constexpr bool is_really_fooable_v = requires(T const& t) { foo(t); };
template <typename T>
std::enable_if_t<!is_fooable_v<T>,void> foo(T) {}
int main() {
foo(moo{});
std::cout << is_fooable_v<moo>; // 0
std::cout << is_really_fooable_v<moo>; // 1
}
是的,如果您愿意,您可以将这些特征叠加在一起,例如:
godbolt https://godbolt.org/z/5Yb599de9
struct a {};
struct b {};
struct c {};
void foo(a) { std::cout << "foo1" << std::endl; }
template<class T> inline constexpr bool is_fooable_v = requires(T const& t) { foo(t); };
template<class T> inline constexpr bool is_really_fooable_v = requires(T const& t) { foo(t); };
template<class T> inline constexpr bool is_really_really_fooable_v = requires(T const& t) { foo(t); };
template <class T, class = std::enable_if_t<std::is_same_v<T, b>>>
std::enable_if_t<!is_fooable_v<T>,void> foo(T) { std::cout << "foo2" << std::endl; }
template <class T>
std::enable_if_t<!is_really_fooable_v<T>,void> foo(T) { std::cout << "foo3" << std::endl; }
int main() {
foo(a{});
foo(b{});
foo(c{});
std::cout << "a: "
<< is_fooable_v<a> << " "
<< is_really_fooable_v<a> << " "
<< is_really_really_fooable_v<a> << std::endl;
std::cout << "b: "
<< is_fooable_v<b> << " "
<< is_really_fooable_v<b> << " "
<< is_really_really_fooable_v<b> << std::endl;
std::cout << "c: "
<< is_fooable_v<c> << " "
<< is_really_fooable_v<c> << " "
<< is_really_really_fooable_v<c> << std::endl;
/* Output:
foo1
foo2
foo3
a: 1 1 1
b: 0 1 1
c: 1 0 1
*/
}
但这会变得非常非常混乱,所以我不会推荐它。