如何强制执行有关指针数据成员的常量正确性

How to enforce const-correctness regarding pointer data-members

经过工作讨论,我们似乎无法对具有指针数据成员的 class 执行 "logical" 常量正确性,因此:

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,因此我们真的不想将其作为数据成员保存(并且可以'不作为参考)。

当然,这里强制执行所有 "bitwise" const 正确性,因为不能从 const 方法中修改 WidgetManager 的数据成员(包括由_pW),但我们希望实现 "logical" 常量正确性,即使指向的成员也无法修改。

我们唯一想到的就是在 Widget 中添加一个 const 和非常量 "getters of this":

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

但这太丑陋了,编译器绝对不能强制执行。

具体来说,尝试为 Widget*const Widget* 重载 operator-> 似乎没有任何改变:ManagerFooConst() 仍然能够调用 _pW->Foo() .

有办法实现吗?

一个简单的解决方案是将 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.

考虑通过成员函数访问您的 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
}

除了这里提出的,你还可以引入一个基于共享指针的"owning 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;    
};

此 class 与共享指针具有相同的功能,但拥有数据的概念不同。

您可以像使用共享指针一样使用它:

class WidgetManager {

    ...

private:
    owning_ptr<Widget> _pW;
}

您可能会使用(或重新实现)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;
};

Demo