uv_
uv_

Reputation: 773

How to enforce const-correctness regarding pointer data-members

Following a discussion at work, we seem to be unable to enforce "logical" const-correctness on a class which has pointer data-members, as such:

class Widget {
public:
    void Foo();
    void FooConst() const;
};

class WidgetManager {
public:
    WidgetManager() : _pW(std::shared_ptr<Widget>(new Widget())) { }

    void ManagerFoo()
    {
        _pW->Foo();         // should be OK, will not compile if declared as "const Widget*"
        _pW->FooConst();    // should be OK
    }

    void ManagerFooConst() const
    {
        _pW->Foo();         // should NOT be OK, will not compile if declared as "const Widget*"
        _pW->FooConst();    // should be OK
    }

    void RegenerateWidget()
    {
        _pW = std::shared_ptr<Widget>(new Widget());
    }

private:
    std::shared_ptr<Widget> _pW;
};

As can be seen, we would like to have WidgetManager::ManagerFooConst() to not be able to call non-const functions of WidgetManager's pointer members, while still allowing them to be called from other, non-const functions of WidgetManager. This means, declaring the pointer as std::shared_ptr<const Widget> (i.e. const Widget*) is out.

In addition, we would like the option to make the pointer reference another Widget during the manager's life-time, so we don't really want to hold it as a data-member (and can't hold it by reference).

Of course, all "bitwise" const-correctness is enforced here, as no data-members of WidgetManager can be modified from within const methods (including the specific Widget pointed by _pW), but we would like to achieve "logical" const-correctness were even pointed members can't be modified.

The only thing we came-up with is adding a const and non-const "getters of this" to Widget:

class Widget {
public:
    void Foo();
    void FooConst() const;

    Widget* GetPtr()    { return this; }
    const Widget* GetConstPtr() const   { return this; }
};

And reverting to use these instead of the arrow operator directly:

void WidgetManager::ManagerFoo()
{
    // shouldn't use "->" directly (lint?)

    _pW->GetPtr()->Foo();
    _pW->GetPtr()->FooConst();
    //_pW->GetConstPtr()->Foo();        // this won't compile (good)
    _pW->GetConstPtr()->FooConst();

}

void WidgetManager::ManagerFooConst() const
{
    // shouldn't use "->" directly (lint?)

    _pW->GetPtr()->Foo();               // shouldn't be used (lint?)
    _pW->GetPtr()->FooConst();          // shouldn't be used (lint?)
    //_pW->GetConstPtr()->Foo();        // this won't compile (good)
    _pW->GetConstPtr()->FooConst();
}

But this is so ugly, and definitely not enforceable by the compiler.

Specifically, trying to overload operator-> for Widget* and const Widget* didn't seem to change anything: ManagerFooConst() was still able to call _pW->Foo().

Is there a way to achieve this?

Upvotes: 3

Views: 507

Answers (4)

Jarod42
Jarod42

Reputation: 217348

You might use (or reimplement) std::experimental::propagate_const

and then your code would be:

class Widget {
public:
    void Foo();
    void FooConst() const;
};

class WidgetManager {
public:
    WidgetManager() : _pW(std::make_shared<Widget>()) {}

    void ManagerFoo()
    {
        _pW->Foo();      // OK
        _pW->FooConst(); // OK
    }

    void ManagerFooConst() const
    {
        _pW->Foo();         // not compile
        _pW->FooConst();    // OK
    }

private:
    std::experimental::propagate_const<std::shared_ptr<Widget>> _pW;
};

Demo

Upvotes: 2

Teivaz
Teivaz

Reputation: 5675

In addition to what has been proposed here you can introduce a concept of "owning pointer" based on shared pointer:

template<class T>
class owning_ptr {
    public:
    owning_ptr(T* data) : mData{data} {}
    // Put as many constructors as you need

    T* operator->() { return mData.get(); }
    const T* operator->() const { return mData.get(); }

private:
    std::shared_ptr<T> mData;    
};

This class has same functionality as shared pointer but a different concept of owning data.

You would use it the same way you'd use shared pointer:

class WidgetManager {

    ...

private:
    owning_ptr<Widget> _pW;
}

Upvotes: 0

Drew Dormann
Drew Dormann

Reputation: 63775

Consider accessing your shared_ptr through a member function that reflects the const-ness of this onto the pointed-to object.

class WidgetManager {

    ...

private:
    std::shared_ptr<Widget> _pW;

    std::shared_ptr<Widget>& get_widget()
    {
        return _pW;
    }

    const std::shared_ptr<const Widget> get_widget() const
    {
        return _pW;
    }
}

You will get the one error you expect here.

void ManagerFoo()
{
    get_widget()->Foo();         // will be OK, will not compile if declared as "const Widget*"
    get_widget()->FooConst();    // will be OK
}

void ManagerFooConst() const
{
    get_widget()->Foo();         // will NOT be OK
    get_widget()->FooConst();    // will be OK
}

Upvotes: 2

YSC
YSC

Reputation: 40080

An easy solution would be to make const and non-const managers two different types:

template<class TWidget>
class WidgetManager {
    // ...
private:
    std::shared_ptr<TWidget> _pW;
};

WidgetManager<const Widget> const_wm;
const_wm.ManagerFoo(); // fail: _pW->Foo(): call non-const function of *_pw: a const Widget.

Upvotes: 0

Related Questions