我有一个检查器函数,它使用
requires
关键字来检测目标函数是否已定义。我想让它与 C++17 一起使用,因此我将检查器从使用 requires
切换为使用表达式 SFINAE。
那时我惊讶地发现,如果我在没有定义目标函数的情况下调用两个检查器,然后定义目标函数并再次检查,
requires
检查器将重用旧结果,而 SFINAE 检查器将更新以正确显示目标函数已定义。
// Compiles on Clang, GCC, and MSVC.
#include <type_traits>
struct MyStruct {};
constexpr bool Checker_SFINAE(...)
{
return false;
}
template <typename T>
constexpr auto Checker_SFINAE(T) -> decltype(Foo(T()), bool())
{
return true;
}
template <typename T>
constexpr auto Checker_requires(T)
{
return std::bool_constant<requires { Foo(T()); }>();
}
static_assert(!Checker_SFINAE(MyStruct()));
static_assert(!Checker_requires(MyStruct()));
void Foo(MyStruct) {}
static_assert(Checker_SFINAE(MyStruct()));
// Will fail is the previous Checker_requires is removed
static_assert(!Checker_requires(MyStruct()));
int main(){}
造成这种差异的原因是什么?两个检查器函数都使用依赖类型作为其返回类型,因此我认为它们的行为相同。
更实际的是,即使函数模板的所有模板参数保持不变,是什么使得函数模板在实例之间的解释也不同?
我做了更多挖掘,并认为我找到了答案。本质上,在声明
Checker_SFINAE()
之前,Foo()
的模板版本不会被实例化。
让我们从基础开始。当调用重载函数时,将调用最有效的函数重载。如果我们稍后添加另一个重载并再次调用该函数,该新重载将包含在候选中。
struct MyStruct {};
constexpr bool HasOverload(...)
{
return false;
}
static_assert(!HasOverload(MyStruct{}));
constexpr bool HasOverload(MyStruct)
{
return true;
}
static_assert(HasOverload(MyStruct{}));
现在让我们回到问题中的示例,稍作修改:
#include <type_traits>
struct MyStruct {};
constexpr bool Checker(...)
{
return false;
}
template <typename T>
constexpr std::enable_if_t<requires { Foo(T()); }, bool> Checker(T)
{
return true;
}
static_assert(!Checker(MyStruct()));
void Foo(MyStruct) {}
static_assert(Checker(MyStruct()));
当首次调用
Checker()
时,函数模板的签名如果被实例化,将无法编译。由于 SFINAE,编译器避免实例化模板。目前我们唯一存在的 Checker()
仍然是 Checker(...)
。
当第二次调用
Checker()
时,在声明 Foo()
后,编译器会再次查看候选者。这次,函数模板的签名can可以在不引入编译错误的情况下实例化,因此它现在实例化模板。现在,这个模板实例化是最佳候选,因此使用它,并且 Check()
返回 true。
区分选择最佳重载候选者(每次调用函数时都会发生)与函数模板实例化(只会发生一次)很重要。一旦模板实例化发生,就无法回头;该新功能现已成为候选功能。使用相同的模板参数实例化函数模板将产生相同的函数,因为它已经被实例化了。
例如,假设我们要反转 Checker,因此基本情况返回 true,并且重载使用 SFINAE 来检查 Foo()
的
absence:
#include <type_traits>
struct MyStruct {};
constexpr bool Checker(...)
{
return true;
}
template <typename T>
constexpr std::enable_if_t<!requires { Foo(T()); }, bool> Checker(T)
{
return false;
}
static_assert(!Checker(MyStruct()));
void Foo(MyStruct) {}
// Note how this hasn't changed.
static_assert(!Checker(MyStruct()));
一旦模板被实例化,就不会再将精灵放回瓶子里。它现在存在,并且将被视为
Foo()
的候选者,这意味着如果 Checker()
曾经以某种类型返回 false,则它将始终以该类型返回 false。
总而言之,模板实例化和函数重载解析是两个独立的事情。实例化发生一次;重载解析发生在每次函数调用时。一旦声明
Checker_SFINAE()
,Foo()
就会引入新的重载候选项,因此它将更改其返回值。 Checker_requires()
只是一个函数模板,因此一旦实例化,它就会被锁定到该值。 requires
关键字是一个转移注意力的东西——除了允许 Foo()
检查存在于函数模板的主体中之外,它对函数的行为没有任何影响。