web-dev-qa-db-fra.com

temps de compilation générique c ++ pour la boucle

Dans certains contextes, il pourrait être utile/nécessaire d'avoir une boucle for évaluée/déroulée au moment de la compilation. Par exemple, pour parcourir les éléments d'un Tuple, il faut utiliser std::get<I>, qui dépend d'un modèle int paramètre I, il doit donc être évalué au moment de la compilation. En utilisant la récursion de compilation, on peut résoudre un problème spécifique, comme par exemple discuté ici , ici et, spécifiquement pour std::Tupleici .

Je m'intéresse cependant à la façon d'implémenter une boucle générique au moment de la compilation for.

Le suivant c++17 le code implémente cette idée

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

Pendant que le code fonctionne, il serait plus agréable de pouvoir simplement fournir une fonction de modèle à la routine compile_time_for, plutôt qu'une classe de modèle à instancier à chaque itération.

Un code comme le suivant, cependant, ne compile pas dans 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;
}

Avec gcc 7.3.0 et l'option std=c++17 la première erreur est

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

Les questions sont:

  1. Existe-t-il un moyen d'écrire compile_time_for tel qu'il accepte une fonction de modèle comme premier argument?
  2. Si la question 1. est positive, y a-t-il une surcharge dans le premier code de travail, du fait que la routine crée un objet de type OperatorType<start> à chaque itération de boucle?
  3. Est-il prévu d'introduire une fonctionnalité telle qu'une boucle de compilation pour la prochaine c++20?
14
francesco
  1. Existe-t-il un moyen d'écrire compile_time_for de telle sorte qu'il accepte une fonction de modèle comme premier argument?

Réponse courte: non.

Réponse longue: une fonction de modèle n'est pas un objet, c'est une collection d'objets et vous pouvez passer à une fonction, comme argument, un objet, pas une collection d'objets.

La solution habituelle à ce type de problème consiste à envelopper la fonction de modèle dans une classe et à passer un objet de la classe (ou simplement le type, si la fonction est encapsulée comme une méthode statique). C'est exactement la solution que vous avez adoptée dans votre code de travail.

  1. Si la question 1. est positive, y a-t-il une surcharge dans le premier code de travail, du fait que la routine crée un objet de type OperatorType à chaque itération de boucle?

La question 1 est négative.

  1. Est-il prévu d'introduire une fonctionnalité comme une boucle de compilation pour le prochain c ++ 20?

Je ne connais pas suffisamment C++ 20 pour répondre à cette question mais je suppose que je ne passe pas un ensemble de fonctions.

Quoi qu'il en soit, vous pouvez faire une sorte de compilation pour la boucle en utilisant std::make_index_sequence/std::index_sequence À partir de C++ 14.

Par exemple, si vous acceptez d'extraire la valeur du touple en dehors de votre fonction myprint(), vous pouvez l'envelopper dans un lambda et écrire quelque chose comme suit (en utilisant également le pliage du modèle C++ 17; en C++ 14 est un peu plus compliqué)

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

Si vous voulez vraiment extraire l'élément Tuple (ou les éléments tuples) à l'intérieur de la fonction, le mieux que je puisse imaginer est de transformer votre premier exemple comme suit

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

- MODIFIER -

Le PO demande

Y a-t-il un avantage à utiliser index_sequence par rapport à mon premier code?

Je ne suis pas un expert mais de cette façon, vous évitez la récursivité. Les compilateurs ont des limites de récursivité, du point de vue du modèle, qui peuvent être strictes. De cette façon, vous les évitez.

De plus, votre code ne se compile pas si vous définissez les paramètres du modèle end > start. (On peut imaginer une situation où vous voulez que le compilateur détermine si une boucle est instanciée)

Je suppose que vous voulez dire que mon code ne compile pas si start > end.

La mauvaise partie est qu'il n'y a pas de vérification sur ce problème, donc le compilateur essaie de compiler mon code également dans ce cas; alors rencontre

 std::make_index_sequence<end-start>{}

end - start est un nombre négatif mais utilisé par un modèle qui attend un nombre non signé. Donc end - start Devient un très grand nombre positif et cela peut poser des problèmes.

Vous pouvez éviter ce problème en imposant une static_assert() à l'intérieur 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)...);
 }

Ou peut-être pouvez-vous utiliser SFINAE pour désactiver la fonction

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

Si vous le souhaitez, en utilisant SFINAE, vous pouvez ajouter une version compile_time_for() surchargée pour gérer le cas 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 */ }
6
max66

Je répondrai à la question comment réparer votre dernier exemple de code.

La raison pour laquelle il ne compile pas est ici:

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

F est un modèle, vous ne pouvez pas avoir d'objet d'une classe de modèle sans que les paramètres de modèle soient remplacés. Par exemple. vous ne pouvez pas avoir sur l'objet de std::vector type, mais peut avoir un objet de std::vector<int>. Je vous suggère de créer F foncteur avec un opérateur de modèle ():

#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;
}
3
Dmitry Gordon