Contatori di tempo di compilazione C ++, rivisitati


28

TL; DR

Prima di provare a leggere l'intero post, sappi che:

  1. una soluzione al problema presentato è stata trovata da me , ma sono ancora desideroso di sapere se l'analisi è corretta;
  2. Ho impacchettato la soluzione in una fameta::counterclasse che risolve alcune stranezze rimanenti. Puoi trovarlo su github ;
  3. 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

  1. Sei davvero d'accordo con questa mia diagnosi?
  2. Se sì, questo nuovo comportamento è richiesto dalla norma? Il precedente era un bug?
  3. 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.

inserisci qui la descrizione dell'immagine

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 Ne Iche 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.

clang ++ crede di aver definito qualcosa che non ha definito

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

  1. La mia soluzione alternativa è C ++ legale o sono riuscito a scoprire un altro bug g ++?
  2. Se è legale, ho quindi scoperto alcuni brutti bug clang ++?
  3. 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



2
Come ricordo il comitato standard, le persone hanno la chiara intenzione di vietare costrutti in fase di compilazione di qualsiasi tipo, forma o forma che non producono lo stesso esatto risultato ogni volta che vengono (ipoteticamente) valutati. Quindi potrebbe trattarsi di un bug del compilatore, potrebbe essere un caso "mal formato, nessuna diagnosi richiesta" o potrebbe essere qualcosa che lo standard ha mancato. Tuttavia va contro lo "spirito della norma". Mi dispiace. Mi sarebbe piaciuto anche compilare i contatori dei tempi.
Bolov,

@HolyBlackCat Devo confessare che trovo molto difficile orientarmi su quel codice. Sembra che potrebbe evitare la necessità di passare esplicitamente un numero monotonicamente crescente come parametro alla 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
Fabio A.

@FabioA. Anche io non capisco del tutto quella risposta. Da quando ho posto questa domanda, mi sono reso conto che non voglio più toccare i contatori di constexpr.
HolyBlackCat

Mentre questo è un piccolo esperimento divertente, qualcuno che ha effettivamente usato quel codice dovrebbe quasi aspettarsi che non funzionerà nelle future versioni di C ++, giusto? In tal senso, il risultato si definisce un bug.
Aziuth

Risposte:


5

Dopo ulteriori approfondimenti, risulta che esiste una modifica minore che può essere eseguita sulla next()funzione, che fa funzionare correttamente il codice su versioni clang ++ precedenti alla 7.0.0, ma fa smettere di funzionare per tutte le altre versioni di clang ++.

Dai un'occhiata al seguente codice, tratto dalla mia precedente soluzione.

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

Se lo presti attenzione, ciò che fa letteralmente è provare a leggere il valore associato slot<N>, aggiungere 1 ad esso e quindi associare questo nuovo valore allo stesso slot<N> .

Quando slot<N>non ha alcun valore associato, slot<Y>viene invece recuperato il valore associato , con Yl'indice più alto inferiore a Ntale a cui slot<Y>è associato un valore.

Il problema con il codice sopra è che, anche se funziona su g ++, clang ++ (giustamente, direi?) Fa restituire in modo reader(0, slot<N>()) permanente tutto ciò che ha restituito quando slot<N>non aveva alcun valore associato. A sua volta, ciò significa che tutti gli slot vengono effettivamente associati al valore di base 0.

La soluzione è trasformare il codice sopra in questo:

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

Si noti che slot<N>()è stato modificato in slot<N-1>(). Ha senso: se voglio associare un valore a slot<N>, significa che nessun valore è ancora associato, quindi non ha senso tentare di recuperarlo. Inoltre, vogliamo aumentare un contatore e il valore del contatore associato slot<N>deve essere uno più il valore associato slot<N-1>.

Eureka!

Ciò rompe le versioni di clang ++ <= 7.0.0, tuttavia.

conclusioni

Mi sembra che la soluzione originale che ho pubblicato abbia un bug concettuale, tale che:

  • g ++ ha stranezze / bug / rilassamento che si cancellano con il bug della mia soluzione e alla fine fanno comunque funzionare il codice.
  • Le versioni di clang ++> 7.0.0 sono più rigorose e non amano il bug nel codice originale.
  • Le versioni di clang ++ <= 7.0.0 hanno un bug che impedisce alla soluzione corretta di funzionare.

Riassumendo, il seguente codice funziona su tutte le versioni di g ++ e 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

Il codice così com'è funziona anche con msvc. Il compilatore icc non attiva SFINAE durante l'utilizzo decltype(counter(slot<N>())), preferendo lamentarsi di non poterlo fare deduce the return type of function "counter(slot<N>)"perché it has not been defined. Credo che questo sia un bug , che può essere risolto facendo SFINAE sul risultato diretto di counter(slot<N>). Funziona anche su tutti gli altri compilatori, ma g ++ decide di sputare una quantità abbondante di avvisi molto fastidiosi che non possono essere disattivati. Quindi, anche in questo caso, #ifdefpotrebbe venire in soccorso.

La prova è su Godbolt , screnshots di seguito.

inserisci qui la descrizione dell'immagine


2
Penso che questo tipo di risposta chiuda l'argomento, ma vorrei comunque sapere se ho ragione nella mia analisi, quindi aspetterò prima di accettare la mia risposta come corretta, sperando che qualcun altro passi e mi dia un indizio migliore o una conferma. :)
Fabio A.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.