у меня есть std::packaged_task
содержащий лямбду, которая захватывает переменную путем копирования. Когда это std::packaged_task
удаляется, я ожидаю, что переменная, живущая внутри лямбды, будет уничтожена, но я заметил, что если я получу связанный std::future
за это std::packaged_task
, future
Объект продлевает время жизни переменной внутри лямбды.
Например:
#include <iostream>
#include <future>
class Dummy
{
public:
Dummy() {std::cout << this << ": default constructed;" << std::endl;}
Dummy(const Dummy&) {std::cout << this << ": copy constructed;" << std::endl;}
Dummy(Dummy&&) {std::cout << this << ": move constructed;" << std::endl;}
~Dummy() {std::cout << this << ": destructed;" << std::endl;}
};
int main()
{
std::packaged_task<void()>* p_task;
{
Dummy ScopedDummy;
p_task = new std::packaged_task<void()>([ScopedDummy](){std::cout << "lambda call with: " << &ScopedDummy << std::endl;});
std::cout << "p_task completed" << std::endl;
}
{
std::future<void> future_result;
{
future_result = p_task->get_future();
(*p_task)();
delete p_task;
}
std::cout << "after p_task has been deleted, the scope of future_result determines the scope of the dummy inside p_task" << std::endl;
}
std::cout << "p_task cleans up when future_result dies" << std::endl;
}
Возможный вывод:
0x7fff9cf873fe: default constructed;
0x7fff9cf873ff: copy constructed;
0x1904b38: move constructed;
0x7fff9cf873ff: destructed;
0x7fff9cf873fe: destructed;
lambda call with: 0x1904b38
after p_task has been deleted, the scope of future_result determines the scope of the dummy inside p_task
0x1904b38: destructed;
p_task cleans up when future_result dies
Таким образом, срок жизни объекта внутри лямбды увеличен за счет future_result
,
Если мы закомментируем строку future_result = p_task->get_future();
Возможный вывод:
0x7fff57087896: default constructed;
0x7fff57087897: copy constructed;
0x197cb38: move constructed;
0x7fff57087897: destructed;
0x7fff57087896: destructed;
lambda call with: 0x197cb38
0x197cb38: destructed;
after p_task has been deleted, the scope of future_result determines the scope of the dummy inside p_task
p_task cleans up when future_result dies
Мне было интересно, какой механизм вступает в игру здесь, делает ли std::future
содержит какую-то ссылку, которая поддерживает связанные объекты?
смотря на исходники gcc7.2.0 packaged_task, мы читаем:
packaged_task(allocator_arg_t, const _Alloc &__a, _Fn &&__fn)
: _M_state(__create_task_state<_Res(_ArgTypes...)>(std::forward<_Fn>(__fn), __a)){}
~packaged_task()
{
if (static_cast<bool>(_M_state) && !_M_state.unique())
_M_state->_M_break_promise(std::move(_M_state->_M_result));
}
где _M_state
является shared_ptr для внутреннего общего ресурса packaged_task. Итак, получается, что НКУ сохраняет вызываемый как часть упакованной_таски общее состояние, следовательно, связывание вызываемого времени жизни, для которого среди packaged_task, future, shared_future умирает последним.
в сравнении, лязг нет, уничтожая вызываемый объект, когда упакованное задание уничтожается (фактически, моя копия clang будет хранить вызываемый элемент как правильный член).
Кто прав? стандарт не очень ясно определяет время существования хранимой задачи; с одной стороны, мы имеем
[[Futures.task]]
packaged_task определяет тип для упаковка функции или вызываемого объекта так что возвращаемое значение функции или вызываемого объекта сохраняется в будущем при его вызове.
packaged_task (F&& f) […] Создает новый объект packaged_task с общим состоянием и инициализирует сохраненная задача объекта с помощью std :: forward (f).
packaged_task (packaged_task&& шк) […]Перемещает сохраненную задачу от rhs к * этому.
reset () […] Эффекты: Как будто * this = packaged_task (std :: move (f)), где f — это задача хранится в * этом.
это говорит о том, что вызываемый объект принадлежит packaged_task, но у нас также есть
[[Futures.state]]
-Многие из классов, представленных в этом подпункте, используют некоторое состояние для передачи результатов. Это общее состояние состоит из некоторой информации о состоянии и некоторого (возможно, еще не оцененного) результата, который может быть (возможно, пустым) значением или исключением. [Примечание: фьючерсы, обещания и задачи, определенные в этом разделе, ссылаются на такое общее состояние. -endnote]
-[Примечание: результатом может быть любой вид объекта включая функцию для вычисления этого результата, как by async […]]
а также
[futures.task.members]
-packaged_task (F&& f); […] вызов копии f должен вести себя так же, как вызов f […] — ~ packaged_task (); Эффекты: Отказ от любого общего состояния
Предполагается, что вызываемое может быть сохранено в общем состоянии, и что не следует полагаться на любое вызываемое поведение для каждого экземпляра ( это может быть интерпретировано как включающее вызываемые побочные эффекты конца жизни; кстати, это также подразумевает, что ваш вызываемый объект не является строго допустимым, поскольку он ведет себя не так, как его копия); кроме того, ничего не упоминается о сохраненной задаче в dtor.
В целом, я думаю, что Clang следует формулировке более последовательно, хотя ничего не кажется эксплицитно запрещающее поведение gcc. Тем не менее, я согласен, что это должно быть лучше задокументировано, потому что в противном случае это может привести к неожиданным ошибкам …
Других решений пока нет …