【问题标题】:Compiler chooses erroneous overload instead of valid overload编译器选择错误重载而不是有效重载
【发布时间】:2016-12-07 20:35:42
【问题描述】:

看看这段代码:

#include <vector>
#include <functional>

template<typename RandIt, typename T, typename Pred>
auto search_with(RandIt begin, RandIt end, const T& value, Pred&& pred) noexcept {
    //...
    return begin;
}

template<typename RandIt, typename T>
auto search_with(RandIt begin, RandIt end, const T& value) noexcept {
    return search_with(begin, end, value, std::less<T>{});
}

template<typename Array, typename T, typename Pred>
auto search_with(const Array& array, const T& value, Pred&& pred) noexcept {
    return search_with(std::begin(array), std::end(array), value, std::forward<Pred>(pred));
}

int main() {
    std::vector<int> v = { 1, 2, 3 };
    search_with(v, 10, std::less<int>{}); // ok
    search_with(v.begin(), v.end(), 10);  // fail!
}

我不明白为什么在第二个search_with 调用中,编译器选择了第三个重载。如果我注释掉第三个重载,那么代码编译得很好。这表明第二个重载在编译时没有被丢弃,它应该是一个有效的重载。

但是,选择了第三个重载,但失败了,因为迭代器没有专门化 std::begin(和 std::end):

main.cpp: In instantiation of 'auto search_with(const Array&, const T&, Pred&&) [with Array = __gnu_cxx::__normal_iterator<int*, std::vector<int> >; T = __gnu_cxx::__normal_iterator<int*, std::vector<int> >; Pred = int]':
main.cpp:23:39:   required from here
main.cpp:17:34: error: no matching function for call to 'begin(const __gnu_cxx::__normal_iterator<int*, std::vector<int> >&)'
     return search_with(std::begin(array), std::end(array), value, std::forward<Pred>(pred));
                        ~~~~~~~~~~^~~~~~~

我原以为会发生相反的情况:第三个重载因为编译失败而被丢弃,而选择了第二个。

但显然情况并非如此,那么这里发生了什么?为什么选择了错误的重载?为什么第三个重载比第二个更好?

【问题讨论】:

  • SFINAE 应用于函数声明,而不是定义,这就是为什么 enable_if 应用于模板参数或返回值,而不是在正文中粘贴此类代码的更漂亮的选项。仅当声明位无法编译时,该函数才会被丢弃。提供的参数在第三个函数的声明中工作得很好,所以它不会被丢弃。
  • 但是,如果您使用尾随返回类型,它似乎可以工作:coliru.stacked-crooked.com/a/5d3d34290d7beb1a
  • @AndyG 啊,那是 SFINAE。 :) 谢谢
  • @Rakete1111 可能他们决定不这样做,因为那时重载解决方案将取决于问题函数的 内容,并且已经有足够的混乱:P
  • 函数重载不能依赖函数体。考虑非模板函数重载的情况。函数体可能不在头文件中,并且在要解决重载时根本不可见。

标签: c++ c++14 overload-resolution


【解决方案1】:

它主要是做第三个参数,它是一个右值。请尝试以下操作,看看为什么它更符合通用参考。

#include <iostream>

template <typename T>
inline void f(T &)
{
    std::cout << "f2" << std::endl;
}

template <typename T>
inline void f(const T &)
{
    std::cout << "f3" << std::endl;
}

template <typename T>
inline void f(T &&)
{
    std::cout << "f4" << std::endl;
}

int main()
{
    int a = 0;
    const int b = 0;
    int &c = a;
    const int &d = b;
    f(1); // f4 : rvalue matched by universal reference
    f(a); // f2 : lvalue matched by reference, T& preferred to const T&
    f(b); // f3 : lvalue matched by reference, can only do const T&
    f(c); // f2 : lvalue reference matched by reference, T& preferred to const T&
    f(d); // f3 : lvalue const reference matched by reference, can only do const T&
    f(std::move(a)); // f4 : rvalue reference, matched by universal reference

}

如果你再抛出一个超载,

 template <typename T>
 inline void f(T);

混进去,你会得到模棱两可的错误,因为它也会给你完美的匹配。

至于前两个右值参数,请考虑以下示例,

template <typename T>
inline void f(T)
{
}

template <typename T>
inline void f(const T &)
{
}

int main() { f(1); }

你会得到一个模棱两可的错误。也就是说,这两个重载同样匹配一个右值。因此,他们无法确定您的示例中选择了哪个重载

【讨论】:

  • 更有趣的是,如果 OP 的代码按值取 pred,部分排序将启动并选择正确的重载。
  • @T.C.确切地。一开始,函数重载很有趣。但是这些天如果可能的话,我只是使用标签重载来省去很多麻烦。 C++ 重载的问题在于,即使经过多年的挣扎,当你对它有所了解时,你仍然会时不时地忘记某些东西并弄错。将通用参考引入混合只会使情况变得更糟。我们是人类,不是编译器。
【解决方案2】:

第三个重载总是更好,除非你将一个 const 左值作为第三个参数传递给你的函数模板。你传递了一个prvalue。转发参考Pred&amp;&amp; 可以更好地匹配这种情况,因此被选中。

您可以使用SFINAE (Substitution Failure Is Not An Error) 技术实现您想要的行为。

template<typename Array, typename T, typename Pred>
auto search_with(const Array& array, const T& value, Pred&& pred) noexcept
    -> decltype(search_with(
           std::begin(array), std::end(array), value, std::forward<Pred>(pred)))
{
    return search_with(std::begin(array), std::end(array), value, std::forward<Pred>(pred));
}

如果decltype(...) 中的表达式无效,这将排除重载。

【讨论】:

  • 我认为除了删除重复之外别无他法,对吧?
  • @Rakete1111 不幸的是。也许在未来的一些 C++ 标准中,但目前,如果你想要适当的 SFINAE 友好函数,你将不得不忍受这种丑陋。
【解决方案3】:

第一个调用是传递满足模板参数的参数值只有一个重载:

template<typename Array, typename T, typename Pred>
auto search_with(const Array& array, const T& value, Pred&& pred) noexcept

第二个调用是传递满足两个重载的模板参数的参数值:

template<typename RandIt, typename T>
auto search_with(RandIt begin, RandIt end, const T& value) noexcept

// where RandIt is std::vector<int>::iterator, and T is int...

template<typename Array, typename T, typename Pred>
auto search_with(const Array& array, const T& value, Pred&& pred) noexcept

// where Array and T are both std::vector<int>::iterator, and Pred is int...

但是,第三个重载是更好的匹配,因为它的参数都是通过引用传递的,所以编译器不必创建不必要的副本。

在第二次重载中,前两个参数是按值传递的,因此必须制作额外的副本。在处理类对象时(可能是 STL 容器的 iterator 类型),这可能会影响重载决议。

编译器会尽可能避免制作不必要的副本。

【讨论】:

  • 为什么编译器不移动迭代器而不是复制它们?
  • 我只是在想这是第三个参数会有所不同... 10 是一个右值,第三个重载中的 Pred && 可能比第二个重载中的 const T & 更好。 .
  • 传递引用仍然比移动更可取(涉及的代码更少)。并且可以将右值分配给 const-reference 参数。所以我认为这仍然是编译器更喜欢通过 const-reference 传递迭代器而不是调用额外的逻辑来通过值传递它们的问题。
  • 我不认为这是因为编译器试图避免复制。当编译器解析模板重载时,是否制作副本并不重要。这三个参数都是右值。第二个重载需要将它们中的每一个匹配到一个左值,而第三个重载将前两个匹配到左值,而第三个则通过通用引用匹配。第三个重载更好地匹配第三个参数。出于任何优化原因,它与编译器“偏好”的内容无关。它必须做“正确”的事情
  • 按引用或按值传递对重载决议没有影响。这与复制完全无关。
猜你喜欢
  • 2014-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多