TL; DR
Prima di provare a leggere l'intero post, sappi che:
- una soluzione al problema presentato è stata trovata da me , ma sono ancora desideroso di sapere se l'analisi è corretta;
- Ho impacchettato la soluzione in una
fameta::counter
classe che risolve alcune stranezze rimanenti. Puoi trovarlo su github ; - puoi vederlo al lavoro su godbolt .
Come tutto è cominciato
Da quando Filip Roséen ha scoperto / inventato, nel 2015, la magia nera che compila i contatori dei tempi è in C ++ , sono stato leggermente ossessionato dal dispositivo, quindi quando il CWG ha deciso che la funzionalità doveva andare, sono rimasto deluso, ma spero ancora che la loro mente potrebbe essere modificato mostrando loro alcuni casi d'uso convincenti.
Poi, un paio di anni fa, ho deciso di dare un'altra occhiata alla cosa, in modo che uberswitch es potesse essere nidificato - un caso d'uso interessante, secondo me - solo per scoprire che non avrebbe più funzionato con le nuove versioni di i compilatori disponibili, anche se il numero 2118 era (ed è ancora ) in stato aperto: il codice si sarebbe compilato, ma il contatore non sarebbe aumentato.
Il problema è stato segnalato sul sito Web di Roséen e recentemente anche su StackOverflow: il C ++ supporta i contatori in fase di compilazione?
Qualche giorno fa ho deciso di provare ad affrontare nuovamente i problemi
Volevo capire cosa era cambiato nei compilatori che rendevano il C ++, apparentemente ancora valido, non funzionare più. A tal fine, ho cercato in lungo e in largo l'interweb per qualcuno che ne avesse parlato, ma senza risultati. Quindi ho iniziato a sperimentare e sono giunto ad alcune conclusioni, che sto presentando qui sperando di ottenere un feedback dal più esperto di me qui intorno.
Di seguito sto presentando il codice originale di Roséen per motivi di chiarezza. Per una spiegazione di come funziona, consultare il suo sito Web :
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");
}
Con i compilatori di recente recente sia g ++ che clang ++, next()
restituisce sempre 1. Avendo sperimentato un po ', il problema almeno con g ++ sembra essere che una volta che il compilatore valuta i parametri predefiniti dei modelli di funzioni alla prima chiamata delle funzioni, ogni successiva chiamata a tali funzioni non attivano una rivalutazione dei parametri predefiniti, quindi non istanziano mai nuove funzioni ma si riferiscono sempre a quelle precedentemente istanziate.
Prime domande
- Sei davvero d'accordo con questa mia diagnosi?
- Se sì, questo nuovo comportamento è richiesto dalla norma? Il precedente era un bug?
- In caso contrario, qual è il problema?
Tenendo presente quanto sopra, mi è venuta in mente una soluzione: contrassegnare ogni next()
invocazione con un ID unico monotonicamente crescente, per passare ai callees, in modo che nessuna chiamata sia la stessa, costringendo quindi il compilatore a rivalutare tutti gli argomenti ogni volta.
Sembra un onere farlo, ma pensandoci si potrebbe semplicemente usare le macro standard __LINE__
o __COUNTER__
simili (ovunque disponibili), nascoste in una counter_next()
macro simile a una funzione.
Quindi ho escogitato quanto segue, che presento nella forma più semplificata che mostra il problema di cui parlerò più avanti.
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>();
È possibile osservare i risultati di cui sopra su godbolt , che ho selezionato per i pazzi.
E come puoi vedere, con trunk g ++ e clang ++ fino alla 7.0.0 funziona! , il contatore aumenta da 0 a 3 come previsto, ma con la versione di clang ++ sopra 7.0.0 non lo è .
Per aggiungere la beffa al danno, in realtà sono riuscito a far schiantare clang ++ fino alla versione 7.0.0, semplicemente aggiungendo un parametro "context" al mix, in modo tale che il contatore sia effettivamente associato a quel contesto e, come tale, possa essere riavviato ogni volta che viene definito un nuovo contesto, che apre la possibilità di utilizzare un numero potenzialmente infinito di contatori. Con questa variante, clang ++ sopra la versione 7.0.0 non si arresta in modo anomalo, ma non produce ancora il risultato previsto. Vivi su Godbolt .
Alla perdita di qualsiasi indizio su ciò che stava accadendo, ho scoperto il sito Web cppinsights.io , che consente di vedere come e quando vengono istanziati i modelli. Usando quel servizio ciò che penso stia accadendo è che clang ++ in realtà non definisce nessuna delle friend constexpr auto counter(slot<N>)
funzioni ogni volta che writer<N, I>
viene istanziato.
Cercare di chiamare esplicitamente counter(slot<N>)
qualsiasi dato N che avrebbe già dovuto essere istanziato sembra dare la base a questa ipotesi.
Tuttavia, se provo a creare un'istanza esplicita writer<N, I>
per qualsiasi dato N
e I
che avrebbe dovuto essere già stato istanziato, allora clang ++ si lamenta di una ridefinizione friend constexpr auto counter(slot<N>)
.
Per testare quanto sopra, ho aggiunto altre due righe al precedente codice sorgente.
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
Puoi vedere tutto da solo su godbolt . Schermata seguente.
Quindi, sembra che Clang ++ crede di aver definito qualcosa che non ha definito , che tipo di fa girare la testa, no?
Seconda serie di domande
- La mia soluzione alternativa è C ++ legale o sono riuscito a scoprire un altro bug g ++?
- Se è legale, ho quindi scoperto alcuni brutti bug clang ++?
- O ho appena scavato nel mondo sotterraneo oscuro di Undefined Behaviour, quindi io stesso sono l'unico da incolpare?
In ogni caso, darei un caloroso benvenuto a chiunque volesse aiutarmi a uscire da questa tana del coniglio, dispensando spiegazioni da mal di testa se necessario. : D
next()
funzione, tuttavia non riesco davvero a capire come funzioni. In ogni caso, ho trovato una risposta al mio problema, qui: stackoverflow.com/a/60096865/566849