Domanda

Diciamo che ho il seguente class X dove voglio tornare accesso a un membro interno:

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

Le due funzioni membro X::Z() e X::Z() const identico codice all'interno delle parentesi graffe.Questo è il codice duplicato e può causare problemi di manutenzione per lunghi funzioni con una logica complessa.

C'è un modo per evitare questa duplicazione del codice?

È stato utile?

Soluzione 2

Sì, è possibile evitare la duplicazione del codice. È necessario utilizzare la funzione membro const per avere la logica e fare in modo che la funzione membro non const richiami la funzione membro const e rilasci il valore restituito in un riferimento non const (o puntatore se le funzioni restituiscono un puntatore):

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

NOTA: È importante che NON metta la logica nella funzione non const e che la funzione const chiami la funzione non const - può comportare un comportamento indefinito. Il motivo è che un'istanza di classe costante viene lanciata come istanza non costante. La funzione membro non const può modificare accidentalmente la classe, che gli stati standard C ++ comporteranno un comportamento indefinito.

Altri suggerimenti

Per una spiegazione dettagliata, consultare l'intestazione " Evita duplicazioni in const e Non - <=> Funzione membro, " a p. 23, nell'articolo 3 & Quot; Utilizzare <=> quando possibile, & Quot; in C ++ efficace , a cura di Scott Meyers, ISBN-13: 9780321334879.

alt text

Ecco la soluzione di Meyers (semplificata):

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

I due cast e la funzione possono essere brutti ma sono corretti. Meyers ha una spiegazione approfondita del perché.

Penso che Scott Meyers' soluzione può essere migliorata in C++11 utilizzando un tempate funzione di supporto.Questo rende l'intento molto più evidenti e possono essere riutilizzati per molti altri getter.

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

Questa funzione di supporto può essere usata nel modo seguente.

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

Il primo argomento è sempre l'puntatore.Il secondo è il puntatore a funzione membro di chiamata.Dopo che una quantità arbitraria di altri argomenti può essere passato in modo che possano essere inoltrati alla funzione.Questo ha bisogno di C++11 a causa della variadic modelli.

Un po ' più dettagliato rispetto a Meyers, ma mi potrebbe fare questo:

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

Il metodo privato ha indesiderati proprietà che restituisce un valore non-const Z& per un const esempio, che è il motivo per cui è privato.Metodi privati possono rompere invarianti di interfaccia esterna (in questo caso il desiderato invariante è "un oggetto const non può essere modificata attraverso i riferimenti ottenuti attraverso a degli oggetti che è-a").

Nota che i commenti sono parte del modello - _getZ interfaccia specifica che non è mai valido per chiamare (a parte le funzioni di accesso, ovviamente):non c'è concepibile vantaggio a farlo comunque, perché è 1 personaggio in più per tipo e non ridotta o codice più veloce.Chiamare il metodo è equivalente alla chiamata di una delle funzioni di accesso con un const_cast, e non si vuole farlo.Se siete preoccupati che errori evidenti (e questo è un giusto obiettivo), quindi chiamare const_cast_getZ invece di _getZ.

A proposito, apprezzo Meyers soluzione.Non ho filosofico di opposizione.Personalmente, però, preferisco un po ' di ripetizione controllata, e un metodo privato che deve essere chiamato solo in alcuni strettamente controllato circostanze, nel corso di un metodo che sembra il rumore di linea.Scegli il tuo veleno e bastone con esso.

[Edit:Kevin ha giustamente sottolineato che _getZ potrebbe desiderare di chiamare un altro metodo (dire generateZ) che è const-specializzati nello stesso modo getZ è.In questo caso, _getZ vorresti vedere un const Z& e devono const_cast prima di ritorno.Che è ancora sicuro, dal momento che la funzione di accesso standard di politiche di tutto, ma non è straordinariamente evidente che è sicuro.Inoltre, se si fa questo e poi cambiare generateZ per restituire sempre const, allora avete bisogno di cambiare getZ sempre tornare const, ma il compilatore non ti dico che si fa.

Quest'ultimo punto il compilatore è anche vero Meyers consigliato modello, ma il primo punto su una non evidente const_cast non è.Così, a saldo, penso che se _getZ si rivelasse necessario un const_cast per il suo valore di ritorno, quindi questo modello perde molto del suo valore nel corso di Meyers è.Quanto soffre anche di svantaggi rispetto ad Meyers, io pensavo di passare alla sua situazione.Refactoring da uno all'altro è facile, e non influisce in alcun altro codice valido in classe, dal momento che solo codice non valido e il boilerplate chiamate _getZ.]

C ++ 17 ha aggiornato la migliore risposta a questa domanda:

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

Questo ha i vantaggi che:

  • È ovvio cosa sta succedendo
  • Ha un sovraccarico minimo di codice - si adatta a una singola riga
  • È difficile sbagliare (può solo buttare via volatile per caso, ma <=> è un qualificatore raro)

Se si desidera percorrere l'intero percorso di detrazione, ciò può essere realizzato con una funzione di supporto

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;

Ora non puoi nemmeno rovinare <=> e l'utilizzo sembra

T & f() {
    return as_mutable(std::as_const(*this).f());
}

Bella domanda e belle risposte. Ho un'altra soluzione, che non usa cast:

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

};

Tuttavia, ha la bruttezza di richiedere un membro statico e la necessità di utilizzare la instance variabile al suo interno.

Non ho considerato tutte le possibili implicazioni (negative) di questa soluzione. Per favore fatemi sapere se presente.

Puoi anche risolverlo con i template. Questa soluzione è leggermente brutta (ma la bruttezza è nascosta nel file .cpp) ma fornisce il controllo della costanza del compilatore e nessuna duplicazione del codice.

.h file:

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

.cpp file:

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

Lo svantaggio principale che posso vedere è che poiché tutta l'implementazione complessa del metodo è in una funzione globale, è necessario acquisire i membri di X utilizzando metodi pubblici come GetVector () sopra (di cui è sempre necessario essere una versione const e non const) o potresti rendere questa funzione un'amica. Ma non mi piacciono gli amici.

[Modifica: rimossa l'inclusione non necessaria di cstdio aggiunta durante il test.]

Che ne dici di spostare la logica in un metodo privato e fare solo il " ottenere il riferimento e restituire " roba dentro i vincitori? In realtà, sarei abbastanza confuso riguardo ai cast statici e const all'interno di una semplice funzione getter, e lo considererei brutto tranne che per circostanze estremamente rare!

È imbarazzante usare il preprocessore?

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

};

Non è elegante come modelli o cast, ma rende il tuo intento (" queste due funzioni devono essere identiche ") piuttosto esplicite.

In genere, le funzioni membro per le quali sono necessarie le versioni const e non const sono getter e setter. Il più delle volte sono a una riga, quindi la duplicazione del codice non è un problema.

L'ho fatto per un amico che giustamente ha giustificato l'uso di const_cast ... non sapendolo, probabilmente avrei fatto qualcosa del genere (non molto elegante):

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

Suggerirei un modello di funzione statica helper privato, come questo:

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

Per quelli (come me) che

  • usa c++17
  • desidera aggiungere la minima quantità di boilerplate / ripetizione e
  • non dispiace usare makros (in attesa di meta-classi ...),

ecco un'altra interpretazione:

#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(a...))>::type {  \
        return const_cast<decltype(func(a...))>(                            \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

È fondamentalmente un mix di risposte da @Pait, @DavidStone e @ sh1. Ciò che aggiunge alla tabella è che si evita solo una riga di codice aggiuntiva che semplicemente nomina la funzione (ma nessun argomento o duplicazione del tipo restituito):

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

Nota: gcc non riesce a compilare questo prima di 8.1, clang-5 e versioni successive e MSVC-19 sono felici (secondo l'esploratore del compilatore ).

Mi sorprende che ci siano così tante risposte diverse, ma quasi tutte si basano su una pesante magia di template. I modelli sono potenti, ma a volte le macro li battono in concisione. La massima versatilità si ottiene spesso combinando entrambi.

Ho scritto una macro FROM_CONST_OVERLOAD() che può essere inserita nella funzione non const per invocare la funzione const.

Esempio di utilizzo:

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

Implementazione semplice e riutilizzabile:

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)

Spiegazione:

Come pubblicato in molte risposte, il modello tipico per evitare la duplicazione del codice in una funzione membro non const è questo:

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

Gran parte di questa piastra della caldaia può essere evitata usando l'inferenza del tipo. Innanzitutto, const_cast può essere incapsulato in WithoutConst(), che determina il tipo del suo argomento e rimuove il qualificatore const. In secondo luogo, un approccio simile può essere utilizzato in WithConst() per qualificare il puntatore this, che consente di chiamare il metodo const-overload.

Il resto è una semplice macro che antepone la chiamata con il this-> correttamente qualificato e rimuove const dal risultato. Poiché l'espressione utilizzata nella macro è quasi sempre una semplice chiamata di funzione con argomenti inoltrati 1: 1, gli svantaggi di macro come la valutazione multipla non entrano in gioco. Anche i puntini di sospensione e __VA_ARGS__ potrebbero essere utilizzati, ma non dovrebbero essere necessari perché le virgole (come separatori di argomenti) si trovano tra parentesi.

Questo approccio ha diversi vantaggi:

  • Sintassi minima e naturale: basta avvolgere la chiamata in FROM_CONST_OVERLOAD( )
  • Non è richiesta alcuna funzione di membro extra
  • Compatibile con C ++ 98
  • Implementazione semplice, nessuna metaprogrammazione del modello e zero dipendenze
  • Estensibile: è possibile aggiungere altre relazioni const (come const_iterator, std::shared_ptr<const T>, ecc.). Per questo, è sufficiente sovraccaricare this->Method(args) per i tipi corrispondenti.

Limitazioni: questa soluzione è ottimizzata per gli scenari in cui il sovraccarico non costante sta facendo esattamente lo stesso del sovraccarico const, in modo che gli argomenti possano essere inoltrati 1: 1. Se la tua logica è diversa e non stai chiamando la versione const tramite <=>, puoi prendere in considerazione altri approcci.

Questo articolo del DDJ mostra come utilizzare la specializzazione dei modelli che non richiede di utilizzare const_cast. Per una funzione così semplice, in realtà non è necessario.

boost :: any_cast (a un certo punto, non lo fa più) usa un const_cast dalla versione const che chiama la versione non const per evitare la duplicazione. Non puoi imporre semantica const sulla versione non const, quindi devi essere molto attento con questo.

Alla fine, la duplicazione del codice è a condizione che i due frammenti siano direttamente uno sopra l'altro.

Per aggiungere alla soluzione fornita da jwfearn e kevin, ecco la soluzione corrispondente quando la funzione restituisce 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;
};

Non ho trovato quello che cercavo, quindi ne ho fatti un paio ...

Questo è un po 'prolisso, ma ha il vantaggio di gestire contemporaneamente molti metodi sovraccarichi con lo stesso nome (e tipo di ritorno):

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

Se hai solo un const metodo per nome, ma hai ancora molti metodi da duplicare, allora potresti preferire questo:

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

Sfortunatamente questo si interrompe non appena si inizia a sovraccaricare il nome (l'elenco degli argomenti dell'argomento del puntatore a funzione sembra essere irrisolto a quel punto, quindi non riesce a trovare una corrispondenza per l'argomento della funzione). Anche se puoi anche usarlo come modello:

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

Ma gli argomenti di riferimento al metodo <=> non corrispondono agli argomenti apparentemente per valore al modello e si interrompe. Non so perché. Ecco perché .

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top