如何编写具有高重载优先级的类似标准的函数

时间:2019-01-28 11:13:28

标签: c++ c++11 ambiguous argument-dependent-lookup

在通用函数中,我使用以下惯用法

template<class It1, class It2>
void do_something(It1 first, It1 second, It2 d_first){
    ... other stuff here...
    using std::copy;
    copy(first, second, d_first);
}

do_something是一个通用函数,不应该知道任何其他库的特定信息(也许std::除外)。

现在假设我的命名空间N中有多个迭代器。

namespace N{

  struct itA{using trait = void;};
  struct itB{using trait = void;};
  struct itC{using trait = void;};

}

我想在此命名空间中重载这些迭代器的副本。 我自然会这样做:

namespace N{
    template<class SomeN1, class SomeN2>
    SomeN2 copy(SomeN1 first, SomeN1 last, SomeN2 d_first){
        std::cout << "here" << std::endl;
    }
}

但是,当我使用do_somethingN::AN::B作为参数调用N::C时,即使这些变量与{{ 1}}。

在上述原始功能的背景下,有没有办法赢得N::copy

尽管我对模板参数设置了约束,但std::copy还是首选。

N::copy

但没有帮助。

我还可以尝试其他哪些解决方法来使一般调用复制,而不是namespace N{ template<class SomeN1, class SomeN2, typename = typename SomeN1::trait> SomeN2 copy(SomeN1 first, SomeN1 last, SomeN2 d_first){ std::cout << "here" << std::endl; } } 而不是std::copy命名空间中的副本。

完整代码:

#include<iostream>
#include<algorithm>
namespace N{
  struct A{};
  struct B{};
  struct C{};
}

namespace N{
    template<class SomeN1, class SomeN2>
    SomeN2 copy(SomeN1 first, SomeN1 last, SomeN2 d_first){
        std::cout << "here" << std::endl;
    }
}

template<class It1, class It2>
void do_something(It1 first, It1 second, It2 d_first){
    using std::copy;
    copy(first, second, d_first); // ambiguous call when It is from namespace N (both `std::copy` and `N::copy` could work.
}

int main(){
    N::A a1, a2, a3;
    do_something(a1, a2, a3); 
}

典型的错误消息是

error: call of overloaded ‘copy(N::A&, N::A&, N::A&)’ is ambiguous


我是否正确地认为C ++概念会更喜欢约束而不是约束更少的函数调用?

7 个答案:

答案 0 :(得分:4)

您可以在迭代器类中将copy()声明为public friend function。 这可以代替部分专业化(对于函数而言这是不可能的),因此,由于它们更加专业化,因此它们将被重载解析所首选:

#include <iostream>
#include <algorithm>
#include <vector>

namespace N
{
    template<class SomeN1, class SomeN2>
    SomeN2 copy(SomeN1 first, SomeN1 last, SomeN2 d_first)
    {
        std::cout << "here" << std::endl;
        return d_first;
    }

    template <class T>
    struct ItBase
    {
        template <class SomeN2>
        friend SomeN2 copy(T first, T last, SomeN2 d_first)
        {
            return N::copy(first, last, d_first);
        }
    };

    struct A : ItBase<A>{};
    struct B : ItBase<B>{};
    struct C : ItBase<C>{};
}

template<class It1, class It2>
void do_something(It1 first, It1 second, It2 d_first){
    using std::copy;
    copy(first, second, d_first);
}

int main(){
    N::A a1, a2, a3;
    std::cout << "do something in N:" << std::endl;
    do_something(a1, a2, a3); 

    std::vector<int> v = {1,2,3};
    std::vector<int> v2(3);
    std::cout << "do something in std:" << std::endl;
    do_something(std::begin(v), std::end(v), std::begin(v2));
    for (int i : v2)
        std::cout << i;
    std::cout << std::endl;
}

请参见this demo以验证其是否有效。

我引入了一个通用基类,该基类为所有迭代器声明了必要的朋友。因此,不必像声明那样声明标签,只需从ItBase继承即可。

注意:如果N::copy()仅应与N中的这些迭代器一起使用,则可能不再需要它们,因为这些朋友功能无论如何都将在N中公开显示(好像它们是免费功能)。


更新:

在注释中建议,如果N中的迭代器始终具有公共基类,则只需使用该基类声明N::copy,例如

namespace N
{
    template <class SomeN2>
    SomeN2 copy(ItBase first, ItBase last, SomeN2 d_first) { ... }
}

不幸的是,这将产生与期望的相反的效果:std::copy始终比N::copy更为可取,因为如果您传递A的实例,则必须将其抛弃以匹配N::copy,而std::copy不需要强制转换。 Here可以看到显然尝试调用std::copy(这会产生错误,因为N::A缺少一些typedef)。

因此,您不能利用公共基类来签名N::copy。我在解决方案中使用一个的唯一原因是避免重复代码(必须在每个迭代器类中声明Friend函数)。我的ItBase根本不参与重载决议。

但是请注意,如果您的迭代器碰巧要在实现N::copy中使用的某些公共成员(无论是否从某个公共基类派生而来并不重要),则可以这样做上面的解决方案是这样的:

namespace N
{
    template <class T>
    struct ItBase
    {
        template <class SomeN2>
        friend SomeN2 copy(T first, T last, SomeN2 d_first)
        {
            first.some_member();
            last.some_member();
            return d_first;
        }
    };

    struct A : ItBase<A>{ void some_member() {} };
    struct B : ItBase<B>{ void some_member() {} };
    struct C : ItBase<C>{ void some_member() {} };
}

请参见here


同一行上,如果A,B,C具有相同的行为,则可以用以某种方式参数化的通用模板类替换它们。

namespace N
{
    template <class T, int I>
    struct ItCommon
    {
       ...
    };
    using A = ItCommon<double,2>;
    using B = ItCommon<int, 3>;
    using C = ItCommon<char, 5>;
}
...
namespace N{
    template<class T, int I, class Other>
    SomeN2 copy(ItCommon<T, I> first, ItCommon<T, I> last, Other){
        ...
    }
} 

由于(非朋友)copy函数肯定比std::copy受约束,并且由于ADL的原因,当其中一个参数属于N时它将具有较高的优先级命名空间。另外,copy函数是非朋友,是可选组件。

答案 1 :(得分:3)

一种可能的解决方案是使用另一个函数模板名称和类型区分符,以允许依赖于参数的名称查找在参数的命名空间中找到关联的函数:

template<class T> struct Tag {};
template<class T> Tag<void> tag(T const&);

template<class It1, class It2>
void mycopy(It1 first, It1 second, It2 d_first, Tag<void>) {
    std::cout << "std::copy\n";
}

template<class It1, class It2>
void mycopy(It1 first, It1 second, It2 d_first) {
    mycopy(first, second, d_first, decltype(tag(first)){}); // Discriminate by the type of It1.
}

namespace N{

    struct itA{using trait = void;};
    Tag<itA> tag(itA);

    template<class It1, class It2>
    void mycopy(It1 first, It1 second, It2 d_first, Tag<itA>) {
        std::cout << "N::mycopy\n";
    }
}

int main() {
    char* p = 0;
    mycopy(p, p, p); // calls std::copy

    N::itA q;
    mycopy(q, q, q); // calls N::mycopy
}

答案 2 :(得分:2)

这似乎满足您的要求:

namespace SpecCopy {

template <typename A, typename B, typename C>
void copy(A &&a, B &&b, C &&c) {
    std::copy(std::forward<A>(a), std::forward<B>(b), std::forward<C>(c));
}

}

template<class It1, class It2>
void do_something(It1 first, It1 second, It2 d_first){
    using namespace SpecCopy;
    copy(first, second, d_first);
}

基本上,它取决于ADL。如果ADL未找到函数,则它将使用SpecCopy::copy,它是std::copy的包装。


因此,如果您这样做:

N::A a1, a2, a3;
do_something(a1, a2, a3);

然后do_something将呼叫N::copy


如果您这样做:

std::vector<int> a1, a2;
do_something(a1.begin(), a1.end(), a2.begin());

然后do_something将呼叫SpecCopy::copy,后者将呼叫std::copy


如果您这样做:

int *a1, *a2, *a3;
do_something(a1, a2, a3);

然后发生与以前相同的事情:do_something将调用SpecCopy::copy,后者将调用std::copy

答案 3 :(得分:2)

在c ++ 11中,您可以使用标签分发。如果您可以对自定义迭代器进行一些更改,则实现起来会更简单。

#include <iostream>
#include <algorithm>
#include <vector>
#include <type_traits>

// indicates that the type doesn't have a tag type (like pointers and standard iterators)
struct no_tag{};

namespace detail 
{
    template <typename T>
    auto tag_helper(int) -> typename T::tag;

    template <typename>
    auto tag_helper(long) -> no_tag;
}

// get T::tag or no_tag if T::tag isn't defined.
template <typename T>
using tag_t = decltype(detail::tag_helper<T>(0));

namespace N
{
    struct my_iterator_tag {};
    struct A{ using tag = my_iterator_tag; };
    struct B{ using tag = my_iterator_tag; };
    struct C{ using tag = my_iterator_tag; };
}

namespace N
{
    template<class SomeN1, class SomeN2>
    SomeN2 copy_helper(SomeN1 first, SomeN1 last, SomeN2 d_first, no_tag)
    {
        std::cout << "calling std::copy\n";
        return std::copy(std::forward<SomeN1>(first), std::forward<SomeN1>(last), std::forward<SomeN2>(d_first));
    }

    template<class SomeN1, class SomeN2>
    SomeN2 copy_helper(SomeN1 first, SomeN1 last, SomeN2 d_first, my_iterator_tag)
    {
        // your custom copy        
        std::cout << "custom copy function\n";
        return {};
    }

    template<class SomeN1, class SomeN2>
    SomeN2 copy(SomeN1 first, SomeN1 last, SomeN2 d_first)
    {
        return copy_helper(std::forward<SomeN1>(first), std::forward<SomeN1>(last), std::forward<SomeN2>(d_first), tag_t<SomeN1>{});
    }
}

template<class It1, class It2>
void do_something(It1 first, It1 second, It2 d_first)
{
    N::copy(first, second, d_first);
}

int main()
{
    N::A a1, a2, a3;
    std::cout << "using custom iterator: ";
    do_something(a1, a2, a3); 

    std::cout << "using vector iterator: ";
    std::vector<int> v;
    do_something(std::begin(v), std::end(v), std::begin(v));

    std::cout << "using pointer: ";
    int* ptr = new int[10];
    do_something(ptr, ptr + 5, ptr);

    return 0;
}

首先,我们将自定义迭代器更改为tag类型(也许更改名称以避免与iterator_category混淆)。 tag可以是您想要的任何类型,只需匹配您在copy_helper中用作标记的类型。

接下来,我们定义一个类型,该类型允许我们访问此tag类型,如果tag不存在,则退回到默认类型。这将帮助我们区分自定义迭代器,标准迭代器和指针。我使用的默认类型为no_tagtag_t通过使用SFINAE和重载分辨率为我们提供了此功能。我们调用函数tag_helper(0),它有两个声明。第一个返回T::tag,第二个返回no_tag。调用tag_helper(0)总是会尝试使用第一个版本,因为int0更适合long。这意味着我们将始终尝试首先访问T::tag。但是,如果这不可能(未定义T::tag),则SFINAE会跳入并跳过tag_helper(int),选择tag_helper(long)

最后,我们只需要为每个标签实现一个复制功能(我称其为copy_helper),并为实现方便而包装另一个复制功能(我使用了N::copy)。然后,包装器函数将创建正确的标记类型并调用正确的帮助器函数。

Here是一个实时示例。

编辑

如果您稍微移动一下代码,则可以断开命名空间N并依赖ADL:

#include <iostream>
#include <algorithm>
#include <vector>
#include <type_traits>

// indicates that the type doesn't have a tag type (like pointers and standard iterators)
struct no_tag{};

namespace detail 
{
    template <typename T>
    auto tag_helper(int) -> typename T::tag;

    template <typename>
    auto tag_helper(long) -> no_tag;
}

// get T::tag or no_tag if T::tag isn't defined.
template <typename T>
using tag_t = decltype(detail::tag_helper<T>(0));

namespace N
{
    struct my_iterator_tag {};
    struct A{ using tag = my_iterator_tag; };
    struct B{ using tag = my_iterator_tag; };
    struct C{ using tag = my_iterator_tag; };

    template<class SomeN1, class SomeN2>
    SomeN2 copy_helper(SomeN1 first, SomeN1 last, SomeN2 d_first, my_iterator_tag)
    {
        // your custom copy        
        std::cout << "custom copy function\n";
        return {};
    }
}

template<class SomeN1, class SomeN2>
SomeN2 copy_helper(SomeN1 first, SomeN1 last, SomeN2 d_first, no_tag)
{
    std::cout << "calling std::copy\n";
    return std::copy(std::forward<SomeN1>(first), std::forward<SomeN1>(last), std::forward<SomeN2>(d_first));
}

template<class It1, class It2>
void do_something(It1 first, It1 second, It2 d_first)
{
    copy_helper(std::forward<It1>(first), std::forward<It1>(second), std::forward<It2>(d_first), tag_t<It1>{});
}

int main()
{
    N::A a1, a2, a3;
    std::cout << "using custom iterator: ";
    do_something(a1, a2, a3); 

    std::cout << "using vector iterator: ";
    std::vector<int> v;
    do_something(std::begin(v), std::end(v), std::begin(v));

    std::cout << "using pointer: ";
    int* ptr = new int[10];
    do_something(ptr, ptr + 5, ptr);

    return 0;
}

答案 4 :(得分:1)

好,建立在@ paler123上,但不检查现有类型,而是检查It1是否是指针:

namespace N{
  struct A{};
  struct B{};
  struct C{};
}

namespace N{
    template<class SomeN1, class SomeN2>
    SomeN2 copy(SomeN1, SomeN1, SomeN2 c){
        std::cout << "here" << std::endl;
        return c;
    }
}
template<class It1, class It2>
void do_something(It1 first, It1 second, It2 d_first){
    if constexpr (std::is_pointer_v<It1>) {
        std::copy(first, second, d_first);
    }
    else
    {
        copy(first, second, d_first);
    }
}


int main(){
    N::A a1, a2, a3;
    do_something(a1, a2, a3); 

    int* b1, *b2, *b3;

    do_something(b1, b2, b3); 
}

仍然是C ++ 17,但是在使用指针的情况下,我们将经过显式的std::copy,否则,我们将依赖ADL。

通常,您的问题是设计问题。您希望在所有情况下都使用std::copy,但N中的对象除外,在这种情况下,您希望ADL可以工作。但是当您强制std::copy时,就删除了正确ADL的选项。您不能拥有所有内容,而必须重新设计代码。

答案 5 :(得分:1)

(这些笔记现已集成到我对@sebrockm答案的编辑中)


为讨论起见,我将使用替代选项为自己的问题写一个答案。

这不是很好,因为它需要将所有N::类包装在另一个模板类中(此处称为wrap)。好消息是do_somethingN类都需要了解特殊的N::copy。代价是main调用者必须显式包装N::类,这很丑陋,但从耦合的角度来看是很好的,因为这是唯一应该了解整个系统的代码。

#include <iostream>
#include <algorithm>
#include <vector>

namespace N{
    struct A{};
    struct B{};
    struct C{};
}

namespace N{

    template<class S> struct wrap : S{};

    template<class SomeN1, class SomeN2>
    SomeN2 copy(wrap<SomeN1> first, wrap<SomeN1> last, wrap<SomeN2> d_first)
    {
        std::cout << "here" << std::endl;
        return d_first;
    }
}

template<class It1, class It2>
void do_something(It1 first, It1 second, It2 d_first){
    using std::copy;
    copy(first, second, d_first);
}

int main(){
    N::wrap<N::A> a1, a2, a3;
    std::cout << "do something in N:" << std::endl;
    do_something(a1, a2, a3); 

    std::vector<int> v = {1,2,3};
    std::vector<int> v2(3);
    std::cout << "do something in std:" << std::endl;
    do_something(std::begin(v), std::end(v), std::begin(v2));
    for (int i : v2)
        std::cout << i;
    std::cout << std::endl;
}

答案 6 :(得分:-2)

建议您看看功能非常强大的新Boost.HOF库。

此功能完全可以满足您的要求:

#include <boost/hof.hpp>

template<class It1, class It2>
void do_something(It1 first, It1 second, It2 d_first){
    namespace hof = boost::hof;

    auto my_copy = hof::first_of(
    [](auto first, auto second, auto d_first) -> decltype(N::copy(first, second, d_first))
    {
        return N::copy(first, second, d_first);
    },
    [](auto first, auto second, auto d_first) -> decltype(std::copy(first, second, d_first))
    {
        return std::copy(first, second, d_first);
    });
    my_copy(first, second, d_first);
}

hof::first_of将选择其返回类型被推导为合法表达式的结果类型的第一个lambda。