【问题标题】:How to write a standard-like function that has high overload priority如何编写具有高重载优先级的类标准函数
【发布时间】:2019-06-21 09:11:39
【问题描述】:

在一个通用函数中,我使用以下成语,

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

但是,当我使用 N::AN::BN::C 参数调用 do_something 时,即使它们与 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;
    }
}

但这无济于事。

我可以尝试哪些其他解决方法来让通用调用 copy 更喜欢参数命名空间中的副本而不是 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&amp;, N::A&amp;, N::A&amp;)’ is ambiguous


我是否认为 C++ 概念会通过更喜欢具有更多约束而不是更少约束的函数调用来提供帮助?

【问题讨论】:

  • 不是骗子,而是related
  • 显然错误来自总是在做using std::copy;
  • @alfC 您的N::copy 也是一个函数模板,它没有与命名空间N 关联的参数。因此,就重载解决而言,它并不比std::copy 好。
  • @MatthieuBrucher,如果我使用std::copy,那么就不可能调用copy 的特殊版本。唯一的方法是重载std::copy(在命名空间std中),我不知道是否允许。
  • @geza,问题是do_something是一个模板函数,不知道命名空间N或库N

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


【解决方案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 的选项。你不可能拥有一切,你必须重新设计你的代码。

【讨论】:

  • 感谢您的选择。指针类型只是一个示例,其他(非标准迭代器)现在将不起作用。换句话说,这会强制所有迭代器和类似指针的迭代器为它们实现copy,即使std::copy 对它们来说没问题。
  • 是的,这就是问题所在,我同意。再次,您遇到的问题是设计问题。在某些情况下,您想尝试依赖 ADL,但在其他情况下却不想。不会工作。
【解决方案2】:

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

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
}

【讨论】:

    【解决方案3】:

    这似乎满足您的要求:

    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

    【讨论】:

    • 我不认为这能解决问题......它将一般do_something与迭代器的特殊代码联系起来......
    • @DanielJour:你能举个例子或更多解释吗?我不明白你的意思。
    • @DanielJour ,geza,正如 DanielJour 所说,do_something 需要了解 SpecCopy 库/命名空间。如果您将SpecCopy 包含在do_something 中,则没有自定义,N::copy 永远不会以这种方式调用。
    • @alfC:嗯,我认为您需要基于 ADL 的解决方案。如果迭代器在namespace N 中,并且你在namespace N 中有一个copy 函数,那么使用此解决方案,N::copy 将被调用。
    • @alfC:我已经编辑了我的答案,所以你可以明白我的意思。
    【解决方案4】:

    建议你看看非常强大的新 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。

    【讨论】:

    • 这看起来像一个有趣的库。但是问题是do_something 仍然需要了解库/命名空间N
    • @alfC 我明白了。我不清楚您是否正在寻求调用copy 的通用解决方案。
    【解决方案5】:

    在 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) 将始终尝试使用第一个版本,因为intlong 更适合0。这意味着我们将始终尝试首先访问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;
    }
    

    【讨论】:

    • 谢谢,但do_something 需要了解N 命名空间,它可能位于断开连接的库中。
    • @alfC 你也可以将代码从copy 直接移动到do_something,并在同一个库中包含copy_helperno_tag 标签。然后你可以再次依赖 ADL。您的断开连接的库必须使用其相应的标记来实现 copy_helper
    • 编辑是迄今为止最接近问题的答案。请注意,我认为 forward&lt;It1&gt; 是无用的,除非您将参数传递为 It1&amp;&amp; first
    【解决方案6】:

    您可以在迭代器类中将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 中公开可见(就好像它们是免费函数一样)。


    更新:

    在 cmets 中,有人建议,如果 N 中的迭代器无论如何都具有公共基类,则只需使用该基类声明 N::copy,例如

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

    不幸的是,这将产生与期望相反的效果:std::copy 将始终比 N::copy 更受青睐,因为如果您传递 A 的实例,则必须向下转换才能匹配 @987654337 @ 而std::copy 不需要演员表。 Here 你可以看到很明显 std::copy 被尝试调用(这会导致错误,因为 N::A 缺少一些类型定义)。

    因此,您不能将公共基类用于N::copy 的签名。我在解决方案中使用它的唯一原因是避免重复代码(必须在每个迭代器类中声明友元函数)。我的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 函数是一个可选组件。

    【讨论】:

    • 这是一个有趣的解决方案,基类中的朋友允许迭代器和算法之间的耦合只声明一次。这样做的问题是N::iterators 必须知道可以实现copyfillaccumulate 等的所有可能算法。
    • @alfC 所以你说copy 只是一个例子,实际上你想用几个标准算法来做到这一点?在这种情况下,无论如何您都必须实现它们,因此只需将它们直接放入ItBase。还是我误会了你?
    • 是的,关键是这些迭代器需要提前知道任何特殊功能。除此之外,这是一个很好的替代设计。
    • 顺便说一句,我认为公共或私人在这里对朋友功能没有影响。朋友功能似乎总是可以访问的(公开的)。
    • @alfC 查看我的更新。如果我误解了您的意思,请同时更新您的问题以详细说明,因为这似乎是一个重要的细节,以便为您提供您正在寻找的答案
    【解决方案7】:

    (这些注释现在已整合到我对@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;
    }
    

    【讨论】:

    • 那或带有朋友功能的方法可能是你能得到的最好的;没有更多可以利用的 ADL 最佳可行候选人选择规则。
    猜你喜欢
    • 2018-02-17
    • 1970-01-01
    • 2017-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多