Come rimuovo la duplicazione del codice tra funzioni simili const e non const?


242

Diciamo che ho il seguente class Xdove voglio restituire l'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() consthanno codice identico all'interno delle parentesi graffe. Questo è un codice duplicato e può causare problemi di manutenzione per funzioni lunghe con logica complessa .

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


In questo esempio, restituirei un valore nel caso const in modo da non poter eseguire il refactoring di seguito. int Z () const {return z; }
Matt Price,

1
Per i tipi fondamentali, hai assolutamente ragione! Il mio primo esempio non è stato molto buono. Diciamo che invece stiamo restituendo invece qualche istanza di classe. (Ho aggiornato la domanda per riflettere questo.)
Kevin,

Risposte:


189

Per una spiegazione dettagliata, vedere l'intestazione "Evita duplicazione in conste constFunzione non membro", a pag. 23, nell'articolo 3 "Usa constogni volta che è possibile", in C ++ efficace , ed. 3d di Scott Meyers, ISBN-13: 9780321334879.

testo alternativo

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


45
Nessuno è mai stato licenziato per aver seguito Scott Meyers :-)
Steve Jessop,

11
witkamp ha ragione sul fatto che in generale è male usare const_cast. Questo è un caso specifico in cui non lo è, come spiega Meyers. @Adam: ROM => const va bene. const == ROM è ovviamente una sciocchezza poiché chiunque può lanciare non-const in const volenti o nolenti: equivale a scegliere di non modificare qualcosa.
Steve Jessop,

44
In generale, suggerirei di utilizzare const_cast invece di static_cast per aggiungere const poiché ti impedisce di modificare il tipo accidentalmente.
Greg Rogers,

6
@HelloGoodbye: penso che Meyers ipotizzi un minimo di intelligenza dal progettista dell'interfaccia di classe. Se get()constrestituisce qualcosa che è stato definito come un oggetto const, allora non dovrebbe esserci affatto una versione non const get(). In realtà il mio pensiero su questo è cambiato nel tempo: la soluzione del modello è l'unico modo per evitare la duplicazione e ottenere la correttezza const verificata dal compilatore, quindi personalmente non userei più un const_castper evitare la duplicazione del codice, sceglierei tra il codice duplicato in un modello di funzione oppure lasciandolo duplicato.
Steve Jessop,

7
I seguenti due modelli aiutano enormemente con la leggibilità di questa soluzione: template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }e template<typename T> T& variable(const T& _) { return const_cast<T&>(_); }. Quindi puoi fare:return variable(constant(*this).get());
Casey Rodarmor il

64

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 NON inserire la logica nella funzione non const e fare in modo che la funzione const chiami la funzione non const - ciò può comportare un comportamento indefinito. Il motivo è che un'istanza di classe costante viene lanciata come un'istanza non costante. La funzione membro non const può modificare accidentalmente la classe, che gli stati standard C ++ comporteranno un comportamento indefinito.


3
Wow ... è orribile. Hai appena aumentato la quantità di codice, diminuito la chiarezza e aggiunto due stinkin 'const_cast <> s. Forse hai in mente un esempio in cui questo ha davvero senso?
Shog9,

14
Ehi, non farlo !, potrebbe essere brutto, ma secondo Scott Meyers, è (quasi) il modo corretto. Vedi C ++ efficace , 3d ed, voce 3 sotto la voce "Evitare la duplicazione nelle funzioni
costanti

17
Mentre capisco che la soluzione potrebbe essere brutta, immagina che il codice che determina cosa restituire è lungo 50 righe. Quindi la duplicazione è altamente indesiderabile, specialmente quando è necessario ricodificare il codice. L'ho incontrato molte volte nella mia carriera.
Kevin,

8
La differenza tra questo e Meyers è che Meyers ha static_cast <const X &> (* this). const_cast serve per rimuovere const, non per aggiungerlo.
Steve Jessop,

8
@VioletGiraffe sappiamo che l'oggetto non era originariamente creato const, in quanto è un membro non const di un oggetto non const, che sappiamo perché siamo in un metodo non const di detto oggetto. Il compilatore non fa questa deduzione, segue una regola conservativa. Perché pensi che const_cast esista, se non per questo tipo di situazione?
Caleth,

47

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 di codice minimo: si adatta a una singola riga
  • È difficile sbagliare (può essere gettato via solo volatileper caso, ma volatileè un raro qualificatore)

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

Ora non puoi nemmeno sbagliare volatilee l'uso sembra

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}

Si noti che "as_mutable" con il sovraccarico del valore const eliminato (che è generalmente preferibile) impedisce all'ultimo esempio di funzionare se f()restituisce Tinvece di T&.
Max Truxa,

1
@MaxTruxa: Sì, e questa è una buona cosa. Se fosse appena compilato, avremmo un riferimento penzolante. Nel caso in cui f()ritorni T, non vogliamo avere due sovraccarichi, la constversione da sola è sufficiente.
David Stone,

È vero, mi scuso per la scoreggia cerebrale completa di ieri, non ho idea di cosa stavo pensando quando ho scritto quel commento. Stavo guardando una coppia getter const / mutable che restituiva a shared_ptr. Quindi quello di cui avevo effettivamente bisogno era qualcosa del genere as_mutable_ptrche sembra quasi identico a quello as_mutablesopra, tranne per il fatto che prende e restituisce a shared_ptre usa std::const_pointer_castinvece di const_cast.
Max Truxa,

1
Se un metodo ritorna, T const*questo si legherebbe T const* const&&piuttosto che legarsi T const* const&(almeno nei miei test). Ho dovuto aggiungere un sovraccarico T const*come tipo di argomento per i metodi che restituiscono un puntatore.
monkey0506,

2
@ monkey0506: ho aggiornato la mia risposta per supportare puntatori e riferimenti
David Stone

34

Penso che la soluzione di Scott Meyers possa essere migliorata in C ++ 11 usando una funzione di supporto temporaneo. Questo rende l'intento molto più ovvio e può essere riutilizzato per molti altri giocatori.

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 utilizzata 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 questo puntatore. Il secondo è il puntatore alla funzione membro da chiamare. Successivamente è possibile passare una quantità arbitraria di argomenti aggiuntivi in ​​modo che possano essere inoltrati alla funzione. Ciò richiede C ++ 11 a causa dei modelli variadici.


3
È un peccato che non dobbiamo std::remove_bottom_constandare std::remove_const.
TBBle

Non mi piace questa soluzione perché incorpora ancora a const_cast. Si potrebbe fare getElementun modello di per sé, e utilizzare il tratto di tipo interno per mpl::conditionali tipi necessari, come iterators o constiterators, se necessario. Il vero problema è come generare una versione const di un metodo quando questa parte della firma non può essere templatizzata?
v.oddou

2
@ v.oddou: std::remove_const<int const&>is int const &(rimuovi la constqualifica di alto livello ), quindi la ginnastica di NonConst<T>questa risposta. Putative std::remove_bottom_constpotrebbe rimuovere la constqualifica di livello inferiore e fare esattamente ciò che NonConst<T>fa qui: std::remove_bottom_const<int const&>::type=> int&.
TBBle

4
Questa soluzione non funziona bene, se getElementè sovraccarica. Quindi il puntatore a funzione non può essere risolto senza fornire esplicitamente i parametri del modello. Perché?
John,

1
È necessario correggere la risposta per utilizzare l'inoltro perfetto di 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)...)); }Completo: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83
ShaulF

22

Un po 'più prolisso di Meyers, ma potrei farlo:

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 la proprietà indesiderabile che restituisce una Z non costante e per un'istanza const, motivo per cui è privata. I metodi privati ​​possono spezzare gli invarianti dell'interfaccia esterna (in questo caso l'invariante desiderato è "un oggetto const non può essere modificato tramite riferimenti ottenuti attraverso di esso con oggetti che ha-a").

Nota che i commenti fanno parte del modello - l'interfaccia di _getZ specifica che non è mai valido chiamarlo (a parte gli accessi, ovviamente): non c'è comunque alcun vantaggio immaginabile farlo, perché è 1 carattere in più da digitare e non lo farà risulta in un codice più piccolo o più veloce. Chiamare il metodo equivale a chiamare uno degli accessor con un const_cast e non vorrai farlo neanche tu. Se sei preoccupato di rendere evidenti gli errori (e questo è un obiettivo equo), chiamalo const_cast_getZ invece di _getZ.

A proposito, apprezzo la soluzione di Meyers. Non ho obiezioni filosofiche. Personalmente, però, preferisco un po 'di ripetizione controllata e un metodo privato che deve essere chiamato solo in determinate circostanze strettamente controllate, rispetto a un metodo che assomiglia al rumore di linea. Scegli il tuo veleno e attaccalo.

[Modifica: Kevin ha giustamente sottolineato che _getZ potrebbe voler chiamare un ulteriore metodo (diciamo generateZ) che è specializzato nello stesso modo di getZ. In questo caso, _getZ visualizzerà una const Z e dovrà trasmetterla prima di tornare. È ancora sicuro, dal momento che l'accessorio della piastra di caldaia controlla tutto, ma non è straordinariamente ovvio che sia sicuro. Inoltre, se lo fai e successivamente cambi generateZ per restituire sempre const, allora devi anche cambiare getZ per restituire sempre const, ma il compilatore non ti dirà che lo fai.

Quest'ultimo punto sul compilatore è vero anche per il modello raccomandato da Meyers, ma il primo punto su un const_cast non ovvio non lo è. Quindi, a conti fatti, penso che se _getZ risulta avere bisogno di un const_cast per il suo valore di ritorno, allora questo modello perde molto del suo valore rispetto a quello di Meyers. Dato che soffre anche di svantaggi rispetto a quelli di Meyers, penso che passerei al suo in quella situazione. Il refactoring dall'uno all'altro è semplice: non influisce su nessun altro codice valido nella classe, poiché solo il codice non valido e la piastra della caldaia chiamano _getZ.]


3
Ciò ha ancora il problema che la cosa restituita potrebbe essere costante per un'istanza costante di X. In tal caso, è comunque necessario un const_cast in _getZ (...). Se utilizzato in modo improprio da sviluppatori successivi, può comunque portare a UB. Se la cosa che viene restituita è "mutabile", questa è una buona soluzione.
Kevin,

1
Qualsiasi funzione privata (diamine, anche quelle pubbliche) può essere utilizzata in modo errato dagli sviluppatori successivi, se scelgono di ignorare le istruzioni BLOCK CAPITAL sul suo uso valido, nel file di intestazione e anche in Doxygen ecc. Non posso fermarlo, e non lo considero un mio problema poiché le istruzioni sono facili da capire.
Steve Jessop,

13
-1: Questo non funziona in molte situazioni. Cosa succede se somethingnella _getZ()funzione è presente una variabile di istanza? Il compilatore (o almeno alcuni compilatori) si lamenterà che dato che _getZ()è const, anche ogni variabile di istanza a cui si fa riferimento è const. Quindi somethingsarebbe const (sarebbe di tipo const Z&) e non potrebbe essere convertito in Z&. Nella mia esperienza (certamente un po 'limitata), il più delle volte somethingè una variabile di istanza in casi come questo.
Gravità

2
@GravityBringer: quindi "qualcosa" deve coinvolgere a const_cast. Doveva essere un segnaposto per il codice richiesto per ottenere un ritorno non const dall'oggetto const, non come un segnaposto per quello che sarebbe stato nel getter duplicato. Quindi "qualcosa" non è solo una variabile di istanza.
Steve Jessop,

2
Vedo. Ciò riduce davvero l'utilità della tecnica, però. Rimuoverei il downvote, ma SO non me lo permette.
Gravità

22

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 il file instance variabile al suo interno.

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


4
Bene, lasciamo andare il semplice fatto che hai aggiunto più boilerplate. Semmai, questo dovrebbe essere usato come esempio del perché la lingua ha bisogno di un modo per modificare i qualificatori di funzione insieme al tipo restituito auto get(std::size_t i) -> auto(const), auto(&&). Perché '&&'? Ah, quindi posso dire:auto foo() -> auto(const), auto(&&) = delete;
kfsone,

gd1: esattamente quello che avevo in mente. @kfsone ed esattamente quello che ho concluso anch'io.
v.oddou

1
@kfsone la sintassi dovrebbe includere la thisparola chiave. Suggerisco che template< typename T > auto myfunction(T this, t args) -> decltype(ident)questa parola chiave verrà riconosciuta come argomento dell'istanza dell'oggetto implicito e consenta al compilatore di riconoscere che myfunction è un membro o T. Tverrà dedotto automaticamente sul sito di chiamata, che sarà sempre il tipo di classe, ma con qualifica CV gratuita.
v.oddou

2
Tale soluzione ha anche il vantaggio (rispetto a const_castquello) di consentire il ritorno iteratore const_iterator.
Jarod42,

1
Se l'implementazione viene spostata nel file cpp (e poiché il metodo da non duplicare non dovrebbe essere banale, probabilmente sarebbe il caso), è staticpossibile farlo nell'ambito del file anziché nell'ambito della classe. :-)
Jarod42

8

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.

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

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

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

[Modifica: rimosso non necessario includere di cstdio aggiunto durante il test.]


3
È sempre possibile rendere la complessa funzione di implementazione un membro statico per ottenere l'accesso ai membri privati. La funzione deve essere dichiarata solo nel file di intestazione della classe, la definizione può risiedere nel file di implementazione della classe. Dopotutto, fa parte dell'implementazione della classe.
CB Bailey,

Aah sì buona idea! Non mi piace la roba del modello che appare nell'intestazione, ma se da qui potenzialmente rende l'implementazione molto più semplice, probabilmente ne vale la pena.
Andy Balaam,

+ 1 a questa soluzione che non duplica alcun codice, né usa alcun brutto const_cast(che potrebbe essere accidentalmente usato per salvare qualcosa che in realtà dovrebbe essere const a qualcosa che non lo è).
Ciao Arrivederci

Oggi questo può essere semplificato con un tipo di ritorno dedotto per il modello (particolarmente utile poiché riduce ciò che deve essere duplicato nella classe nel caso membro).
Davis Herring,

3

Che ne dici di spostare la logica in un metodo privato e fare solo le cose "ottieni il riferimento e restituisci" all'interno dei getter? 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!


Per evitare comportamenti indefiniti è comunque necessario un const_cast. Vedi la risposta di Martin York e il mio commento lì.
Kevin,

1
Kevin, quale risposta di Martin York
Peter Nimmo

2

Usare il preprocessore è imbarazzante?

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 esplicito il tuo intento ("queste due funzioni devono essere identiche").


1
Ma poi devi stare attento con le barre rovesciate (come al solito per le macro multilinea) e inoltre perdi l'evidenziazione della sintassi nella maggior parte degli editor (se non in tutti).
Ruslan,

2

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 di caldaia può essere evitata usando l'inferenza di tipo. Innanzitutto, const_castpuò 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 WithConst()per qualificare il thispuntatore const , che consente di chiamare il metodo const-overload.

Il resto è una semplice macro che antepone la chiamata con la qualifica corretta this->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. Le ellissi e __VA_ARGS__potrebbero anche essere utilizzate, ma non dovrebbero essere necessarie perché virgole (come separatori di argomenti) tra parentesi.

Questo approccio ha diversi vantaggi:

  • Sintassi minima e naturale: basta avvolgere la chiamata FROM_CONST_OVERLOAD( )
  • Nessuna funzione di membro extra richiesta
  • Compatibile con C ++ 98
  • Implementazione semplice, nessuna metaprogrammazione del modello e zero dipendenze
  • Extensible: altri rapporti const possono essere aggiunti (come const_iterator, std::shared_ptr<const T>e così via). Per questo, è sufficiente sovraccaricare WithoutConst()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 this->Method(args), puoi prendere in considerazione altri approcci.


2

Per quelli (come me) che

  • usa c ++ 17
  • desidera aggiungere la minima quantità di boilerplate / ripetizione e
  • non preoccuparti di usare le macro (mentre aspetti 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(std::forward<T>(a)...))>::type   \
    {                                                                       \
        return const_cast<decltype(func(std::forward<T>(a)...))>(           \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

È fondamentalmente un mix di risposte da @Pait, @DavidStone e @ sh1 ( EDIT : e un miglioramento da @cdhowie). Ciò che aggiunge alla tabella è che si ottiene 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 ).


Questo ha funzionato semplicemente per me. Questa è un'ottima risposta, grazie!
Breve

Gli decltype()s non dovrebbero anche usare std::forwardgli argomenti per assicurarsi che stiamo usando il giusto tipo di ritorno nel caso in cui abbiamo sovraccarichi get()che accettano diversi tipi di riferimenti?
cdhowie,

@cdhowie Puoi fornire un esempio?
axxel

@axxel È inventato da morire, ma ecco qui . La NON_CONSTmacro deduce il tipo restituito in modo errato ed const_castè nel tipo sbagliato a causa della mancanza di inoltro nei decltype(func(a...))tipi. Sostituirli con decltype(func(std::forward<T>(a)...)) risolve questo . (C'è solo un errore del linker perché non ho mai definito nessuno dei X::getsovraccarichi dichiarati .)
cdhowie

1
Grazie @cdhowie, ho sfruttato il tuo esempio per utilizzare effettivamente i sovraccarichi non const: coliru.stacked-crooked.com/a/0cedc7f4e789479e
axxel

1

Ecco una versione C ++ 17 della funzione helper statica modello, con e test SFINAE opzionale.

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

Versione completa: https://godbolt.org/z/mMK4r3


1

Mi è venuta in mente una macro che genera automaticamente coppie di funzioni 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;}
};

Vedi la fine della risposta per l'implementazione.

L'argomento di MAYBE_CONSTè duplicato. Nella prima copia, CVviene sostituito con nulla; e nella seconda copia viene sostituito con const.

Non c'è limite al numero di volte che CVpossono apparire nell'argomento macro.

C'è un leggero inconveniente però. Se CVappare tra parentesi, questa coppia di parentesi deve essere preceduta da 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 &); )

Implementazione:

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

Implementazione pre-C ++ 20 che non supporta 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

0

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.


2
Questo può essere vero per la maggior parte del tempo. Ma ci sono eccezioni.
Kevin,

1
comunque, un setter non ha molto senso;)
jwfearn,

Intendevo dire che il non const getter è effettivamente un setter. :)
Dima,

0

L'ho fatto per un amico che giustamente ha giustificato l'uso di const_cast... non sapendolo avrei probabilmente 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;
}

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

-1

Questo articolo DDJ mostra come utilizzare la specializzazione del modello che non richiede l'utilizzo di 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.

Alla fine, la duplicazione del codice va bene purché i due frammenti siano direttamente uno sopra l'altro.


L'articolo DDJ sembra riferirsi agli iteratori, il che non è rilevante per la domanda. I costanti non sono dati costanti, ma sono iteratori che puntano a dati costanti.
Kevin,

-1

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

-1

Non ho trovato quello che cercavo, quindi ho arrotolato un paio dei miei ...

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 constmetodo per nome, ma hai ancora molti metodi da duplicare, 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 alla funzione sembra non essere risolto a quel punto, quindi non riesce a trovare una corrispondenza per l'argomento della funzione). Anche se puoi modellarti anche tu:

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

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.