【问题标题】:C++ lambda: how to avoid slicing a reference if captured by valueC ++ lambda:如果按值捕获,如何避免对引用进行切片
【发布时间】:2016-01-13 02:36:47
【问题描述】:

我有一个方法,它接受一个参数,该参数是对基类的引用,我通过将方法实现包装在 queue<function<void()>> 中来将方法体的调用排入队列

问题是我希望按值捕获方法的参数,以便队列中的每个 lambda 可以使用自己的副本执行。

但如果我按值捕获,则引用参数的 lambda 副本似乎会将其分割为基类副本,而不是引用中的实际派生类。

如果我改为通过引用捕获参数,我确实会在 lambda 中获得实际的派生类,但 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

到目前为止,我设法在方法调用中保留派生类型的唯一方法是:

  1. 从调用代码传递堆分配对象。
  2. 在 lambda 中通过引用捕获。
  3. 确保在调用代码中不改变原始代码。
  4. 方法返回后终于删除了堆obj。

像这样:

    DerivedObj* obj = new DerivedObj();
    someMethod(*obj);
    delete obj;

但我希望能够只从调用代码堆栈传递一个引用,并且即使在 someMethod 内部发生触发对 someMethod 的另一个调用的情况也不会受到影响。

有什么想法吗?

我想到的一种方法,但我不知道该怎么做,在“someMethod”内部,将参数移动到堆中,执行 lambda,然后最终删除它(因为调用者不会真正使用它调用此方法后)。但不确定这是否真的是 hacky(我只是想它,因为这有点像 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 设为模板方法?您无法复制没有类型的对象。或者只是传递shared_ptr
  • @BryanChen 智能指针可以工作,但我试图避免它。与模板方法相同。
  • 请看C++ Core Guidelines。我认为用 C++11 或 C++14 标记的 C++ 代码应该认真考虑使用智能指针而不是裸 new 和 delete。
  • @LebenAsa 使用原始指针是一个选择问题,在这种情况下,只有一个建议的解决方案(我对此不满意,这就是为什么我首先要询问替代方案的问题) .智能指针只是该语言的一个特性(与 Objective-c 不同,它们内置于语言中)并且它们不是在所有上下文中的正确选择(即,此代码将在紧密的渲染循环中运行)。最后,11 和 14 标签是为了防止 11 或 14 规范具有一些防止切片的功能,而不是暗示样式。仍然,要点:)。
  • 在 c++14 中,您可以将 auto frozenEvent 替换为 std::unique_ptr&lt;Event&gt;,然后通过移动而不是按值进行捕获。

标签: c++ c++11 lambda c++14


【解决方案1】:

如果不进行一些侵入性更改,似乎不可能达到您想要的结果。目前,您有一个调用者更改或销毁其对象,而不关心引用是否仍在队列中。对于这种经常来电者,您唯一的选择就是制作副本。创建 lambda 的函数不知道你要传递什么类型的对象,所以它不知道如何复制它。

有不同的方法可以解决您的问题:您可以让调用者知道额外的引用,方法是让它持有 shared_ptr 并将共享指针复制到 lambda 中。这解决了生命周期问题,但仍然取决于调用者不修改对象。您还可以让编译器通过使该函数成为模板来为每个派生类生成不同的入队函数。模板的每个实例都知道如何复制其特定类型。您已经驳回了这两种解决方案。我只知道另外一种方法,即向您的基类添加一个虚拟克隆函数,您在派生类中实现该函数以创建堆副本。

【讨论】:

  • 看起来像。谢谢。我现在采用堆复制方法(如我的问题的更新所示)。
  • @SaldaVonSchwartz,如果您在调用者中静态知道类型,则可以将该函数设为模板。但除此之外,clone 方法是唯一的方法。您可以尝试使用Boost.Type Erasure 来处理克隆(它可以处理克隆对象而无需为其添加基类,但要学习的东西很多)。
  • @JanHudec 试图远离 Boost,因为我是故意“从头开始”做这些事情的。不过谢谢。
  • @SaldaVonSchwartz 从头开始​​做所有事情可能是一种非常有教育意义的经历,但您应该记住,不使用智能指针与 C++ 开发的总体趋势相矛盾。特别是因为我们现在有了 unique_ptr ,它应该不会引入手动使用 new 和正确删除的开销。如果您打算从事现代 C++ 项目(无论是开源项目还是某些雇主),您应该习惯使用标准库提供的工具。了解最常见的 boost 库也有帮助。不过,类型擦除并不常用。
【解决方案2】:

您可以使用[DerivedObj obj=obj](){} 而不是[=]{} 为您的lambda,它会以您想要的方式准确捕获您想要的内容。

【讨论】:

    【解决方案3】:

    改为使用指针作为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
    

    【讨论】:

    • 如果你按值捕获指针,你得到的结果与按引用捕获对象相同:你仍然没有得到对象的副本。所以这并不能回答我认为的问题。
    • 啊,是的。我错过了 OP 希望为队列中的每个项目获取副本的事实。
    • 我更新了我的问题以显示队列的用例,因为它可能会进一步说明问题。到目前为止,我通过让有问题的对象有一个克隆方法来修复它,我用它来创建它的堆版本,以便在方法内部使用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多