- PVSM.RU - https://www.pvsm.ru -
Недавно на Хабре проскакивала новость [1] о Magnit Tech++ Meet Up, и в ней упоминалась задачка, которая меня заинтересовала. В оригинале задачка формулируется так:
Определена функция с сигнатурой:
void do_something(bool a, int b, std::string_view c)
Определить функцию, принимающую в произвольном порядке аргументы типов
bool
,int
,std::string_view
и вызывающую функциюdo_something
с переданными параметрами в качестве аргументов.
Я придумал несколько решений этой задачки, а здесь предлагаю два варианта ее решения - сначала банальный (и плохой), а затем самый с моей точки зрения оптимальный. Промежуточные варианты приводить не буду.
Итак, начнем с объявления этой самой функции-обертки:
template<typename... Ts>
void wrapper(Ts&&... args)
{
static_assert(sizeof...(args) == 3, "Invalid number of arguments");
[...]
}
Принимаем произвольное количество универсальных ссылок [2] на объекты различных типов в качестве аргументов, и сразу проверяем, что переданных аргументов ровно три. Пока все идет хорошо [3]. Дальше нам нужно как-то выстроить их в правильном порядке и засунуть в do_something
. Первая (и самая глупая) мысль - использовать std::tuple
:
template<typename... Ts>
void wrapper(Ts&&... args)
{
static_assert(sizeof...(args) == 3, "Invalid number of arguments");
std::tuple<bool, int, std::string_view> f_args;
[... как-то заполняем f_args ...]
// и вызываем do_something с аргументами в нужном порядке
std::apply(do_something, f_args);
}
Следующий вопрос - как заполнить f_args
? Очевидно, нужно как-то пройтись по изначальным аргументам (args
) и распихать их по элементам std::tuple
в правильном порядке с использованием вспомогательной лямбды вроде такой:
auto bind_arg = [&](auto &&arg) {
using arg_type = typename std::remove_reference<decltype(arg)>::type;
if constexpr (std::is_same<arg_type, bool>::value) {
std::get<0>(f_args) = std::forward<decltype(arg)>(arg);
} else if constexpr (std::is_same<arg_type, int>::value) {
std::get<1>(f_args) = std::forward<decltype(arg)>(arg);
} else if constexpr (std::is_same<arg_type, std::string_view>::value) {
std::get<2>(f_args) = std::forward<decltype(arg)>(arg);
} else {
static_assert(false, "Invalid argument type"); // не сработает
}
};
Но тут нас ждет мелкая помеха - эта лямбда не компилируется. Причина в том, что поскольку проверяемое выражение вstatic_assert
(тупо false
) не зависит от аргумента лямбды, то static_assert
срабатывает не тогда, когда создается конкретный экземпляр лямбды из ее шаблона, а еще во время компиляции самого шаблона. Решение простое - заменить false
на что-то, зависящее от arg
. Например, крайне сомнительно, что arg
здесь когда-нибудь будет иметь тип void
:
static_assert(std::is_void<decltype(arg)>::value, "Invalid argument type");
Так, с этим понятно. Как дальше вызвать эту bind_arg
для каждого элемента из args
? На помощь приходят свертки [4]:
(bind_arg(std::forward<decltype(args)>(args)), ...);
Здесь мы выполняем унарную свертку с использованием comma operator, что в нашем случае преобразуется компилятором в примерно следующее выражение (я использовал индексы в квадратных скобках исключительно для наглядности):
(bind_arg(std::forward<decltype(args[0])>(args[0])),
(bind_arg(std::forward<decltype(args[1])>(args[1])),
(bind_arg(std::forward<decltype(args[2])>(args[2])))));
Так, хорошо. Но есть одна проблема: как узнать, все ли элементы std::tuple
инициализированы правильно? Ведь wrapper
может быть вызван как-нибудь вот так:
wrapper(false, false, 1);
и в первый элемент f_args
значение будет записано дважды, а последний так и останется value-initialized [5] в значение по умолчанию. Непорядок. Придется налепить рантайм-костылей:
template<typename... Ts>
void wrapper(Ts&&... args)
{
static_assert(sizeof...(args) == 3, "Invalid number of arguments");
std::tuple<bool, bool, bool> is_arg_bound;
std::tuple<bool, int, std::string_view> f_args;
auto bind_arg = [&](auto &&arg) {
using arg_type = typename std::remove_reference<decltype(arg)>::type;
if constexpr (std::is_same<arg_type, bool>::value) {
std::get<0>(is_arg_bound) = true;
std::get<0>(f_args) = std::forward<decltype(arg)>(arg);
} else if constexpr (std::is_same<arg_type, int>::value) {
std::get<1>(is_arg_bound) = true;
std::get<1>(f_args) = std::forward<decltype(arg)>(arg);
} else if constexpr (std::is_same<arg_type, std::string_view>::value) {
std::get<2>(is_arg_bound) = true;
std::get<2>(f_args) = std::forward<decltype(arg)>(arg);
} else {
static_assert(std::is_void<decltype(arg)>::value, "Invalid argument type");
}
};
(bind_arg(std::forward<decltype(args)>(args)), ...);
if (!std::apply([](auto... is_arg_bound) { return (is_arg_bound && ...); }, is_arg_bound)) {
std::cerr << "Invalid arguments" << std::endl;
return;
}
std::apply(do_something, f_args);
}
Да, это работает, но... как-то не радует [6]. Во-первых, рантайм-костыли, а хотелось бы, чтобы все проверки выполнялись исключительно в compile time. Во-вторых, при засовывании в std::tuple
происходит совершенно лишнее копирование или перемещение аргумента. В-третьих, происходят совершенно лишние value initialization элементов при создании самого std::tuple
. Да, для типов аргументов из задачи это не выглядит страшным, а что, если будет что-то потяжелее? Плохо, громоздко, некрасиво, неэффективно.
А что, если подойти с другой стороны?
Что, если вместо промежуточного хранения аргументов в кортеже мы будем сразу получать аргумент нужного типа? Что-нибудь вроде:
template<typename... Ts>
void wrapper(Ts&&... args)
{
static_assert(sizeof...(args) == 3, "Invalid number of arguments");
do_something(get_arg_of_type<bool>(std::forward<Ts>(args)...),
get_arg_of_type<int>(std::forward<Ts>(args)...),
get_arg_of_type<std::string_view>(std::forward<Ts>(args)...));
}
Дело за малым - написать эту самую get_arg_of_type()
. Начнем с простого - с сигнатуры:
template<typename R, typename... Ts>
R get_arg_of_type(Ts&&... args)
{
[...]
}
То есть мы имеем в составе аргументов шаблона тип R
(тот, что функция должна найти и вернуть), а в составе аргументов функции - набор разнотипных аргументов args
, среди которых, собственно, и нужно искать. Но как же по ним пройтись? Воспользуемся compile time рекурсией:
template<typename R, typename T, typename... Ts>
R get_arg_of_type(T&& arg, Ts&&... args)
{
using arg_type = typename std::remove_reference<decltype(arg)>::type;
if constexpr (std::is_same<arg_type, R>::value) {
return std::forward<T>(arg);
} else if constexpr (sizeof...(args) > 0) {
return get_arg_of_type<R>(std::forward<Ts>(args)...);
} else {
static_assert(std::is_void<decltype(arg)>::value, "An argument with the specified type was not found");
}
}
Модифицируем сигнатуру, выделяя первый аргумент отдельно, сверяем его тип с R,
если совпал - сразу возвращаем, если нет - смотрим, остались ли у нас еще аргументы в args
и вызываем get_arg_of_type()
рекурсивно (сдвинув аргументы на один влево), если нет - печатаем ошибку времени компиляции.
Почти хорошо, но... не совсем. Остается одно лишнее копирование/перемещение - ведь, возвращая объект типа R
, компилятор вынужден его создать, а RVO здесь не сработает. Что же делать? На помощь приходит decltype(auto) [7]:
template<typename R, typename T, typename... Ts>
decltype(auto) get_arg_of_type(T&& arg, Ts&&... args)
{
[... все остальное без изменений ...]
}
и вуаля - теперь get_arg_of_type()
вместо объекта возвращает ссылку строго того же типа, что и у первого аргумента arg
.
Итак, никаких рантайм-костылей, никаких лишних копирований или перемещений (обертка совершенно прозрачна в этом смысле), никаких дополнительных инициализаций. На этом варианте я решил остановиться, но будет любопытно увидеть какой-нибудь еще более эффективный вариант в комментариях. Поиграться с последним решением вживую можно здесь [8] (std::string_view
там заменен на более "тяжелый" std::string
для более наглядной демонстрации работы perfect forwarding).
Автор:
KanuTaH
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/370243
Ссылки в тексте:
[1] новость: https://habr.com/ru/company/magnit/news/t/591583/
[2] универсальных ссылок: https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
[3] Пока все идет хорошо: https://www.anekdot.ru/id/77862/
[4] свертки: https://en.cppreference.com/w/cpp/language/fold
[5] value-initialized: https://en.cppreference.com/w/cpp/language/value_initialization
[6] не радует: https://www.anekdot.ru/id/96339/
[7] decltype(auto): https://en.cppreference.com/w/cpp/language/auto
[8] здесь: https://godbolt.org/z/KdPPxjceo
[9] Источник: https://habr.com/ru/post/593429/?utm_source=habrahabr&utm_medium=rss&utm_campaign=593429
Нажмите здесь для печати.