const correct composition using std::unique_ptr / std::shared_ptr

Viewed 362

Currently I am wondering how to correctly use an std::unique_ptr as a member variable regarding const correctness.

The following example allows to change the content owned by my_foo despite is being const:

#include <iostream>
#include <memory>

struct foo {
    foo() : value_ptr_(std::make_unique<int>(3)) {}
    void increment() const {
        ++(*value_ptr_);
    }
    int get_value() const {
        return *value_ptr_;
    }
    std::unique_ptr<int> value_ptr_;
};

int main() {
    const foo my_foo;
    std::cout << my_foo.get_value() << std::endl;
    my_foo.increment(); // But my_foo is const!
    std::cout << my_foo.get_value() << std::endl;
}

Replacing std::make_unique<T> with std::make_unique<const T> seems like a good solution at first glance. However this disallows to change the content of my_foo even if it is non-const:

#include <iostream>
#include <memory>

struct foo {
    foo() : value_ptr_(std::make_unique<int>(3)) {}
    void increment() {
        ++(*value_ptr_);
    }
    int get_value() const {
        return *value_ptr_;
    }
    std::unique_ptr<const int> value_ptr_;
};

int main() {
    foo my_foo;
    std::cout << my_foo.get_value() << std::endl;
    my_foo.increment(); // compiler error
    std::cout << my_foo.get_value() << std::endl;
}

Having a pointer to an int like in this minimal example is of course not very meaningful, but in real code the unique_ptr could hold a pointer to a base class of something polymorphic, i.e. an object we would not be able to simply store by value.

So how can this situation be handled better?

2 Answers

you can inherit std::unique_ptr and override just 3 (4 for unique_ptr<T[]>) methods, providing const/non-const overloads:

template <typename T>
struct propagating_unique_ptr : std::unique_ptr<T> {
    using unique_ptr<T>::unique_ptr;
    using unique_ptr<T>::operator =;

    const T *get() const noexcept {
        return unique_ptr<T>::get();
    }
    T *get() noexcept {
        return unique_ptr<T>::get();
    }

    const T &operator *() const noexcept {
        return unique_ptr<T>::operator *();
    }
    T &operator *() noexcept {
        return unique_ptr<T>::operator *();
    }

    const T *operator -> () const noexcept {
        return unique_ptr<T>::get();
    }
    T *operator -> () noexcept {
        return unique_ptr<T>::get();
    }
}; 

The way I do this is by providing an internal protocol to provide access to the correctly-consted reference to the underlying implementation.

Something like this:

struct foo {
    // standard (in your codebase) protocol to express the impl base class
    using underlying_impl = int;

    // standard protocol to express ownership semantics
    using implementation_handle = std::unique_ptr<underlying_impl>;

    // construction via private 'construct' protocol    
    foo() : value_ptr_(construct(3)) {}

    // all internal access to the implementation via a the protocol
    // of get_impl()
    auto operator++() -> foo&
    {
        // not-const - compiles fine
        ++get_impl();
        return *this;
    }

    void increment() const {
// now won't compile - get_impl() propagates const correctly
//        ++get_impl();
    }

private:

    static auto construct(int val) -> implementation_handle
    {
        return std::make_unique<underlying_impl>(val);
    }

    // two versions of get_impl() - const and mutable
    auto get_impl() const -> underlying_impl const&
    {
        return *value_ptr_;
    }

    auto get_impl() -> underlying_impl&
    {
        return *value_ptr_;
    }

    // actual storage of the implementation handle
    implementation_handle value_ptr_;
};
Related