Const C ++ DRY Strategies

10

Per evitare duplicazioni costanti non banali di C ++, ci sono casi in cui const_cast potrebbe funzionare, ma una funzione const privata che restituisce non-const non lo sarebbe?

Nell'articolo Effective C ++ di Scott Meyers, suggerisce che un const_cast combinato con un cast statico può essere un modo efficace e sicuro per evitare il codice duplicato, ad esempio

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 pericolosa.

Il codice sotto è un contro-esempio che mostra:

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

Le strategie const_cast di evitare la duplicazione del codice sono considerate buone prassi? Preferiresti invece la strategia del metodo privato? Ci sono casi in cui const_cast funzionerebbe, ma non sarebbe un metodo privato? 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 {};
    
posta JDiMatteo 15.07.2015 - 20:06
fonte

2 risposte

5

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

  1. se stai scrivendo una accessor, considera se hai davvero bisogno dell'accessor, vedi la risposta di cmaster e link
  2. basta duplicare il codice se è banale (ad es. restituendo un membro)
  3. non utilizzare mai un const_cast per evitare la duplicazione correlata a cost
  4. per evitare duplicazioni non banali, utilizzare una funzione const privata che restituisce un non-const che chiama sia le funzioni pubbliche const che non-const

per es.

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

Questa è la funzione di conversione privata che restituisce un modello non costante .

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

    
risposta data 16.07.2015 - 18:04
fonte
1

Sì, hai ragione: molti programmi C ++ che tentano la cost-correttezza sono in netto contrasto con il principio DRY e anche il membro privato che restituisce non-const è un po 'troppo complesso per comodità.

Tuttavia, si perde un'osservazione: la duplicazione del codice dovuta alla cost-correttezza è sempre un problema se si sta dando accesso ad altri codici ai membri dei dati. Questo di per sé è in violazione dell'incapsulamento. Generalmente, questo tipo di duplicazione del codice si verifica principalmente in accessor semplici (dopotutto, si sta passando l'accesso a membri già esistenti, il valore di ritorno non è generalmente il risultato di un calcolo).

La mia esperienza è che buone astrazioni non tendono ad includere gli accessor. Di conseguenza, in gran parte evito 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 ottenere effettivamente un'astrazione dalle mie classi e dalle loro singole funzioni membro, invece di usare solo i miei oggetti come contenitori di dati. Ma questo stile ha anche abbastanza successo nell'evitare le tonnellate di accessori a riga singola ripetitivi const / non-const che sono così comuni nella maggior parte dei codici.

    
risposta data 16.07.2015 - 22:00
fonte

Leggi altre domande sui tag