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

How to write a standard-like function that has high overload priority

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

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 时,我得到 "ambiguous call to copy",即使它们与 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++ 概念会通过更喜欢约束多于约束少的函数调用来提供帮助?

好的,在@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 的选项。你不能拥有一切,你必须重新设计你的代码。

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

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
}

这似乎符合您的要求:

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::copySpecCopy::copy会调用std::copy.


如果你这样做:

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

然后和以前一样发生同样的事情:do_something 将调用 SpecCopy::copy,这将调用 std::copy.

建议您看一下非常强大的新 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 将 select 其 return 类型被推断为合法表达式的结果类型的第一个 lambda。

在 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)。第一个 returns T::tag 而第二个 returns 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;
}

您可以在迭代器 class 中将 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 以验证其是否有效。

我介绍了一个公共基础 class,它为所有迭代器声明了必要的友元。因此,无需像您尝试的那样声明标签,您只需继承 ItBase.

注意:如果 N::copy() 应该只与 N 中的这些迭代器一起使用,则可能不再需要它,因为这些友元函数无论如何都会在 N 中公开可见(就好像它们是免费功能一样)。


更新:

在评论中,有人建议,如果 N 中的迭代器有一个共同的基数 class,只需用这个基数 class 声明 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 缺少一些类型定义)。

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

但是请注意,如果您的迭代器恰好有一些您想要在 N::copy 的实现中使用的公共成员(是否派生自某些公共基础 class 并不重要) ,你可以像这样用我上面的解决方案来做到这一点:

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 具有共同的行为,则可以用以某种方式参数化的共同模板 class 替换它们。

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,当其中一个参数属于 [=17] 时它将具有高优先级=] 命名空间。此外,作为非好友,此 copy 函数是一个可选组件。

(这些注释现在已整合到我对@sebrockm 的回答的编辑中)


为了讨论,我将使用替代选项写下我自己的问题的答案。

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

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