为什么SFINAE函数模板和常规函数模板的绑定规则不同?

问题描述 投票:0回答:1

我有一个检查器函数,它使用

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(){}

造成这种差异的原因是什么?两个检查器函数都使用依赖类型作为其返回类型,因此我认为它们的行为相同。

更实际的是,即使函数模板的所有模板参数保持不变,是什么使得函数模板在实例之间的解释也不同?

c++ templates sfinae dependent-name requires-expression
1个回答
0
投票

我做了更多挖掘,并认为我找到了答案。本质上,在声明

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()
检查存在于函数模板的主体中之外,它对函数的行为没有任何影响。

© www.soinside.com 2019 - 2024. All rights reserved.