c++pointersconstantssmart-pointers

Propagate constness to data pointed by member variables


It is often quite confusing to C++ newcomers that const member functions are allowed to call non-const methods on objects referenced by the class (either by pointer or reference). For example, the following is perfectly correct:

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

However, it would sometimes be rather handy if the constness would propagate to pointed objects (I voluntarily used the PImpl idiom because it is one of the case where I think "constness propagation" would be very useful).

When using pointers, this can easily be achieved by using some kind of smart pointer with operators overloaded 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_;
};

Now, I just need to modify SomeClass::impl_ to be a const_propagating_ptr<SomeClassImpl> to obtain the wanted behavior.

So I have a few questions about this:

  1. Are there some issues with constness propagation that I have overlooked?
  2. If not, are there any libraries that provide classes to obtain constness propagation?
  3. Wouldn't it be useful that the common smart pointers (unique_ptr, shared_ptr, etc.) provide some mean to obtain this behavior (for example through a template parameter)?

Solution

    1. As @Alf P. Steinbach noted, you overlooked the fact that copying your pointer would yield a non-const object pointing to the same underlying object. Pimpl (below) nicely circumvent the issue by performing a deep-copy, unique_ptr circumvents it by being non-copyable. It is much easier, of course, if the pointee is owned by a single entity.

    2. Boost.Optional propagates const-ness, however it's not exactly a pointer (though it models the OptionalPointee concept). I know of no such other library.

    3. I would favor that they provide it by default. Adding another template parameter (traits class I guess) does not seem worth the trouble. However that would radically change the syntax from a classic pointer, so I am not sure that people would be ready to embrace it.


    Code of the Pimpl class

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