【问题标题】:Error in template instantiation before overloading重载前模板实例化错误
【发布时间】:2017-10-11 14:05:48
【问题描述】:

给定以下代码

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

【问题讨论】:

    标签: c++ c++11 templates overloading template-instantiation


    【解决方案1】:

    (对缺乏正确的标准术语表示歉意,正在努力解决......)

    在调用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&amp; 调用 lambda。

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

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

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

    ...不会发生错误,因为通过 SFINAE 将认为过载是不可行的。此外,您将能够检测 lambda 调用是否对特定类型有效(即 T 是否支持 operator++()?) 来自 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),您也会遇到同样的错误。

    【讨论】:

    • 实例化过程中会发生错误”而不是什么时候?替代是它自己的步骤吗?
    • @RyanHaining:在替换步骤中,decltype(...) 尾随返回类型实例化了 lambda 的主体......这会导致错误。
    • 这里有什么建议的方法来避免这个问题?只需切换到decltype(auto)?我希望 foo 函数对返回类型是 SFINAE 友好的,并且用户代码也可以工作
    • @Curious:如果您希望 foo 对 SFINAE 友好,那么您需要尾随 decltype(...)。不幸的是,需要限制的是 lambda ......我过去也遇到过同样的问题 - lambda 的提供者需要适当地限制它。
    • 我认为这个答案是不正确的,因为它没有解决问题的症结所在,即 lambda 实例化是否在替换的直接上下文中......
    【解决方案2】:

    实际上,我认为问题的关键在于

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

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

    例如,如果 lambda 返回类型是明确的:

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

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

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

    【讨论】:

    • 这是有道理的,但请注意 lambda 不再“对 SFINAE 友好”。您可能想检查++val 是否实际上是来自Something::fooval 的有效操作。如果你只是说-&gt; void,它可能看起来像一个有效的操作,即使它不是,产生一个硬错误,就像 OP 的例子一样。
    • @VittorioRomeo ... 嗯,但它并没有给出 OP 代码的硬错误,这就是重点...
    • 这是我发现的问题:wandbox.org/permlink/g7fNJgknnVXj7vrt - 我希望在此处选择 /* fallback */,但 -&gt; void 选择了另一个重载并导致硬错误。正确约束 lambda 会选择回退。
    • 在上面的链接中,将-&gt; void 更改为-&gt; decltype(++val, void()) 可以防止“硬错误”并调用Something::foo/* fallback */ 重载。
    • @VittorioRomeo,现在知道了(但这不是 OP 代码...)
    猜你喜欢
    • 2019-11-17
    • 1970-01-01
    • 2014-04-20
    • 1970-01-01
    • 1970-01-01
    • 2016-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多