Modo idiomatico per distinguere due costruttori zero-arg


41

Ho una lezione come questa:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

Di solito, per impostazione predefinita, voglio inizializzare (zero) l' countsarray come mostrato.

In posizioni selezionate identificate dalla profilazione, tuttavia, vorrei sopprimere l'inizializzazione dell'array, perché so che l'array sta per essere sovrascritto, ma il compilatore non è abbastanza intelligente da capirlo.

Qual è un modo idiomatico ed efficiente per creare un costruttore "secondario" a zero-arg?

Attualmente sto usando una classe di tag uninit_tagche viene passata come argomento fittizio, in questo modo:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

Quindi chiamo il costruttore no-init come event_counts c(uninit_tag{});quando voglio sopprimere la costruzione.

Sono aperto a soluzioni che non comportano la creazione di una classe fittizia o che sono più efficienti in qualche modo, ecc.


"perché so che l'array sta per essere sovrascritto" Sei sicuro al 100% che il tuo compilatore non stia già facendo questa ottimizzazione per te? caso in questione: gcc.godbolt.org/z/bJnAuJ
Frank

6
@Frank - Sento che la risposta alla tua domanda è nella seconda metà della frase che hai citato? Non appartiene alla domanda, ma può succedere una varietà di cose: (a) spesso il compilatore non è abbastanza forte da eliminare i negozi morti (b) a volte solo un sottoinsieme degli elementi viene sovrascritto e questo sconfigge il ottimizzazione (ma solo lo stesso sottoinsieme viene letto in seguito) (c) a volte il compilatore potrebbe farlo, ma viene sconfitto, ad esempio, perché il metodo non è incorporato.
BeeOnRope,

Hai altri costruttori nella tua classe?
NathanOliver,

1
@Frank - eh, il tuo caso in questione mostra che gcc non elimina i negozi morti? In effetti, se mi avessi fatto indovinare, avrei pensato che gcc avrebbe risolto questo caso molto semplice, ma se fallisce qui immagina un caso leggermente più complicato!
BeeOnRope,

1
@uneven_mark - sì, gcc 9.2 lo fa a -O3 (ma questa ottimizzazione è rara rispetto a -O2, IME), ma le versioni precedenti no. In generale, l'eliminazione dei negozi morti è una cosa, ma è molto fragile e soggetta a tutti i soliti avvertimenti, come il compilatore che è in grado di vedere i negozi morti nello stesso momento in cui vede i negozi dominanti. Il mio commento è stato più per chiarire ciò che Frank stava cercando di dire perché ha detto "caso in questione: (link godbolt)" ma il link mostra entrambi i negozi in esecuzione (quindi forse mi manca qualcosa).
BeeOnRope,

Risposte:


33

La soluzione che hai già è corretta, ed è esattamente quello che vorrei vedere se stavo rivedendo il tuo codice. È il più efficiente possibile, chiaro e conciso.


1
Il problema principale che ho è se dovrei dichiarare un nuovo uninit_tagsapore in ogni luogo in cui voglio usare questo linguaggio. Speravo ci fosse già qualcosa di simile a un simile indicatore, forse dentro std::.
BeeOnRope,

9
Non esiste una scelta ovvia dalla libreria standard. Non definirei un nuovo tag per ogni classe in cui desidero questa funzione, definirei un no_inittag a livello di progetto e lo utilizzerei in tutte le mie classi dove è necessario.
John Zwinck,

2
Penso che la libreria standard abbia tag virili per differenziare iteratori e cose simili e le due std::piecewise_construct_te std::in_place_t. Nessuno di questi sembra ragionevole usare qui. Forse vorresti definire un oggetto globale del tuo tipo da usare sempre, quindi non hai bisogno delle parentesi graffe in ogni chiamata del costruttore. L'STL lo fa con std::piecewise_constructper std::piecewise_construct_t.
n314159

Non è il più efficiente possibile. Nella convenzione di chiamata di AArch64, ad esempio, il tag deve essere allocato in pila, con effetti knock-on (non può nemmeno chiamare in coda ...): godbolt.org/z/6mSsmq
TLW

1
@TLW Una volta aggiunto il corpo ai costruttori non c'è allocazione dello stack, godbolt.org/z/vkCD65
R2RT

8

Se il corpo del costruttore è vuoto, può essere omesso o predefinito:

struct event_counts {
    std::uint64_t counts[MAX_COUNTERS];
    event_counts() = default;
};

Quindi l' inizializzazione predefinita event_counts counts; rimarrà counts.countsnon inizializzata (l'inizializzazione predefinita è una no-op qui) e l' inizializzazione del event_counts counts{}; valore valuterà l'inizializzazione counts.counts, riempiendola efficacemente di zeri.


3
Ma poi devi ricordare di usare l'inizializzazione del valore e OP vuole che sia sicuro per impostazione predefinita.
doc

@doc, sono d'accordo. Questa non è la soluzione esatta per ciò che vuole OP. Ma questa inizializzazione imita i tipi predefiniti. Perché int i;accettiamo che non sia inizializzato a zero. Forse dovremmo anche accettare che event_counts counts;non è inizializzato a zero e rendere il event_counts counts{};nostro nuovo default.
Evg

6

Mi piace la tua soluzione. Potresti anche aver considerato la struttura nidificata e la variabile statica. Per esempio:

struct event_counts {
    static constexpr struct uninit_tag {} uninit = uninit_tag();

    uint64_t counts[MAX_COUNTS];

    event_counts() : counts{} {}

    explicit event_counts(uninit_tag) {}

    // more stuff

};

Con la variabile statica la chiamata del costruttore non inizializzata può sembrare più conveniente:

event_counts e(event_counts::uninit);

Ovviamente puoi introdurre una macro per salvare la digitazione e renderla più una caratteristica sistematica

#define UNINIT_TAG static constexpr struct uninit_tag {} uninit = uninit_tag();

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}

3

Penso che un enum sia una scelta migliore di una tag class o di un bool. Non è necessario passare un'istanza di una struttura ed è chiaro al chiamante quale opzione si sta ottenendo.

struct event_counts {
    enum Init { INIT, NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts(Init init = INIT) {
        if (init == INIT) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

Quindi la creazione di istanze è simile alla seguente:

event_counts e1{};
event_counts e2{event_counts::INIT};
event_counts e3{event_counts::NO_INIT};

Oppure, per renderlo più simile all'approccio della classe tag, utilizzare un enum a valore singolo anziché la classe tag:

struct event_counts {
    enum NoInit { NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    explicit event_counts(NoInit) {}
};

Quindi ci sono solo due modi per creare un'istanza:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};

Sono d'accordo con te: l'enum è più semplice. Ma forse hai dimenticato questa frase:event_counts() : counts{} {}
bluastro

@bluish, la mia intenzione non era quella di inizializzare countsincondizionatamente, ma solo quando INITè impostato.
TimK,

@bluish Penso che la ragione principale per scegliere una classe di tag non sia quella di raggiungere la semplicità, ma di segnalare che l'oggetto non inizializzato è speciale, cioè utilizza la funzionalità di ottimizzazione piuttosto che parte normale dell'interfaccia di classe. Entrambi boole enumsono decenti, ma dobbiamo essere consapevoli del fatto che l'uso del parametro anziché del sovraccarico ha una sfumatura semantica leggermente diversa. Nel primo si parametrizza chiaramente un oggetto, quindi la posizione inizializzata / non inizializzata diventa il suo stato, mentre passare un oggetto tag a ctor è più come chiedere alla classe di eseguire una conversione. Quindi non è IMO una questione di scelta sintattica.
doc

@TimK Ma l'OP vuole che il comportamento predefinito sia l'inizializzazione dell'array, quindi penso che la soluzione alla domanda dovrebbe includere event_counts() : counts{} {}.
bluastro

@bluish Nel mio suggerimento originale countsè inizializzato da a std::fillmeno che non NO_INITsia richiesto. L'aggiunta del costruttore predefinito come suggerito potrebbe comportare due modi diversi di eseguire l'inizializzazione predefinita, il che non è un'ottima idea. Ho aggiunto un altro approccio che evita l'utilizzo std::fill.
TimK,

1

Puoi prendere in considerazione un'inizializzazione in due fasi per la tua classe:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

    void set_zero() {
       std::fill(std::begin(counts), std::end(counts), 0u);
    }
};

Il costruttore sopra non inizializza l'array a zero. Per azzerare gli elementi dell'array, è necessario chiamare la funzione membro set_zero()dopo la costruzione.


7
Grazie, ho preso in considerazione questo approccio, ma desidero qualcosa che mantenga il valore predefinito sicuro, ovvero zero per impostazione predefinita, e solo in alcuni punti selezionati ignoro il comportamento a quello non sicuro.
BeeOnRope,

3
Ciò richiederà particolare attenzione, ma gli usi che dovrebbero essere non inizializzati. Quindi è una fonte extra di bug rispetto alla soluzione di OP.
noce,

@BeeOnRope si potrebbe anche fornire std::functioncome argomento di costruzione qualcosa di simile a set_zerocome argomento predefinito. Passereste quindi una funzione lambda se volete un array non inizializzato.
doc

1

Lo farei così:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(bool initCounts) {
        if (initCounts) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};

Il compilatore sarà abbastanza intelligente da saltare tutto il codice quando lo usi event_counts(false)e puoi dire esattamente cosa intendi invece di rendere l'interfaccia della tua classe così strana.


8
Hai ragione sull'efficienza, ma i parametri booleani non rendono il codice client leggibile. Quando leggi insieme e vedi la dichiarazione event_counts(false), cosa significa? Non hai idea senza tornare indietro e guardare il nome del parametro. Meglio usare almeno un enum o, in questo caso, una classe sentinel / tag come mostrato nella domanda. Quindi, ottieni una dichiarazione più simile event_counts(no_init), che è ovvia per tutti nel suo significato.
Cody Grey

Penso che questa sia anche una soluzione decente. È possibile eliminare ctor predefinito e utilizzare il valore predefinito event_counts(bool initCountr = true).
doc

Inoltre, ctor dovrebbe essere esplicito.
doc

sfortunatamente attualmente C ++ non supporta parametri nominati, ma possiamo usare boost::parametere chiamare event_counts(initCounts = false)per leggibilità
phuclv

1
Stranamente, @doc, in event_counts(bool initCounts = true)realtà è un costruttore predefinito, dato che ogni parametro ha un valore predefinito. Il requisito è solo che sia richiamabile senza specificare argomenti, event_counts ec;non importa se è privo di parametri o utilizza valori predefiniti.
Justin Time - Ripristina Monica il

1

Userei una sottoclasse solo per risparmiare un po 'di battitura:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    event_counts(uninit_tag) {}
};    

struct event_counts_no_init: event_counts {
    event_counts_no_init(): event_counts(uninit_tag{}) {}
};

Si può sbarazzarsi della classe fittizia dal cambiando l'argomento del costruttore, non l'inizializzazione di boolo into qualcosa, in quanto non deve essere più mnemonico.

Puoi anche scambiare l'eredità e definirla events_count_no_initcon un costruttore predefinito come Evg ha suggerito nella loro risposta, e quindi avere events_countla sottoclasse:

struct event_counts_no_init {
    uint64_t counts[MAX_COUNTERS];
    event_counts_no_init() = default;
};

struct event_counts: event_counts_no_init {
    event_counts(): event_counts_no_init{} {}
};

Questa è un'idea interessante, ma sento anche che introdurre un nuovo tipo causerà attrito. Ad esempio, quando in realtà voglio un non inizializzato event_counts, voglio che sia di tipo event_count, non event_count_uninitialized, quindi dovrei tagliare proprio come in costruzione event_counts c = event_counts_no_init{};, che penso elimini la maggior parte dei risparmi nella digitazione.
BeeOnRope,

@BeeOnRope Bene, per la maggior parte degli scopi un event_count_uninitializedoggetto è un event_countoggetto. Questo è il punto centrale dell'eredità, non sono tipi completamente diversi.
Ross Ridge,

D'accordo, ma il problema è "per la maggior parte degli scopi". Essi non sono intercambiabili - per esempio, se si cerca di vedere Assegnare ecual ecfunziona, ma non il contrario. Oppure, se si utilizzano le funzioni del modello, sono tipi diversi e terminano con istanze diverse anche se il comportamento finisce per essere identico (e talvolta non lo sarà, ad esempio, con i membri del modello statico). Soprattutto con un uso intenso di autoquesto può sicuramente emergere ed essere fonte di confusione: non vorrei che il modo in cui un oggetto era stato inizializzato si riflettesse permanentemente nel suo tipo.
BeeOnRope,
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.