Я пишу асинхронную функцию в стиле Boost.Asio, которая принимает CompletionToken
аргумент.
Аргумент может быть функцией, объектом функции, лямбда-выражением, будущим, ожидаемым и т. д.
CompletionToken
— параметр шаблона. Если я не ограничу аргумент, функция может соответствовать неожиданному параметру, например int
. Поэтому я хочу написать какое-то ограничение, используя std::enable_if
. (Моя среда - С++ 17).
Если CompletionToken
принимает параметры, то я могу использовать std::is_invocable
для проверки.
См. f1
в моем примере кода.
Однако у меня возникла проблема. Если CompletionToken
не принимает параметров, то boost::asio::use_future
совершает ошибку.
См. f2
в моем примере кода.
После некоторых попыток и ошибок я получил свое решение. Это объединение std::is_invocable
и use_future_t
проверки по ИЛИ (||
).
См. f3
в моем примере кода.
Но это не так элегантно. Кроме того, я не уверен, что Boost.Asio поддерживает другие функции, например, use_awaitable_t требует аналогичной проверки прямого соответствия.
Я пытался найти Boost.Asio, предоставляющий признаки типа или предикат, например is_completion_token
, но не смог его найти.
Есть ли лучший способ проверить CompletionToken
?
Ссылка на Godbolt https://godbolt.org/z/sPeMo1GEK
Полный код:
#include <type_traits>
#include <boost/asio.hpp>
// Callable T takes one argument
template <
typename T,
std::enable_if_t<std::is_invocable_v<T, int>>* = nullptr
>
void f1(T) {
}
// Callable T takes no argument
template <
typename T,
std::enable_if_t<std::is_invocable_v<T>>* = nullptr
>
void f2(T) {
}
template <template <typename...> typename, typename>
struct is_instance_of : std::false_type {};
template <template <typename...> typename T, typename U>
struct is_instance_of<T, T<U>> : std::true_type {};
// Callable T takes no argument
template <
typename T,
std::enable_if_t<
std::is_invocable_v<T> ||
is_instance_of<boost::asio::use_future_t, T>::value
>* = nullptr
>
void f3(T) {
}
int main() {
// no error
f1([](int){});
f1(boost::asio::use_future);
// same rule as f1 but use_future got compile error
f2([](){});
f2(boost::asio::use_future); // error
// a little complecated typechecking, then no error
f3([](){});
f3(boost::asio::use_future);
}
Выходы:
Output of x86-64 clang 13.0.1 (Compiler #1)
<source>:45:5: error: no matching function for call to 'f2'
f2(boost::asio::use_future); // error
^~
<source>:17:6: note: candidate template ignored: requirement 'std::is_invocable_v<boost::asio::use_future_t<std::allocator<void>>>' was not satisfied [with T = boost::asio::use_future_t<>]
void f2(T) {
^
1 error generated.
Если у вас есть концепции С++ 20, посмотрите ниже. В противном случае читайте дальше.
Если вы хотите правильно реализовать протокол асинхронных результатов с помощью Asio, вы должны использовать трейт async_result
или async_initiate
, как описано здесь.
Это должен быть надежный ключ для SFINAE. Аргументы шаблона для async_result
включают токен и подпись(и) завершения:
#include <boost/asio.hpp>
#include <iostream>
using boost::asio::async_result;
template <typename Token,
typename R = typename async_result<std::decay_t<Token>, void(int)>::return_type>
void f1(Token&&) {
std::cout << __PRETTY_FUNCTION__ << "\n";
}
template <typename Token,
typename R = typename async_result<std::decay_t<Token>, void()>::return_type>
void f2(Token&&) {
std::cout << __PRETTY_FUNCTION__ << "\n";
}
int main() {
auto cb1 = [](int) {};
f1(cb1);
f1(boost::asio::use_future);
f1(boost::asio::use_awaitable);
f1(boost::asio::detached);
f1(boost::asio::as_tuple(boost::asio::use_awaitable));
auto cb2 = []() {};
f2(cb2);
f2(boost::asio::use_future);
f2(boost::asio::use_awaitable);
f2(boost::asio::detached);
f2(boost::asio::as_tuple(boost::asio::use_awaitable));
}
Уже печатает
void f1(Token&&) [with Token = main()::<lambda(int)>&; R = void]
void f1(Token&&) [with Token = const boost::asio::use_future_t<>&; R = std::future<int>]
void f1(Token&&) [with Token = const boost::asio::use_awaitable_t<>&; R = boost::asio::awaitable<int, boost::asio::any_io_executor>]
void f1(Token&&) [with Token = const boost::asio::detached_t&; R = void]
void f1(Token&&) [with Token = boost::asio::as_tuple_t<boost::asio::use_awaitable_t<> >; R = boost::asio::awaitable<std::tuple<int>, boost::asio::any_io_executor>]
void f2(Token&&) [with Token = main()::<lambda()>&; R = void]
void f2(Token&&) [with Token = const boost::asio::use_future_t<>&; R = std::future<void>]
void f2(Token&&) [with Token = const boost::asio::use_awaitable_t<>&; R = boost::asio::awaitable<void, boost::asio::any_io_executor>]
void f2(Token&&) [with Token = const boost::asio::detached_t&; R = void]
void f2(Token&&) [with Token = boost::asio::as_tuple_t<boost::asio::use_awaitable_t<> >; R = boost::asio::awaitable<std::tuple<>, boost::asio::any_io_executor>]
Теперь имейте в виду, что приведенное выше является «слишком слабым» из-за частичного создания экземпляра шаблона. Некоторые части async_result
на самом деле не используются. Это означает, что f2(cb1);
на самом деле скомпилируется.
Связанные документы даже включают концепцию C++20 completion_token_for<Sig>
, которая позволяет вам быть точным без усилий: Live On Compiler Explorer
template <boost::asio::completion_token_for<void(int)> Token> void f1(Token&&) {
std::cout << __PRETTY_FUNCTION__ << "\n";
}
template <boost::asio::completion_token_for<void()> Token> void f2(Token&&) {
std::cout << __PRETTY_FUNCTION__ << "\n";
}
На практике вы всегда будете следовать рецепту Asio, и это гарантирует использование всех частей. Помимо примера в документации, вы можете искать существующие ответы
Пример:
template <typename Token>
typename asio::async_result<std::decay_t<Token>, void(error_code, int)>::return_type
async_f1(Token&& token) {
auto init = [](auto completion) {
auto timer =
std::make_shared<asio::steady_timer>(boost::asio::system_executor{}, 1s);
std::thread(
[timer](auto completion) {
error_code ec;
timer->wait(ec);
std::move(completion)(ec, 42);
},
std::move(completion))
.detach();
};
return asio::async_result<std::decay_t<Token>, void(error_code, int)>::initiate(
init, std::forward<Token>(token));
}
...Это похоже на async_result<>::initiate
. Но ошибка происходит немного глубже в библиотеке asio. Я ищу способ сделать ошибку в f1()
и f2()
, используя SFINAE. Я добавил недопустимый токен в ваш пример compiler-explorer.com/z/KT57hzK3o , но об ошибке не сообщается. Если бы я мог написать хороший SFINAE в f1()
и f2()
точке, было бы здорово. Что вы думаете?
Я предполагаю, что Boost.Asio не предоставляет способа проверки проверки токена для асинхронных функций пользователя, таких как токен аргумента f1()
и f2()
. Но их реализация обычно использует async_result<>::initiate
или async_compose
. И они проверяют токен, и если пользователь передал неверный токен, сообщается об ошибке. Ошибка достаточно проста для понимания и легко находит точку вызова. Поэтому Boost.Asio считает, что проверка f1()
и f2()
не нужна. Это мое понимание, пока.
Извините, я что-то неправильно понимаю. Только когда я использую asio::async_result<...)>::return_type
в качестве возвращаемого типа, точка вызова включается в журнал ошибок компиляции. Если я заменил его на auto
, что означает отсутствие проверки, то точка вызова исчезла из журнала ошибок компиляции. Поэтому использование asio::async_result<...)>::return_type
— это решение, которое я хочу знать. Еще раз спасибо.
«Я предполагаю, что Boost.Asio не дает возможности проверить валидацию токена» — что не так с asio::completion_token_for
? Я думаю, вы, вероятно, сможете найти удобную для С++ 17 реализацию (деталь), которую вы можете использовать повторно.
Об использовании return_type
- да, это то, о чем я предупреждал в ответе ""слишком слабый" из-за частичного создания экземпляра шаблона. Некоторые части async_result на самом деле не используются". Имейте в виду, вам не нужно беспокоиться, поскольку async_compose
по определению создает экземпляр всего класса.
Я не проверял реализацию asio::completion_token_for
, потому что моя среда не C++20, а C++17. Я посмотрю и попробую портировать реализацию C++17. Возможно, может и не получиться. Просто я предполагаю, что если есть способ реализовать asio::completion_token_for
совместимые с SFINAE дружественные type_traits , Boost.Asio предоставит его как часть библиотеки. В любом случае попробую.
Я не понимаю, зачем они это предоставляют, потому что это не добавляет ценности. Причина, по которой концепция добавляет ценность, заключается именно в том, что она может привести к более четким сообщениям об ошибках. Если вы можете явно принудительно выполнить ранние проверки, вы всегда можете превратить это в static_assert, что сделает его «более простым» для ваших пользователей (так что вместо SFINAE, что приводит к... сложной диагностике компилятора). Имейте в виду, что диагностика для C++17 всегда будет сложной, но потенциально вы можете добавить static_assert в «помощь».
Давайте продолжим обсуждение в чате.
Спасибо. Когда я создаю
async_result<...>::initiate(...)
в функцииf1()
иf2()
и передаю неверный токен, такой как int, я получаю сообщение об ошибке/usr/include/boost/asio/compose.hpp:380:5: error: called object type 'int' is not a function or function pointer
. Я могу получить точку вызова из журнала ошибок (например, обратная трассировка). Это не так сложно, так что это приемлемое решение. В моем реальном коде используетсяasync_compose
...