将 std::function<void(Derived*)> 转换为 std::function<void(Base*)>

14

首先,我定义了两个类,它们是互相继承的。

class A {
};
class B : public A {
};

然后,我声明一个使用std::function<void(A*)>的函数:

void useCallback(std::function<void(A*)> myCallback);

最后,我从其他地方接收到一个不同(但在理论上兼容)类型的std::function,我想在我的回调函数中使用它:

std::function<void(B*)> thisIsAGivenFunction;

useCallback(thisIsAGivenFunction);

我的编译器(clang++)拒绝了这个代码,因为thisIsAGivenFunction的类型与预期类型不匹配。但是,由于B继承自A,所以让thisIsAGivenFunction被接受是有意义的。

它应该被接受吗?如果不是,为什么?如果是,那么我做错了什么?


1
可能是C++模板多态性的重复问题。 - Samuel
3
不是重复的,std::function 支持这样的 转换。但是在另一方面,你可以将 std::function<void(A*)> 转换为 std::function<void(B*)>,因为作用于 A* 的函数在接收到 B* 实例时也可以执行相同的操作,但反过来则不行。 - Piotr Skotnicki
你的问题来源于使用 std::function 作为回调而非接受一个 Functor 作为模板参数。除非有充分的理由需要这样做,否则请不要这样做。 - pmr
1
你的回调函数将什么传递给你的std::function?如果它向其中传递了一个指向class C:public A{}的指针,那么thisIsAGivenFunction会做什么?请回答这两个问题,它们都不是修辞问题。 - Yakk - Adam Nevraumont
3个回答

18

假设您的类层次结构稍微复杂一些:

struct A { int a; };
struct B : A { int b; };
struct C : A { int c; };

你可以使用以下函数:

void takeA(A* ptr)
{
    ptr->a = 1;
}

void takeB(B* ptr)
{
    ptr->b = 2;
}

因此,我们可以说takeA可用于任何派生自A类(或A本身)的实例,并且takeB可用于任何B类的实例:

takeA(new A);
takeA(new B);
takeA(new C);

takeB(new B);
// takeB(new A); // error! can't convert from A* to B*
// takeB(new C); // error! can't convert from C* to B*

现在,std::function是什么呢?它是一个可调用对象的包装器。它并不太关心存储函数对象的签名,只要该对象可以使用其std::function包装器的参数进行调用即可。请注意保留HTML标签。
std::function<void(A*)> a; // can store anything that is callable with A*
std::function<void(B*)> b; // can store anything that is callable with B*

你想做的是将 std::function<void(B*)> 转换为 std::function<void(A*)>。换句话说,你想要存储可调用对象,该对象接受 B*,并将其放入函数接受 A* 的包装类中。是否存在从 A*B* 的隐式转换?答案是否定的。
也就是说,可以使用指向类 C 实例的指针调用 std::function<void(A*)>
std::function<void(A*)> a = &takeA;
a(new C); // valid! C* is forwarded to takeA, takeA is callable with C*

如果std::function<void(A*)>可以“包装”仅采用B*的可调用对象实例,您希望它如何与C*一起使用?
std::function<void(B*)> b = &takeB;
std::function<void(A*)> a = b;
a(new C); // ooops, takeB tries to access ptr->b field, that C class doesn't have!

幸运的是,上面的代码无法编译。

然而,将其相反地做是可以的:

std::function<void(A*)> a = &takeA;
std::function<void(B*)> b = a;
b(new B); // ok, interface is narrowed to B*, but takeA is still callable with B*

2

它可以工作,但是方向相反:

struct A {};
struct B: A {};

struct X {};
struct Y: X {};

static X useCallback(std::function<X(B)> callback) {
    return callback({});
}

static Y cb(A) {
    return {};
}

int main() {
    useCallback(cb);
}

回调函数的签名声明了将会传递给它的内容以及返回的内容。如果回调函数不太关心类型,那么特定的回调函数可以采用不太具体的类型。同样地,它也可以返回更具体的类型,多余的信息将被剔除。请参考协变与逆变类型(简单来说就是输入/输出)。

好的。请参见std :: function中的逆变性http://cpptruths.blogspot.com/2015/11/covariance-and-contravariance-in-c.html#function_contravariance - Sumant

2

当有可能传递给你一个随机的水果,包括梨子时,你无法通过 &Foo(Apple) 来传递参数。


我不理解。在我的情况下,我的函数useCallback期望传入一个随机的"Fruit",但当我传入一个"Pear"时,编译器会抱怨。 - Ecco
1
@Ecco。我认为这是因为你的useCallback期望调用一个带有Fruit(可以是PearApple)的函数,而你提供了一个至少需要一个Pear的函数。 - Niall
2
Niall是对的。useCallback可能会给你的thisIsAGivenFunction一个指向C对象的A*指针。你的函数必须接受所有A对象,而不是一个子集。 - MSalters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接