编译器选择错误的重载而不是有效的重载

Compiler chooses erroneous overload instead of valid overload

看看这段代码:

#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));
                        ~~~~~~~~~~^~~~~~~

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

但显然不是这样,那么这里发生了什么?为什么选择了错误的重载?为什么第三个重载比第二个更匹配?

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

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

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

在第二次重载时,前两个参数按值传递,因此必须进行额外的复制。在处理 class 对象(可能是 STL 容器的 iterator 类型)时,这会影响重载解析。

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

它主要是做第三个参数,它是一个右值。试试下面的方法,看看为什么它能更好地匹配通用参考。

#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); }

你会得到一个不明确的错误。也就是说,两个重载同样匹配右值。所以他们不确定在你的例子中选择了哪个重载

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

您可以使用 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(...) 中的表达式无效,这将排除重载。