Я немного смущен или параноик, учитывая закономерность:
void setup(boost::asio::io_context &context) {
const auto completion_handler = [](std::exception_ptr ptr) {
if (ptr) {
std::cout << "Rethrowing in completion handler" << std::endl;
std::rethrow_exception(ptr);
} else {
std::cout << "Completed without error" << std::endl;
}
};
boost::asio::co_spawn(context, coroutine_with_rethrow_completion_handler(), completion_handler);
}
Время жизни completion_handler
, т. е. локальное, ок?
AFAIK, конечно, любой локальный захват был бы плохим, так как они бы вышли за рамки, когда в конечном итоге boost::asio::io_context
запустит этот обработчик. А как насчет времени жизни этого самого обработчика, то есть функтора, то есть самой лямбды?
boost::asio::co_spawn
принимает &&
, который AFAIK должен быть ссылкой на пересылку (в списке шаблонов этой функции boost есть много макросов), и идеально перенаправляет эту функцию завершения в внутренности boost::asio и документацию по co_spawn
не высказывает каких-либо пожизненных замечаний по поводу токена завершения.
Поэтому я боюсь, что в конце концов в boost::asio::io_context
будет храниться только ссылка на эту лямбду, то есть context
, и когда мы фактически выполним лямбду в io_context::run
в main
после того, как лямбда выйдет за пределы области действия в setup
, у нас будет UB.
#include <iostream>
#include <boost/asio.hpp>
boost::asio::awaitable<void> coroutine_with_rethrow_completion_handler() {
std::cout << "Coroutine executes with rethrow completion handler\n";
throw std::runtime_error("Test throw from coroutine!");
co_return;
}
void setup(boost::asio::io_context &context) {
const auto completion_handler = [](std::exception_ptr ptr) {
if (ptr) {
std::cout << "Rethrowing in completion handler" << std::endl;
std::rethrow_exception(ptr);
} else {
std::cout << "Completed without error" << std::endl;
}
};
boost::asio::co_spawn(context, coroutine_with_rethrow_completion_handler(), completion_handler);
}
int main() {
boost::asio::io_context context;
setup(context);
std::thread t([&context]() {
try {
while (true) {
context.run();
return;
}
} catch (std::exception &e) {
std::cerr << "Exception in context::run(): " << e.what() << "\n";
}
});
t.join();
}
Лямбда без захвата ([]
) похожа на простую функцию. Таким образом, completion_handler
не больше и не меньше, чем указатель на эту функцию. Сама функция должна иметь время жизни от начала до конца процесса - независимо от ее области действия. Я не вижу ничего, что могло бы зависнуть... (Или сопрограммы меняют эти "правила"?)
@Caleth Потому что CompletionToken находится в списке шаблонов в определении этой перегрузки co_spawn, и если я правильно понимаю Clion, то этот BOOST_ASIO_INITFN_AUTO_RESULT_TYPE расширяется до чего-то вроде Typename Completion Token .. Completion Token&&.
@Scheff'sCat Все эти стирания типов и «это похоже на экземпляр функциональной структуры» вызывают у меня очень неясное представление о времени жизни лямбда-выражений, особенно когда они затем передаются в библиотеку тяжелой пересылки шаблонов. Конечно, если бы вы могли указать на параграф в стандарте C++, в котором говорится, что лямбда-выражение похоже на функцию, т.е. имеет статическое время жизни, я был бы очень благодарен :-)
Я на 99% уверен, что в конечном итоге он окажется нессылочным элементом данных какого-то объекта.
@Caleth Ваш 1% и мои ~ 10% неопределенности - вот что делает меня параноиком :-)
Извините, я скорее практик, чем языковой юрист. Я пытался гуглить, но ничего серьезного не нашел. То, что я нашел, может меня поддержать: Жизненный цикл лямбды без захвата , blog.the-pans.com/cpp-coroutine-and-lambdas-lifetime , clang.llvm.org/extra/clang-tidy /checks/cppcoreguidelines/…
Также blog.the-pans.com/cpp-coroutine-and-lambdas-lifetime
Насколько я понимаю, один говорит, что это все еще проблематично, другой - нет, но оба только размышляют о реализации среды выполнения C++. По какой-то причине я прочитал, что лямбда похожа на структуру с членом объектов списка захвата и указателем на функцию со стертым типом. То, что код в лямбда-выражении будет указывать на статическую продолжительность хранения, кажется очевидным, но как насчет указателя в структуре? Он будет уничтожен, поэтому его разыменование позже будет UB. И, конечно же, этот шаблон взят из проекта, страдающего от UB. Вот почему я должен быть уверен ;-(
Я имею в виду, что сейчас кажется, что тип этой лямбды действительно является просто указателем функции на статическое хранилище, то есть функцией. Но как насчет времени жизни самого указателя? Его автоматическое хранилище настолько мертво при вызове io_context::run, что возникает большой вопрос, скопировало ли дерево вызовов co_spawn этот указатель или ссылалось на него. В первом случае все будет в порядке, так как статическое хранилище все еще существует, но в более позднем случае сначала попытается разыменовать мертвый указатель, который, к счастью, все еще указывает на статическое хранилище, то есть UB.
Интересный вопрос, если для беззахватного закрытия нет UB, связанного с завершением жизни. Все потому, что замыкание должно вести себя аналогично ссылочному коду, предлагаемому стандартным документом (§ 8.4.5.1 Типы замыканий):
struct Closure {
template<class T> auto operator()(T t) const { ... }
template<class T> static auto lambda_call_operator_invoker(T a) {
// forwards execution to operator()(a) and therefore has
// the same return type deduced
...
}
template<class T> using fptr_t =
decltype(lambda_call_operator_invoker(declval<T>())) (*)(T);
template<class T> operator fptr_t<T>() const {
return &lambda_call_operator_invoker;
}
};
Если у нас есть экземпляр, его можно привести к указателю на функцию, после чего сам объект больше не нужен — приведение возвращает адрес статической функции-члена. И замыкание будет обнаружено SFINAE как нечто конвертируемое в указатель на функцию в co_spawn
, после чего будет сохранен только указатель. Если уж как co_spawn
реализовано, то приходится полагаться на качество реализации.
Я смущен, что вы считаете этот вопрос интересным, но отвечаете на весьма ограниченную его часть. В частности, ваш ответ зависит от того, «его можно привести к указателю на функцию», что вызывает вопрос, насколько ответ актуален на практике. Ваш ответ кажется актуальным только тогда, когда ОП редактирует свой код, чтобы он читал co_spawn(context, coroutine_with_rethrow_completion_handler(), +completion_handler);
или что-то подобное?
Я имею в виду, что почти попросил ссылку на стандарт cpp для пожизненных дебатов типа. Насколько я понимаю, оба ваших ответа дополняют мое понимание, то есть рабочую модель, которая: boost::asio::co_spawn
будет копировать в пределах setup
, по крайней мере, для этой специализации (обработчик исключений). И, написав эти строки, я только что понял, что в противном случае, независимо от типа completion_handler
, пока это именованная переменная, а не std::move
d, это будет небезопасно. Хм.
Протокол CompletionToken
отделяет механизм завершения. Что сохраняется, зависит от типа токена завершения.
Механизм специализирован с помощью признака asio::async_result
: https://www.boost.org/doc/libs/1_85_0/doc/html/boost_asio/reference/async_result.html#boost_asio.reference.async_result.requirements
Одним из типов, предоставляемых этим признаком, является complete_handler_type : «Конкретный тип обработчика завершения для конкретной подписи».
Априори ясно, что может потребоваться создание нового экземпляра. Это означает, что время существования экземпляра определяется асинхронной операцией, а не вызывающим кодом.
Для обычного вызываемого объекта в качестве токена завершения completion_handler_type
является синонимом типа аргумента обработчика пользователя:
using H = decltype(completion_handler);
using Protocol = boost::asio::async_result<boost::asio::decay_t<H>, void(std::exception_ptr)>;
Protocol::completion_handler_type stored{std::move(completion_handler)};
Что реализовано // ЗДЕСЬ:
template <typename CompletionToken,
BOOST_ASIO_COMPLETION_SIGNATURE... Signatures>
class completion_handler_async_result
{
public:
typedef CompletionToken completion_handler_type; // HERE
typedef void return_type;
explicit completion_handler_async_result(completion_handler_type&)
{
}
Распад гарантирует, что любые ссылочные/константные/изменчивые уточнения будут удалены. Следовательно: обработчик копируется или перемещается.
Как вы и ожидали, обработчик не сохраняется по ссылке. Причина, по которой он используется как универсальная ссылка, состоит в том, чтобы приспособить
_Фактически, адаптеры токенов уже могут привязывать ссылки к токену/обработчику.
bind_executor
/bind_cancellation_slot
хранит копии, ноasio::redirect_error
хранит ссылку на объектerror_code
. Несмотря на это, сам обработчик будет скопирован/перемещен.
Если вы поделитесь ссылкой на код, демонстрирующий UB, я, возможно, поищу потенциальные проблемы.
Честно говоря, поскольку это была идеальная пересылка ссылки на пересылку, а параметр — именованная переменная, я предположил, что она будет принимать ее по ссылке. Но я думаю, что старый добрый std::decay
предотвратил это, как в std::bind или std::thread(). Если нет, мне придется пересмотреть свое понимание пересылки ссылок. Спасибо большое, теперь мне нужно решить, кто получит оценку, +1 обоим. И база кода является коммерческой, в несколько тысяч LOC, с NDA, но спасибо :-) Лучшее предположение на данный момент, возможно, это версия clang против аномалий ibstdc++ или ошибки сокета ОС, или восходящего потока, или ложноотрицательного результата в статическом анализе...
Как вы думаете, почему это ссылка на пересылку, а не ссылка на rvalue?