Я реализую свой собственный класс, который обеспечивает ленивую инициализацию своего члена. И я столкнулся со странным поведением захвата 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 вылетает с этой ошибкой:
Необработанное исключение в 0x642DF4CB (msvcr120.dll) в lambda_this_capture_test.exe: 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
внутри конструктора. Но при этом вы должны убедиться, что лямбда не переживает объект, чей this
оно захвачено. В противном случае, что захватили this
становится висящим указателем.
Что именно и происходит в вашем случае. Baz
чья this
захвачен является временным, построенным внутри main
лямбда с return Baz("123")
, Затем, когда Baz
создается внутри Lazy<Baz>
, std::function
перенесен из этого временного Baz
в Baz
указал на Lazy<Baz>::inst
, но захваченный this
внутри переместившейся лямбды все еще указывает на оригинал, временный Baz
объект. Этот объект затем выходит из области видимости и бам, у вас есть свисающий указатель
Комментарий от Дунхуй Чжан ниже (используя enable_shared_from_this
и захватывая shared_ptr
в дополнение к this
) предоставляет потенциальное решение вашей проблемы. Ваш Lazy<T>
класс хранит T
экземпляры, принадлежащие std::unique_ptr<T>
, Если вы измените подпись функтора на std::function<std::unique_ptr<T>()>
, вы избавитесь от проблемы, так как объект, созданный ленивым инициализатором, будет тем же объектом, который хранится в Lazy
и так захваченный this
не истечет преждевременно.
Проблема в том, что this
захваченный является конкретным объектом. Вы копируете лямбду без изменения this
это захвачено. this
затем болтается, и ваш код ломается.
Вы можете использовать умные указатели для управления этим; но вы, вероятно, вместо этого хотите перебазировать его.
Я бы изменил 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"); } };