Задачка о функции-обертке, принимающей аргументы в произвольном порядке, и ее решение на C++17

от автора

Недавно на Хабре проскакивала новость о 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");    [...] }

Принимаем произвольное количество универсальных ссылок на объекты различных типов в качестве аргументов, и сразу проверяем, что переданных аргументов ровно три. Пока все идет хорошо. Дальше нам нужно как-то выстроить их в правильном порядке и засунуть в 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? На помощь приходят свертки:

(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 в значение по умолчанию. Непорядок. Придется налепить рантайм-костылей:

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); }

Да, это работает, но… как-то не радует. Во-первых, рантайм-костыли, а хотелось бы, чтобы все проверки выполнялись исключительно в 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):

template<typename R, typename T, typename... Ts> decltype(auto) get_arg_of_type(T&& arg, Ts&&... args) {   [... все остальное без изменений ...] }

и вуаля — теперь get_arg_of_type() вместо объекта возвращает ссылку строго того же типа, что и у первого аргумента arg.

Итак, никаких рантайм-костылей, никаких лишних копирований или перемещений (обертка совершенно прозрачна в этом смысле), никаких дополнительных инициализаций. На этом варианте я решил остановиться, но будет любопытно увидеть какой-нибудь еще более эффективный вариант в комментариях. Поиграться с последним решением вживую можно здесь (std::string_view там заменен на более «тяжелый» std::string для более наглядной демонстрации работы perfect forwarding).


ссылка на оригинал статьи https://habr.com/ru/articles/593429/