Как написать стандартную функцию с высоким приоритетом перегрузки - PullRequest
0 голосов
/ 28 января 2019

В универсальной функции я использую следующую идиому:

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

, но это не помогает.

Какие другие обходные пути можно использовать для универсального вызова копирования, чтобы предпочесть копию в пространстве имен аргументов, а не 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 голосов
/ 29 января 2019

Вы можете объявить 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 не имеет некоторых typedefs).

Таким образом, вы не можете использовать общий базовый класс дляподпись 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() {} };
}

См. здесь как это работает.


В тех же строках, если 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){
        ...
    }
} 

Поскольку это (не в друзьях) * 1054Функция * определенно более ограничена, чем std::copy, и из-за ADL она будет иметь высокий приоритет, когда один из аргументов принадлежит пространству имен N.Кроме того, функция copy, будучи не другом, является необязательным компонентом.

0 голосов
/ 29 января 2019

(Эти заметки теперь включены в мое редактирование ответа @ 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;
}
0 голосов
/ 28 января 2019

В 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;
}
0 голосов
/ 28 января 2019

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

0 голосов
/ 28 января 2019

Это, кажется, соответствует вашим требованиям:

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.

0 голосов
/ 28 января 2019

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

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
}
0 голосов
/ 28 января 2019

ОК, опираясь на @ 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.У вас не может быть всего, и вы должны изменить свой код.

...