重载前模板实例化错误

Error in template instantiation before overloading

给定以下代码

#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/j24Pe9qOXV0oHcA8

当我尝试编译它时,我收到错误消息说我不允许修改 main 中 lambda 中的常量值。这意味着模板都在 class 中以某种方式被实例化,这导致了一个硬错误,因为错误在 lambda 的主体中。

关于这方面的规则是什么?为什么重载解析会尝试实例化一个永远不会被调用的模板? const 永远不应该在这里被调用,为什么它要尝试完全实例化它?

然而奇怪的是,当我通过 decltype(auto) 将定义更改为 return 并添加代码以执行与尾随 return 类型建议相同的事情时,我不' 看到一个错误。说明模板没有完全实例化?

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) 版本有效...

(抱歉缺少正确的标准术语,正在处理中...)

调用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 使用 automatic return 类型推导,这需要实例化 lambda 的主体。

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

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

...不会发生错误,因为过载将被 SFINAE 视为不可行。此外,您将能够检测 lambda 调用是否对特定类型有效 (即 T 是否支持 operator++()?) 来自 Something::foo .


当您将 return 类型更改为 decltype(auto) 时,return 类型将从函数体中推导出来。

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).

其实我觉得问题的关键在于

Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure. [ Note: The substitution into types and expressions can result in effects such as the instantiation of class template specializations and/or function template specializations, the generation of implicitly-defined functions, etc. Such effects are not in the “immediate context” and can result in the program being ill-formed. — end note ]

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

例如,如果 lambda return 类型明确:

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

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

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