«Лямбда» это » захват возвращает мусор

Я реализую свой собственный класс, который обеспечивает ленивую инициализацию своего члена. И я столкнулся со странным поведением захвата 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 внутри конструкторы?

4

Решение

В общем, это действительно для захвата 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 не истечет преждевременно.

8

Другие решения

Проблема в том, что 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"); } };
0

По вопросам рекламы [email protected]