26

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)?
Luc Touraille
  • 79,925
  • 15
  • 92
  • 137
  • 4
    What if I just copy the smart-pointer? Voila, I have a non-const one. – Cheers and hth. - Alf Jan 18 '11 at 22:47
  • 1
    `T const * const operator->() const { return ptr_; }` - probably no need for the second `const` here – Andriy Tylychko Jan 18 '11 at 22:49
  • @Alf and @robin: The sketch I gave of a possible implementation is probably littered with bugs (despite its short size :)), it is not the central point of the question. However, your feedback is really appreciated! Regarding the copy issue, I don't see at the moment how we could prevent that from being possible, but often you can't completely prevent yourself from shooting yourself in the foot (for example, you can always `const_cast` away constness, it doesn't mean that const is useless). Regarding the second comment, you are right @robin, I mistakenly did that to prevent `ptr_` from being... – Luc Touraille Jan 18 '11 at 23:09
  • [cont.] modified by the caller, which is stupid because the pointer is returned by value...I'll remove that const right away. – Luc Touraille Jan 18 '11 at 23:10
  • constness is a property of a reference, not of the referenced object. – Dan D. Jan 18 '11 at 23:59
  • 7
    There is now an official proposal [propagate_const](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4372.html) for C++17 – M.M Feb 26 '15 at 01:05

4 Answers4

2
  1. As @Alf P. Steinbach noted, you oversaw 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&> {};
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
2

One approach is to just not use the pointer directly except through two accessor functions.

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();
    }
};
Michael Anderson
  • 70,661
  • 7
  • 134
  • 187
  • This is the solution I initially tried, except that I used a nested class instance to encapsulate the pointer, in order to prevent its access from the outer class (only the getters had access thanks to friend declarations). However, this solution adds a lot of boilerplate code for each pointer for which we need const propagation. Moreover, IMHO, using the getters and setters decrease readability (properties are one of my favorite C# features :)!). – Luc Touraille Jan 19 '11 at 10:00
1

For the record, I just found out that the Loki library does provide a const propagating pointer (ConstPropPtr<T>). It looks just like the one in the question, except that it also deletes the wrapped pointer in its destructor, and it is used to implement a Pimpl class similar to the one proposed by @Matthieu (but not copyable).

Community
  • 1
  • 1
Luc Touraille
  • 79,925
  • 15
  • 92
  • 137
0

If you think it should "propagate" const-ness, then it means you don't really believe it is a pointer (or reference), but you believe it is a container: if the value is constant when the object is constant, it's because the object contains the value.

So copying the object copies the value, at least logically (CoW).

If you insist that it is a pointer/reference IOW that you can copy the object while sharing the contained value, then you have an unsound (contradicting) interface.

Conclusion: make up your mind. It is either a container or a pointer.

A pointer does not propagate const-ness, by definition.

Andro Selva
  • 53,910
  • 52
  • 193
  • 240
curiousguy
  • 8,038
  • 2
  • 40
  • 58
  • 2
    I think you missed the point: even when an object is contained (owned) by another object, you sometimes need to store it using a pointer (polymorphism, lazy initialization, compilation firewall, ...). It is in these cases that const propagation would be useful: since *conceptually* the pointee is contained by the object, const methods of the object should not be allowed to modify it. However, I agree that my sample class `SomeClass` should have either defined a copy constructor or disallowed copy, to avoid accidentaly sharing `impl_` between `SomeClass`' instances. – Luc Touraille Nov 23 '11 at 13:43
  • _"I think you missed the point: you may want to propagates const_" I get that, and you missed my point: you want to propagate const-ness because you do not want **pointer** semantic. Please do not call that class `something_pointer`! "_even when an object is contained (owned) by another object, you sometimes need to store it using a pointer_" Yes, indeed. – curiousguy Nov 24 '11 at 02:04
  • BTW, in case someone would argue that names are not so important: if you call something a "pointer" (that does not actually have pointer semantic), some other programmer might come along and add pointer-related stuff that doesn't quite make sense for something that is not supposed to have pointer semantic. By calling it a container you (mostly) avoid this risk. – curiousguy Nov 24 '11 at 02:32
  • 2
    Ok, I get your point and mostly agree. I was a little confused by your use of the word "container": it could apply both to the enclosing class (`SomeClass` in the example) and to the wrapper encapsulating the data member (`const_propagating_ptr` could be renamed `container`), but I thought you wanted to apply it to the original data member (`impl_`), which did not make many sense. – Luc Touraille Nov 24 '11 at 09:11