C++ lambda:如果按值捕获,如何避免切片引用
C++ lambda: how to avoid slicing a reference if captured by value
我有一个方法接受一个参数,该参数是对基 class 的引用,我通过将方法实现包装在 queue<function<void()>>
中来排队调用方法体
问题是我希望按值捕获方法的参数,以便队列中的每个 lambda 都可以使用自己的副本执行。
但是,如果我按值捕获,引用参数的 lambda 副本似乎将其切片,给我留下一个基础 class 副本,而不是引用中实际派生的 class。
如果我改为通过引用捕获参数,我确实会在 lambda 中得到实际派生的 class,但是 obj 可能会在方法调用之间超出范围,或者它的状态可能会改变。
请注意,该方法应该是可重入的,但不是异步的,也不是并发的。
这是我的意思的一个例子(省略队列):
struct BaseObj {
virtual ~BaseObj() = default;
};
struct DerivedObj : public BaseObj {
};
void someMethod(BaseObj& obj) {
// obj is of type BaseObj:
std::cout << "\nobj type:" << typeid(obj).name();
auto refLambda = [&] {
// captured obj is of type DerivedObj:
std::cout << "\nrefLambda::obj type:" << typeid(obj).name();
};
auto valLambda = [=] {
// captured obj is of type BaseObj:
// presumably because it was copied by value, which sliced it.
std::cout << "\nvalLambda::obj type:" << typeid(obj).name();
};
refLambda();
valLambda();
}
这样调用方法时的输出:
DerivedObj obj{};
someMethod(obj);
是:
obj type:10DerivedObj
refLambda::obj type:10DerivedObj
valLambda::obj type:7BaseObj
到目前为止,我设法在方法调用中保留派生类型的唯一方法是:
- 从调用代码传递堆分配对象。
- 在 lambda 中通过引用捕获。
- 确保不要改变调用代码中的原始代码。
- 在方法returns之后终于删除了堆obj。
像这样:
DerivedObj* obj = new DerivedObj();
someMethod(*obj);
delete obj;
但我希望能够只从调用代码堆栈传递一个引用,并且即使在 someMethod
内部发生某些事情触发了对 someMethod
.[=19= 的另一次调用也没有问题]
有什么想法吗?
我想到但不确定该怎么做的一种方法是,在 `someMethod' 中,将参数移动到堆中,执行 lambda,然后最后删除它(因为调用者不会真正在调用此方法后使用它)。但不确定这是否真的有问题(我只是想到它,因为这有点像 Objective-C 块所做的)。
更新:
这是我目前的解决方案:
void Object::broadcast(Event& event) {
auto frozenEvent = event.heapClone();
auto dispatchBlock = [=]() {
for (auto receiver : receivers) {
receiver.take(event);
}
delete frozenEvent;
_private->eventQueue.pop();
if (!_private->eventQueue.empty()) {
_private->eventQueue.front()();
}
};
_private->eventQueue.push(dispatchBlock);
if (_private->eventQueue.size() == 1) {
_private->eventQueue.front()();
}
}
是的,我知道,我正在使用原始指针...(eeeeevil...:p),但至少我可以使用 ref 参数保留方法的签名。
克隆方法大致如下:
template <class T>
struct ConcreteEvent : public Event {
virtual Event* heapClone() {
return new T(*(T*)this);
}
// .... more stuff.
};
改用指针作为 someMethod
参数:
void someMethod(BaseObj* obj) {
std::cout << "\nobj type:" << typeid(*obj).name();
auto refLambda = [&] {
std::cout << "\nrefLambda::obj type:" << typeid(*obj).name();
};
auto valLambda = [=] {
std::cout << "\nvalLambda::obj type:" << typeid(*obj).name();
};
refLambda();
valLambda();
}
int main() {
DerivedObj obj;
someMethod(&obj);
}
在 VS2013 中测试它会打印:
obj type:struct DerivedObj
refLambda::obj type:struct DerivedObj
valLambda::obj type:struct DerivedObj
如果不进行一些侵入性更改,似乎无法实现您想要的结果。目前,您有一个调用者更改或销毁其对象而不关心引用是否仍在队列中。对于这种经常来电的人,您唯一的选择就是制作副本。创建 lambda 的函数不知道你要传递什么类型的对象,所以它不知道如何复制它。
有不同的方法可以解决您的问题:您可以让调用者知道额外的引用,方法是让它持有 shared_ptr
并将共享指针复制到 lambda 中。这解决了 life-time 问题,但仍然取决于调用者不修改对象。您还可以让编译器为每个派生的 class 生成不同的排队函数,方法是使该函数成为模板。模板模板的每个实例都知道如何复制其特定类型。您已经驳回了这两种解决方案。我只知道另一种方法,即向您在派生 classes 中实现的基础 class 添加虚拟克隆函数以创建堆副本。
而不是 [=]{}
你的 lambda,你可以做 [DerivedObj obj=obj](){}
这将准确地捕捉你想要的东西。
我有一个方法接受一个参数,该参数是对基 class 的引用,我通过将方法实现包装在 queue<function<void()>>
问题是我希望按值捕获方法的参数,以便队列中的每个 lambda 都可以使用自己的副本执行。
但是,如果我按值捕获,引用参数的 lambda 副本似乎将其切片,给我留下一个基础 class 副本,而不是引用中实际派生的 class。
如果我改为通过引用捕获参数,我确实会在 lambda 中得到实际派生的 class,但是 obj 可能会在方法调用之间超出范围,或者它的状态可能会改变。
请注意,该方法应该是可重入的,但不是异步的,也不是并发的。
这是我的意思的一个例子(省略队列):
struct BaseObj {
virtual ~BaseObj() = default;
};
struct DerivedObj : public BaseObj {
};
void someMethod(BaseObj& obj) {
// obj is of type BaseObj:
std::cout << "\nobj type:" << typeid(obj).name();
auto refLambda = [&] {
// captured obj is of type DerivedObj:
std::cout << "\nrefLambda::obj type:" << typeid(obj).name();
};
auto valLambda = [=] {
// captured obj is of type BaseObj:
// presumably because it was copied by value, which sliced it.
std::cout << "\nvalLambda::obj type:" << typeid(obj).name();
};
refLambda();
valLambda();
}
这样调用方法时的输出:
DerivedObj obj{};
someMethod(obj);
是:
obj type:10DerivedObj
refLambda::obj type:10DerivedObj
valLambda::obj type:7BaseObj
到目前为止,我设法在方法调用中保留派生类型的唯一方法是:
- 从调用代码传递堆分配对象。
- 在 lambda 中通过引用捕获。
- 确保不要改变调用代码中的原始代码。
- 在方法returns之后终于删除了堆obj。
像这样:
DerivedObj* obj = new DerivedObj();
someMethod(*obj);
delete obj;
但我希望能够只从调用代码堆栈传递一个引用,并且即使在 someMethod
内部发生某些事情触发了对 someMethod
.[=19= 的另一次调用也没有问题]
有什么想法吗?
我想到但不确定该怎么做的一种方法是,在 `someMethod' 中,将参数移动到堆中,执行 lambda,然后最后删除它(因为调用者不会真正在调用此方法后使用它)。但不确定这是否真的有问题(我只是想到它,因为这有点像 Objective-C 块所做的)。
更新:
这是我目前的解决方案:
void Object::broadcast(Event& event) {
auto frozenEvent = event.heapClone();
auto dispatchBlock = [=]() {
for (auto receiver : receivers) {
receiver.take(event);
}
delete frozenEvent;
_private->eventQueue.pop();
if (!_private->eventQueue.empty()) {
_private->eventQueue.front()();
}
};
_private->eventQueue.push(dispatchBlock);
if (_private->eventQueue.size() == 1) {
_private->eventQueue.front()();
}
}
是的,我知道,我正在使用原始指针...(eeeeevil...:p),但至少我可以使用 ref 参数保留方法的签名。
克隆方法大致如下:
template <class T>
struct ConcreteEvent : public Event {
virtual Event* heapClone() {
return new T(*(T*)this);
}
// .... more stuff.
};
改用指针作为 someMethod
参数:
void someMethod(BaseObj* obj) {
std::cout << "\nobj type:" << typeid(*obj).name();
auto refLambda = [&] {
std::cout << "\nrefLambda::obj type:" << typeid(*obj).name();
};
auto valLambda = [=] {
std::cout << "\nvalLambda::obj type:" << typeid(*obj).name();
};
refLambda();
valLambda();
}
int main() {
DerivedObj obj;
someMethod(&obj);
}
在 VS2013 中测试它会打印:
obj type:struct DerivedObj
refLambda::obj type:struct DerivedObj
valLambda::obj type:struct DerivedObj
如果不进行一些侵入性更改,似乎无法实现您想要的结果。目前,您有一个调用者更改或销毁其对象而不关心引用是否仍在队列中。对于这种经常来电的人,您唯一的选择就是制作副本。创建 lambda 的函数不知道你要传递什么类型的对象,所以它不知道如何复制它。
有不同的方法可以解决您的问题:您可以让调用者知道额外的引用,方法是让它持有 shared_ptr
并将共享指针复制到 lambda 中。这解决了 life-time 问题,但仍然取决于调用者不修改对象。您还可以让编译器为每个派生的 class 生成不同的排队函数,方法是使该函数成为模板。模板模板的每个实例都知道如何复制其特定类型。您已经驳回了这两种解决方案。我只知道另一种方法,即向您在派生 classes 中实现的基础 class 添加虚拟克隆函数以创建堆副本。
而不是 [=]{}
你的 lambda,你可以做 [DerivedObj obj=obj](){}
这将准确地捕捉你想要的东西。