Как написать стандартную функцию с высоким приоритетом перегрузки

В общей функции я использую следующую идиому,

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_something с аргументом N::A, N::B или N::C я получаю "неоднозначный вызов копирования", даже если они находятся в том же пространстве имен, что и 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&, N::A&, N::A&) is ambiguous


Правильно ли я считаю, что концепции C++ здесь помогут, предпочитая вызовы функций с большим количеством ограничений, чем с меньшими ограничениями?

Ответы

Ответ 1

Вы можете объявить copy() как общедоступную функцию друга в ваших классах итераторов. Это работает как замена частичной специализации (что невозможно для функций), так что для них предпочтительнее разрешение перегрузки, поскольку они более специализированы:

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

Посмотрите эту демонстрацию, чтобы убедиться, что она работает.

Я ввел общий базовый класс, который объявляет необходимых друзей для всех ваших итераторов. Таким образом, вместо объявления тега, как вы пытались, вам просто нужно наследовать от 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 не требуется приведение. Здесь вы можете видеть, что очевидно, что вызывается std::copy (что выдает ошибку, потому что в N::A отсутствуют некоторые определения типов).

Таким образом, вы не можете использовать общий базовый класс для подписи N::copy. Единственная причина, по которой я использовал ее в своем решении, заключалась в том, чтобы избежать дублирования кода (необходимость объявлять функцию друга в каждом классе итераторов). My 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() {} };
}

Посмотрите здесь, как это работает.


В этих же строках, если 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 является необязательным компонентом.

Ответ 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.

Ответ 4

В 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_tag. tag_t предоставляет нам эту функциональность, используя SFINAE и разрешение перегрузки. Мы вызываем функцию tag_helper(0) которая имеет два объявления. Первый возвращает T::tag а второй возвращает no_tag. Вызов tag_helper(0) всегда будет пытаться использовать первую версию, потому что int лучше соответствует 0 чем long. Это означает, что мы всегда будем сначала пытаться получить доступ к T::tag. Однако, если это невозможно (T::tag не определен), включается SFINAE и Skipps tag_helper(int) выбирает tag_helper(long).

Наконец, нам просто нужно реализовать функцию копирования для каждого тега (я назвал его copy_helper) и другую функцию копирования для удобства (я использовал N::copy). Затем функция-обертка создает правильный тип тега и вызывает правильную вспомогательную функцию.

Вот живой пример.

редактировать

Если вы немного передвинете код, вы можете отключить пространство имен 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;
}

Ответ 5

Хорошо, основываясь на @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); 
}

Все еще С++ 17, но в случае с указателями мы проходим через явный std::copy противном случае мы полагаемся на ADL.

В общем, ваша проблема - это проблема дизайна. Вы хотите использовать std::copy для всех случаев, кроме объектов из N, и в этом случае вы надеетесь, что ADL будет работать. Но так как вы заставили std::copy, вы удалили опцию для правильного ADL. Вы не можете иметь все, и вы должны изменить свой код.

Ответ 6

(Эти заметки теперь включены в мой ответ на редактирование @sebrockm)


Для обсуждения напишу ответ на свой вопрос с альтернативным вариантом.

Это не очень хорошо, потому что нужно обернуть все N:: классы в другой шаблонный класс (здесь это называется wrap). Хорошо, что do_something или N классы должны знать о специальном 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;
}

Ответ 7

Предлагаем вам взглянуть на очень мощную новую библиотеку 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 выберет первую лямбду, чей тип возвращаемого значения является типом результата допустимого выражения.