Lambda 的"this"捕获返回垃圾

Lambda's "this" capture returns garbage

本文关键字:返回 this Lambda      更新时间:2023-10-16

我正在实现自己的类,它提供其成员的延迟初始化。我遇到了一个奇怪的行为,在lambda中捕获this

下面是一个重现这个错误的例子。

//Baz.h
#include <memory>
#include <functional>
#include "Lazy.hpp"
struct Foo
{
    std::string str;
    Foo() = default;
    Foo(std::string str) : str(str) {}
    Foo(Foo&& that) : str(that.str) {  }
};
class Baz
{
    std::string str;
    Lazy<std::unique_ptr<Foo>> foo;
public:
    Baz() = default;
    Baz(const std::string& str) : str(str)
    {
        //lazy 'this->foo' initialization. 
        //Is capturing of 'this' valid inside ctors???.
        this->foo = { [this] { return buildFoo(); } };
    }
    Baz(Baz&& that) : foo(std::move(that.foo)), str(that.str) { }
    std::string getStr() const
    {
        return this->foo.get()->str;
    }
private:
    std::unique_ptr<Foo> buildFoo()
    {
        //looks like 'this' points to nothing here.
        return std::make_unique<Foo>(str); //got error on this line
    }
};
int _tmain(int argc, _TCHAR* argv[])
{
    ///Variant 1 (lazy Foo inside regular Baz):
    Baz baz1("123");
    auto str1 = baz1.getStr();
    ///Variant 2 (lazy Foo inside lazy Baz):
    Lazy<Baz> lazy_baz = { [](){ return Baz("123"); } };
    auto& baz2 = lazy_baz.get(); //get() method returns 'inst' member (and initialize it if it's not initialized) see below
    auto str2 = baz2.getStr();
    return 0;
}

变量1工作良好。

变种2崩溃,出现以下错误:

lambda_this_capture_test.exe中0x642DF4CB (msvcr120.dll)的未处理异常:0xC0000005:访问冲突读取位置0x00E0FFFC.

我使用vc++120编译器(从VS2013)。

这是我的简化Lazy类:

#pragma once
#include <memory>
#include <atomic>
#include <mutex>
#include <functional>
#include <limits>
template<
    class T,
        typename = std::enable_if_t<
        std::is_move_constructible<T>::value &&
        std::is_default_constructible<T>::value
        >
>
class Lazy
{
    mutable std::unique_ptr<T> inst;
    std::function<T(void)> func;
    mutable std::atomic_bool initialized;
    mutable std::unique_ptr<std::mutex> mutex;
public:
    Lazy()
        : mutex(std::make_unique<std::mutex>())
        , func([]{ return T(); })
    {
        this->initialized.store(false);
    }
    Lazy(std::function<T(void)> func)
        : func(std::move(func))
        , mutex(std::make_unique<std::mutex>())
    {
        this->initialized.store(false);
    }
//... <move ctor + move operator>
    T& get() const
    {
        if (!initialized.load())
        {
            std::lock_guard<std::mutex> lock(*mutex);
            if (!initialized.load())
            {
                inst = std::make_unique<T>(func());
                initialized.store(true);
            }
        }
        return *inst;
    }
};

所以我的问题是:为什么这个例子会崩溃?它是有效的捕获this内部构造器?

一般来说,在构造函数中捕获this是有效的。但是这样做时,您必须确保lambda不会比它捕获的this的对象活得更久。否则,捕获的this将变成悬空指针。

这正是在你的情况下发生的事情。捕获thisBazmain作用域内的临时构造(由return Baz("123")创建)。然后,当在Lazy<Baz>内部创建Baz时,std::function从临时Baz移动到Lazy<Baz>::inst所指向的Baz,但是在移动lambda的内部捕获的this仍然指向原始的临时Baz对象。这个对象就会超出作用域,然后砰的一声,你就有了一个悬浮指针。

下面张东辉的评论(使用enable_shared_from_this并在this之外捕获shared_ptr)为您的问题提供了一个潜在的解决方案。Lazy<T>类将T实例存储为std::unique_ptr<T>所拥有。如果将函子签名更改为std::function<std::unique_ptr<T>()>,就可以解决这个问题,因为惰性初始化器创建的对象将与存储在Lazy中的对象相同,因此捕获的this不会过早过期。

问题是捕获的this是一个特定的对象。在不更改捕获的this的情况下复制lambda。然后this悬挂,你的代码中断。

你可以使用智能指针来管理这个;但是你可能想要重新定义它。

我会修改Lazy。Lazy需要源代码T

我想给它一个签名。

template<
  class Sig, class=void
>
class Lazy;
template<
  class T,
  class...Sources
>
class Lazy<
  T(Sources...),
  std::enable_if_t<
    std::is_move_constructible<T>::value &&
    std::is_default_constructible<T>::value
  >
>
{
  std::function<T(Sources...)> func;
  // ...
  Lazy(std::function<T(Sources...)> func)
  // ...
  T& get(Sources...srcs) const {
  // ...
            inst = std::make_unique<T>(func(std::forward<Sources>(srcs)...));
  // ...

现在Baz有一个

Lazy<std::unique_ptr<Foo>(Baz const*)> foo;

与调整的ctor和getStr:

Baz(const std::string& str) : str(str)
{
    this->foo = { [](Baz const* baz) { return baz->buildFoo(); } };
}
std::string getStr() const
{
    return this->foo.get(this)->str;
}

,在main中,我们声明Baz来自非源数据:

Lazy<Baz()> lazy_baz = { []{ return Baz("123"); } };