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