3

给定以下代码

#include <type_traits>
#include <utility>

template <typename T>
class Something {
public:
    template <typename F>
    auto foo(F&&)
        -> decltype(std::declval<F>()(std::declval<T&>())) {}
    template <typename F>
    auto foo(F&&) const
        -> decltype(std::declval<F>()(std::declval<const T&>())) {}
};

int main() {
    auto something = Something<int>{};
    something.foo([](auto& val) {
        ++val;
    });
}

https://wandbox.org/permlink/j24Pe9qOXV0oHc​​A8

当我尝试编译它时,我收到错误消息,说我不允许在 main 中修改 lambda 中的 const 值。这意味着模板都在类中被实例化,这会导致硬错误,因为错误在 lambda 的主体中。

对此有何规定?为什么重载解析会尝试实例化一个永远不会被调用的模板?永远不应该在这里调用 const ,那么为什么要尝试完全实例化它呢?

然而奇怪的是,当我更改要返回的定义decltype(auto)并添加代码以执行与尾随返回类型建议的相同的事情时,我没有看到错误。表明模板没有被完全实例化?

template <typename F>
decltype(auto) foo(F&& f) {
    auto t = T{};
    f(t);
}
template <typename F>
decltype(auto) foo(F&& f) const {
    const auto t = T{};
    f(t);
}

我猜编译器在使用传递的函数至少实例化签名之前不知道要调用哪个函数。但这并不能解释为什么 decltype(auto) 版本有效......

4

2 回答 2

4

(为缺乏正确的标准术语道歉,正在努力……)

something.foo被调用时,必须考虑所有可能的重载:

template <typename F>
auto foo(F&&)
    -> decltype(std::declval<F>()(std::declval<T&>())) {}

template <typename F>
auto foo(F&&) const
    -> decltype(std::declval<F>()(std::declval<const T&>())) {}

为了检查重载是否可行,decltype(...)编译器需要评估尾随。第一个decltype将被评估而没有错误,它将评估为void.

第二个将导致错误,因为您尝试使用const T&.

由于 lambda 不受约束,因此在 lambda 主体的实例化过程中会发生错误。发生这种情况是因为(默认情况下)lambda 使用自动返回类型推导,这需要实例化 lambda 的主体。

因此,不可行的重载将因此导致编译错误,而不是让 SFINAEd 退出。如果您将 lambda 约束如下...

something.foo([](auto& val) -> decltype(++val, void()) {
    ++val;
});

...不会发生错误,因为 SFINAE 将认为过载是不可行的。此外,您将能够检测 lambda 调用是否Toperator++()来自.Something::foo


当您将返回类型更改为decltype(auto)时,返回类型是从函数体推导出来的。

template <typename F>
decltype(auto) foo(F&& f) {
    auto t = T{};
    f(t);
}

template <typename F>
decltype(auto) foo(F&& f) const {
    const auto t = T{};
    f(t);
}

由于您的something实例是非const,因此const将在此处采用非限定重载。如果您main的定义如下:

int main() {
    const auto something = Something<int>{};
    something.foo([](auto& val) {
        ++val;
    });
}

你会得到同样的错误,即使是decltype(auto).

于 2017-10-11T14:25:26.880 回答
1

实际上,我认为问题的要点是

只有函数类型及其模板参数类型的直接上下文中的无效类型和表达式会导致推导失败。[注意:对类型和表达式的替换可能会导致类模板特化和/或函数模板特化的实例化,隐式定义函数的生成等效果。这些效果不在“直接上下文”中,并且可以导致程序格式错误。——尾注]

所以,问题是,是否应该在其直接上下文中考虑在推断其返回类型期间触发 lambda 的实例化?

例如,如果 lambda 返回类型是显式的:

something.foo([](auto& val) -> void {
    ++val;
});

代码编译(没有sfinae,只是非常量是最佳匹配)。

但是,OP 的 lambda 具有自动返回类型推导,因此 lambda 被实例化并且上述规则适用。

于 2017-10-11T15:01:26.763 回答