Idiomatische Methode zur Unterscheidung von zwei Null-Arg-Konstruktoren

41

Ich habe eine Klasse wie diese:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

Normalerweise möchte ich das countsArray standardmäßig (Null) wie gezeigt initialisieren .

An ausgewählten Stellen, die durch Profilerstellung identifiziert wurden, möchte ich jedoch die Array-Initialisierung unterdrücken, da ich weiß, dass das Array bald überschrieben wird, der Compiler jedoch nicht intelligent genug ist, um dies herauszufinden.

Was ist eine idiomatische und effiziente Methode, um einen solchen "sekundären" Null-Arg-Konstruktor zu erstellen?

Derzeit verwende ich eine Tag-Klasse, uninit_tagdie wie folgt als Dummy-Argument übergeben wird:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

Dann rufe ich den No-Init-Konstruktor auf, wie event_counts c(uninit_tag{});wenn ich die Konstruktion unterdrücken möchte.

Ich bin offen für Lösungen, bei denen keine Dummy-Klasse erstellt wird oder die in irgendeiner Weise effizienter sind usw.

BeeOnRope
quelle
"Weil ich weiß, dass das Array bald überschrieben wird" Sind Sie zu 100% sicher, dass Ihr Compiler diese Optimierung nicht bereits für Sie durchführt? Beispiel: gcc.godbolt.org/z/bJnAuJ
Frank
6
@Frank - Ich glaube, die Antwort auf Ihre Frage befindet sich in der zweiten Hälfte des von Ihnen zitierten Satzes? Es gehört nicht in die Frage, aber eine Vielzahl von Dingen kann passieren: (a) Oft ist der Compiler einfach nicht stark genug, um die toten Speicher zu eliminieren. (B) Manchmal wird nur eine Teilmenge der Elemente überschrieben und dies besiegt die Optimierung (aber nur dieselbe Teilmenge wird später gelesen) (c) Manchmal könnte der Compiler dies tun, wird jedoch z. B. besiegt, weil die Methode nicht inline ist.
BeeOnRope
Haben Sie andere Konstruktoren in Ihrer Klasse?
NathanOliver
1
@Frank - eh, Ihr typisches Beispiel zeigt, dass gcc die toten Speicher nicht beseitigt? Wenn Sie mich erraten hätten, hätte ich gedacht, dass gcc diesen sehr einfachen Fall richtig machen würde, aber wenn er hier fehlschlägt, stellen Sie sich einen etwas komplizierteren Fall vor!
BeeOnRope
1
@uneven_mark - ja, gcc 9.2 macht es bei -O3 (aber diese Optimierung ist im Vergleich zu -O2, IME ungewöhnlich), frühere Versionen jedoch nicht. Im Allgemeinen ist die Beseitigung von toten Speichern eine Sache, aber sie ist sehr fragil und unterliegt allen üblichen Einschränkungen, z. B. dass der Compiler die toten Speicher gleichzeitig sehen kann, während er die dominierenden Speicher sieht. Mein Kommentar war eher, um zu verdeutlichen, was Frank zu sagen versuchte, weil er "Beispiel: (Godbolt-Link)" sagte, aber der Link zeigt, dass beide Geschäfte ausgeführt werden (also fehlt mir vielleicht etwas).
BeeOnRope

Antworten:

33

Die Lösung, die Sie bereits haben, ist korrekt und genau das, was ich sehen möchte, wenn ich Ihren Code überprüfe. Es ist so effizient wie möglich, klar und prägnant.

John Zwinck
quelle
1
Die Hauptfrage, die ich habe, ist, ob ich uninit_tagan jedem Ort, an dem ich diese Redewendung verwenden möchte , einen neuen Geschmack deklarieren sollte . Ich hatte gehofft, dass es schon so etwas wie einen Indikatortyp gibt, vielleicht in std::.
BeeOnRope
9
Es gibt keine offensichtliche Auswahl aus der Standardbibliothek. Ich würde nicht für jede Klasse, in der ich diese Funktion verwenden möchte, ein neues Tag definieren. Ich würde ein projektweites no_initTag definieren und es in allen meinen Klassen verwenden, in denen es benötigt wird.
John Zwinck
2
Ich denke, die Standardbibliothek hat männliche Tags zur Unterscheidung von Iteratoren und solchen Dingen und den beiden std::piecewise_construct_tund std::in_place_t. Keiner von ihnen scheint hier vernünftig zu sein. Vielleicht möchten Sie ein globales Objekt Ihres Typs definieren, das immer verwendet werden soll, damit Sie die Klammern nicht in jedem Konstruktoraufruf benötigen. Die STL macht das mit std::piecewise_constructfür std::piecewise_construct_t.
n314159
Es ist nicht so effizient wie möglich. Im AArch64 Aufrufkonvention zum Beispiel hat die Tag - Stack-zugewiesen sein, mit Folgewirkungen (kann nicht Tail-Call entweder ...): godbolt.org/z/6mSsmq
TLW
1
@ TLW Sobald Sie Body zu Konstruktoren hinzufügen, gibt es keine Stapelzuweisung
R2RT
8

Wenn der Konstruktorkörper leer ist, kann er weggelassen oder voreingestellt werden:

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

Dann Standardinitialisierung event_counts counts; wird verlassen counts.countsnicht initialisierte (Standard Initialisierung ist ein No-op hier) und Wert Initialisierung event_counts counts{}; initialize Wert wird counts.counts, effektiv mit Nullen gefüllt wird .

Evg
quelle
3
Andererseits müssen Sie daran denken, die Wertinitialisierung zu verwenden, und OP möchte, dass diese standardmäßig sicher ist.
Dok.
@doc, ich stimme zu. Dies ist nicht die genaue Lösung für das, was OP will. Diese Initialisierung ahmt jedoch integrierte Typen nach. Denn int i;wir akzeptieren, dass es nicht nullinitialisiert ist. Vielleicht sollten wir auch akzeptieren, dass dies event_counts counts;nicht auf Null initialisiert ist, und event_counts counts{};unseren neuen Standard festlegen.
Evg
6

Ich mag deine Lösung. Möglicherweise haben Sie auch verschachtelte Strukturen und statische Variablen berücksichtigt. Zum Beispiel:

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

};

Mit statischen Variablen scheint ein nicht initialisierter Konstruktoraufruf bequemer zu sein:

event_counts e(event_counts::uninit);

Sie können natürlich ein Makro einführen, um die Eingabe zu speichern und es systematischer zu gestalten

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

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}
doc
quelle
3

Ich denke, eine Aufzählung ist eine bessere Wahl als eine Tag-Klasse oder ein Bool. Sie müssen keine Instanz einer Struktur übergeben, und dem Aufrufer ist klar, welche Option Sie erhalten.

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);
        }
    }
};

Das Erstellen von Instanzen sieht dann folgendermaßen aus:

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

Oder verwenden Sie anstelle der Tag-Klasse eine einwertige Aufzählung, um den Tag-Klassen-Ansatz zu verbessern:

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

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

Dann gibt es nur zwei Möglichkeiten, eine Instanz zu erstellen:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};
TimK
quelle
Ich stimme Ihnen zu: Aufzählung ist einfacher. Aber vielleicht haben Sie diese Zeile vergessen:event_counts() : counts{} {}
bläulich
@bluish, meine Absicht war es nicht countsbedingungslos zu initialisieren , sondern nur wenn INITgesetzt ist.
TimK
@bluish Ich denke, der Hauptgrund für die Auswahl einer Tag-Klasse besteht nicht darin, Einfachheit zu erreichen, sondern zu signalisieren, dass ein nicht initialisiertes Objekt etwas Besonderes ist, dh es verwendet eine Optimierungsfunktion anstelle eines normalen Teils der Klassenschnittstelle. Beide boolund enumsind anständig, aber wir müssen uns bewusst sein, dass die Verwendung von Parametern anstelle von Überladung einen etwas anderen semantischen Farbton hat. Im ersten Fall parametrisieren Sie ein Objekt eindeutig, sodass die initialisierte / nicht initialisierte Haltung zu seinem Status wird, während das Übergeben eines Tag-Objekts an ctor eher der Aufforderung an die Klasse entspricht, eine Konvertierung durchzuführen. IMO ist also keine Frage der syntaktischen Wahl.
Doc
@TimK Aber das OP möchte, dass das Standardverhalten die Initialisierung des Arrays ist, daher denke ich, dass Ihre Lösung für die Frage Folgendes enthalten sollte event_counts() : counts{} {}.
bläulich
@bluish In meinem ursprünglichen Vorschlag countswird von initialisiert, std::fillsofern nicht NO_INITangefordert. Wenn Sie den von Ihnen vorgeschlagenen Standardkonstruktor hinzufügen, können Sie die Standardinitialisierung auf zwei verschiedene Arten durchführen. Dies ist keine gute Idee. Ich habe einen anderen Ansatz hinzugefügt, der die Verwendung vermeidet std::fill.
TimK
1

Möglicherweise möchten Sie eine zweiphasige Initialisierung für Ihre Klasse in Betracht ziehen :

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

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

Der obige Konstruktor initialisiert das Array nicht auf Null. Um die Elemente des Arrays auf Null zu setzen, müssen Sie die Elementfunktion set_zero()nach der Erstellung aufrufen .

眠 り ネ ロ ク
quelle
7
Vielen Dank, ich habe diesen Ansatz in Betracht gezogen, möchte aber etwas, das die Standardeinstellung sicher hält - dh standardmäßig Null, und nur an einigen ausgewählten Stellen überschreibe ich das Verhalten auf die unsichere.
BeeOnRope
3
Dies erfordert besondere Sorgfalt, außer den Verwendungen, die nicht initialisiert werden sollen. Es ist also eine zusätzliche Fehlerquelle im Vergleich zur OP-Lösung.
Walnuss
@BeeOnRope könnte man auch std::functionals Konstruktorargument mit etwas ähnlichem wie set_zeroals Standardargument versehen. Sie würden dann eine Lambda-Funktion übergeben, wenn Sie ein nicht initialisiertes Array möchten.
Doc
1

Ich würde es so machen:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

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

Der Compiler ist intelligent genug, um den gesamten Code zu überspringen, wenn Sie ihn verwenden event_counts(false), und Sie können genau sagen, was Sie meinen, anstatt die Benutzeroberfläche Ihrer Klasse so seltsam zu machen.

Matt Timmermans
quelle
8
Sie haben Recht mit der Effizienz, aber boolesche Parameter sorgen nicht für lesbaren Client-Code. event_counts(false)Was bedeutet das, wenn Sie mitlesen und die Erklärung sehen ? Sie haben keine Ahnung, ohne den Namen des Parameters zu überprüfen. Verwenden Sie mindestens eine Aufzählung oder in diesem Fall eine Sentinel / Tag-Klasse, wie in der Frage gezeigt. Dann erhalten Sie eine ähnliche Erklärung event_counts(no_init), die für jeden in ihrer Bedeutung offensichtlich ist.
Cody Gray
Ich denke, das ist auch eine anständige Lösung. Sie können den Standard-Ctor verwerfen und den Standardwert verwenden event_counts(bool initCountr = true).
Dok.
Außerdem sollte ctor explizit sein.
Doc
Leider unterstützt C ++ derzeit keine benannten Parameter, aber wir können die Lesbarkeit verwenden boost::parameterund aufrufenevent_counts(initCounts = false)
phuclv
1
Witzigerweise @doc, event_counts(bool initCounts = true)eigentlich ist ein Default - Konstruktor, aufgrund jeder Parameter einen Standardwert. Die Anforderung ist nur, dass es ohne Angabe von Argumenten aufrufbar ist, event_counts ec;sich nicht darum kümmert, ob es parameterlos ist oder Standardwerte verwendet.
Justin Time - Stellen Sie Monica
1

Ich würde eine Unterklasse verwenden, um ein bisschen Tipparbeit zu sparen:

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{}) {}
};

Sie können die Dummy-Klasse entfernen, indem Sie das Argument des nicht initialisierenden Konstruktors in booloder intoder so ändern , da es nicht mehr mnemonisch sein muss.

Sie können die Vererbung auch vertauschen und events_count_no_initmit einem Standardkonstruktor definieren, wie er in der Antwort von Evg vorgeschlagen wurde, und dann events_countdie Unterklasse sein:

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{} {}
};
Ross Ridge
quelle
Dies ist eine interessante Idee, aber ich habe auch das Gefühl, dass die Einführung eines neuen Typs Reibung verursachen wird. Wenn ich zum Beispiel tatsächlich eine Uninitialisierung möchte event_counts, möchte ich, dass sie vom Typ ist event_count, nicht vom Typ , event_count_uninitializedalso sollte ich direkt bei der Konstruktion abschneiden event_counts c = event_counts_no_init{};, was meiner Meinung nach die meisten Einsparungen beim Tippen eliminiert.
BeeOnRope
@BeeOnRope Nun, für die meisten Zwecke ist ein event_count_uninitializedObjekt ein event_countObjekt. Das ist der springende Punkt bei der Vererbung, es sind keine völlig unterschiedlichen Typen.
Ross Ridge
Einverstanden, aber das Problem ist "für die meisten Zwecke". Sie sind nicht austauschbar - zB wenn Sie versuchen , um zu sehen , assign ecuzu eces funktioniert, aber nicht umgekehrt. Oder wenn Sie Vorlagenfunktionen verwenden, handelt es sich um unterschiedliche Typen, die mit unterschiedlichen Instanziierungen enden, selbst wenn das Verhalten identisch ist (und manchmal nicht, z. B. bei statischen Vorlagenelementen). Besonders bei starker Beanspruchung autokann dies definitiv auftauchen und verwirrend sein: Ich möchte nicht, dass sich die Art und Weise, wie ein Objekt initialisiert wurde, dauerhaft in seinem Typ widerspiegelt.
BeeOnRope