TL; DR
Avant d'essayer de lire tout ce post, sachez que:
- 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;
- J'ai regroupé la solution dans une
fameta::counter
classe qui résout quelques bizarreries restantes. Vous pouvez le trouver sur github ; - 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
- Êtes-vous réellement d'accord avec mon diagnostic?
- Si oui, ce nouveau comportement est-il imposé par la norme? Le précédent était-il un bug?
- 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.
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 N
et I
que 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.
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
- Ma solution de contournement est-elle légale en C ++, ou ai-je réussi à découvrir un autre bogue g ++?
- Si c'est légal, ai-je donc découvert des bugs clang ++ désagréables?
- 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É
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/566849Réponses:
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.
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 choseslot<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 àN
celui quislot<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 base0
.La solution est de transformer le code ci-dessus en celui-ci:
Notez que cela
slot<N>()
a été modifié enslot<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:
Pour résumer tout cela, le code suivant fonctionne sur toutes les versions de g ++ et clang ++.
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 fairededuce the return type of function "counter(slot<N>)"
carit has not been defined
. Je crois que c'est un bug , qui peut être contourné en faisant SFINAE sur le résultat direct decounter(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,#ifdef
pourrait venir à la rescousse.La preuve est sur godbolt , présentée ci-dessous.
la source