Как сделать перегрузку функций с помощью std :: shared_ptr <void>и другого типа std :: shared_ptr? - PullRequest
0 голосов
/ 06 февраля 2019

Попробуйте следующий код:

#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>)>){}
         ^

Есть ли способ обойти эту неоднозначность?Возможно с СФИНАЕ?

Ответы [ 2 ]

0 голосов
/ 06 февраля 2019

Почему это происходит

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

Причина заключается в том, что при сравнении того, являются ли интерфейсы функций болееобщее или более конкретное правило состоит в том, что возвращаемые типы должны быть «ковариантными», а типы аргументов должны быть «контравариантными» ( Wikipedia ; см. также это SO Q & A ).То есть

Учитывая (псевдокод) типы интерфейсов функций

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...)> шаблона конструктора

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)));
}
0 голосов
/ 06 февраля 2019

Я в замешательстве, но я пытаюсь объяснить.

Я вижу, что ваша лямбда может быть принята как 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().

Итак, двусмысленность.

Есть ли способ обойти эту двусмысленность?Возможно с SFINAE?

Возможно с диспетчеризацией тегов.

Вы можете добавить неиспользуемый аргумент и шаблон 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 version").

...