Costanti strategie C ++ DRY


14

Per evitare la duplicazione relativa a const C ++ non banale, ci sono casi in cui const_cast funzionerebbe ma una funzione const privata che restituisce non const non lo farebbe?

In Effective C ++ item 3 di Scott Meyers , suggerisce che un const_cast combinato con un cast statico può essere un modo efficace e sicuro per evitare duplicati di codice, ad es.

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 continua spiegando che avere la funzione const chiama la funzione non const è pericoloso.

Il codice seguente è un contro-esempio che mostra:

  • contrariamente a quanto suggerisce Meyers, a volte il const_cast combinato con un cast statico è pericoloso
  • a volte avere la funzione const chiamata non-const è meno pericoloso
  • a volte in entrambi i modi l'uso di const_cast nasconde errori del compilatore potenzialmente utili
  • evitare un'altra const_cast e avere un membro privato const aggiuntivo che restituisce una non const è un'altra opzione

Una delle strategie const_cast per evitare la duplicazione del codice è considerata una buona pratica? Preferiresti invece la strategia del metodo privato? Ci sono casi in cui const_cast funzionerebbe ma un metodo privato no? Ci sono altre opzioni (oltre alla duplicazione)?

La mia preoccupazione per le strategie const_cast è che anche se il codice è corretto quando è scritto, in seguito durante la manutenzione il codice potrebbe diventare errato e il const_cast nasconderebbe un utile errore del compilatore. Sembra che una funzione privata comune sia generalmente più sicura.

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

Un getter che restituisce solo un membro è più corto di uno che lancia e chiama l'altra versione di se stesso. Il trucco è pensato per funzioni più complicate in cui il guadagno della deduplicazione supera i rischi del casting.
Sebastian Redl,

@SebastianRedl Sono d'accordo sul fatto che la duplicazione sarebbe migliore se solo tornassi membro. Immagina che sia più complicato, ad esempio invece di restituire mConstLongLived, potremmo chiamare una funzione su mConstLongLived che restituisce un riferimento const che quindi viene usato per chiamare un'altra funzione che restituisce un riferimento const che non possediamo e che abbiamo accesso solo una versione const di. Spero che il punto sia chiaro che const_cast può rimuovere const da qualcosa a cui altrimenti non avremmo accesso non const.
JDiMatteo,

4
Tutto questo sembra ridicolo con semplici esempi, ma la duplicazione relativa a const si presenta in codice reale, gli errori del compilatore const sono utili nella pratica (spesso per cogliere errori stupidi) e sono sorpreso che la proposta soluzione "C ++ efficace" sia una strana e coppie di cast apparentemente soggette a errori. Un membro const privato che restituisce una non const sembra chiaramente superiore a un doppio cast, e voglio sapere se c'è qualcosa che mi manca.
JDiMatteo,

Risposte:


8

Quando si implementano le funzioni const e non const che differiscono solo se il ptr / riferimento restituito è const, la migliore strategia DRY è:

  1. se si scrive un accessor, considerare se hai davvero bisogno dell'accessor , vedi la risposta di cmaster e http://c2.com/cgi/wiki?AccessorsAreEvil
  2. basta duplicare il codice se è banale (ad es. solo restituire un membro)
  3. non usare mai const_cast per evitare la duplicazione relativa a const
  4. per evitare duplicazioni non banali, utilizzare una funzione const privata che restituisce una non const che le funzioni pubbliche const e non const chiamano

per esempio

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

Chiamiamo questa funzione const privata che restituisce un modello non const .

Questa è la migliore strategia per evitare duplicazioni in modo diretto, pur consentendo al compilatore di eseguire verifiche potenzialmente utili e riportare messaggi di errore relativi a const.


i tuoi argomenti sono piuttosto convincenti, ma sono piuttosto perplesso su come puoi ottenere un riferimento non const a qualcosa da constun'istanza (a meno che il riferimento non sia a qualcosa dichiarato mutable, o a meno che tu non impieghi un const_castma in entrambi i casi non esiste un probkem per cominciare ). Inoltre non sono riuscito a trovare nulla sulla "funzione const privata che restituisce un modello non const" (se si intendeva essere uno scherzo per chiamarlo modello ... non è divertente;)
idclev 463035818

1
Ecco un esempio di compilazione basato sul codice nella domanda: ideone.com/wBE1GB . Scusate, non volevo dirlo come uno scherzo, ma non intendevo dargli un nome qui (nel caso improbabile che meriti un nome), e ho aggiornato il testo nella risposta per cercare di renderlo più chiaro. Sono passati alcuni anni da quando ho scritto questo, e non ricordo perché pensavo che un esempio che passasse un riferimento nel costruttore fosse rilevante.
JDiMatteo,

Grazie per l'esempio, non ho tempo ora, ma ci tornerò sicuramente. FWIW qui è una risposta che presenta lo stesso approccio e nei commenti questioni analoghe sono state sottolineato: stackoverflow.com/a/124209/4117728
idclev 463.035.818

1

Sì, hai ragione: molti programmi C ++ che tentano la correttezza const sono in grave violazione del principio DRY, e anche il membro privato che restituisce non const è un po 'troppa complessità per comodità.

Tuttavia, ti manca un'osservazione: la duplicazione del codice a causa della correttezza const è sempre un problema solo se stai dando accesso ad altro codice ai tuoi membri dei dati. Questo di per sé è in violazione dell'incapsulamento. In genere, questo tipo di duplicazione del codice si verifica principalmente nei semplici accessori (dopo tutto, si sta concedendo l'accesso a membri già esistenti, il valore di ritorno non è generalmente il risultato di un calcolo).

La mia esperienza è che le buone astrazioni non tendono ad includere gli accessori. Di conseguenza, evito ampiamente questo problema definendo le funzioni membro che effettivamente fanno qualcosa, piuttosto che fornire semplicemente accesso ai membri dei dati; Provo a modellare il comportamento anziché i dati. La mia intenzione principale in questo è di ricavare un po 'di astrazione da entrambe le mie classi e dalle loro singole funzioni membro, invece di usare semplicemente i miei oggetti come contenitori di dati. Ma questo stile ha anche abbastanza successo nell'evitare le tonnellate di accessori ripetitivi a una riga costanti / non costanti che sono così comuni nella maggior parte dei codici.


Sembra in discussione se gli accessori sono buoni o meno, ad esempio vedere la discussione su c2.com/cgi/wiki?AccessorsAreEvil . In pratica, indipendentemente da ciò che pensi degli accessori, le basi di codice di grandi dimensioni spesso li usano e, se li usano, sarebbe meglio aderire al principio DRY. Quindi penso che la domanda meriti più di una risposta che non dovresti farla.
JDiMatteo,

1
È sicuramente una domanda che vale la pena di fare :-) E non negherò nemmeno di tanto in tanto che hai bisogno di accessori. Sto semplicemente dicendo che uno stile di programmazione che non si basa su accessori riduce notevolmente il problema. Non risolve del tutto il problema, ma almeno è abbastanza buono per me.
cmaster - ripristina monica il
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.