c ++ универсальное время компиляции для цикла - PullRequest
14 голосов
/ 12 апреля 2019

В некоторых случаях может быть полезно / необходимо, чтобы цикл for оценивался / развертывался во время компиляции. Например, чтобы перебрать элементы tuple, нужно использовать std::get<I>, что зависит от шаблона int параметр I, следовательно, он должен быть оценен во время компиляции. Используя рекурсию компиляции, можно решить конкретную проблему, как, например, обсуждалось здесь , здесь и, в частности, для std::tuple здесь .

Однако меня интересует, как реализовать универсальный цикл for времени компиляции.

Следующий код c++17 реализует эту идею

#include <utility>
#include <tuple>
#include <string>
#include <iostream>

template <int start, int end, template <int> class OperatorType, typename... Args>
void compile_time_for(Args... args)
{
  if constexpr (start < end)
         {
           OperatorType<start>()(std::forward<Args>(args)...);
           compile_time_for<start + 1, end, OperatorType>(std::forward<Args>(args)...);
         }    
}

template <int I>
struct print_tuple_i {
  template <typename... U>
  void operator()(const std::tuple<U...>& x) { std::cout << std::get<I>(x) << " "; }
};

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0, 3, print_tuple_i>(x);

  return 0;
}

Хотя код работает, было бы лучше иметь возможность просто предоставлять функцию-шаблон для подпрограммы compile_time_for, а не класс-шаблон для создания экземпляра на каждой итерации.

Однако код, подобный следующему, не компилируется в c++17

#include <utility>
#include <tuple>
#include <string>
#include <iostream>

template <int start, int end, template <int, typename...> class F, typename... Args>
void compile_time_for(F f, Args... args)
{
  if constexpr (start < end)
         {
           f<start>(std::forward<Args>(args)...);
           compile_time_for<start + 1, end>(f, std::forward<Args>(args)...);
         }    
}

template <int I, typename... U>
void myprint(const std::tuple<U...>& x) { std::cout << std::get<I>(x) << " "; }

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0, 3>(myprint, x);

  return 0;
}

С gcc 7.3.0 и опцией std=c++17 первая ошибка

for2.cpp:7:25: error: ‘auto’ parameter not permitted in this context
 void compile_time_for(F f, Args... args)

Вопросы:

  1. Есть ли способ написать compile_time_for такой, чтобы он принимал функцию шаблона в качестве первого аргумента?
  2. Если вопрос 1. положительный, есть ли издержки в первом рабочем коде из-за того, что подпрограмма создает объект типа OperatorType<start> на каждой итерации цикла?
  3. Планируется ли в следующем c++20? 1045 * ввести такую ​​функцию, как время компиляции цикла?

Ответы [ 2 ]

6 голосов
/ 12 апреля 2019
  1. Есть ли способ написать compile_time_for так, чтобы он принимал шаблонную функцию в качестве первого аргумента?

Краткий ответ: нет.

Длинный ответ: шаблонная функция не является объектом, это набор объектов, и вы можете передать функции в качестве аргумента объект, а не набор объектов.

Обычное решение этой проблемыТип проблемы заключается в том, чтобы обернуть функцию шаблона внутри класса и передать объект класса (или просто тип, если функция обернута как статический метод).Это именно то решение, которое вы приняли в своем рабочем коде.

Если вопрос 1. положительный, есть ли издержки в первом рабочем коде из-за того, что подпрограмма создает объект типа OperatorType на каждой итерации цикла?

Вопрос 1 отрицательный.

Планируется ли ввести такую ​​функцию, как время компиляции для цикла, в грядущем c ++ 20?

Я недостаточно знаю C ++ 20, чтобы ответить на этот вопросвопрос, но я полагаю, не передавая набор функций.

В любом случае, вы можете сделать своего рода время компиляции для цикла, используя std::make_index_sequence / std::index_sequence начиная с C ++ 14.

Например, если вы согласитесь извлечь значение touple вне вашей функции myprint(), вы можете заключить его в лямбду и написать что-нибудь следующим образом (используя также сворачивание шаблона C ++ 17; в C ++ 14 немного большесложный)

#include <utility>
#include <tuple>
#include <string>
#include <iostream>

template <typename T>
void myprint (T const & t)
 { std::cout << t << " "; }

template <std::size_t start, std::size_t ... Is, typename F, typename ... Ts>
void ctf_helper (std::index_sequence<Is...>, F f, std::tuple<Ts...> const & t)
 { (f(std::get<start + Is>(t)), ...); }

template <std::size_t start, std::size_t end, typename F, typename ... Ts>
void compile_time_for (F f, std::tuple<Ts...> const & t)
 { ctf_helper<start>(std::make_index_sequence<end-start>{}, f, t); }

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0, 3>([](auto const & v){ myprint(v); }, x);

  return 0;
}

Если вы действительно хотите извлечь элемент кортежа (или элементы кортежей) внутри функции, лучшее, что я могу себе представить, это преобразовать ваш первый пример следующим образом

#include <utility>
#include <tuple>
#include <string>
#include <iostream>

template <std::size_t start, template <std::size_t> class OT,
          std::size_t ... Is, typename... Args>
void ctf_helper (std::index_sequence<Is...> const &, Args && ... args)
 { (OT<start+Is>{}(std::forward<Args>(args)...), ...); }

template <std::size_t start, std::size_t end,
          template <std::size_t> class OT, typename... Args>
void compile_time_for (Args && ... args)
 { ctf_helper<start, OT>(std::make_index_sequence<end-start>{},
                         std::forward<Args>(args)...); }

template <std::size_t I>
struct print_tuple_i
 {
   template <typename ... U>
   void operator() (std::tuple<U...> const & x)
    { std::cout << std::get<I>(x) << " "; }
 };

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0u, 3u, print_tuple_i>(x);

  return 0;
}

- РЕДАКТИРОВАТЬ -

ОП спрашивает

Есть ли какое-то преимущество использования index_sequence над моим первым кодом?

IЯ не эксперт, но таким образом вы избегаете рекурсии.С точки зрения шаблона, у компиляторов есть пределы рекурсии, которые могут быть строгими.Таким образом вы избегаете их.

Кроме того, ваш код не компилируется, если вы установите параметры шаблона end > start.(Можно представить ситуацию, когда вы хотите, чтобы компилятор вообще определил, был ли создан цикл)

Я полагаю, вы имеете в виду, что мой код не компилируется, если start > end.

Плохая часть в том, что здесь нет проверки этой проблемы, поэтому компилятор пытается скомпилировать мой код и в этом случае;поэтому встречайте

 std::make_index_sequence<end-start>{}

, где end - start - отрицательное число, но используемое шаблоном, ожидающим число без знака.Так что end - start становится очень большим положительным числом, и это может вызвать проблемы.

Вы можете избежать этой проблемы, налагая static_assert() внутри compile_time_for()

template <std::size_t start, std::size_t end,
          template <std::size_t> class OT, typename... Args>
void compile_time_for (Args && ... args)
 { 
   static_assert( end >= start, "start is bigger than end");

   ctf_helper<start, OT>(std::make_index_sequence<end-start>{},
                         std::forward<Args>(args)...);
 }

Или, возможно, вы можете использоватьSFINAE для отключения функции

template <std::size_t start, std::size_t end,
          template <std::size_t> class OT, typename... Args>
std::enable_if_t<(start <= end)> compile_time_for (Args && ... args)
 { ctf_helper<start, OT>(std::make_index_sequence<end-start>{},
                         std::forward<Args>(args)...); }

Если вы хотите, с помощью SFINAE вы можете добавить перегруженную compile_time_for() версию для управления end < start кейсом

template <std::size_t start, std::size_t end,
          template <std::size_t> class OT, typename ... Args>
std::enable_if_t<(start > end)> compile_time_for (Args && ...)
 { /* manage the end < start case in some way */ }
3 голосов
/ 12 апреля 2019

Я отвечу на вопрос, как исправить ваш последний пример кода.

Причина, по которой он не компилируется, находится здесь:

template <int start, int end, template <int, typename...> class F, typename... Args>
void compile_time_for(F f, Args... args)
                      /\

F - шаблон, вы не можете иметь объект класса шаблона без подстановки параметров шаблона. Например. Вы не можете иметь объект типа std::vector, но можете иметь объект std::vector<int>. Предлагаю сделать функтор F с оператором шаблона ():

#include <utility>
#include <tuple>
#include <string>
#include <iostream>

template <int start, int end, typename F, typename... Args>
void compile_time_for(F f, Args... args)
{
  if constexpr (start < end)
         {
           f.template operator()<start>(std::forward<Args>(args)...);
           compile_time_for<start + 1, end>(f, std::forward<Args>(args)...);
         }    
}

struct myprint
{
    template <int I, typename... U>
    void operator()(const std::tuple<U...>& x) { std::cout << std::get<I>(x) << " "; }
};

int main()
{
  std::tuple<int, int, std::string> x{1, 2, "hello"};

  compile_time_for<0, 3>(myprint(), x);

  return 0;
}
...