Initialisieren Sie mehrere konstante Klassenmitglieder mit einem Funktionsaufruf C ++

50

Wenn ich zwei verschiedene konstante Elementvariablen habe, die beide basierend auf demselben Funktionsaufruf initialisiert werden müssen, gibt es eine Möglichkeit, dies zu tun, ohne die Funktion zweimal aufzurufen?

Zum Beispiel eine Bruchklasse, bei der Zähler und Nenner konstant sind.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator(a/gcd(a,b)), denominator(b/gcd(a,b))
    {

    }
private:
    const int numerator, denominator;
};

Dies führt zu Zeitverschwendung, da die GCD-Funktion zweimal aufgerufen wird. Sie können auch ein neues Klassenmitglied definieren gcd_a_bund zuerst die Ausgabe von gcd der Ausgabe in der Initialisierungsliste zuweisen. Dies würde jedoch zu einer Verschwendung von Speicher führen.

Gibt es im Allgemeinen eine Möglichkeit, dies ohne verschwendete Funktionsaufrufe oder Speicher zu tun? Können Sie vielleicht temporäre Variablen in einer Initialisierungsliste erstellen? Vielen Dank.

Qq0
quelle
5
Haben Sie den Beweis, dass "die GCD-Funktion zweimal aufgerufen wird"? Es wird zweimal erwähnt, aber das ist nicht dasselbe wie ein Compiler, der Code ausgibt, der ihn zweimal aufruft. Ein Compiler kann daraus schließen, dass es sich um eine reine Funktion handelt, und seinen Wert bei der zweiten Erwähnung wiederverwenden.
Eric Towers
6
@EricTowers: Ja, in einigen Fällen können Compiler das Problem in der Praxis manchmal umgehen. Aber nur, wenn sie die Definition (oder eine Anmerkung in einem Objekt) sehen können, sonst keine Möglichkeit, zu beweisen, dass es rein ist. Sie sollten mit aktivierter Link-Time-Optimierung kompilieren, aber nicht jeder tut dies. Und die Funktion könnte sich in einer Bibliothek befinden. Oder betrachten wir den Fall einer Funktion , die tut Nebenwirkungen haben, und es genau einmal Aufruf ist eine Frage der Korrektheit?
Peter Cordes
@EricTowers Interessanter Punkt. Ich habe tatsächlich versucht, dies zu überprüfen, indem ich eine print-Anweisung in die GCD-Funktion eingefügt habe, aber jetzt ist mir klar, dass dies verhindern würde, dass es sich um eine reine Funktion handelt.
Qq0
@ Qq0: Sie können dies überprüfen, indem Sie sich den als asm generierten Compiler ansehen, z. B. mit dem Godbolt-Compiler-Explorer mit gcc oder clang -O3. Aber wahrscheinlich würde es für jede einfache Testimplementierung tatsächlich den Funktionsaufruf inline. Wenn Sie __attribute__((const))den Prototyp verwenden oder rein verwenden, ohne eine sichtbare Definition anzugeben, sollte GCC oder Clang die Eliminierung gemeinsamer Subexpressionen (CSE) zwischen den beiden Aufrufen mit demselben Argument durchführen. Beachten Sie, dass Drews Antwort auch für nicht reine Funktionen funktioniert, daher ist es viel besser und Sie sollten sie immer dann verwenden, wenn die Funktion möglicherweise nicht inline ist.
Peter Cordes
Im Allgemeinen werden nicht statische konstante Elementvariablen am besten vermieden. Einer der wenigen Bereiche, in denen const nicht oft alles gilt. Beispielsweise können Sie keine Klassenobjekte zuweisen. Sie können_back in einen Vektor einfügen, jedoch nur, solange das Kapazitätslimit keine Größenänderung bewirkt.
Doug

Antworten:

66

Gibt es im Allgemeinen eine Möglichkeit, dies ohne verschwendete Funktionsaufrufe oder Speicher zu tun?

Ja. Dies kann mit einem delegierenden Konstruktor erfolgen , der in C ++ 11 eingeführt wurde.

Eine Delegation von Konstruktor ist eine sehr effiziente Art und Weise temporäre Werte für den Bau benötigt zu erwerben , bevor alle Membervariablen initialisiert werden.

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Call gcd ONCE, and forward the result to another constructor.
    Fraction(int a, int b) : Fraction(a,b,gcd(a,b))
    {
    }
private:
    // This constructor is private, as it is an
    // implementation detail and not part of the public interface.
    Fraction(int a, int b, int g_c_d) : numerator(a/g_c_d), denominator(b/g_c_d)
    {
    }
    const int numerator, denominator;
};
Drew Dormann
quelle
Wäre der Aufwand für den Aufruf eines anderen Konstruktors aus Interesse erheblich?
Qq0
1
@ Qq0 Hier können Sie feststellen, dass bei aktivierten bescheidenen Optimierungen kein Overhead entsteht.
Drew Dormann
2
@ Qq0: C ++ basiert auf modernen optimierenden Compilern. Sie können diese Delegierung trivial einbinden, insbesondere wenn Sie sie in der Klassendefinition (in .h) sichtbar machen , auch wenn die eigentliche Konstruktordefinition für Inlining nicht sichtbar ist. Das heißt, der gcd()Aufruf würde in jede Konstruktor-Aufrufseite einbinden und nur einen callprivaten Konstruktor mit drei Operanden überlassen .
Peter Cordes
10

Die Mitgliedsvariablen werden in der Reihenfolge initialisiert, in der sie in der Klassendeklaration deklariert sind. Daher können Sie (mathematisch) Folgendes tun:

#include <iostream>
int gcd(int a, int b){return 2;}; // Greatest Common Divisor of (4, 6) just to test
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator{a/gcd(a,b)}, denominator(b/(a/numerator))
    {

    }
//private:
    const int numerator, denominator;//make sure that they are in this order
};
//Test
int main(){
    Fraction f{4,6};
    std::cout << f.numerator << " / " << f.denominator;
}

Sie müssen keine anderen Konstruktoren aufrufen oder erstellen.

asmmo
quelle
6
ok, das funktioniert speziell für GCD, aber viele andere Anwendungsfälle können die 2. Konstante wahrscheinlich nicht aus den Argumenten und der ersten ableiten. Und wie geschrieben hat dies eine zusätzliche Unterteilung, die ein weiterer Nachteil gegenüber dem Ideal ist, das der Compiler möglicherweise nicht optimiert. GCD kostet möglicherweise nur eine Abteilung, daher ist dies möglicherweise fast so schlimm wie ein zweimaliger Aufruf von GCD. (Angenommen, die Aufteilung dominiert die Kosten anderer Vorgänge, wie dies bei modernen CPUs häufig der Fall ist.)
Peter Cordes,
@PeterCordes, aber die andere Lösung hat einen zusätzlichen Funktionsaufruf und weist mehr Befehlsspeicher zu.
Asmmo
1
Sprechen Sie über Drews delegierenden Konstruktor? Dies kann natürlich die Fraction(a,b,gcd(a,b))Delegation in den Anrufer einbinden, was zu geringeren Gesamtkosten führt. Dieses Inlining ist für den Compiler einfacher, als die zusätzliche Unterteilung darin rückgängig zu machen. Ich habe es nicht auf godbolt.org versucht, aber du könntest es, wenn du neugierig bist. Verwenden Sie gcc oder clang -O3wie bei einem normalen Build. (C ++ basiert auf der Annahme eines modernen Optimierungs-Compilers, daher Funktionen wie constexpr)
Peter Cordes
-3

@Drew Dormann gab eine ähnliche Lösung wie ich es mir vorgestellt hatte. Da OP niemals erwähnt, dass der ctor nicht geändert werden kann, kann dies aufgerufen werden mit Fraction f {a, b, gcd(a, b)}:

Fraction(int a, int b, int tmp): numerator {a/tmp}, denominator {b/tmp}
{
}

Nur auf diese Weise gibt es keinen zweiten Aufruf einer Funktion, eines Konstruktors oder auf andere Weise, sodass keine Zeit verschwendet wird. Und es wird kein Speicher verschwendet, da ohnehin ein temporärer Speicher erstellt werden müsste. Sie können ihn also auch gut nutzen. Es vermeidet auch eine zusätzliche Aufteilung.

ein betroffener Bürger
quelle
3
Durch Ihre Bearbeitung wird die Frage nicht einmal beantwortet. Jetzt muss der Anrufer ein drittes Argument übergeben? Ihre ursprüngliche Version, die die Zuweisung im Konstruktorkörper verwendet, funktioniert nicht für const, aber zumindest für andere Typen. Und welche zusätzliche Teilung vermeiden Sie "auch"? Du meinst gegen Asmmos Antwort?
Peter Cordes
1
Ok, habe meine Ablehnung entfernt, nachdem Sie Ihren Standpunkt erklärt haben. Dies scheint jedoch ziemlich offensichtlich schrecklich zu sein und erfordert, dass Sie einen Teil der Konstruktorarbeit manuell in jeden Aufrufer einbinden. Dies ist das Gegenteil von DRY (wiederholen Sie sich nicht) und der Verkapselung der Verantwortung / Interna der Klasse. Die meisten Leute würden dies nicht als akzeptable Lösung betrachten. Angesichts der Tatsache, dass es eine C ++ 11-Methode gibt, um dies sauber zu machen, sollte dies niemand tun, es sei denn, er steckt in einer älteren C ++ - Version fest, und die Klasse hat nur sehr wenige Aufrufe an diesen Konstruktor.
Peter Cordes
2
@aconcernedcitizen: Ich meine nicht aus Leistungsgründen, ich meine aus Gründen der Codequalität. Wenn Sie jemals die interne Funktionsweise dieser Klasse geändert haben, müssen Sie alle Aufrufe des Konstruktors suchen und das dritte Argument ändern. Dieses Extra ,gcd(foo, bar)ist zusätzlicher Code, der aus jeder Call-Site in der Quelle herausgerechnet werden könnte und sollte . Dies ist ein Problem der Wartbarkeit / Lesbarkeit, nicht der Leistung. Der Compiler wird es höchstwahrscheinlich zur Kompilierungszeit einbinden, was Sie für die Leistung wünschen.
Peter Cordes
1
@PeterCordes Sie haben Recht, jetzt sehe ich, dass meine Gedanken auf die Lösung gerichtet waren, und ich habe alles andere ignoriert. In jedem Fall bleibt die Antwort, wenn auch nur zum Schämen. Wann immer ich Zweifel habe, weiß ich, wo ich suchen muss.
ein besorgter Bürger
1
Betrachten Sie auch den Fall von Fraction f( x+y, a+b ); Um es so zu schreiben BadFraction f( x+y, a+b, gcd(x+y, a+b) );, müssten Sie tmp vars schreiben oder verwenden. Oder noch schlimmer: Was ist, wenn Sie schreiben möchten Fraction f( foo(x), bar(y) );? Dann muss die Aufrufsite einige tmp-Variablen deklarieren, um Rückgabewerte zu speichern, oder diese Funktionen erneut aufrufen und hoffen, dass der Compiler sie entfernt. Dies vermeiden wir. Möchten Sie den Fall debuggen, dass ein Aufrufer die Argumente verwechselt, gcddamit es nicht die GCD der ersten beiden Argumente ist, die an den Konstruktor übergeben wurden? Nein? Dann machen Sie diesen Fehler nicht möglich.
Peter Cordes