C++:如何通过指向其基类子对象的指针防止修改派生对象?

3
以下是一个简单(但可编译)的示例,说明了可能的切片赋值场景。
#include <string>

struct Base
{
    // Mutating method. Not a chance of making it virtual.
    template <typename Anything>
    Base& operator=(const Anything& x)
    {
        m_int = x.AsInteger();
        return *this;
    }

    int AsInteger() const
    {
        return m_int;
    }

    int  m_int;
};

struct Derived : public Base
{
    template <typename Anything>
    Derived& operator=(const Anything& x)
    {
        m_text = x.AsString();
        Base::operator=(x);
        return *this;
    }
    const std::string& AsString() const
    {
        return m_text;
    }

    // Invariant: Derived::m_text matches Base::m_x.
    std::string   m_text;
};

void ExamineBase(const Base* b)
{
    b->AsInteger();
}

void ExamineBase(const Base& b)
{
    b.AsInteger();
}

void InitBase(Base* b)
{
    *b = Base();
}

void InitBase(Base& b)
{
    b = Base();
}


int main()
{
    Base           b;
    InitBase(b);          // <----- (1)
    InitBase(&b);         // <----- (2)

    Derived        d;
    Derived&       ref = d;
    Derived*       ptr = &d;

    ExamineBase(ref);     // <----- (3)
    ExamineBase(ptr);     // <----- (4)

    InitBase(ref);        // <----- (5)
    InitBase(ptr);        // <----- (6)

    return 0;
}

第1行、第2行、第3行和第4行是好的。

第5行和第6行存在问题:它们只更改完整对象中的基础子对象,显然破坏了 Base::m_int 和 Derived::m_text 之间的一致性。

我有兴趣防止这种切片修改发生,同时保留第1行、第2行、第3行和第4行的有效性。

因此,问题是:

a)有没有什么技巧可以防止通过指向派生类的指针调用基类的非const成员函数?

b)有没有什么技巧可以阻止从Derived *到Base *的标准隐式转换,但仍允许从Derived *到const Base *的转换?


2
不是针对你具体问题的答案,但听起来你的设计有些靠不住,因为有这样的要求。继承的整个重点在于你想要一个“是一个”关系。我的直觉是Base应该是Derived的成员。 - user4442671
抱歉在问题中没有涉及到这个方面...是的,你说得对。继承可能不是这个关系的正确选择。我只是想允许在任何非变异访问Base足够的地方使用Derived。为了给你个形象化印象,我们可以想象Base是一个类似std::span的非拥有访问器,而Derived则是一组拥有容器中的某个类。 - Igor G
一个虚拟的复制赋值运算符可能会起作用。 - NathanOliver
@NathanOliver,不幸的是让所有的突变方法(或运算符)都成为虚方法并不是一个选项。虚方法不能作为模板,而将其设为模板则是必要的。首先,一个类可以接受各种初始化器类型。另一个原因是,突变操作可能会带有一些SFINAE内容,以便std::is_assignable会报告正确的结果。 - Igor G
@NathanOliver 对,这会防止特定的使用。我想我过于扩大了 OP 的问题范围,涉及到了一般的变异。 - user4442671
显示剩余2条评论
1个回答

2
免责声明:我按照问题的要求回答,但如果您想知道如何实现这一点,那么很可能您的设计有问题。
简短回答:使用公有继承是无法实现这一点的。公有继承的整个目的就是可以将指向Derived对象的引用或指针用作Base对象的引用或指针,而不管上下文是什么。
因此,实现这一点的方法是通过私有继承或成员变量,并只通过返回const引用或指针的访问器公开Base成员。
#include <string>
struct Base
{
    // Mutating method. Not a chance of making it virtual.
    template <typename Anything>
    Base& operator=(const Anything& x)
    {
        m_int = x.AsInteger();
        return *this;
    }

    int AsInteger() const
    {
        return m_int;
    }

    int  m_int;
};

struct Derived : private Base
{
    template <typename Anything>
    Derived& operator=(const Anything& x)
    {
        m_text = x.AsString();
        Base::operator=(x);
        return *this;
    }
    const std::string& AsString() const
    {
        return m_text;
    }

    const Base& base() const {return *this;}

    // Invariant: Derived::m_text matches Base::m_x.
    std::string   m_text;
};

void ExamineBase(const Base* b)
{
    b->AsInteger();
}

void ExamineBase(const Base& b)
{
    b.AsInteger();
}

void InitBase(Base* b)
{
    *b = Base();
}

void InitBase(Base& b)
{
    b = Base();
}


int main()
{
    Base           b;
    InitBase(b);          // <----- (1)
    InitBase(&b);         // <----- (2)

    Derived        d;
    Derived&       ref = d;
    Derived*       ptr = &d;

    ExamineBase(ref.base());     // <----- (3)
    ExamineBase(&ptr->base());     // <----- (4)

    InitBase(ref.base());        // <----- BOOM!
    InitBase(&ptr->base());        // <----- BOOM!

    return 0;
}

谢谢您的建议,Frank,但是我希望避免显式调用转换函数(即base())的必要性...我希望在任何const Base适用的地方都可以使用Derived。这就是为什么我从“is-a”关系开始:在每个非变异上下文中,Derived都是一个Base - Igor G

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接