如何使用 std::shared_ptr<void> 和另一种类型的 std::shared_ptr 进行函数重载?

How to do function overloading with std::shared_ptr<void> and another type of 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>)>){}
         ^

有什么办法可以解决这种歧义吗?也许与 SFINAE?

我很困惑,但我试着解释一下。

我看到 std::function<void(std::shared_ptr<void>)>std::function<void(std::shared_ptr<int>)> 都可以接受您的 lambda;您可以验证以下两行编译

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 的共享指针;您可以验证以下行 compile

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().

的两个版本

所以歧义。

Is there any way to workaround this ambiguity? Perhaps with 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" 是首选,因为 0int)和来自第二次调用的 "int version"(只有 F() "int version" 匹配)。

为什么会发生

基本上解释了发生了什么。但令人惊讶的是:

  • 您可以从 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> 的 lambda 隐式转换为 std::function<void(std::shared_ptr<int>)>.

  • 您不能将参数类型为 std::shared_ptr<int> 的 lambda 隐式转换为 std::function<void(std::shared_ptr<void>)>.

原因是在比较函数接口是更通用还是更具体时,规则是return类型必须是“协变的”,但参数类型必须是“逆变的”(Wikipedia; see also this SO Q&A).即,

Given the (pseudo-code) function interface types

C func1(A1, A2, ..., An)
D func2(B1, B2, ..., Bn)

then any function which is an instance of the func2 type is also an instance of the func1 type if D can convert to C and every Ai can convert to the corresponding Bi.

要了解为什么会这样,请考虑如果我们允许 std::function<std::shared_ptr<T>> 类型的 functionfunction 转换然后尝试调用它们会发生什么。

如果我们将 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);

Remarks: This constructor template shall not participate in overload resolution unless f is Lvalue-callable for argument types ArgTypes... and return type R.

其中“Lvalue-callable”本质上意味着具有给定类型的完美转发参数的函数调用表达式是有效的,并且如果 R 不是 cv void,表达式可以隐式转换为 R,加上当 f 是指向成员 and/or 的指针的情况下的注意事项,一些参数类型是 std::reference_wrapper<X>.

当尝试从任何可调用类型转换为 std::function 时,此定义本质上会自动检查逆变参数类型,因为它会检查目标 function 类型的参数类型是否为有效参数源可调用类型(允许允许的隐式转换)。

(在 C++17 之前,std::function::function(F) 模板构造函数根本没有任何 SFINAE 样式的限制。这对于像这样的重载情况以及试图检查转换是否正确的模板来说是个坏消息有效。)

请注意,参数类型的逆变实际上至少出现在 C++ 语言的另一种情况下(即使它不是允许的虚函数覆盖)。指向成员值的指针可以被认为是一个函数,它将 class 对象作为输入,returns 成员左值作为输出。 (并且从指向成员的指针初始化或分配 std::function 将以完全相同的方式解释其含义。)并且鉴于 class B 是 public 的明确基础 class 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)));
}