Compteurs de temps de compilation C ++, revisités

28

TL; DR

Avant d'essayer de lire tout ce post, sachez que:

  1. une solution au problème présenté a été trouvée par moi - même , mais j'ai toujours hâte de savoir si l'analyse est correcte;
  2. J'ai regroupé la solution dans une fameta::counterclasse qui résout quelques bizarreries restantes. Vous pouvez le trouver sur github ;
  3. vous pouvez le voir à l' oeuvre sur godbolt .

Comment tout a commencé

Depuis que Filip Roséen a découvert / inventé, en 2015, la magie noire qui compile les compteurs de temps en C ++ , j'ai été légèrement obsédé par l'appareil, donc quand le CWG a décidé que la fonctionnalité devait disparaître, j'ai été déçu, mais j'espère toujours que leur esprit pourrait être modifié en leur montrant quelques cas d'utilisation convaincants.

Puis, il y a quelques années j'ai décidé de jeter un oeil à la chose de nouveau, de sorte que uberswitch es pourraient être imbriquées - un cas d'utilisation intéressante, à mon avis - pour découvrir que ce ne serait pas plus travailler avec les nouvelles versions de les compilateurs disponibles, même si le problème 2118 était (et est toujours ) à l'état ouvert: le code se compilerait, mais le compteur n'augmenterait pas.

Le problème a été signalé sur le site Web de Roséen et récemment également sur stackoverflow: C ++ prend-il en charge les compteurs de compilation?

Il y a quelques jours, j'ai décidé d'essayer à nouveau de résoudre les problèmes

Je voulais comprendre ce qui avait changé dans les compilateurs qui faisaient que le C ++, apparemment toujours valide, ne fonctionnait plus. À cette fin, j'ai cherché loin et loin sur Internet pour que quelqu'un en ait parlé, mais en vain. J'ai donc commencé à expérimenter et je suis parvenu à certaines conclusions, que je présente ici dans l'espoir d'obtenir un retour d'informations de la part de personnes plus compétentes que moi.

Ci-dessous, je présente le code original de Roséen par souci de clarté. Pour une explication de son fonctionnement, veuillez consulter son site Internet :

template<int N>
struct flag {
  friend constexpr int adl_flag (flag<N>);
};

template<int N>
struct writer {
  friend constexpr int adl_flag (flag<N>) {
    return N;
  }

  static constexpr int value = N;
};

template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
  return N;
}

template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
  return R;
}

int constexpr reader (float, flag<0>) {
  return 0;
}

template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
  return R;
}

int main () {
  constexpr int a = next ();
  constexpr int b = next ();
  constexpr int c = next ();

  static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}

Avec les compilateurs récents g ++ et clang ++, next() renvoie toujours 1. Après avoir expérimenté un peu, le problème au moins avec g ++ semble être qu'une fois que le compilateur évalue les paramètres par défaut des modèles de fonctions la première fois que les fonctions sont appelées, tout appel ultérieur à ces fonctions ne déclenchent pas de réévaluation des paramètres par défaut, n'instanciant donc jamais de nouvelles fonctions mais se référant toujours à celles précédemment instanciées.


Premières questions

  1. Êtes-vous réellement d'accord avec mon diagnostic?
  2. Si oui, ce nouveau comportement est-il imposé par la norme? Le précédent était-il un bug?
  3. Sinon, quel est le problème?

En gardant à l’esprit ce qui précède, j’ai trouvé une solution: marquez next() solution invocation avec un identifiant unique augmentant de façon monotone, pour passer aux callees, afin qu'aucun appel ne soit le même, ce qui oblige le compilateur à réévaluer tous les arguments chaque fois.

Cela semble un fardeau de le faire, mais en y réfléchissant, on pourrait simplement utiliser les macros standard __LINE__ou __COUNTER__similaires (lorsqu'elles sont disponibles), cachées dans une counter_next()macro de type fonction.

J'ai donc proposé ce qui suit, que je présente sous la forme la plus simplifiée qui montre le problème dont je parlerai plus tard.

template <int N>
struct slot;

template <int N>
struct slot {
    friend constexpr auto counter(slot<N>);
};

template <>
struct slot<0> {
    friend constexpr auto counter(slot<0>) {
        return 0;
    }
};

template <int N, int I>
struct writer {
    friend constexpr auto counter(slot<N>) {
        return I;
    }

    static constexpr int value = I-1;
};

template <int N, typename = decltype(counter(slot<N>()))>
constexpr int reader(int, slot<N>, int R = counter(slot<N>())) {
    return R;
};

template <int N>
constexpr int reader(float, slot<N>, int R = reader(0, slot<N-1>())) {
    return R;
};

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

int a = next<11>();
int b = next<34>();
int c = next<57>();
int d = next<80>();

Vous pouvez observer les résultats de ce qui précède sur godbolt , que j'ai capturé pour les paresseux.

entrez la description de l'image ici

Et comme vous pouvez le voir, avec trunk g ++ et clang ++ jusqu'à 7.0.0 ça marche! , le compteur passe de 0 à 3 comme prévu, mais avec la version clang ++ supérieure à 7.0.0, il ne .

Pour ajouter l'insulte à la blessure, j'ai réussi à faire planter clang ++ jusqu'à la version 7.0.0, en ajoutant simplement un paramètre "context" au mixage, de telle sorte que le compteur est réellement lié à ce contexte et, en tant que tel, peut être redémarré chaque fois qu'un nouveau contexte est défini, ce qui ouvre la possibilité d'utiliser une quantité potentiellement infinie de compteurs. Avec cette variante, clang ++ au-dessus de la version 7.0.0 ne plante pas, mais ne produit toujours pas le résultat attendu. Vivez sur Godbolt .

À perte de tout indice sur ce qui se passait, j'ai découvert le site Web cppinsights.io , qui permet de voir comment et quand les modèles sont instanciés. En utilisant ce service, je pense que ce qui se passe est que clang ++ ne définit en fait aucune des friend constexpr auto counter(slot<N>)fonctions chaque fois qu'il writer<N, I>est instancié.

Essayer d'appeler explicitement counter(slot<N>)pour tout N donné qui aurait déjà dû être instancié semble fonder cette hypothèse.

Cependant, si j'essaie d'instancier explicitement writer<N, I>pour une donnée Net Ique cela aurait déjà dû être instancié, alors clang ++ se plaint d'un redéfini friend constexpr auto counter(slot<N>).

Pour tester ce qui précède, j'ai ajouté deux lignes supplémentaires au code source précédent.

int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;

Vous pouvez tout voir par vous-même sur Godbolt . Capture d'écran ci-dessous.

clang ++ croit qu'il a défini quelque chose qu'il croit qu'il n'a pas défini

Donc, il semble que clang ++ pense avoir défini quelque chose qu'il croit ne pas avoir défini , ce qui fait tourner la tête, n'est-ce pas?


Deuxième lot de questions

  1. Ma solution de contournement est-elle légale en C ++, ou ai-je réussi à découvrir un autre bogue g ++?
  2. Si c'est légal, ai-je donc découvert des bugs clang ++ désagréables?
  3. Ou est-ce que je viens de me plonger dans le monde sombre du comportement indéfini, donc je suis moi-même le seul à blâmer?

En tout état de cause, je souhaiterais chaleureusement la bienvenue à tous ceux qui voudraient m'aider à sortir de ce terrier de lapin, en me donnant des explications sur les maux de tête si besoin est. :RÉ

Fabio A.
la source
2
Si je me souviens bien, les membres du comité standard ont clairement l'intention de ne pas autoriser les constructions au moment de la compilation de toute sorte, forme ou forme qui ne produisent pas exactement le même résultat chaque fois qu'elles sont (hypothétiquement) évaluées. Il peut donc s'agir d'un bogue du compilateur, il peut s'agir d'un cas «mal formé, aucun diagnostic requis» ou il peut s'agir de quelque chose que la norme a manqué. Cela va néanmoins à l'encontre de "l'esprit de la norme". Je suis désolé. J'aurais aussi aimé compiler des compteurs de temps.
bolov
@HolyBlackCat Je dois avouer que je trouve très difficile de comprendre ce code. Il semble que cela pourrait éviter la nécessité de passer explicitement un nombre croissant monotone comme paramètre à la next()fonction, mais je ne peux pas vraiment comprendre comment cela fonctionne. En tout état de cause, j'ai trouvé une réponse à mon propre problème, ici: stackoverflow.com/a/60096865/566849
Fabio A.
@FabioA. Moi aussi, je ne comprends pas entièrement cette réponse. Depuis que j'ai posé cette question, j'ai réalisé que je ne voulais plus jamais toucher aux compteurs constexpr.
HolyBlackCat
Bien qu'il s'agisse d'une petite expérience de réflexion amusante, quelqu'un qui a réellement utilisé ce code devrait à peu près s'attendre à ce qu'il ne fonctionne pas dans les futures versions de C ++, non? En ce sens, le résultat se définit comme un bug.
Aziuth

Réponses:

5

Après une enquête plus approfondie, il s'avère qu'il existe une modification mineure qui peut être effectuée sur la next()fonction, ce qui fait que le code fonctionne correctement sur les versions de clang ++ au-dessus de 7.0.0, mais l'empêche de fonctionner pour toutes les autres versions de clang ++.

Jetez un œil au code suivant, extrait de ma solution précédente.

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

Si vous y prêtez attention, ce qu'il fait littéralement, c'est d'essayer de lire la valeur associée slot<N>, d'y ajouter 1, puis d'associer cette nouvelle valeur à la même chose slot<N> .

Quand slot<N>n'a pas de valeur associée, la valeur associée à slot<Y>est récupérée à la place, Yétant le plus haut indice inférieur à Ncelui qui slot<Y>a une valeur associée.

Le problème avec le code ci-dessus est que, même s'il fonctionne sur g ++, clang ++ (à juste titre, je dirais?) Fait renvoyer de manière reader(0, slot<N>()) permanente tout ce qu'il a renvoyé slot<N>sans valeur associée. À son tour, cela signifie que tous les emplacements sont effectivement associés à la valeur de base 0.

La solution est de transformer le code ci-dessus en celui-ci:

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}

Notez que cela slot<N>()a été modifié en slot<N-1>(). Cela a du sens: si je veux associer une valeur à slot<N>, cela signifie qu'aucune valeur n'est encore associée, donc cela n'a aucun sens d'essayer de la récupérer. De plus, nous voulons augmenter un compteur, et la valeur du compteur associé à slot<N>doit être un plus la valeur associée à slot<N-1>.

Eureka!

Cela brise les versions clang ++ <= 7.0.0, cependant.

Conclusions

Il me semble que la solution originale que j'ai publiée a un bug conceptuel, tel que:

  • g ++ a quirk / bug / relaxation qui s'annule avec le bogue de ma solution et fait finalement fonctionner le code.
  • les versions de clang ++> 7.0.0 sont plus strictes et n'aiment pas le bogue du code d'origine.
  • Les versions de clang ++ <= 7.0.0 ont un bogue qui empêche la solution corrigée de fonctionner.

Pour résumer tout cela, le code suivant fonctionne sur toutes les versions de g ++ et clang ++.

#if !defined(__clang_major__) || __clang_major__ > 7
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}
#else
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}
#endif

Le code en l'état fonctionne également avec msvc. Le compilateur icc ne déclenche pas SFINAE lors de l'utilisation decltype(counter(slot<N>())), préférant se plaindre de ne pas pouvoir le faire deduce the return type of function "counter(slot<N>)"car it has not been defined. Je crois que c'est un bug , qui peut être contourné en faisant SFINAE sur le résultat direct de counter(slot<N>). Cela fonctionne également sur tous les autres compilateurs, mais g ++ décide de cracher une quantité abondante d'avertissements très ennuyeux qui ne peuvent pas être désactivés. Ainsi, dans ce cas également, #ifdefpourrait venir à la rescousse.

La preuve est sur godbolt , présentée ci-dessous.

entrez la description de l'image ici

Fabio A.
la source
2
Je pense que cette réponse ferme en quelque sorte le sujet, mais j'aimerais toujours savoir si j'ai raison dans mon analyse, donc j'attendrai avant d'accepter ma propre réponse comme correcte, en espérant que quelqu'un d'autre passera et me donnera un meilleur indice ou une confirmation. :)
Fabio A.