web-dev-qa-db-fra.com

Pourquoi une boucle for n'est-elle pas une expression au moment de la compilation?

Si je veux faire quelque chose comme itérer sur un Tuple, je dois recourir à la métaprogrammation de modèles fous et aux spécialisations d'assistance de modèle. Par exemple, le programme suivant ne fonctionnera pas:

#include <iostream>
#include <Tuple>
#include <utility>

constexpr auto multiple_return_values()
{
    return std::make_Tuple(3, 3.14, "pi");
}

template <typename T>
constexpr void foo(T t)
{
    for (auto i = 0u; i < std::Tuple_size<T>::value; ++i)
    {
        std::get<i>(t);
    }    
}

int main()
{
    constexpr auto ret = multiple_return_values();
    foo(ret);
}

Parce que i ne peut pas être const ou nous ne pourrions pas l'implémenter. Mais les boucles for sont une construction au moment de la compilation qui peut être évaluée statiquement. Les compilateurs sont libres de le supprimer, de le transformer, de le plier, de le dérouler ou de faire ce qu'ils veulent avec lui grâce à la règle du "comme si". Mais alors pourquoi les boucles ne peuvent-elles pas être utilisées de manière constante? Il n'y a rien dans ce code qui doit être fait lors de "l'exécution". Les optimisations du compilateur en sont la preuve.

Je sais que vous pourriez potentiellement modifier i à l'intérieur du corps de la boucle, mais le compilateur peut toujours le détecter. Exemple:

// ...snip...

template <typename T>
constexpr int foo(T t)
{
    /* Dead code */
    for (auto i = 0u; i < std::Tuple_size<T>::value; ++i)
    {
    }    
    return 42;
}

int main()
{
    constexpr auto ret = multiple_return_values();
    /* No error */
    std::array<int, foo(ret)> arr;
}

Étant donné que std::get<>() est une construction au moment de la compilation, contrairement à std::cout.operator<<, Je ne vois pas pourquoi elle est interdite.

20
user6416815

πάντα ῥεῖ a donné une bonne et utile réponse, je voudrais mentionner un autre problème avec constexpr for.

En C++, au niveau le plus fondamental, toutes les expressions ont un type qui peut être déterminé statiquement (au moment de la compilation). Il y a des choses comme RTTI et boost::any Bien sûr, mais elles sont construites au-dessus de ce cadre, et le type statique d'une expression est un concept important pour comprendre certaines des règles de la norme.

Supposons que vous puissiez parcourir un conteneur hétérogène en utilisant une fantaisie pour la syntaxe, comme ceci peut-être:

std::Tuple<int, float, std::string> my_Tuple;
for (const auto & x : my_Tuple) {
  f(x);
}

Ici, f est une fonction surchargée. De toute évidence, la signification recherchée est d'appeler différentes surcharges de f pour chacun des types du tuple. Ce que cela signifie vraiment, c'est que dans l'expression f(x), la résolution de surcharge doit s'exécuter trois fois différentes. Si nous respectons les règles actuelles de C++, la seule façon dont cela peut avoir un sens est de dérouler la boucle en trois corps de boucle différents, avant nous essayons de comprendre quels sont les types d'expressions .

Et si le code est réellement

for (const auto & x : my_Tuple) {
  auto y = f(x);
}

auto n'est pas magique, cela ne signifie pas "pas d'informations sur le type", cela signifie, "déduisez le type, s'il vous plaît, compilateur". Mais il est clair qu'il faut vraiment trois types différents de y en général.

D'un autre côté, il y a des problèmes délicats avec ce genre de chose - en C++, l'analyseur doit pouvoir savoir quels noms sont des types et quels noms sont des modèles afin d'analyser correctement le langage. L'analyseur peut-il être modifié pour effectuer un certain déroulement des boucles des boucles constexpr for Avant que tous les types ne soient résolus? Je ne sais pas mais je pense que cela pourrait ne pas être trivial. Il y a peut-être une meilleure façon ...

Pour éviter ce problème, dans les versions actuelles de C++, les utilisateurs utilisent le modèle de visiteur. L'idée est que vous aurez une fonction ou un objet fonction surchargé et qu'il sera appliqué à chaque élément de la séquence. Ensuite, chaque surcharge a son propre "corps", il n'y a donc aucune ambiguïté quant aux types ou aux significations des variables qu'ils contiennent. Il existe des bibliothèques comme boost::fusion Ou boost::hana Qui vous permettent de faire une itération sur des séquences hétérogènes en utilisant une vue donnée - vous utiliseriez leur mécanisme au lieu d'une boucle for.

Si vous pouviez faire constexpr for Avec seulement des points, par exemple.

for (constexpr i = 0; i < 10; ++i) { ... }

cela pose la même difficulté que hétérogène pour la boucle. Si vous pouvez utiliser i comme paramètre de modèle à l'intérieur du corps, vous pouvez créer des variables qui font référence à différents types dans différentes exécutions du corps de la boucle, puis il n'est pas clair quels doivent être les types statiques des expressions. .

Donc, je ne suis pas sûr, mais je pense qu'il peut y avoir des problèmes techniques non triviaux associés à l'ajout d'une fonctionnalité constexpr for À la langue. Le modèle de visiteur/les fonctionnalités de réflexion prévues peuvent finir par être moins un casse-tête OMI ... qui sait.


Permettez-moi de donner un autre exemple auquel je viens de penser qui montre la difficulté que cela implique.

En C++ normal, le compilateur connaît le type statique de chaque variable de la pile et peut donc calculer la disposition du cadre de pile pour cette fonction.

Vous pouvez être sûr que l'adresse d'une variable locale ne changera pas pendant l'exécution de la fonction. Par exemple,

std::array<int, 3> a{{1,2,3}};
for (int i = 0; i < 3; ++i) {
    auto x = a[i];
    int y = 15;
    std::cout << &y << std::endl;
}

Dans ce code, y est une variable locale dans le corps d'une boucle for. Il a une adresse bien définie tout au long de cette fonction, et l'adresse imprimée par le compilateur sera la même à chaque fois.

Quel devrait être le comportement d'un code similaire avec constexpr pour?

std::Tuple<int, long double, std::string> a{};
for (int i = 0; i < 3; ++i) {
    auto x = std::get<i>(a);
    int y = 15;
    std::cout << &y << std::endl;
}

Le fait est que le type de x est déduit différemment à chaque passage dans la boucle - puisqu'il a un type différent, il peut avoir une taille et un alignement différents sur la pile. Étant donné que y vient après sur la pile, cela signifie que y peut changer son adresse sur différentes exécutions de la boucle - non?

Quel devrait être le comportement si un pointeur vers y est pris en un seul passage dans la boucle, puis déréférencé dans un passage ultérieur? Devrait-il s'agir d'un comportement non défini, même s'il serait probablement légal dans le code "no-constexpr for" similaire avec std::array Montré ci-dessus?

L'adresse de y ne doit-elle pas être modifiée? Le compilateur doit-il remplir l'adresse de y pour que le plus grand des types du Tuple puisse être hébergé avant y? Cela signifie-t-il que le compilateur ne peut pas simplement dérouler les boucles et commencer à générer du code, mais qu'il doit dérouler au préalable chaque instance de la boucle, puis collecter toutes les informations de type de chacune des instanciations N, puis trouver une mise en page satisfaisante?

Je pense qu'il vaut mieux utiliser une extension de pack, il est beaucoup plus clair comment il est censé être implémenté par le compilateur, et à quel point il sera efficace lors de la compilation et de l'exécution.

10
Chris Beck

Voici un moyen de le faire qui ne nécessite pas trop de passe-partout, inspiré de http://stackoverflow.com/a/26902803/1495627 :

template<std::size_t N>
struct num { static const constexpr auto value = N; };

template <class F, std::size_t... Is>
void for_(F func, std::index_sequence<Is...>)
{
  using expander = int[];
  (void)expander{0, ((void)func(num<Is>{}), 0)...};
}

template <std::size_t N, typename F>
void for_(F func)
{
  for_(func, std::make_index_sequence<N>());
}

Ensuite, vous pouvez faire:

for_<N>([&] (auto i) {      
  std::get<i.value>(t); // do stuff
});

Si vous avez un compilateur C++ 17 accessible, il peut être simplifié pour

template <class F, std::size_t... Is>
void for_(F func, std::index_sequence<Is...>)
{
  (func(num<Is>{}), ...);
}
7

Pourquoi une boucle for n'est-elle pas une expression au moment de la compilation?

Parce qu'une boucle for() est utilisée pour définir le flux de contrôle d'exécution dans le langage c ++.

En général, les modèles variadic ne peuvent pas être décompressés dans les instructions de flux de contrôle d'exécution en c ++.

 std::get<i>(t);

ne peut pas être déduit au moment de la compilation, car i est une variable d'exécution.

Utilisez déballage des paramètres du modèle variadic à la place.


Vous pourriez également trouver ce message utile (si cela ne fait même pas remarquer un doublon ayant des réponses à votre question):

itérer sur Tuple

4

En C++ 20 la plupart des std::algorithm les fonctions seront constexpr. Par exemple, en utilisant std::transform, de nombreuses opérations nécessitant une boucle peuvent être effectuées au moment de la compilation. Considérez cet exemple calculant la factorielle de chaque nombre dans un tableau au moment de la compilation (adapté de documentation Boost.Hana ):

#include <array>
#include <algorithm>

constexpr int factorial(int n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}

template <typename T, std::size_t N, typename F>
constexpr std::array<std::result_of_t<F(T)>, N>
transform_array(std::array<T, N> array, F f) {
    auto array_f = std::array<std::result_of_t<F(T)>, N>{};
    // This is a constexpr "loop":
    std::transform(array.begin(), array.end(), array_f.begin(), [&f](auto el){return f(el);});
    return array_f;
}

int main() {
    constexpr std::array<int, 4> ints{{1, 2, 3, 4}};
    // This can be done at compile time!
    constexpr std::array<int, 4> facts = transform_array(ints, factorial);
    static_assert(facts == std::array<int, 4>{{1, 2, 6, 24}}, "");
}

Voyez comment le tableau facts peut être calculé au moment de la compilation en utilisant une " boucle", c'est-à-dire un std::algorithm. Au moment d'écrire ces lignes, vous avez besoin d'une version expérimentale de la dernière version de clang ou gcc que vous pouvez essayer sur godbolt.org . Mais bientôt C++ 20 sera entièrement implémenté par tous les principaux compilateurs dans les versions.

0
Romeo Valentin