5.3 对象赋值语义学

设计一个 class 时, 并以一个 class object 指定给另一个 class object 时, 我们有三种选择:

  1. 什么也不做, 实施默认行为
  2. 提供一个 explicit copy assignment operator
  3. 明确拒绝把一个 class object 指定给另一个 class object.

如果选择到三点, 那么只需要将 copy assignment operator 声明为 private, 且不提供其定义( C++11 标准里可以使 copy assignment operator+() = delete) 这样我们就不能在任何场合(除了 member functions 以及 friends 之中) 进行赋值操作. 一旦某个 member function 或 friend 企图影响一份拷贝, 程序在链接时就会失败.
这一节主要是验证 copy assignment 的语意, 以及它们如何被模塑出来, 用 Point class 来讨论:

class Point
{
public:
    Point(float x = 0.0, float y = 0.0);
    // no virtual function
protected:
    float _x, _y;
};

没有理由要禁止拷贝一个 Point object. 那么问题就变成了: 默认行为是否足够?如果要支持的只是一个简单的拷贝操作,那么默认行为不仅足够,而且有效率.
只有在默认行为不够安全或不够正确时,才需要自己设计一个 copy assignment operator. 默认的 memberwise copy 行为不够安全吗? 不正确吗?不,由于坐标内都自带数值,所以不会发生 别名化(aliasing) 或 内存泄露(memory leak). 如果我们自己提供一个 copy assignment operator, 程序反而会执行得比较慢.
如果不对 Point 供应一个 copy assignment operator, 而光是依赖默认的 memberwise copy, 编译器会产生一个实体吗? 答案和 copy constructor 的情况一样: 不会! 因为此 class 已经有了 bitwise copy 语意, 所以 implicit copy assignment operator 被视为 trivial, 因此不会被合成出来.
一个 class 对于默认的 copy assignment operator, 在以下情况不会表现出 bitwise copy 语意:

  1. 当 class 内带一个 member object, 而其 class 有一个 copy assignment operator 时.
  2. 当一个 class 的 base class 有一个 copy assignment operator 时.
  3. 当一个 class 声明了任何 virtual functions(此时不可拷贝右端的 class object 的 vptr 地址, 因为它可能是一个 derived class object).
  4. 当 class 继承一个 virtual base class(不论此 base class 有没有 copy operator 时).
    C++ standard 上说, copy assignment operator 并不表示 bitwise copy semantics 是 nontrivial, 实际上, 只有 nontrivial instances 才会被合成出来.
    于是, 对于 Point class, 这样的赋值操作:
Point a, b;
...
a = b;

由 bitwise copy 完成, 把 Point b 拷贝到 Point a, 没有 copy assignment operator 被调用. 从语意上或效率上来考虑, 这都是我们需要的.
还可以提供一个 copy constructor, 为的是把 name return vlaue(NRV) 优化打开。copy constructor 的出现不应该让我们认为也一定要提供一个 copy assignment operator.

现在导入一个 copy assignment operator, 用以说明该 operator 在继承之下的行为:

inline Point& Point::operator=(const Point &p)
{
    _x = p._x;
    _y = p._y;
    return *this;
}
//现在派生一个 Point3d class
//注意是虚拟继承
class Point3d: virtual public Point
{
public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
    //...
protected:
    float _z;
};

如果没有为 Point3d 定义一个 copy assignment operator, 编译器就必须合成一个(因为之前的第二和第四项理由):

//可能的合成结果:
inline Point3d& Point3d::operator=(Point3d* const this, const Point3d &p)
{
    this->Point::operator=(p);

    //memberwise copy the derived class members
    _z = p._z;
    return *this;
}

copy assignment operator 有一个非正交的情况, 那就是缺乏 member assignment list。因此不能够写:

//不支持
inline Point3d& Point3d::operator=(const Point3d &p3d): Point(p3d), z(p3d._z){ }

必须调用以下两种形式,才能调用 base class 的 copy assignment operator

Point::operator=(p3d);	// p3d 先转化为 Point 类型
或者,
( *(Point*)this ) = p3d;	// p3d赋值给point指针类型

缺少 copy assignment list, 编译器就没法压抑上一层 base class 的 copy operators 被调用。例如下面也是虚拟继承 Point 的 Vertex copy operator:

//class Vertex : virtrual public Point
inline Vertex& Vertex::operator=(const Vertex &v)
{
    this->Point::operator=(v);	// 复制子类的 Point 部分数据
    _next = v._next;
    return *this;
}

现在再从 Point3d 和 Vertex 中派生出 Vertex3d, 下面是 Vertex3d 的 copy assignment operator:

inline Vertex3d&
Vertex3d::operator=(const Vertex3d &v)
{
// 因为调用的是定义好的operator=(),所以只处理定义好的那一部分
    this->Point::operator=(v);	// 子类转化为父类 Point
    this->Point3d::operator=(v);
    this->Vertex::operator=(v);
    //...
}

编译器如何在 Point3d 和 Vertex 的 copy assignment operator 中压抑 Point 的 copy assignment operator 呢? 编译器不能够重复传统的 constructor 解决方案(附加额外参数), 这是因为, 和 constructor 以及 destructor 不同的是, 取 copy assignment operator 地址的操作是合法的. 以下是合法的代码:

// bzf: 只为了说明可以取址?
typedef Point3d& (Point3d::*pmfPoint3d)(const Point3d&);	// 函数指针
pmfPoint3d pmf = &Point3d::operator=;	// 将operator=函数名给一个函数指针
(x.*pmf)(x);

然而我们却无法支持它, 我们仍需要根据其独特的继承体系安插任何可能数目的参数给 copy assignment operator. 这一点在我们支持由 class objects(内带 virtual base clases) 所组成的数组的配置操作时, 也被证明是很有问题的.
另一个方法是, 编译器可能为 copy assignment operator 产生分化函数(split functions)以支持这个 class 成为 most-derived class 或成为中间的 base class. 如果 copy assignment operator 被编译器产生的话, 那么 split function 解决方案可以说是定义明确的, 但如果是由 class 的设计者完成的, 那么就不能算定义明确了. 比如, 一个人如何分化下面这样的函数呢?

// init_bases() 为 virtual
inline Veretx3d& Vertex3d::operator=(const Vertex3d &v)
{
    init_bases( v );
}

事实上, copy assignment operator 在虚拟继承的情况下行为不佳, 需要小心的设计和说明. 许多编译器甚至并不尝试取得正确的语意, 它们在每一个中间的 copy assignment operator 中调用每一个 base class instance, 于是造成 virtual base class copy assignment operator 的多个实体被调用. 目前许多的编译器都是如此, 你的是怎样呢? C+ Standard 又是怎么说的呢?
C++ Standard 也并没有规定那些代表 virtual base class 的 subobject 是否该被 implicit defined 的 copy assignment operator 指派(赋值, assign) 内容一次以上.
如果在语言的层面上解决这个问题, 那么应该为 copy assignment operator 提供一个附加的 member copy list. 简单地说, 任何一个解决方案如果是以程序操作为基础, 将导致较高的复杂度和较大的错误倾向. 一般公认, 这是语言的一大弱点, 也是一个人应该小心检验其程序代码的地方(当他使用 virtual base ckasses 时).
其实有一种方法可以保证 most-deirved class 会引发 virtual base class subobject 的 copy 行为, 那就是在 derived class 的copy assignment operator 函数实体的最后, 明确调用那个 operator, 像这样:

inline Vertex3d&
Vertex3d::operator=(const Vertexd &v)
{
    this->Point3d::operator=(v);
    this->Vertex::operator=(v);
    //must place this if your cpmlier does
    //not suppress intermediate invcations
    this->Point:operator=(v);
    ...
}

这并不能够省略 subobject 的多重拷贝, 但却可以保证语意正确. 另一个解决方案是把 virtual subobject 拷贝到一个分离的函数中, 并根据call path 条件化的调用它。
但还是建议尽可能不要允许一个 virtual base class 的拷贝操作. 甚至还有一个更为过分的建议: 不要在任何 virtual base class 中声明数据。

// 为什么是虚基类,而不是其他:为了解决父类的多重定义。
// 虚基类在菱形继承时候表现的比较显眼?

posted @ 2021-04-23 17:38  点|滴  阅读(6)  评论(0)    收藏  举报