Задачка о функции-обертке, принимающей аргументы в произвольном порядке, и ее решение на 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/post/593429/


Интересные статьи

Интересные статьи

10 ноября 2020 года произошло во многом эпохальное событие в индустрии микропроцессоров - компания Apple презентовала новый Mac Mini, главной фишкой которого являлся чип собственной разработки Apple M...
Привет, Хаброжители! Уэйн Винстон научит вас быстро анализировать данные, принимать решения, подводить итоги, составлять отчеты, обрабатывать данные и строить аналитические модели в Micr...
Существует традиция, долго и дорого разрабатывать интернет-магазин. :-) Лакировать все детали, придумывать, внедрять и полировать «фишечки» и делать это все до открытия магазина.
В данной статье решим 23-е задание с сайта pwnable.kr, узнаем, что такое stack canary и подключим libc в python. Организационная информацияСпециально для тех, кто хочет узнавать что-то новое...
Здравствуйте, в эфире опять Радио SQL! Сегодня у нас решение задачи, которую мы передавали в нашем предыдущем эфире, и обещали разобрать в следующий раз. И вот этот следующий раз наступил. Задач...