Как сделать перегрузку функций с помощью std :: shared_ptr <void> и другого типа std :: shared_ptr?
Попробуйте этот следующий код:
#include <functional>
#include <memory>
class C {
public:
void F(std::function<void(std::shared_ptr<void>)>){}
void F(std::function<void(std::shared_ptr<int>)>){}
};
int main(){
C c;
c.F([](std::shared_ptr<void>) {});
}
Вы увидите ошибку компиляции:
prog.cc:12:7: error: call to member function 'F' is ambiguous
c.F([](std::shared_ptr<void>) {});
~~^
prog.cc:6:10: note: candidate function
void F(std::function<void(std::shared_ptr<void>)>){}
^
prog.cc:7:10: note: candidate function
void F(std::function<void(std::shared_ptr<int>)>){}
^
Есть ли способ обойти эту двусмысленность? Возможно со СФИНАЕ?
Ответы
Ответ 1
Я в замешательстве, но я пытаюсь объяснить.
Я вижу, что ваша лямбда может быть принята как std::function<void(std::shared_ptr<void>)>
и std::function<void(std::shared_ptr<int>)>
; Вы можете проверить, что обе следующие строки компилируются
std::function<void(std::shared_ptr<void>)> f0 = [](std::shared_ptr<void>){};
std::function<void(std::shared_ptr<int>)> f1 = [](std::shared_ptr<void>){};
И это потому, что (я полагаю) общий указатель на int
может быть преобразован в общий указатель на void
; Вы можете проверить, что следующая строка компилируется
std::shared_ptr<void> sv = std::shared_ptr<int>{};
На данный момент мы видим, что вызов
c.F([](std::shared_ptr<void>) {});
вы не передаете std::function<void(std::shared_ptr<void>)>
в F()
; вы передаете объект, который можно преобразовать как в std::function<void(std::shared_ptr<void>)>
и в std::function<void(std::shared_ptr<int>)>
; поэтому объект, который можно использовать для вызова обеих версий F()
.
Так что двусмысленность.
Есть ли способ обойти эту двусмысленность? Возможно со СФИНАЕ?
Может быть с диспетчеризацией тегов.
Вы можете добавить неиспользованный аргумент и шаблон F()
void F (std::function<void(std::shared_ptr<void>)>, int)
{ std::cout << "void version" << std::endl; }
void F (std::function<void(std::shared_ptr<int>)>, long)
{ std::cout << "int version" << std::endl; }
template <typename T>
void F (T && t)
{ F(std::forward<T>(t), 0); }
Этот способ вызова
c.F([](std::shared_ptr<void>) {});
c.F([](std::shared_ptr<int>){});
вы получаете "void version" из первого вызова (оба сопоставления не шаблонные F()
но "void version" предпочтительнее, потому что 0
- это int
) и "int version" из второго вызова (только F()
"int версия "соответствует".
Ответ 2
Почему так происходит
Ответ max66 в основном объясняет, что происходит. Но может быть немного удивительно, что:
-
Вы можете неявно конвертировать из std::shared_ptr<int>
в std::shared_ptr<void>
а не наоборот.
-
Вы можете неявно преобразовывать из std::function<void(std::shared_ptr<void>)>
в std::function<void(std::shared_ptr<int>)>
а не наоборот.
-
Вы можете неявно преобразовать лямбду с аргументом типа std::shared_ptr<void>
в std::function<void(std::shared_ptr<int>)>
.
-
Вы не можете неявно преобразовать лямбду с аргументом типа std::shared_ptr<int>
в std::function<void(std::shared_ptr<void>)>
.
Причина в том, что при сравнении того, являются ли интерфейсы функций более общими или более конкретными, правило заключается в том, что возвращаемые типы должны быть "ковариантными", а типы аргументов должны быть "контравариантными" (Википедия; см. Также эти вопросы и ответы по SO). То есть,
Учитывая (псевдокод) функции интерфейсов типов
C func1(A1, A2, ..., An)
D func2(B1, B2, ..., Bn)
тогда любая функция, которая является экземпляром типа func2
также является экземпляром типа func1
если D
может преобразовать в C
и каждый Ai
может преобразовать в соответствующий Bi
.
Чтобы понять, почему это так, рассмотрим, что произойдет, если мы позволим преобразования function
function
для типов std::function<std::shared_ptr<T>>
а затем попытаемся вызвать их.
Если мы конвертируем std::function<void(std::shared_ptr<void>)> a;
в std::function<void(std::shared_ptr<int>)> b;
, тогда b
действует как оболочка, содержащая копию a
и перенаправляющая вызовы к нему. Тогда b
может быть вызван с любым std::shared_ptr<int> pi;
, Можно ли передать его копию? a
Конечно, потому что он может конвертировать std::shared_ptr<int>
в std::shared_ptr<void>
.
Если мы конвертируем std::function<void(std::shared_ptr<int>)> c;
в std::function<void(std::shared_ptr<void>)> d;
, тогда d
действует как оболочка, содержащая копию c
и перенаправляющая вызовы к ней. Тогда d
может быть вызван с любым std::shared_ptr<void> pv;
, Может ли он передать его в копию c
? Не безопасно! Там нет преобразования из std::shared_ptr<void>
для std::shared_ptr<int>
и даже если представить d
как - то пытается использовать std::static_pointer_cast
или подобный, pv
не может указывать на int
вообще.
Действующее стандартное правило, поскольку C++ 17 ([func.wrap.func.con]/7) соответствует std::function<R(ArgTypes...)>
шаблона конструктора std::function<R(ArgTypes...)>
template<class F> function(F f);
Примечания: Этот шаблон конструктора не должен участвовать в разрешении перегрузки, если только f
является Lvalue-callable для типов аргументов ArgTypes...
и типа возврата R
где "Lvalue-callable" по существу означает, что выражение вызова функции с идеально переданными аргументами заданных типов является допустимым, и если R
не cv void
, выражение может неявно преобразовываться в R
, плюс соображения для случаев, когда f
является указателем члену и/или некоторым типам аргументов являются std::reference_wrapper<X>
.
Это определение по существу автоматически проверяет наличие контравариантных типов аргументов при попытке преобразования из любого вызываемого типа в std::function
, поскольку оно проверяет, являются ли типы аргументов целевого типа function
допустимыми аргументами для исходного вызываемого типа (с учетом разрешенных неявных преобразований).).
(До C++ 17 конструктор шаблона std::function::function(F)
вообще не имел никаких ограничений в стиле SFINAE. Это было плохой новостью для ситуаций перегрузки, подобных этой, и для шаблонов, которые пытались проверить, выполняется ли преобразование был действительным.)
Обратите внимание, что на самом деле противоречивость типов аргументов проявляется по крайней мере в одной другой ситуации в языке C++ (даже если это не разрешенное переопределение виртуальной функции). Указатель на значение члена можно рассматривать как функцию, которая принимает объект класса в качестве входных данных и возвращает элемент lvalue в качестве выходных данных. (Инициализация или назначение std::function
из указателя на член будет интерпретировать значение именно таким образом.) И учитывая, что класс B
является общедоступной однозначной базой класса D
, мы имеем, что D*
может неявно преобразовать в B*
но не наоборот, и MemberType B::*
может преобразовываться в MemberType D::*
но не наоборот.
Что делать
Диспетчеризация тегов max66 предполагает одно из решений.
Или для SFINAE,
void F(std::function<void(std::shared_ptr<void>)>);
void F(std::function<void(std::shared_ptr<int>)>);
// For a type that converts to function<void(shared_ptr<void>)>,
// call that overload, even though it likely also converts to
// function<void(shared_ptr<int>)>:
template <typename T>
std::enable_if_t<
std::is_convertible_v<T&&, std::function<void(std::shared_ptr<void>)>> &&
!std::is_same_v<std::decay_t<T>, std::function<void(std::shared_ptr<void>)>>>
F(T&& func)
{
F(std::function<void(std::shared_ptr<void>)>(std::forward<T>(func)));
}
Ответ 3
этот язык - мерзость; рад видеть его увядание в популярности