2011-01-18 14 views
22

Spesso è abbastanza confuso per i nuovi arrivati ​​C++ che le funzioni membro const sono autorizzati a chiamare metodi non-const sugli oggetti a cui fa riferimento la classe (tramite puntatore o riferimento). Ad esempio, il seguente è perfettamente corretto:Propagare la costanza ai dati puntati dalle variabili membro

class SomeClass 
{ 
    class SomeClassImpl; 
    SomeClassImpl * impl_; // PImpl idiom 

    public:  

    void const_method() const; 
}; 

struct SomeClass::SomeClassImpl 
{ 
    void non_const_method() { /*modify data*/ } 
}; 

void SomeClass::const_method() const 
{ 
    impl_->non_const_method(); //ok because impl_ is const, not *impl_ 
}; 

Tuttavia, sarebbe a volte essere piuttosto utile se la costanza si propaga agli oggetti appuntiti (io ho usato volontariamente l'idioma Pimpl perché è uno dei casi in cui credo " la propagazione della costanza "sarebbe molto utile).

Quando si utilizza puntatori, questo può essere facilmente raggiunto utilizzando una sorta di puntatore intelligente con gli operatori di overload on constness:

template < typename T > 
class const_propagating_ptr 
{ 
    public: 

    const_propagating_ptr(T * ptr) : ptr_(ptr) {} 

    T  & operator*()  { return *ptr_; } 
    T const & operator*() const { return *ptr_; } 

    T  * operator->()  { return ptr_; } 
    T const * operator->() const { return ptr_; } 

    // assignment operator (?), get() method (?), reset() method (?) 
    // ... 

    private: 

    T * ptr_; 
}; 

Ora, ho solo bisogno di modificare SomeClass::impl_ essere un const_propagating_ptr<SomeClassImpl> per ottenere il comportamento desiderato .

Così ho alcune domande su questo:

  1. ci sono alcuni problemi con costanza propagazione che ho trascurato?
  2. In caso contrario, esistono librerie che forniscono classi per ottenere la propagazione della costanza?
  3. Non sarebbe utile che i puntatori intelligenti comuni (unique_ptr, shared_ptr, ecc.) Forniscano un mezzo per ottenere questo comportamento (ad esempio attraverso un parametro template)?
+3

Cosa succede se copio lo smart-pointer? Voilà, ne ho uno non-const. –

+1

'T const * const operator ->() const {return ptr_; } '- Probabilmente non c'è bisogno del secondo' const' qui –

+0

@Alf e @robin: lo schizzo che ho dato di una possibile implementazione è probabilmente pieno di bug (nonostante le sue ridotte dimensioni :)), non è il punto centrale del domanda. Tuttavia, il tuo feedback è molto apprezzato! Per quanto riguarda il problema della copia, al momento non vedo come impedire che ciò sia possibile, ma spesso non riesci a impedirti completamente di spararti nel piede (ad esempio, puoi sempre const_cast'stare via, non significa che const è inutile). Per quanto riguarda il secondo commento, hai ragione @robin, l'ho fatto erroneamente per impedire che 'ptr_' venga ... –

risposta

2
  1. Come osservato @Alf P. Steinbach, si ha supervisionato il fatto che la copia il puntatore dovesse produrre un non-const che punta allo stesso oggetto sottostante. Pimpl (sotto) eludono correttamente il problema eseguendo una copia profonda, unique_ptr lo circoscrive non essendo copiabili. È molto più facile, ovviamente, se l'oggetto è posseduto da una singola entità.

  2. Boost.Optional si propaga costanza, tuttavia non è esattamente un puntatore (anche se modella il concetto OptionalPointee). Non conosco nessuna altra biblioteca.

  3. Mi piacerebbe che lo forniscano di default. L'aggiunta di un altro parametro di modello (la classe dei tratti credo) non sembra valere la pena. Tuttavia ciò cambierebbe radicalmente la sintassi da un puntatore classico, quindi non sono sicuro che le persone sarebbero pronte ad abbracciarlo.


Codice della classe di Pimpl

template <class T> 
class Pimpl 
{ 
public: 
    /** 
    * Types 
    */ 
    typedef T value; 
    typedef const T const_value; 
    typedef T* pointer; 
    typedef const T* const_pointer; 
    typedef T& reference; 
    typedef const T& const_reference; 

    /** 
    * Gang of Four 
    */ 
    Pimpl() : _value(new T()) {} 
    explicit Pimpl(const_reference v) : _value(new T(v)) {} 

    Pimpl(const Pimpl& rhs) : _value(new T(*(rhs._value))) {} 

    Pimpl& operator=(const Pimpl& rhs) 
    { 
    Pimpl tmp(rhs); 
    swap(tmp); 
    return *this; 
    } // operator= 

    ~Pimpl() { boost::checked_delete(_value); } 

    void swap(Pimpl& rhs) 
    { 
    pointer temp(rhs._value); 
    rhs._value = _value; 
    _value = temp; 
    } // swap 

    /** 
    * Data access 
    */ 
    pointer get() { return _value; } 
    const_pointer get() const { return _value; } 

    reference operator*() { return *_value; } 
    const_reference operator*() const { return *_value; } 

    pointer operator->() { return _value; } 
    const_pointer operator->() const { return _value; } 

private: 
    pointer _value; 
}; // class Pimpl<T> 

// Swap 
template <class T> 
void swap(Pimpl<T>& lhs, Pimpl<T>& rhs) { lhs.swap(rhs); } 

// Not to be used with pointers or references 
template <class T> class Pimpl<T*> {}; 
template <class T> class Pimpl<T&> {}; 
2

Un approccio è quello di non utilizzare il puntatore direttamente se non attraverso due funzioni di accesso.

class SomeClass 
{ 
    private: 
    class SomeClassImpl; 
    SomeClassImpl * impl_; // PImpl idiom - don't use me directly! 

    SomeClassImpl * mutable_impl() { return impl_; } 
    const SomeClassImpl * impl() const { return impl_; } 

    public:  

    void const_method() const 
    { 
     //Can't use mutable_impl here. 
     impl()->const_method(); 
    } 
    void non_const_method() const 
    { 
     //Here I can use mutable_impl 
     mutable_impl()->non_const_method(); 
    } 
}; 
+0

Questa è la soluzione I inizialmente provato, tranne che ho usato un'istanza di classe nidificata per incapsulare il puntatore, al fine di impedire il suo accesso dalla classe esterna (solo i getter hanno avuto accesso grazie alle dichiarazioni di amicizia). Tuttavia, questa soluzione aggiunge un sacco di codice boilerplate per ogni puntatore per il quale abbiamo bisogno della propagazione const. Inoltre, IMHO, usando getter e setter diminuisce la leggibilità (le proprietà sono una delle mie caratteristiche C# preferite!). –

1

Per la cronaca, ho appena scoperto che il Loki library fornisce un puntatore const moltiplicazione (ConstPropPtr<T>). Sembra proprio come quello nella domanda, tranne che cancella anche il puntatore avvolto nel suo distruttore, ed è usato per implementare un Pimpl class simile a the one proposed by @Matthieu (ma non copiabile).

0

Se si pensa che dovrebbe "diffondere" const-ness, allora significa che non si ha realmente credete che sia un puntatore (o di riferimento), ma credo che sia un contenitore di : se il valore è costante quando l'oggetto è costante, è perché l'oggetto contiene il valore.

Quindi copiare l'oggetto copia il valore, almeno logicamente (CoW).

Se si insiste che sia un puntatore/riferimento IOW che è possibile copiare l'oggetto mentre si condivide il valore contenuto, allora si ha un'interfaccia (contraddittoria) non valida.

Conclusione: prendere una decisione. È un contenitore o un puntatore.

Un puntatore non si propaga const-ness, per definizione.

+2

Penso che ti sia sfuggito il punto: anche quando un oggetto è contenuto (posseduto) da un altro oggetto, a volte devi memorizzarlo usando un puntatore (polimorfismo, inizializzazione pigra, firewall di compilazione, ...). È in questi casi che la propagazione delle cost sarebbe utile: poiché * concettualmente * il pointee è contenuto dall'oggetto, i metodi const dell'oggetto non dovrebbero essere autorizzati a modificarlo. Tuttavia, sono d'accordo che la mia classe di esempio 'SomeClass' dovrebbe aver definito un costruttore di copie o una copia non autorizzata, per evitare la condivisione accidentale di' impl_' tra istanze di 'SomeClass' '. –

+0

_ "Penso che ti sia sfuggito il punto: potresti voler propagare const_" Ho capito, e hai perso il mio punto: vuoi propagare la costante perché non vuoi ** puntatore ** semantico. Per favore, non chiamare quella classe 'something_pointer'! "Anche quando un oggetto è contenuto (posseduto) da un altro oggetto, a volte è necessario memorizzarlo usando un puntatore_" Sì, davvero. – curiousguy

+0

BTW, nel caso in cui qualcuno sosterrebbe che i nomi non sono così importanti: se si chiama qualcosa un "puntatore" (che in realtà non ha il puntatore semantico), potrebbe venire un altro programmatore e aggiungere cose legate al puntatore che non lo fanno ha senso per qualcosa che non dovrebbe avere il puntatore semantico. Chiamandolo come un contenitore (per lo più) eviti questo rischio. – curiousguy

Problemi correlati