Jak usunąć duplikację kodu między podobnymi stałymi i nie stałymi funkcjami członka?

242

Powiedzmy, że mam następujące miejsce, w class Xktórym chcę zwrócić dostęp do członka wewnętrznego:

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    Z& Z(size_t index)
    {
        // massive amounts of code for validating index

        Z& ret = vecZ[index];

        // even more code for determining that the Z instance
        // at index is *exactly* the right sort of Z (a process
        // which involves calculating leap years in which
        // religious holidays fall on Tuesdays for
        // the next thousand years or so)

        return ret;
    }
    const Z& Z(size_t index) const
    {
        // identical to non-const X::Z(), except printed in
        // a lighter shade of gray since
        // we're running low on toner by this point
    }
};

Te dwie funkcje składowe X::Z()i X::Z() constmają identyczny kod wewnątrz szelki. Jest to zduplikowany kod i może powodować problemy z obsługą długich funkcji o złożonej logice .

Czy istnieje sposób na uniknięcie tego powielania kodu?

Kevin
źródło
W tym przykładzie zwróciłbym wartość w przypadku const, więc nie można refaktoryzować poniżej. int Z () const {return z; }
Matt Price,
1
W przypadku podstawowych typów masz całkowitą rację! Mój pierwszy przykład nie był zbyt dobry. Powiedzmy, że zamiast tego zwracamy zamiast tego instancję klasy. (Zaktualizowałem pytanie, aby to odzwierciedlić.)
Kevin,

Odpowiedzi:

189

Szczegółowe wyjaśnienie znajduje się w tytule „Unikaj powielania w funkcjach innych constniż elementy constczłonkowskie” na s. 9. 23, w punkcie 3 „Używaj, constkiedy to możliwe”, w Effective C ++ , 3d wyd. Scott Meyers, ISBN-13: 9780321334879.

alternatywny tekst

Oto rozwiązanie Meyersa (uproszczone):

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

Dwie rzutowania i wywołanie funkcji mogą być brzydkie, ale są poprawne. Meyers ma dokładne wyjaśnienie, dlaczego.

jwfearn
źródło
45
Nikt nigdy nie został zwolniony za śledzenie Scott Meyers :-)
Steve Jessop
11
witkamp ma rację, że ogólnie źle jest używać const_cast. Jest to szczególny przypadek, w którym tak nie jest, jak wyjaśnia Meyers. @Adam: ROM => const jest w porządku. const == ROM jest oczywiście nonsensem, ponieważ każdy może rzucić non-const na const willy-nilly: to po prostu rezygnacja z modyfikacji.
Steve Jessop
44
Ogólnie sugerowałbym użycie const_cast zamiast static_cast, aby dodać const, ponieważ zapobiega to przypadkowej zmianie typu.
Greg Rogers
6
@HelloGoodbye: Myślę, że Meyers zakłada odrobinę inteligencji od projektanta interfejsu klasy. Jeśli get()constzwraca coś, co zostało zdefiniowane jako obiekt stały, to w ogóle nie powinna istnieć wersja nie stała get(). W rzeczywistości moje myślenie o tym zmieniło się z czasem: rozwiązanie szablonu jest jedynym sposobem uniknięcia duplikacji i uzyskania stałej poprawności sprawdzanej przez kompilator, więc osobiście nie używałbym więcej const_castw celu uniknięcia powielania kodu, wybrałbym skopiowany kod do szablonu funkcji lub pozostawiając go zduplikowanym.
Steve Jessop,
7
Następujące dwa szablony niezwykle ułatwiają czytelność tego rozwiązania: template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }i template<typename T> T& variable(const T& _) { return const_cast<T&>(_); }. Następnie możesz zrobić:return variable(constant(*this).get());
Casey Rodarmor
64

Tak, można uniknąć duplikacji kodu. Musisz użyć funkcji const członka, aby mieć logikę, a funkcja non-const członka wywoła funkcję const członka i ponownie wyśle ​​wartość zwracaną do odwołania non-const (lub wskaźnika, jeśli funkcje zwrócą wskaźnik):

class X
{
   std::vector<Z> vecZ;

public:
   const Z& z(size_t index) const
   {
      // same really-really-really long access 
      // and checking code as in OP
      // ...
      return vecZ[index];
   }

   Z& z(size_t index)
   {
      // One line. One ugly, ugly line - but just one line!
      return const_cast<Z&>( static_cast<const X&>(*this).z(index) );
   }

 #if 0 // A slightly less-ugly version
   Z& Z(size_t index)
   {
      // Two lines -- one cast. This is slightly less ugly but takes an extra line.
      const X& constMe = *this;
      return const_cast<Z&>( constMe.z(index) );
   }
 #endif
};

UWAGA: Ważne jest, aby NIE umieszczać logiki w funkcji non-const, a funkcja const wywoła funkcję non-const - może to spowodować niezdefiniowane zachowanie. Powodem jest to, że instancja klasy stałej jest rzutowana jako instancja niestała. Niepodstawowa funkcja członkowska może przypadkowo zmodyfikować klasę, co spowoduje, że stany standardu C ++ spowodują niezdefiniowane zachowanie.

Kevin
źródło
3
Wow ... to okropne. Właśnie zwiększyłeś ilość kodu, zmniejszyłeś klarowność i dodałeś dwa śmierdzące const_cast <> s. Być może masz na myśli przykład, w którym to ma sens?
Shog9,
14
Hej, nie rób tego !, może być brzydka, ale według Scotta Meyersa jest (prawie) właściwa droga. Patrz Skuteczne C ++ , wyd. 3d, pozycja 3 pod nagłówkiem „Unikanie duplikacji w stałych i nie-kosztowych funkcjach członkowskich.
jwfearn 23.09.08
17
Rozumiem, że rozwiązanie może być brzydkie, ale wyobraź sobie, że kod określający, co należy zwrócić, ma 50 wierszy. W takim przypadku powielanie jest wysoce niepożądane - szczególnie, gdy trzeba ponownie rozłożyć kod na czynniki. Spotkałem się z tym wiele razy w mojej karierze.
Kevin,
8
Różnica między tym a Meyerem polega na tym, że Meyers ma static_cast <const X &> (* this). const_cast służy do usuwania const, a nie dodawania go.
Steve Jessop,
8
@ VioletGiraffe wiemy, że obiekt nie został pierwotnie utworzony const, ponieważ jest on elementem non-const obiektu non const, co wiemy, ponieważ jesteśmy w metodzie non-const tego obiektu. Kompilator nie wnioskuje o tym, kieruje się konserwatywną regułą. Jak myślisz, dlaczego istnieje const_cast, jeśli nie w takiej sytuacji?
Caleth
47

C ++ 17 zaktualizował najlepszą odpowiedź na to pytanie:

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

Ma to takie zalety, że:

  • To oczywiste, co się dzieje
  • Ma minimalny narzut kodu - mieści się w jednej linii
  • Trudno się pomylić (można go rzucić tylko volatileprzez przypadek, ale volatilejest rzadkim kwalifikatorem)

Jeśli chcesz przejść pełną trasę dedukcyjną, możesz to osiągnąć, korzystając z funkcji pomocnika

template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
    return const_cast<T &>(value);
}
template<typename T>
constexpr T * as_mutable(T const * value) noexcept {
    return const_cast<T *>(value);
}
template<typename T>
constexpr T * as_mutable(T * value) noexcept {
    return value;
}
template<typename T>
void as_mutable(T const &&) = delete;

Teraz nie możesz nawet zepsuć się volatile, a wygląda to na użycie

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}
David Stone
źródło
Zauważ, że „as_mutable” z usuniętym przeciążeniem wartości stałej (co jest ogólnie preferowane) uniemożliwia działanie ostatniego przykładu, jeśli f()zwraca Tzamiast T&.
Max Truxa,
1
@MaxTruxa: Tak, i to jest dobra rzecz. Gdyby to po prostu skompilować, mielibyśmy wiszące odniesienie. W przypadku f()zwrotów Tnie chcemy mieć dwóch przeciążeń, constwystarczy sama wersja.
David Stone
Bardzo prawda, przepraszam za mój pełny pierd mózgu, nie mam pojęcia, o czym myślałem, kiedy napisałem ten komentarz. Patrzyłem na stałą / zmienną parę getterów zwracającą a shared_ptr. Więc tak naprawdę potrzebowałem czegoś, as_mutable_ptrco wygląda prawie identycznie jak as_mutablepowyżej, z tym wyjątkiem, że bierze i zwraca a shared_ptri używa std::const_pointer_castzamiast const_cast.
Max Truxa,
1
Jeśli metoda zwróci, T const*wiązałoby się to T const* const&&raczej z wiązaniem niż z T const* const&(przynajmniej w moich testach tak się stało). Musiałem dodać przeciążenie dla T const*jako typu argumentu dla metod zwracających wskaźnik.
monkey0506,
2
@ monkey0506: Zaktualizowałem swoją odpowiedź do wskaźników wsparcia i referencji
David Stone
34

Myślę, że rozwiązanie Scotta Meyersa można ulepszyć w C ++ 11 za pomocą funkcji pomocnika tymczasowego. To sprawia, że ​​intencja jest o wiele bardziej oczywista i może być ponownie wykorzystana dla wielu innych osób pobierających.

template <typename T>
struct NonConst {typedef T type;};
template <typename T>
struct NonConst<T const> {typedef T type;}; //by value
template <typename T>
struct NonConst<T const&> {typedef T& type;}; //by reference
template <typename T>
struct NonConst<T const*> {typedef T* type;}; //by pointer
template <typename T>
struct NonConst<T const&&> {typedef T&& type;}; //by rvalue-reference

template<typename TConstReturn, class TObj, typename... TArgs>
typename NonConst<TConstReturn>::type likeConstVersion(
   TObj const* obj,
   TConstReturn (TObj::* memFun)(TArgs...) const,
   TArgs&&... args) {
      return const_cast<typename NonConst<TConstReturn>::type>(
         (obj->*memFun)(std::forward<TArgs>(args)...));
}

Tej funkcji pomocnika można użyć w następujący sposób.

struct T {
   int arr[100];

   int const& getElement(size_t i) const{
      return arr[i];
   }

   int& getElement(size_t i) {
      return likeConstVersion(this, &T::getElement, i);
   }
};

Pierwszym argumentem jest zawsze ten wskaźnik. Drugi to wskaźnik do funkcji członka, którą należy wywołać. Następnie można przekazać dowolną liczbę dodatkowych argumentów, aby można je było przekazać do funkcji. Wymaga to C ++ 11 z powodu różnych szablonów.

Pait
źródło
3
Szkoda, że ​​nie musimy std::remove_bottom_constiść std::remove_const.
TBBle
Nie podoba mi się to rozwiązanie, ponieważ nadal zawiera const_cast. Możesz getElementsam stworzyć szablon i użyć cechy tego typu w środku do mpl::conditionaltypów, których potrzebujesz, takich jak iterators lub constiterators, jeśli to konieczne. Prawdziwy problem polega na tym, jak wygenerować stałą wersję metody, gdy nie można szablonować tej części podpisu?
v.oddou
2
@ v.oddou: std::remove_const<int const&>is int const &(usuń constkwalifikacje na najwyższym poziomie ), stąd gimnastyka NonConst<T>w tej odpowiedzi. Domniemany std::remove_bottom_constmoże usunąć constkwalifikację najniższego poziomu i zrobić dokładnie to, co NonConst<T>tutaj: std::remove_bottom_const<int const&>::type=> int&.
TBBle
4
To rozwiązanie nie działa dobrze, jeśli getElementjest przeciążone. Wówczas wskaźnika funkcji nie można rozwiązać bez jawnego podania parametrów szablonu. Czemu?
John
1
Musisz naprawić swoją odpowiedź, aby korzystać z idealnego przekazywania w C ++ 11: likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TArgs>(args)...)); }Wypełnij: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83
ShaulF
22

Trochę bardziej gadatliwy niż Meyers, ale mógłbym to zrobić:

class X {

    private:

    // This method MUST NOT be called except from boilerplate accessors.
    Z &_getZ(size_t index) const {
        return something;
    }

    // boilerplate accessors
    public:
    Z &getZ(size_t index)             { return _getZ(index); }
    const Z &getZ(size_t index) const { return _getZ(index); }
};

Metoda prywatna ma niepożądaną właściwość polegającą na tym, że zwraca nie-stałą Z i dla stałej instancji, dlatego jest prywatna. Prywatne metody mogą łamać niezmienniki interfejsu zewnętrznego (w tym przypadku pożądanym niezmiennikiem jest „obiekt stały nie może być modyfikowany poprzez referencje uzyskane przez niego do obiektów, które posiada”).

Zauważ, że komentarze są częścią wzorca - interfejs _getZ określa, że ​​nigdy nie można go wywoływać (poza akcesoriami, oczywiście): i tak nie ma żadnych korzyści, ponieważ jest to 1 dodatkowy znak do wpisania i nie będzie skutkuje mniejszym lub szybszym kodem. Wywołanie metody jest równoważne z wywołaniem jednego z akcesorów za pomocą const_cast, a ty też tego nie chciałbyś zrobić. Jeśli martwisz się, że błędy będą oczywiste (a to uczciwy cel), nazwij to const_cast_getZ zamiast _getZ.

Nawiasem mówiąc, doceniam rozwiązanie Meyersa. Nie mam przeciwko temu filozoficznego sprzeciwu. Osobiście jednak wolę odrobinę kontrolowanego powtarzania i metodę prywatną, którą można wywołać tylko w ściśle ściśle kontrolowanych okolicznościach, niż metodę, która wygląda jak szum linii. Wybierz swoją truciznę i trzymaj się jej.

[Edycja: Kevin słusznie zauważył, że _getZ może chcieć wywołać kolejną metodę (powiedzmy generateZ), która jest wyspecjalizowana w tej samej zasadzie co getZ. W takim przypadku _getZ zobaczyłby const Z i musiałby go const_cast przed powrotem. Nadal jest to bezpieczne, ponieważ akcesorium do płyty kotłowej porządkuje wszystko, ale nie jest szczególnie oczywiste, że jest bezpieczne. Ponadto, jeśli to zrobisz, a następnie zmienisz generatorZ, aby zawsze zwracał stałą, musisz również zmienić getZ, aby zawsze zwracał stałą, ale kompilator nie powie ci, że to zrobisz.

Ten ostatni punkt dotyczący kompilatora jest również zgodny z zalecanym wzorcem Meyersa, ale pierwszy punkt dotyczący nieoczywistego const_cast nie jest. Podsumowując, myślę, że jeśli _getZ okaże się, że potrzebuje const_cast dla jego wartości zwracanej, wówczas ten wzór traci dużo swojej wartości w porównaniu z wartością Meyersa. Ponieważ ma również wady w porównaniu do Meyersa, myślę, że w tej sytuacji przeszedłbym na jego. Refaktoryzacja od jednego do drugiego jest łatwa - nie wpływa na żaden inny poprawny kod w klasie, ponieważ tylko niepoprawny kod i płyta główna wywołują _getZ.]

Steve Jessop
źródło
3
Nadal występuje problem polegający na tym, że zwracana rzecz może być stała dla stałej instancji X. W takim przypadku nadal potrzebujesz const_cast w _getZ (...). W przypadku niewłaściwego wykorzystania przez późniejszych programistów, nadal może prowadzić do UB. Jeśli zwracana rzecz jest „zmienna”, jest to dobre rozwiązanie.
Kevin
1
Każda prywatna funkcja (do cholery, również publiczna) może być źle używana przez późniejszych programistów, jeśli zdecydują się zignorować instrukcje BLOCK CAPITAL dotyczące jej prawidłowego użycia, w pliku nagłówkowym, a także w Doxygen itp. Nie mogę tego zatrzymać, i nie uważam tego za mój problem, ponieważ instrukcje są łatwe do zrozumienia.
Steve Jessop
13
-1: To nie działa w wielu sytuacjach. Co jeśli somethingw _getZ()funkcji jest zmienna instancji? Kompilator (lub przynajmniej niektóre kompilatory) będą narzekać, że skoro _getZ()jest const, to każda zmienna instancji, do której się odwołuje, jest również const. Tak więc somethingbyłby const (byłby typu const Z&) i nie mógłby zostać przekonwertowany na Z&. Z mojego (co prawda nieco ograniczonego) doświadczenia somethingwynika , że przez większość czasu w takich przypadkach jest to zmienna instancji.
Grawitacja
2
@GravityBringer: wtedy „coś” musi obejmować const_cast. Miał być miejscem na kod wymaganym do uzyskania niezmiennego zwrotu z obiektu const, a nie jako miejscem na to , co byłoby w zduplikowanym getterze. Zatem „coś” nie jest tylko zmienną instancji.
Steve Jessop,
2
Widzę. To jednak naprawdę zmniejsza przydatność tej techniki. Chciałbym usunąć głos negatywny, ale SO mi na to nie pozwala.
Grawitacja
22

Ładne pytanie i ładne odpowiedzi. Mam inne rozwiązanie, które nie używa rzutów:

class X {

private:

    std::vector<Z> v;

    template<typename InstanceType>
    static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) {
        // massive amounts of code for validating index
        // the instance variable has to be used to access class members
        return instance.v[i];
    }

public:

    const Z& get(std::size_t i) const {
        return get(*this, i);
    }

    Z& get(std::size_t i) {
        return get(*this, i);
    }

};

Ma jednak brzydotę polegającą na wymaganiu członu statycznego i konieczności użycia instancezmiennej wewnątrz niego.

Nie rozważyłem wszystkich możliwych (negatywnych) implikacji tego rozwiązania. Daj mi znać, jeśli w ogóle.

gd1
źródło
4
Cóż, odejdźmy od prostego faktu, że dodałeś więcej płyt kotłowych. Jeśli już, należy to wykorzystać jako przykład tego, dlaczego język potrzebuje sposobu modyfikowania kwalifikatorów funkcji wraz z typem zwracanym auto get(std::size_t i) -> auto(const), auto(&&). Czemu '&&'? Ach, więc mogę powiedzieć:auto foo() -> auto(const), auto(&&) = delete;
kfsone
gd1: dokładnie to, co miałem na myśli. @ kfsone i dokładnie to, co doszedłem.
v.oddou
1
@ kfsone składnia powinna zawierać thissłowo kluczowe. Sugeruję, template< typename T > auto myfunction(T this, t args) -> decltype(ident)że to słowo kluczowe zostanie rozpoznane jako domyślny argument instancji obiektu i pozwoli kompilatorowi rozpoznać, że moja funkcja jest członkiem lub T. Tzostaną automatycznie wydedukowane na stronie połączenia, która zawsze będzie rodzajem klasy, ale z bezpłatną kwalifikacją cv.
v.oddou
2
To rozwiązanie ma również tę zaletę (w porównaniu z const_casttym), że umożliwia powrót iteratori const_iterator.
Jarod42,
1
Jeśli implementacja zostanie przeniesiona do pliku cpp (a ponieważ metoda niepublicowania nie powinna być trywialna, prawdopodobnie tak by było), staticmożna to zrobić w zakresie pliku zamiast zakresu klasy. :-)
Jarod42
8

Możesz to również rozwiązać za pomocą szablonów. To rozwiązanie jest nieco brzydkie (ale brzydota jest ukryta w pliku .cpp), ale zapewnia sprawdzanie ciągłości kompilatora i brak duplikacji kodu.

plik .h:

#include <vector>

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    const std::vector<Z>& GetVector() const { return vecZ; }
    std::vector<Z>& GetVector() { return vecZ; }

    Z& GetZ( size_t index );
    const Z& GetZ( size_t index ) const;
};

plik .cpp:

#include "constnonconst.h"

template< class ParentPtr, class Child >
Child& GetZImpl( ParentPtr parent, size_t index )
{
    // ... massive amounts of code ...

    // Note you may only use methods of X here that are
    // available in both const and non-const varieties.

    Child& ret = parent->GetVector()[index];

    // ... even more code ...

    return ret;
}

Z& X::GetZ( size_t index )
{
    return GetZImpl< X*, Z >( this, index );
}

const Z& X::GetZ( size_t index ) const
{
    return GetZImpl< const X*, const Z >( this, index );
}

Główną wadą, jaką widzę, jest to, że ponieważ cała złożona implementacja metody jest funkcją globalną, musisz albo zdobyć elementy X przy użyciu publicznych metod, takich jak GetVector () powyżej (z których zawsze musi istnieć const i non-const version) lub możesz uczynić tę funkcję przyjazną. Ale nie lubię przyjaciół.

[Edycja: usunięto niepotrzebne dołączenie cstdio dodane podczas testowania.]

Andy Balaam
źródło
3
Zawsze możesz ustawić złożoną funkcję implementacyjną jako element statyczny, aby uzyskać dostęp do elementów prywatnych. Funkcja musi być zadeklarowana tylko w pliku nagłówkowym klasy, definicja może znajdować się w pliku implementacyjnym klasy. W końcu jest to część implementacji klasy.
CB Bailey,
Ach tak, dobry pomysł! Nie podoba mi się to, że szablony pojawiają się w nagłówku, ale skoro tutaj potencjalnie upraszcza to implementację, to chyba warto.
Andy Balaam
+ 1 do tego rozwiązania, które nie powiela żadnego kodu ani nie używa brzydkich const_cast(które mogłyby zostać przypadkowo użyte do zbrojenia czegoś, co tak naprawdę powinno być const do czegoś, co nie jest).
HelloGoodbye,
Obecnie można to uprościć za pomocą dedukcyjnego typu zwrotu dla szablonu (szczególnie przydatne, ponieważ zmniejsza to, co musi zostać zduplikowane w klasie w przypadku elementu).
Davis Herring
3

Co powiesz na przeniesienie logiki do metody prywatnej i wykonywanie tylko czynności „pobierz referencję i zwróć” wewnątrz modułów pobierających? Właściwie byłbym dość zdezorientowany co do rzutów statycznych i stałych w prostej funkcji gettera i uważałbym to za brzydkie, z wyjątkiem wyjątkowo rzadkich okoliczności!

MP24
źródło
Aby uniknąć nieokreślonego zachowania, nadal potrzebujesz const_cast. Zobacz odpowiedź Martina Yorka i mój komentarz tam.
Kevin
1
Kevin, jaka odpowiedź Martina Yorka
Peter Nimmo
2

Czy używanie preprocesora jest oszustwem?

struct A {

    #define GETTER_CORE_CODE       \
    /* line 1 of getter code */    \
    /* line 2 of getter code */    \
    /* .....etc............. */    \
    /* line n of getter code */       

    // ^ NOTE: line continuation char '\' on all lines but the last

   B& get() {
        GETTER_CORE_CODE
   }

   const B& get() const {
        GETTER_CORE_CODE
   }

   #undef GETTER_CORE_CODE

};

Nie jest tak fantazyjny jak szablony lub rzutowania, ale sprawia, że ​​twoja intencja („te dwie funkcje mają być identyczne”) jest dość wyraźna.

użytkownik1476176
źródło
1
Ale wtedy musisz uważać na odwrotne ukośniki (jak zwykle w przypadku makr wielowierszowych), a ponadto tracisz podświetlanie składni w większości (jeśli nie wszystkich) edytorów.
Ruslan
2

Zaskakuje mnie, że istnieje tak wiele różnych odpowiedzi, ale prawie wszystkie polegają na dużej magii szablonów. Szablony są potężne, ale czasami makra pobijają je w zwięzłości. Maksymalna wszechstronność jest często osiągana przez połączenie obu.

Napisałem makro, FROM_CONST_OVERLOAD()które można umieścić w funkcji non-const, aby wywołać funkcję const.

Przykładowe użycie:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

Prosta implementacja wielokrotnego użytku:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

Wyjaśnienie:

Jak napisano w wielu odpowiedziach, typowy wzorzec unikania duplikacji kodu w funkcji niezrzeszonej jest następujący:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

Wiele z tej płyty kotłowej można uniknąć, korzystając z wnioskowania o typie. Po pierwsze, const_castmoże być enkapsulowany WithoutConst(), co określa typ jego argumentu i usuwa kwalifikator const. Po drugie, podobne podejście można zastosować WithConst()do stałej kwalifikacji thiswskaźnika, co umożliwia wywołanie metody przeciążenia const.

Reszta to proste makro, które poprzedza połączenie poprawnie zakwalifikowaną this->i usuwa const z wyniku. Ponieważ wyrażenie używane w makrze jest prawie zawsze prostym wywołaniem funkcji z argumentami przekazanymi w stosunku 1: 1, nie pojawiają się wady makr, takich jak wielokrotna ocena. Elipsa i __VA_ARGS__może być również używana, ale nie powinna być potrzebna, ponieważ przecinki (ponieważ separatory argumentów) występują w nawiasach.

Takie podejście ma kilka zalet:

  • Minimalna i naturalna składnia - wystarczy zawinąć połączenie FROM_CONST_OVERLOAD( )
  • Nie jest wymagana dodatkowa funkcja członka
  • Kompatybilny z C ++ 98
  • Prosta implementacja, brak metaprogramowania szablonu i zerowe zależności
  • Przesuwne: inne stosunki const można dodawać (jak const_iterator, std::shared_ptr<const T>itp). W tym celu wystarczy przeciążenie WithoutConst()odpowiednich typów.

Ograniczenia: to rozwiązanie jest zoptymalizowane do scenariuszy, w których przeciążenie ciągłe działa dokładnie tak samo jak przeciążenie ciągłe, dzięki czemu argumenty można przekazywać 1: 1. Jeśli Twoja logika jest this->Method(args)inna i nie wywołujesz stałej wersji przez , możesz rozważyć inne podejścia.

TheOperator
źródło
2

Dla tych (jak ja), którzy

  • użyj c ++ 17
  • chcesz dodać najmniejszą liczbę płyt / powtórzeń i
  • nie przejmuj się używaniem makr (czekając na meta-klasy ...),

oto kolejne ujęcie:

#include <utility>
#include <type_traits>

template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};

#define NON_CONST(func)                                                     \
    template <typename... T> auto func(T&&... a)                            \
        -> typename NonConst<decltype(func(std::forward<T>(a)...))>::type   \
    {                                                                       \
        return const_cast<decltype(func(std::forward<T>(a)...))>(           \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

Jest to w zasadzie połączenie odpowiedzi z @Pait, @DavidStone i @ sh1 ( EDYCJA : i ulepszenie z @cdhowie). To, co dodaje do tabeli, polega na tym, że unikasz tylko jednego dodatkowego wiersza kodu, który po prostu nazywa nazwę funkcji (ale nie ma powielania argumentów ani typów zwracanych):

class X
{
    const Z& get(size_t index) const { ... }
    NON_CONST(get)
};

Uwaga: gcc nie kompiluje tego przed wersją 8.1, clang-5 i nowsze wersje, a także MSVC-19 są zadowoleni (według eksploratora kompilatora ).

axxel
źródło
To po prostu zadziałało dla mnie. To świetna odpowiedź, dziękuję!
Krótki
Czy nie powinny decltype()również używać std::forwardargumentów, aby upewnić się, że używamy odpowiedniego typu zwrotu w przypadku, gdy mamy przeciążenia, get()które przyjmują różne typy referencji?
cdhowie
@cdhowie Czy możesz podać przykład?
axxel
@axxel Jest wymyślony jak diabli, ale proszę bardzo . W NON_CONSTmakro rozpoznaje typ zwracany niepoprawnie i const_castS do niewłaściwego typu ze względu na brak spedycji w decltype(func(a...))typów. Zastąpienie ich decltype(func(std::forward<T>(a)...)) rozwiązuje to . (Jest tylko błąd linkera, ponieważ nigdy nie zdefiniowałem żadnego z zadeklarowanych X::getprzeciążeń.)
cdhowie
1
Dzięki @cdhowie, zirytowałem twój przykład, aby faktycznie użyć nieobciążonych przeciążeń: coliru.stacked-crooked.com/a/0cedc7f4e789479e
axxel
1

Oto wersja C ++ 17 szablonu statycznej funkcji pomocniczej z opcjonalnym testem SFINAE.

#include <type_traits>

#define REQUIRES(...)         class = std::enable_if_t<(__VA_ARGS__)>
#define REQUIRES_CV_OF(A,B)   REQUIRES( std::is_same_v< std::remove_cv_t< A >, B > )

class Foobar {
private:
    int something;

    template<class FOOBAR, REQUIRES_CV_OF(FOOBAR, Foobar)>
    static auto& _getSomething(FOOBAR& self, int index) {
        // big, non-trivial chunk of code...
        return self.something;
    }

public:
    auto& getSomething(int index)       { return _getSomething(*this, index); }
    auto& getSomething(int index) const { return _getSomething(*this, index); }
};

Pełna wersja: https://godbolt.org/z/mMK4r3

atablash
źródło
1

Wymyśliłem makro, które automatycznie generuje pary funkcji const / non-const.

class A
{
    int x;    
  public:
    MAYBE_CONST(
        CV int &GetX() CV {return x;}
        CV int &GetY() CV {return y;}
    )

    //   Equivalent to:
    // int &GetX() {return x;}
    // int &GetY() {return y;}
    // const int &GetX() const {return x;}
    // const int &GetY() const {return y;}
};

Zobacz koniec odpowiedzi na wdrożenie.

Argument argumentu MAYBE_CONSTjest zduplikowany. W pierwszym egzemplarzu CVzostaje zastąpiony niczym; aw drugiej kopii zastąpiono go const.

Nie ma ograniczenia, ile razy CVmoże występować w argumencie makra.

Jest jednak niewielka niedogodność. Jeśli CVpojawia się w nawiasach, ta para nawiasów musi być poprzedzona CV_IN:

// Doesn't work
MAYBE_CONST( CV int &foo(CV int &); )

// Works, expands to
//         int &foo(      int &);
//   const int &foo(const int &);
MAYBE_CONST( CV int &foo CV_IN(CV int &); )

Realizacja:

#define MAYBE_CONST(...) IMPL_CV_maybe_const( (IMPL_CV_null,__VA_ARGS__)() )
#define CV )(IMPL_CV_identity,
#define CV_IN(...) )(IMPL_CV_p_open,)(IMPL_CV_null,__VA_ARGS__)(IMPL_CV_p_close,)(IMPL_CV_null,

#define IMPL_CV_null(...)
#define IMPL_CV_identity(...) __VA_ARGS__
#define IMPL_CV_p_open(...) (
#define IMPL_CV_p_close(...) )

#define IMPL_CV_maybe_const(seq) IMPL_CV_a seq IMPL_CV_const_a seq

#define IMPL_CV_body(cv, m, ...) m(cv) __VA_ARGS__

#define IMPL_CV_a(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_b)
#define IMPL_CV_b(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_a)

#define IMPL_CV_const_a(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_b)
#define IMPL_CV_const_b(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_a)

Implementacja wcześniejsza niż C ++ 20, która nie obsługuje CV_IN:

#define MAYBE_CONST(...) IMPL_MC( ((__VA_ARGS__)) )
#define CV ))((

#define IMPL_MC(seq) \
    IMPL_MC_end(IMPL_MC_a seq) \
    IMPL_MC_end(IMPL_MC_const_0 seq)

#define IMPL_MC_identity(...) __VA_ARGS__
#define IMPL_MC_end(...) IMPL_MC_end_(__VA_ARGS__)
#define IMPL_MC_end_(...) __VA_ARGS__##_end

#define IMPL_MC_a(elem) IMPL_MC_identity elem IMPL_MC_b
#define IMPL_MC_b(elem) IMPL_MC_identity elem IMPL_MC_a
#define IMPL_MC_a_end
#define IMPL_MC_b_end

#define IMPL_MC_const_0(elem)       IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a(elem) const IMPL_MC_identity elem IMPL_MC_const_b
#define IMPL_MC_const_b(elem) const IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a_end
#define IMPL_MC_const_b_end
HolyBlackCat
źródło
0

Zazwyczaj funkcje składowe, dla których potrzebujesz wersji const i non-const, to gettery i setters. Przeważnie są one jednowarstwowe, więc duplikacja kodu nie stanowi problemu.

Dima
źródło
2
To może być prawda przez większość czasu. Ale są wyjątki.
Kevin,
1
getters i tak, const ustawiający nie ma większego sensu;)
jwfearn 23.09.08
Miałem na myśli to, że non-const getter jest faktycznie setera. :)
Dima,
0

Zrobiłem to dla przyjaciela, który słusznie uzasadnił użycie const_cast... nie wiedząc o tym, prawdopodobnie zrobiłbym coś takiego (niezbyt elegancko):

#include <iostream>

class MyClass
{

public:

    int getI()
    {
        std::cout << "non-const getter" << std::endl;
        return privateGetI<MyClass, int>(*this);
    }

    const int getI() const
    {
        std::cout << "const getter" << std::endl;
        return privateGetI<const MyClass, const int>(*this);
    }

private:

    template <class C, typename T>
    static T privateGetI(C c)
    {
        //do my stuff
        return c._i;
    }

    int _i;
};

int main()
{
    const MyClass myConstClass = MyClass();
    myConstClass.getI();

    MyClass myNonConstClass;
    myNonConstClass.getI();

    return 0;
}
matovitch
źródło
0

Sugerowałbym szablon funkcji statycznej pomocnika prywatnego, taki jak ten:

class X
{
    std::vector<Z> vecZ;

    // ReturnType is explicitly 'Z&' or 'const Z&'
    // ThisType is deduced to be 'X' or 'const X'
    template <typename ReturnType, typename ThisType>
    static ReturnType Z_impl(ThisType& self, size_t index)
    {
        // massive amounts of code for validating index
        ReturnType ret = self.vecZ[index];
        // even more code for determining, blah, blah...
        return ret;
    }

public:
    Z& Z(size_t index)
    {
        return Z_impl<Z&>(*this, index);
    }
    const Z& Z(size_t index) const
    {
        return Z_impl<const Z&>(*this, index);
    }
};
Daty
źródło
-1

W tym artykule DDJ pokazano sposób korzystania ze specjalizacji szablonów, która nie wymaga użycia const_cast. Jednak dla tak prostej funkcji tak naprawdę nie jest ona potrzebna.

boost :: any_cast (w pewnym momencie już nie używa) używa const_cast z wersji const wywołującej wersję non-const, aby uniknąć powielania. Nie możesz jednak nałożyć stałej semantyki na wersję inną niż const, więc musisz być bardzo ostrożny.

W końcu pewne powielanie kodu jest w porządku, o ile dwa fragmenty znajdują się bezpośrednio nad sobą.

Greg Rogers
źródło
Artykuł DDJ zdaje się odnosić do iteratorów - co nie ma związku z pytaniem. Stałe iteratory nie są stałymi danymi - są iteratorami, które wskazują na stałe dane.
Kevin,
-1

Aby dodać do dostarczonego rozwiązania jwfearn i kevin, oto odpowiednie rozwiązanie, gdy funkcja zwróci shared_ptr:

struct C {
  shared_ptr<const char> get() const {
    return c;
  }
  shared_ptr<char> get() {
    return const_pointer_cast<char>(static_cast<const C &>(*this).get());
  }
  shared_ptr<char> c;
};
Christer Swahn
źródło
-1

Nie znalazłem tego, czego szukałem, więc wyrzuciłem kilka własnych ...

Ten jest trochę niewygodny, ale ma tę zaletę, że obsługuje wiele przeciążonych metod o tej samej nazwie (i typie zwracanym) jednocześnie:

struct C {
  int x[10];

  int const* getp() const { return x; }
  int const* getp(int i) const { return &x[i]; }
  int const* getp(int* p) const { return &x[*p]; }

  int const& getr() const { return x[0]; }
  int const& getr(int i) const { return x[i]; }
  int const& getr(int* p) const { return x[*p]; }

  template<typename... Ts>
  auto* getp(Ts... args) {
    auto const* p = this;
    return const_cast<int*>(p->getp(args...));
  }

  template<typename... Ts>
  auto& getr(Ts... args) {
    auto const* p = this;
    return const_cast<int&>(p->getr(args...));
  }
};

Jeśli masz tylko jedną constmetodę na nazwę, ale nadal istnieje wiele metod do powielenia, możesz wybrać:

  template<typename T, typename... Ts>
  auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) {
    return const_cast<T*>((this->*f)(args...));
  }

  int* getp_i(int i) { return pwrap(&C::getp_i, i); }
  int* getp_p(int* p) { return pwrap(&C::getp_p, p); }

Niestety to się psuje, gdy tylko zaczniesz przeciążać nazwę (lista argumentów argumentu wskaźnika funkcji wydaje się w tym momencie nierozwiązana, więc nie może znaleźć dopasowania dla argumentu funkcji). Chociaż możesz również rozwiązać ten problem:

  template<typename... Ts>
  auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }

Ale argumenty odwołujące się do constmetody nie pasują do argumentów najwyraźniej według wartości do szablonu i psuje się. Nie pewny dlaczego. Oto dlaczego .

sh1
źródło