Lambda 的 "this" 捕获 returns 垃圾
Lambda's "this" capture returns garbage
我正在实现我自己的 class,它提供了其成员的惰性初始化。而且我遇到了在 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 崩溃并出现此错误:
Unhandled exception at 0x642DF4CB (msvcr120.dll) in lambda_this_capture_test.exe: 0xC0000005: Access violation reading location 0x00E0FFFC.
我正在使用 vc++120 编译器(来自 VS2013)。
这是我的简化版 Lazy
class:
#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
将成为悬空指针。
这正是您的情况。捕获 this
的 Baz
是在 main
作用域的 lambda(由 [=16= 创建的)内部构建的临时构造。然后,当创建 Baz
在 Lazy<Baz>
中,std::function
从那个临时的 Baz
移动到 Lazy<Baz>::inst
指向的 Baz
, 但是捕获的 this
在移动的 lambda 内部仍然指向原始的临时 Baz
对象。 该对象然后超出范围并且 wham,你有一个悬空指针.
下面 Donghui Zhang 的评论(使用 enable_shared_from_this
并在 this
之外捕获 shared_ptr
)为您的问题提供了一个潜在的解决方案。您的 Lazy<T>
class 将 T
个实例存储为 std::unique_ptr<T>
拥有。如果将仿函数签名更改为 std::function<std::unique_ptr<T>()>
,您将摆脱这个问题,因为惰性初始化程序创建的对象将与存储在 Lazy
中的对象相同,因此捕获的 this
不会提前过期。
问题是捕获的 this
是一个特定的对象。您在不更改捕获的 this
的情况下复制 lambda。 this
然后悬空,您的代码中断。
你可以使用智能指针来管理它;但您可能想对其进行变基。
我会修改Lazy
。 Lazy 需要 source 以及 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"); } };
我正在实现我自己的 class,它提供了其成员的惰性初始化。而且我遇到了在 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 崩溃并出现此错误:
Unhandled exception at 0x642DF4CB (msvcr120.dll) in lambda_this_capture_test.exe: 0xC0000005: Access violation reading location 0x00E0FFFC.
我正在使用 vc++120 编译器(来自 VS2013)。
这是我的简化版 Lazy
class:
#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
将成为悬空指针。
这正是您的情况。捕获 this
的 Baz
是在 main
作用域的 lambda(由 [=16= 创建的)内部构建的临时构造。然后,当创建 Baz
在 Lazy<Baz>
中,std::function
从那个临时的 Baz
移动到 Lazy<Baz>::inst
指向的 Baz
, 但是捕获的 this
在移动的 lambda 内部仍然指向原始的临时 Baz
对象。 该对象然后超出范围并且 wham,你有一个悬空指针.
下面 Donghui Zhang 的评论(使用 enable_shared_from_this
并在 this
之外捕获 shared_ptr
)为您的问题提供了一个潜在的解决方案。您的 Lazy<T>
class 将 T
个实例存储为 std::unique_ptr<T>
拥有。如果将仿函数签名更改为 std::function<std::unique_ptr<T>()>
,您将摆脱这个问题,因为惰性初始化程序创建的对象将与存储在 Lazy
中的对象相同,因此捕获的 this
不会提前过期。
问题是捕获的 this
是一个特定的对象。您在不更改捕获的 this
的情况下复制 lambda。 this
然后悬空,您的代码中断。
你可以使用智能指针来管理它;但您可能想对其进行变基。
我会修改Lazy
。 Lazy 需要 source 以及 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"); } };