Const C ++ DRY Strategie

14

Aby uniknąć nietrywialnego powielania związanego z C ++ const, czy istnieją przypadki, w których const_cast działałoby, ale prywatna funkcja const zwracająca non-const nie?

W Effective C ++ pozycja 3 Scott Meyers sugeruje, że const_cast w połączeniu z rzutowaniem statycznym może być skutecznym i bezpiecznym sposobem na uniknięcie powielania kodu, np.

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 wyjaśnia dalej, że wywołanie funkcji const przez funkcję non-const jest niebezpieczne.

Poniższy kod jest przykładem pokazującym:

  • wbrew sugestii Meyersa, czasami const_cast w połączeniu z obsadą statyczną jest niebezpieczny
  • czasami posiadanie wywołania funkcji const, non-const, jest mniej niebezpieczne
  • czasami w obu przypadkach użycie const_cast ukrywa potencjalnie przydatne błędy kompilatora
  • unikanie const_cast i posiadanie dodatkowego const prywatnego członka zwracającego non-const to kolejna opcja

Czy któraś ze strategii const_cast unikania powielania kodu jest uważana za dobrą praktykę? Wolałbyś zamiast tego strategię prywatnej metody? Czy istnieją przypadki, w których const_cast działałoby, ale nie działała metoda prywatna? Czy istnieją inne opcje (oprócz powielania)?

Moje obawy dotyczące strategii const_cast są takie, że nawet jeśli kod jest poprawny podczas pisania, później podczas konserwacji kod może stać się niepoprawny, a const_cast ukryje przydatny błąd kompilatora. Wygląda na to, że wspólna funkcja prywatna jest ogólnie bezpieczniejsza.

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
źródło
Pobierający, który właśnie zwraca członka, jest krótszy niż ten, który rzuca i wywołuje inną wersję siebie. Sztuczka przeznaczona jest do bardziej skomplikowanych funkcji, w których zysk deduplikacji przewyższa ryzyko rzutowania.
Sebastian Redl,
@SebastianRedl Zgadzam się, że powielanie byłoby lepsze, gdyby tylko powracający członek. Wyobraź sobie, że jest to bardziej skomplikowane, np. Zamiast zwracać mConstLongLived, moglibyśmy wywołać funkcję na mConstLongLived, która zwraca stałą referencję, która jest następnie używana do wywołania innej funkcji, która zwraca stałą const, której nie posiadamy i mamy dostęp tylko do stała wersja. Mam nadzieję, że jasne jest, że const_cast może usunąć const z czegoś, do czego inaczej nie mielibyśmy dostępu non-const.
JDiMatteo,
4
To wszystko wydaje się śmieszne z prostymi przykładami, ale w prawdziwym kodzie pojawia się duplikacja const, błędy kompilatora const są przydatne w praktyce (często do łapania głupich błędów) i jestem zaskoczony, że proponowane rozwiązanie „skutecznego C ++” jest dziwne i pozornie podatna na błędy para rzutów. Prywatny członek const zwracający non-const wydaje się wyraźnie lepszy od podwójnej obsady i chcę wiedzieć, czy czegoś brakuje.
JDiMatteo,

Odpowiedzi:

8

W przypadku implementacji stałych i niepowiązanych funkcji składowych, które różnią się tylko tym, czy zwracany ptr / referencja jest const, najlepszą strategią DRY jest:

  1. pisząc akcesor, zastanów się, czy naprawdę potrzebujesz akcesorium, zobacz odpowiedź cmastera i http://c2.com/cgi/wiki?AccessorsAreEvil
  2. po prostu zduplikuj kod, jeśli jest trywialny (np. po prostu zwracając członka)
  3. nigdy nie używaj const_cast, aby uniknąć powielania związanego z const
  4. aby uniknąć nietrywialnego powielania, użyj funkcji const prywatnej zwracającej wartość non-const wywoływaną zarówno przez const, jak i non-const funkcje publiczne

na przykład

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

Nazwijmy to prywatną funkcją const zwracającą wzorzec non-const .

Jest to najlepsza strategia unikania powielania w prosty sposób, a jednocześnie pozwala kompilatorowi przeprowadzać potencjalnie przydatne kontrole i zgłaszać komunikaty o błędach związanych z const.

JDiMatteo
źródło
twoje argumenty są raczej przekonujące, ale jestem raczej zdziwiony, w jaki sposób można uzyskać niezmienne odniesienie do czegoś z constinstancji (chyba że odniesienie dotyczy czegoś zadeklarowanego mutablelub chyba, że ​​zastosujesz, const_castale w obu przypadkach nie ma na początku problemu ). Nie mogłem też znaleźć niczego na temat „prywatnej funkcji const zwracającej wzorzec non-const” (jeśli miał być żartem, aby nazwać to wzorem… to nie jest śmieszne;)
idclev 463035818 10.09
1
Oto przykład kompilacji oparty na kodzie w pytaniu: ideone.com/wBE1GB . Przepraszam, nie chciałem powiedzieć, że to żart, ale chciałem nadać mu nazwę (w mało prawdopodobnym przypadku, że zasługuje na nazwę) i zaktualizowałem sformułowanie w odpowiedzi, aby wyjaśnić to bardziej. Minęło kilka lat, odkąd to napisałem, i nie pamiętam, dlaczego uważałem, że przykład podania referencji w konstruktorze jest istotny.
JDiMatteo
Dzięki za przykład, nie mam teraz czasu, ale na pewno do niego wrócę. Fwiw tutaj jest odpowiedzią na to samo podejście, aw komentarzach wskazano na podobne problemy: stackoverflow.com/a/124209/4117728
idclev 463035818
1

Tak, masz rację: wiele programów w C ++, które próbują const-poprawność, rażąco narusza zasadę DRY, a nawet prywatny członek powracający non-const jest nieco zbyt skomplikowany dla wygody.

Pomija się jednak jedno spostrzeżenie: powielanie kodu z powodu stałej poprawności zawsze stanowi problem tylko wtedy, gdy udostępniasz inny kod członkom danych. To samo w sobie stanowi naruszenie enkapsulacji. Zasadniczo ten rodzaj duplikacji kodu występuje głównie w prostych akcesoriach (w końcu przekazujesz dostęp do już istniejących elementów, wartość zwracana zasadniczo nie jest wynikiem obliczeń).

Z mojego doświadczenia wynika, że ​​dobre abstrakty nie zawierają zwykle akcesoriów. W związku z tym w dużej mierze unikam tego problemu, definiując funkcje składowe, które faktycznie coś robią, a nie tylko zapewniając dostęp do danych; Staram się modelować zachowanie zamiast danych. Moim głównym celem w tym przypadku jest uzyskanie pewnej abstrakcji zarówno z moich klas, jak i ich poszczególnych funkcji składowych, zamiast po prostu używania moich obiektów jako kontenerów danych. Ale ten styl z powodzeniem pozwala również uniknąć mnóstwa powtarzających się jednowierszowych akcesoriów const / non-const, które są tak powszechne w większości kodów.

cmaster - przywróć monikę
źródło
Wydaje się, że dyskusja dotyczy tego, czy akcesory są dobre, np. Patrz dyskusja na c2.com/cgi/wiki?AccessorsAreEvil . W praktyce, niezależnie od tego, co myślisz o akcesoriach, często korzystają z nich duże bazy kodu, a jeśli z nich skorzystają, lepiej będzie przestrzegać zasady DRY. Myślę więc, że pytanie zasługuje na większą odpowiedź niż to, że nie powinieneś go zadawać.
JDiMatteo
1
To zdecydowanie pytanie, które warto zadać :-) I nawet nie zaprzeczę, że od czasu do czasu potrzebujesz akcesoriów. Mówię tylko, że styl programowania, który nie jest oparty na akcesoriach, znacznie zmniejsza problem. Nie rozwiązuje to całkowicie problemu, ale jest dla mnie wystarczająco dobre.
cmaster