Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I write a const propagating pointer type wrapper?

Tags:

c++

c++17

To better understand the C++ type system, I have endeavored to write a pointer wrapper class which propagates constness similar to std::experimental::propagate_const:

template <typename Pointee> class Ptr {
public:
  Ptr() = delete;

  explicit Ptr(Pointee *);

  Ptr(const Ptr<Pointee> &) = delete;
  Ptr(Ptr<Pointee> &&);

  Ptr<Pointee> &operator=(const Ptr<Pointee> &) = delete;
  Ptr<Pointee> &operator=(Ptr<Pointee> &&);

  ~Ptr()  = default;

  const Pointee *operator->() const;
  Pointee *operator->();
  const Pointee &operator*() const;
  Pointee &operator*();

private:
  Pointee *mPtr;
};

The wrapper is intended to provide near raw pointer like behavior while also enforcing a kind of 'deep' const correctness and guarding against unintentional aliasing.

To this end, the copy constructor and copy assignment operator are deleted:

  1. To prevent unintentional aliasing of the pointed-to object by copying from a Ptr.
  2. To prevent non-const access of a const pointed-to object by copying a const Ptr into a non-const Ptr.

However, the above design has two unfortunate consequences.

  1. A const Ptr cannot be moved into a either a const or non-const Ptr. When C++17's mandatory RVO does not apply, this means that a const Ptr object cannot be returned from a function.
  2. Due to C++17's mandatory copy/move-elision, in certain situations a non-const Ptr can be constructed from a const Ptr even though no such viable constructor exists. For instance, the code below will compile just fine (ignore the memory leak/raw new for the purpose of demonstration):
const Ptr<int> allocateImmutableInt(int val) { return Ptr<int>(new int(val)); }


void foo() {
  Ptr<int> immutableInt = allocateImmutableInt(0); // Initializes non-const Ptr from const Ptr 
  *immutableInt = 100;  // Oops, changed value of 'immutable' object
}

The first problem can be partially solved by introducing a move ctor that accepts a const rvalue reference (although this feels somewhat strange and non-idiomatic):

  Ptr(const Ptr<Pointee> &&);

However, this actually worsens the second problem. Now a const Ptr can be move constructed into a non-const Ptr even without mandatory move/copy-elision. As far as I can tell, to get around this issue we would need a so called 'const constructor', ie a constructor which can only be invoked to produce a const object:

  Ptr(const Ptr<Pointee>&&) const;

Even if c++ supported such a constructor, the second problem would still remain, as c++17 specifically ignores cv-qualification and the viability of constructors when deciding if mandatory move/copy-elision can be applied when initializing an object. There does not currently appear to be a way to ask c++ to check if a copy/move would be viable before applying mandatory copy/move elision to object initialization.

As far as I can tell, std::experimental::propagate_const suffers from these same issues. I am wondering if I have encountered a fundamental limitation of c++, or if I am designing the Ptr wrapper incorrectly? I am aware that these issues can likely be eliminated by creating two types, a Ptr for non-const access and ConstPtr for const-only access. However, this defeats the purpose of creating a const-propagating wrapper in the first place.

Perhaps I have just stumbled upon the reason why both an iterator type and a const_iterator type exist.

like image 320
Ezra Stein Avatar asked Nov 01 '25 09:11

Ezra Stein


1 Answers

You are looking at problems that do not really exist.

  1. To prevent non-const access of a const pointed-to object by copying a const Ptr into a non-const Ptr.

This should not be a goal, as it goes against the idea of propagating const.

There are two aspects to propagating const. First, when the pointer is const-qualified, the object is also const-qualified. This aspect you have covered. Second, when the pointer is not const-qualified, the object uses its natural qualification. That is, if you can copy a const Ptr into a non-const Ptr, then that change propagates to the object, potentially making the object also non-const. This is desired propagation, not something to prevent.

Keep in mind a major use case for const propagation: class members. Propagating const for a member pointer helps ensure const-correctness by making the pointed-to data const in const-qualified member functions. Your imagined problems are not applicable to this use case. Don't make the situation more complex than it needs to be.

I am aware that these issues can likely be eliminated by creating two types, a Ptr for non-const access and ConstPtr for const-only access.

This is not necessary. If the object is supposed to remain const even when the pointer is not const, then the type should be Ptr<const T> instead of Ptr<T>. As an example, your "immutable int" should look more like the following.

Ptr<const int> allocateImmutableInt(int val) { return Ptr<const int>(new int(val)); }
    ^^^^^                                                 ^^^^^

The const has been moved to qualify the int, making the int immutable regardless of the const-qualification of the Ptr.

Furthermore, you might note that new int(val) returns an int* that gets implicitly converted to const int* for your constructor. You might want to replicate this implicit conversion for Ptr. A constructor like Ptr(Ptr<std::remove_const<Pointee>> &&) would do the trick, as long as it is defined only when Pointee is const-qualified (to avoid having a conflict with the regular move constructor).

like image 157
JaMiT Avatar answered Nov 03 '25 00:11

JaMiT



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!