Как обеспечить const-корректность в отношении данных-членов указателя


После обсуждения в работе, мы, кажется, не в состоянии обеспечить "логическую" const-корректность для класса, который имеет данные-члены указателя, как таковые:

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

Как можно видеть, мы хотели бы иметь WidgetManager::ManagerFooConst(), Чтобы не иметь возможности вызывать неконстантные функции членов указателя WidgetManager, в то же время позволяя им вызываться из других, неконстантных функций WidgetManager. Это означает, что объявление указателя как std::shared_ptr<const Widget> (то есть const Widget*) не выполняется.

Кроме того, мы хотели бы иметь возможность сделать указатель ссылается на другой Widget в течение жизни менеджера, поэтому мы действительно не хотим удерживать его как элемент данных (и не можем удерживать его по ссылке).

Конечно, вся" побитовая " const-корректность применяется здесь, так как никакие данные-члены WidgetManager не могут быть изменены из методов const (включая конкретные Widget, на которые указывает _pW), но мы хотели бы достичь "логической" const-корректности, если бы даже указываемые члены не могли быть изменены.

Единственное, что мы придумали, это добавление const и non-const "геттеров this" к Widget:

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

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

И возвращаясь к использованию их вместо оператора стрелки напрямую:

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();
}

Но это так уродливо, и определенно не может быть осуществлено компилятором.

В частности, попытка перегрузить operator-> для Widget* и const Widget*, похоже, ничего не изменила: ManagerFooConst() все еще мог вызывать _pW->Foo().

Есть ли способ достичь этого?
4 3

4 ответа:

Вы можете использовать (или переопределить) std::experimental::propagate_const

И тогда ваш код будет:

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

Демо

Рассмотрите возможность доступа к вашему shared_ptr через функцию-член, которая отражает постоянство this на объект с указателем.

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

Вы получите одну ошибку, которую ожидаете здесь.

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
}

Простым решением было бы сделать const и non-const менеджеров двух различных типов:

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.

В дополнение к тому, что было предложено здесь, вы можете ввести понятие "владения указателем", основанное на разделяемом указателе:

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

Этот класс имеет ту же функциональность, что и общий указатель, но другую концепцию владения данными.

Вы бы использовали его так же, как вы бы использовали общий указатель:

class WidgetManager {

    ...

private:
    owning_ptr<Widget> _pW;
}