Const C ++ DRY-Strategien

14

Gibt es Fälle, in denen const_cast funktioniert, eine private const-Funktion, die nicht-const zurückgibt, jedoch nicht, um nicht-triviale C ++ const-bezogene Duplikationen zu vermeiden?

In Scott Meyers ' Effective C ++, Punkt 3, schlägt er vor, dass ein const_cast in Kombination mit einem statischen Cast eine effektive und sichere Methode sein kann, um doppelten Code zu vermeiden, z

const void* Bar::bar(int i) const
{
  ...
  return variableResultingFromNonTrivialDotDotDotCode;
}
void* Bar::bar(int i)
{
  return const_cast<void*>(static_cast<const Bar*>(this)->bar(i));
}

Meyers erklärt weiter, dass es gefährlich ist, wenn die const-Funktion die Nicht-const-Funktion aufruft.

Der folgende Code ist ein Gegenbeispiel:

  • Entgegen Meyers Vorschlag ist der const_cast in Kombination mit einem statischen Cast manchmal gefährlich
  • Manchmal ist es weniger gefährlich, wenn die const-Funktion den non-const aufruft
  • manchmal verbergen beide Methoden, die einen const_cast verwenden, potenziell nützliche Compiler-Fehler
  • Das Vermeiden eines const_cast und das Zurückgeben eines Nicht-const durch ein zusätzliches privates const-Mitglied ist eine weitere Option

Gilt eine der Strategien von const_cast zur Vermeidung von Code-Duplikationen als bewährte Methode? Möchten Sie stattdessen die Strategie der privaten Methode bevorzugen? Gibt es Fälle, in denen const_cast funktionieren würde, eine private Methode jedoch nicht? Gibt es andere Möglichkeiten (außer der Vervielfältigung)?

Mein Anliegen bei den const_cast-Strategien ist, dass selbst wenn der Code beim Schreiben korrekt ist, der Code später während der Wartung inkorrekt werden könnte und der const_cast einen nützlichen Compilerfehler verbergen würde. Es scheint, als ob eine gemeinsame private Funktion im Allgemeinen sicherer ist.

class Foo
{
  public:
    Foo(const LongLived& constLongLived, LongLived& mutableLongLived)
    : mConstLongLived(constLongLived), mMutableLongLived(mutableLongLived)
    {}

    // case A: we shouldn't ever be allowed to return a non-const reference to something we only have a const reference to

    // const_cast prevents a useful compiler error
    const LongLived& GetA1() const { return mConstLongLived; }
    LongLived& GetA1()
    {
      return const_cast<LongLived&>( static_cast<const Foo*>(this)->GetA1() );
    }

    /* gives useful compiler error
    LongLived& GetA2()
    {
      return mConstLongLived; // error: invalid initialization of reference of type 'LongLived&' from expression of type 'const LongLived'
    }
    const LongLived& GetA2() const { return const_cast<Foo*>(this)->GetA2(); }
    */

    // case B: imagine we are using the convention that const means thread-safe, and we would prefer to re-calculate than lock the cache, then GetB0 might be correct:

    int GetB0(int i) { return mCache.Nth(i); }
    int GetB0(int i) const { return Fibonachi().Nth(i); }

    /* gives useful compiler error
    int GetB1(int i) const { return mCache.Nth(i); } // error: passing 'const Fibonachi' as 'this' argument of 'int Fibonachi::Nth(int)' discards qualifiers
    int GetB1(int i)
    {
      return static_cast<const Foo*>(this)->GetB1(i);
    }*/

    // const_cast prevents a useful compiler error
    int GetB2(int i) { return mCache.Nth(i); }
    int GetB2(int i) const { return const_cast<Foo*>(this)->GetB2(i); }

    // case C: calling a private const member that returns non-const seems like generally the way to go

    LongLived& GetC1() { return GetC1Private(); }
    const LongLived& GetC1() const { return GetC1Private(); }

  private:
    LongLived& GetC1Private() const { /* pretend a bunch of lines of code instead of just returning a single variable*/ return mMutableLongLived; }

    const LongLived& mConstLongLived;
    LongLived& mMutableLongLived;
    Fibonachi mCache;
};

class Fibonachi
{ 
    public:
      Fibonachi()
      {
        mCache.push_back(0);
        mCache.push_back(1);
      }

      int Nth(int n) 
      {
        for (int i=mCache.size(); i <= n; ++i)
        {
            mCache.push_back(mCache[i-1] + mCache[i-2]);
        }
        return mCache[n];
      }

      int Nth(int n) const
      {
          return n < mCache.size() ? mCache[n] : -1;
      }
    private:
      std::vector<int> mCache;
};

class LongLived {};
JDiMatteo
quelle
Ein Getter, der gerade ein Member zurückgibt, ist kürzer als einer, der die andere Version von sich selbst ausführt und aufruft. Der Trick ist für kompliziertere Funktionen gedacht, bei denen der Deduplizierungsgewinn das Casting-Risiko überwiegt.
Sebastian Redl
@SebastianRedl Ich bin damit einverstanden, dass eine Vervielfältigung besser wäre, wenn Sie nur Mitglied werden. Stellen Sie sich vor, es ist komplizierter, z. B. könnten wir anstelle von mConstLongLived eine Funktion für mConstLongLived aufrufen, die eine konstante Referenz zurückgibt, die dann zum Aufrufen einer anderen Funktion verwendet wird, die eine konstante Referenz zurückgibt, die wir nicht besitzen und auf die wir nur Zugriff haben eine konstante Version von. Ich hoffe, der Punkt ist klar, dass der const_cast const von etwas entfernen kann, auf das wir sonst keinen Nicht-const-Zugriff hätten.
JDiMatteo
4
Dies alles scheint mit einfachen Beispielen irgendwie lächerlich, aber es kommt in echtem Code zu einer Vervielfältigung von Konstanten, Konstanten-Compiler-Fehlern, die in der Praxis nützlich sind (oft, um dumme Fehler aufzufangen), und ich bin überrascht, dass die vorgeschlagene "effektive C ++" - Lösung seltsam ist und scheinbar fehleranfälliges Paar von Darstellern. Ein privates Const-Mitglied, das einen Nicht-Const zurückgibt, scheint einer Doppelbesetzung eindeutig überlegen zu sein, und ich möchte wissen, ob mir etwas fehlt.
JDiMatteo

Antworten:

7

Bei der Implementierung von konstanten und nicht konstanten Memberfunktionen, die sich nur dadurch unterscheiden, ob es sich bei dem zurückgegebenen ptr / reference um const handelt, ist die beste DRY-Strategie:

  1. Wenn Sie einen Accessor schreiben, prüfen Sie, ob Sie den Accessor wirklich benötigen. Lesen Sie hierzu die Antwort von cmaster und http://c2.com/cgi/wiki?AccessorsAreEvil
  2. Dupliziere einfach den Code, wenn er trivial ist (z. B. wenn du nur ein Mitglied zurückschickst)
  3. Verwenden Sie niemals einen const_cast, um Doppelungen im Zusammenhang mit const zu vermeiden
  4. Um nicht triviale Duplikationen zu vermeiden, verwenden Sie eine private const-Funktion, die eine non-const zurückgibt, die sowohl von der const-Funktion als auch von der public-Funktion aufgerufen wird

z.B

public:
  LongLived& GetC1() { return GetC1Private(); }
  const LongLived& GetC1() const { return GetC1Private(); }
private:
  LongLived& GetC1Private() const { /* non-trivial DRY logic here */ }

Nennen wir dies die private const-Funktion, die ein Muster zurückgibt, das nicht const ist .

Dies ist die beste Strategie, um Duplikationen auf einfache Weise zu vermeiden und dem Compiler dennoch die Möglichkeit zu geben, potenziell nützliche Überprüfungen durchzuführen und const-bezogene Fehlermeldungen zu melden.

JDiMatteo
quelle
Ihre Argumente sind ziemlich überzeugend, aber ich bin ziemlich verwirrt darüber, wie Sie einen nicht konstanten Verweis auf etwas aus einer constInstanz erhalten können (es sei denn, der Verweis bezieht sich auf etwas Deklariertes mutableoder Sie verwenden ein, const_castaber in beiden Fällen gibt es zunächst kein Problem ). Auch konnte ich nichts auf der "private const-Funktion zurückgeben Nicht-const-Muster" (wenn es ein Witz sein sollte, um es Muster zu nennen ... es ist nicht lustig;)
idclev 463035818
1
Hier ist ein Kompilierungsbeispiel basierend auf dem Code in der Frage: ideone.com/wBE1GB . Tut mir leid, ich habe es nicht als Scherz gemeint, aber ich wollte ihm hier einen Namen geben (in dem unwahrscheinlichen Fall, dass er einen Namen verdient), und ich habe den Wortlaut in der Antwort aktualisiert, um dies klarer zu machen. Es ist ein paar Jahre her, seit ich das geschrieben habe, und ich kann mich nicht erinnern, warum ich ein Beispiel für relevant hielt, bei dem eine Referenz im Konstruktor übergeben wurde.
JDiMatteo
Vielen Dank für das Beispiel, ich habe jetzt keine Zeit, aber ich werde auf jeden Fall darauf zurückkommen. Fwiw hier ist eine Antwort, die den gleichen Ansatz darstellt und in den Kommentaren wurden ähnliche Probleme hervorgehoben: stackoverflow.com/a/124209/4117728
idclev 463035818
1

Ja, Sie haben Recht: Viele C ++ - Programme, die versuchen, die Konstanten zu korrigieren, verstoßen stark gegen das DRY-Prinzip, und selbst das private Mitglied, das die Konstanten nicht zurückgibt, ist ein bisschen zu komplex für den Komfort.

Sie übersehen jedoch eine Beobachtung: Eine Codeduplizierung aufgrund von Konstanten-Korrektheit ist immer nur dann ein Problem, wenn Sie anderen Codezugriff auf Ihre Datenelemente gewähren. Dies an sich verstößt gegen die Kapselung. Im Allgemeinen tritt diese Art der Code-Vervielfältigung meistens bei einfachen Zugriffsmethoden auf (schließlich geben Sie den Zugriff an bereits vorhandene Mitglieder weiter, der Rückgabewert ist im Allgemeinen nicht das Ergebnis einer Berechnung).

Ich habe die Erfahrung gemacht, dass gute Abstraktionen nicht dazu neigen, Accessoren einzuschließen. Infolgedessen vermeide ich dieses Problem weitgehend, indem ich Elementfunktionen definiere, die tatsächlich etwas tun, anstatt nur den Zugriff auf Datenelemente zu ermöglichen. Ich versuche, Verhalten statt Daten zu modellieren. Meine Hauptabsicht dabei ist es, eine gewisse Abstraktion sowohl meiner Klassen als auch ihrer einzelnen Elementfunktionen zu erzielen, anstatt nur meine Objekte als Datencontainer zu verwenden. Aber dieser Stil ist auch recht erfolgreich, um die Unmengen von sich wiederholenden Einzeilen-Zugriffsmethoden (const / non-const) zu vermeiden, die in den meisten Codes so häufig vorkommen.

In cmaster stellst du Monica wieder her
quelle
Es steht zur Debatte, ob Accessoren gut sind oder nicht, z. B. die Diskussion unter c2.com/cgi/wiki?AccessorsAreEvil . Unabhängig davon, was Sie von Accessoren halten, werden sie in der Praxis häufig von großen Codebasen verwendet. Wenn sie sie verwenden, ist es besser, das DRY-Prinzip einzuhalten. Daher denke ich, dass die Frage eher eine Antwort verdient, als dass Sie sie nicht stellen sollten.
JDiMatteo
1
Es ist definitiv eine Frage, die es wert ist, gestellt zu werden :-) Und ich werde nicht einmal leugnen, dass Sie von Zeit zu Zeit Accessoren benötigen. Ich sage nur, dass ein Programmierstil, der nicht auf Zugriffsmethoden basiert, das Problem erheblich verringert. Es löst das Problem nicht vollständig, aber es ist zumindest gut genug für mich.
cmaster - wieder monica