Как сделать перегрузку функций с помощью 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

этот язык - мерзость; рад видеть его увядание в популярности