Jak usunąć duplikację kodu pomiędzy podobnymi funkcjami const i non-const member?

Załóżmy, że mam następujący class X gdzie chcę zwrócić dostęp do wewnętrznego członka:

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

Dwie funkcje członowe X::Z() i X::Z() const mają identyczny kod wewnątrz szelek. Jest to zduplikowany kod i może powodować problemy konserwacyjne dla długich funkcji ze złożoną logiką .

Czy istnieje sposób, aby uniknąć powielania kodu?

Author: sharptooth, 2008-09-24

15 answers

Aby uzyskać szczegółowe wyjaśnienie, patrz nagłówek "Avoid Duplicate in const and Non - const Member Function", na str. 23, w punkcie 3 "Use const ilekroć to możliwe", w Effective C++, 3d ed by Scott Meyers, ISBN-13: 9780321334879.

alt text

Oto rozwiązanie (uproszczone):

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

Dwa rzuty i wywołanie funkcji może być brzydkie, ale jest poprawne. Meyers ma dokładne wyjaśnienie dlaczego.

 153
Author: jwfearn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-01 01:24:08

Tak, możliwe jest uniknięcie powielania kodu. Musisz użyć funkcji const member, aby mieć logikę i aby funkcja non-const member wywołała funkcję const member i ponownie oddała zwracaną wartość do odniesienia non-const (lub wskaźnika, jeśli funkcje zwracają 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 umieścić logikę w funkcji non-const i mieć const-function wywołać funkcję non-const -- może to spowodować undefined zachowanie. Powodem jest to, że stała instancja klasy jest rzucana jako niestała instancja. Funkcja non-const member może przypadkowo zmodyfikować klasę, co w standardowych Stanach C++ spowoduje nieokreślone zachowanie.

 52
Author: Kevin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-16 22:29:54

Myślę, że rozwiązanie Scotta Meyersa można poprawić w C++11 za pomocą funkcji tempate helper. To sprawia, że intencja jest o wiele bardziej oczywista i może być ponownie wykorzystana dla wielu innych getterów.

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)...));
}

Tę funkcję pomocniczą można wykorzystać 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);
   }
};

Pierwszy argument jest zawsze wskaźnikiem this. Drugi jest wskaźnikiem do funkcji członka do wywołania. Następnie można przekazać dowolną ilość dodatkowych argumentów, aby mogły zostać przekazane do funkcji. To potrzebuje C++11 ze względu na zmienne szablony.

 29
Author: Pait,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-19 10:08:06

Trochę bardziej gadatliwy niż Meyers, ale mogę 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ść, że zwraca non-const z& dla instancji const, dlatego jest prywatna. Metody prywatne mogą łamać niezmienniki interfejsu zewnętrznego (w tym przypadku pożądanym niezmiennikiem jest "obiekt const nie może być modyfikowany poprzez referencje uzyskane przez niego do obiektów, które posiada-a").

Zauważ, że komentarze są częścią interfejsu pattern-_getZ określa, że nigdy nie jest poprawne nazywanie go (oczywiście poza accessorami): i tak nie ma możliwej korzyści z robienia tego, ponieważ jest to 1 więcej znaków do wpisania i nie spowoduje mniejszego lub szybszego kodu. Wywołanie tej metody jest równoznaczne z wywołaniem jednego z accesorów z const_cast, i nie chcesz tego robić. Jeśli martwisz się, że błędy są oczywiste (a to uczciwy cel), nazwij to const_cast_getZ zamiast _getZ.

Przy okazji, doceniam rozwiązanie Meyersa. I nie mam do tego żadnych filozoficznych zastrzeżeń. Osobiście jednak wolę odrobinę kontrolowanego powtarzania i prywatną metodę, którą należy wywoływać tylko w pewnych ściśle kontrolowanych okolicznościach, niż metodę, która wygląda jak szum linii. Wybierz truciznę i trzymaj się jej.

[Edit: Kevin słusznie zauważył, że _getZ może chcieć wywołać inną metodę (powiedzmy generateZ), która jest const-wyspecjalizowana w taki sam sposób, jak getZ. W tym przypadku _getZ zobaczyłby const z & i musiałby const_cast to before return. To jest nadal bezpieczne, ponieważ accessor boilerplate policza wszystko, ale nie jest oczywiste, że jest bezpieczne. Co więcej, jeśli to zrobisz, a potem zmienisz generateZ na always return const, wtedy musisz również zmienić getZ na always return const, ale kompilator nie powie Ci, że to robisz.

Ten ostatni punkt o kompilatorze jest również prawdą o zalecanym wzorze Meyersa, ale pierwszy punkt o nieoczywistym const_cast nie jest. tak dalej balance myślę, że jeśli _getZ okaże się potrzebować const_cast dla jego wartości zwrotnej, to ten wzór traci dużo swojej wartości nad Meyers 's. ponieważ ma również wady w porównaniu do Meyers' s, myślę, że przełączyłbym się na jego w tej sytuacji. Refaktoryzacja od jednego do drugiego jest łatwa-nie wpływa na żaden inny poprawny kod w klasie, ponieważ tylko nieprawidłowy kod i kocioł wywołują _getZ.]

 18
Author: Steve Jessop,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-24 13:13:32

Ładne pytanie i miłe odpowiedzi. Mam inne rozwiązanie, które nie używa odlewó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ę wymagającą statycznego członu i potrzebę użycia wewnątrz niego zmiennej instance.

Nie wzięłam pod uwagę wszystkich możliwych (negatywnych) implikacji tego rozwiązania. Proszę dać mi znać, jeśli w ogóle.
 15
Author: gd1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-19 16:54:36

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());
}

To ma tę zaletę, że:

  • jest oczywiste, co się dzieje
  • ma minimalny narzut kodu -- mieści się w jednej linii
  • Nie jest to jednak żaden problem, ponieważ nie jest on w stanie rozwiązać problemu.]}

Jeśli chcesz przejść pełną trasę dedukcyjną, to można to osiągnąć poprzez posiadanie funkcji pomocniczej

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

Now you can ' t even mess up volatile, a użycie wygląda jak

T & f() {
    return as_mutable(std::as_const(*this).f());
}
 13
Author: David Stone,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-08 16:16:42

Możesz również rozwiązać to za pomocą szablonów. To rozwiązanie jest nieco brzydkie (ale brzydota jest ukryta w .plik cpp), ale zapewnia sprawdzanie przez kompilator stałości i nie duplikuje 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 w funkcji globalnej, musisz albo zdobyć członków X używając publicznych metod, takich jak GetVector () powyżej (z których zawsze trzeba być const i non-const Wersja) lub można uczynić tę funkcję przyjacielem. Ale nie lubię przyjaciół.

[Edit: Usunięto niepotrzebne włączenie cstdio dodane podczas testów.]

 6
Author: Andy Balaam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-13 16:33:38

Może przeniesiemy logikę do prywatnej metody i zrobimy tylko rzeczy "get the reference and return" wewnątrz getterów? Właściwie, byłbym dość zdezorientowany co do statycznych i kontrastowych odlewów wewnątrz prostej funkcji gettera i uznałbym to za brzydkie, z wyjątkiem niezwykle rzadkich okoliczności!

 3
Author: MP24,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-23 22:05:02

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

#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;
}
 1
Author: matovitch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-16 13:13:06

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

};

To nie jest tak fantazyjne jak szablony lub odlewy, ale to sprawia, że intencje ("te dwie funkcje mają być identyczne") dość wyraźne.

 1
Author: user1476176,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-14 02:33:05

Zazwyczaj funkcje Członkowskie, dla których potrzebujesz wersji const i non-const, to gettery i settery. W większości przypadków są one Jednowierszowe, więc powielanie kodu nie stanowi problemu.

 0
Author: Dima,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-23 21:11:44

Aby dodać do rozwiązania jwfearn i kevin podane, oto odpowiednie rozwiązanie, gdy funkcja zwraca 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;
};
 0
Author: Christer Swahn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-20 14:06:10

Proponuję szablon statycznej funkcji prywatnego pomocnika, taki:

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);
    }
};
 0
Author: dats,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-07 14:25:20

Nie znalazłem tego, czego szukałem, więc wrzuciłem kilka swoich...

Ta jest trochę słowna, ale ma tę zaletę, że obsługuje wiele przeciążonych metod o tej samej nazwie (i typie zwracania) 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ą metodę const na nazwę, ale nadal wiele metod do powielenia, możesz wybrać to:

  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 przeładowywać nazwę (lista argumentów wskaźnika funkcji wydaje się być nierozwiązane w tym momencie, więc nie może znaleźć dopasowania do argumentu funkcji). Chociaż możesz też z tego wyjść:

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

Ale argumenty odniesienia do metody const nie zgadzają się z argumentami pozornie wartościowymi do szablonu i się psują. Nie wiem dlaczego.Oto dlaczego .

 0
Author: sh1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:33

Ten artykuł DDJ pokazuje sposób korzystania ze specjalizacji szablonu, który nie wymaga użycia const_cast. Dla tak prostej funkcji to naprawdę nie jest potrzebne choć.

Boost:: any_cast (w pewnym momencie już tego nie robi) używa const_cast z wersji const wywołującej wersję non-const, aby uniknąć powielania. Nie możesz narzucić semantyki const na wersję non-const, więc musisz być bardzo ostrożny z tym.

W końcu jakaś duplikacja kodu jest w porządku, o ile dwa fragmenty są bezpośrednio nad sobą.

 -1
Author: Greg Rogers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-23 21:03:38