对象切片会破坏封装吗?

Does object slicing break encapsulation?

我的看法是,在 OOP 中,public 接口的一个作用是确保对象始终处于有效状态(这是您不能 memcpy 的一个重要原因到非 pod 类型)。例如。一个非常基本、迂腐的例子:(请忽略 java-like set/get):

class Restricted {
protected:
  int a;

public:
  Restricted() : a{0} {}

  // always returns a non-negative number
  auto get() const -> int {
    return a;
  }

  auto set(int pa) -> void {
    if (pa < 0) 
      a = 0;
    else
      a = pa;
  }

  auto do_something() {
    // here I code with the assumption that
    // a is not negative
  }
};

在此示例中,class Restricted 的建模方式使得 Restricted 对象始终包含一个非负数。这就是我为 Restricted 定义有效状态的方式。通过查看界面,我可以说 Restricted ::get 总是 return 一个非负数。用户无法让 Restricted 持有负数。

a 受到保护,让选项轻松扩展 class。所以让我们用一个允许所有数字的类型扩展 Restricted

class Extended : public Restricted {
public:
  Extended()  { a = -1; }

  auto set(int pa) -> void {
    a = pa;
  }

  auto do_something() {
    // now a can be negative, so I take that into account
  }
};

乍一看一切正常。 Extended 不会修改 Restricted 或其行为。 Restricted 仍然是一样的,我们只是添加了另一种也允许负数的类型。

除了我们最初对 Restricted 的假设不再成立。用户可以轻松获得一个 Restricted 包含负数的对象(处于无效状态的对象),因为:

C++ 允许在没有任何警告的情况下进行对象切片:

Restricted r = Extended{};

// or

Extended e;
e.set(-24);

Restricted r = e;

// and then:

r.do_something(); // oups

有些东西不对:

C++ 给了你一把可以同时射击你双脚的枪。一直都是这样。

如果你想防止上述情况发生,删除基础class的复制构造函数,并让派生class的复制构造函数负责复制基础class.

你也可以使用私有继承。

您可以选择满足您的 class 设计要求的最佳解决方案。

was it wrong to set a as protected if I expect a to be always non-negative? Why?

是的,这是错误的。 Restricted 的接口 要求 a 是非负数。那就是类型设置的不变量。而且它没有 virtual 函数来允许派生的 class 覆盖该不变量。

通过违反该不变性(并且由于您奇怪地缺少 virtual 函数),Extended 正在打破 OOP 的基本规则:派生的 class 实例应该能够被视为 (public) 基础 class 的实例。那并不意味着切片;我的意思是,您应该能够将 Extended 传递给一个函数,该函数将 pointer/reference 传递给 Restricted,并且一切都应该像在与 Extended 对话一样工作.

was it wrong to create Extended as a subclass of Restricted? If so, why?

错误的是:

  1. a 设为 protected 而不是 private.

  2. 使Restricted的界面非virtual.

is it wrong for C++ to allow object slicing?

没有