Для этого примера:
#include <vector>
#include <future>
#include <memory>
struct S {
std::string mName = "";
};
std::vector<std::future<void>> futures;
int main(){
{
auto s = std::make_shared<S>();
futures.emplace_back(std::async([s2=s]{}));
}
// will s2 be alive here assuming the async execution is done?
}
сохранит ли сохраненное будущее лямбду в живых или эта лямбда и ее захваты по значению будут уничтожены после завершения асинхронного выполнения?
@3CxEZiVlQ Они спрашивают о s, которое представляет собой лямбду, хранящуюся в глобальном объекте будущего. Не объект области видимости блока s.
Было бы странно, если бы в будущем объект функции был уничтожен раньше, чем его собственный. Я ожидаю, что функция будет частью общего состояния и не будет уничтожена до тех пор, пока не будет последней ссылки.
Я не думаю, что в Стандарте об этом что-то сказано, и, кроме побочного эффекта в ~S, нет никакого способа использовать это. Итак, как вы собираетесь использовать это на практике? И что значит после казни? -- когда будущее станет готовым?
gcc, по крайней мере, не уничтожает захват, пока вы не получите результат из будущего: godbolt.org/z/chq4cqraK
std::futures, возвращаемые std::async, блокируются. Таким образом, разрушение фьючерсов будет синхронизировано с завершением ступеней. И лямбда будет жива до тех пор, пока она нужна потоку, поэтому ее захват по переменным значения тоже будет активен. (Так что на самом деле фьючерсы без std::jthread — это способ синхронизации с потоком в стиле RAII)
@3CxEZiVlQ Отредактирован вопрос, теперь захват называется s2, что должно прояснить ситуацию.
@NathanOliver Функциональный объект в асинхронном режиме можно вызвать только один раз. Имеет смысл очистить его, как только он будет закончен; зачем хранить ресурсы, которые гарантированно никогда больше не будут использованы? Если бы я писал спецификацию для std Future, я бы заставил ее уничтожить вызываемый объект после его завершения и просто сохранить возвращаемое значение (или исключение). То есть, если бы я вообще не забыл это указать!
муаз, ты хочешь знать, что происходит на практике или что гарантирует стандарт? Я подозреваю, что это указано в стандарте, но я не уверен.
@Yakk - Адам Невромонт - вопрос сформулирован неправильно. Есть две точки, в которых будущее может избавиться от callable. Во-первых, когда общее состояние становится готовым, и во-вторых, когда значение извлекается из общего состояния. Компиляторы делают последнее, хотя ничто не мешает им сделать первое: godbolt.org/z/Gjxo9x6hz
@Gene Если вы добавите в свой код общее будущее (вот так), время жизни вызываемого объекта теперь продлится .get() ... на самом деле без веской причины. Я имею в виду, что причина в том, что внутренний объект общего состояния с возвращаемым значением также содержит вызываемый объект, но на самом деле у этих двух нет причин разделять время жизни.
@Yakk-AdamNevraumont Меня действительно беспокоит то, что говорит стандарт, или, по крайней мере, gcc и msvc, я просмотрел стандарт и ничего не нашел, но не уверен, что я что-то пропустил.





Объявление std::async, которое вы используете, соответствует [futures.async]:
template<class F, class... Args>
[[nodiscard]] future<invoke_result_t<decay_t<F>, decay_t<Args>...>>
async(F&& f, Args&&... args);
Прежде всего, исходный лямбда-объект, переданный для f, является временным объектом, который будет уничтожен в конце полного выражения.
futures.emplace_back(std::async([s2=s]{}))
и его захват s2 вместе с ним. Однако std::async переместится из лямбды в реальный объект, который будет вызван. Эта копия построена как будто с помощью auto(std::forward<F>(f)) (см. ниже). Вероятно, ваш вопрос заключается в том, когда этот объект будет уничтожен, забрав с собой последнюю ссылку на объект S.
Если вы не задаете аргумент политики запуска для std::async, по умолчанию используется std::launch::async | std::launch::deferred, что означает, что реализация может выбрать любую из двух политик. См. [futures.async]/3.
Если реализация выбирает std::launch::async, то она будет вести себя так, как если бы запускался поток, который выполняется ([futures.async]/3.1):
invoke(auto(std::forward<F>(f)), auto(std::forward<Args>(args))...)
В этом случае копия вызываемого объекта является временным объектом в этом выражении. Он будет уничтожен в потоке, вызывающем вызываемый объект, как только он вернется.
Если реализация выбирает std::launch::deferred, то она должна хранить копию g из f, инициализированную auto(std::forward<F>(f)), в общем состоянии std::future, и аналогичным образом копии xyz... из args..., инициализированные auto(std::forward<Args>(args)).... Когда вызывается отложенная функция, она должна выполнить
invoke(std::move(g), std::move(xyz)...)
См. [futures.async]/3.2.
Здесь уничтожение копии f не является частью выражения или отложенным вызовом функции.
[futures.async]/3.2 далее не уточняется, когда эти объекты будут уничтожены.
Я думаю, учитывая, что [futures.async]/3.2 четко указывает, когда и как копия f хранится в общем состоянии, и не говорит, что она заменяется или высвобождается, реализация не должна просто уничтожать ее. потому что он больше не нужен после завершения отложенного вызова.
std::future::wait также не указывает, что он высвобождает общее состояние будущего, поэтому он сам также не должен разрушать g.
С другой стороны, std::future::get предназначен для освобождения всего общего состояния и поэтому должен уничтожить g.
Перемещение std::future в futures также переместит состояние g, создав еще одну копию лямбды, но снова перенеся владение объектом S на s2 в новую копию.
Итак, для вашего конкретного примера, я думаю, неизвестно, будет ли объект S еще жив в закомментированной строке. Это зависит от того, какую политику выберет реализация: если она выберет launch::async, то она будет уничтожена, точнее, ее уничтожение произойдет в другом потоке несинхронно с достижением строки комментария. Если он выберет launch::deferred, то он не будет уничтожен (поскольку вы не вызвали ни одну функцию-член std::future, которая освободит его состояние).
sвыходит за рамки, в закомментированной строке он не может быть живым.