5.3 物件複製語意學(Object Copy Semantics)
當我們設計一個class,並以一個class object賦值給另一個class object時,我們就用到了拷貝複製操作,我們可以選擇預設的淺拷貝,也可以自己提供一個explicit copy assignment operator,或者顯示的拒絕把一個class object賦值給另一個class object(將copy assignment operator宣告為private)。
為了驗證copy assignment operator,再一次用到Point class:
class Point { public: Point(float x = 0.0,float y = 0.0,float z = 0.0); //...(沒有virtual function) protected: float _x,_y; };
對於上述程式碼,如果要支援一個簡單的拷貝操作,那麼預設的行為不但足夠而且有效率,只有在預設行為所導致語義不安全或不正確的情況下,才需要設計一個copy assignment operator。
由於此class已經有bitwise copy語意,所以implicit copy assignment operator和copy constructor一樣被視為毫無用處,也根本不會合成出來。
C++ Standard上說copy assignment operators並不表示bitwise copy semantics是nontrivial,只有nontrivial 才會被合成出來,以下情況不會表現bitwise copy語意:
- 當class內含一個member object,而其class有一個copy assignment operator時。
- 當一個class 的base class有一個copy assignment operator 時。
- 當一個class聲明瞭任何virtual function(我們一定不要拷貝右端class object的vptr地址,因為它可能是一個derived class object)。
- 當class 繼承自一個virtual base class(不論此時base class有沒有copy operator)時。
對於Point class,這樣的賦值操作:
Point a,b;
...
a = b;
由於是bitwise copy完成,把Point b拷貝到Point a,期間沒有copy assignment operator被呼叫;我們還可以提供一個copy constructor,把NRV優化開啟,copy constructor出現不應該讓我們也一定提供一個copy assignment operator。
現在匯入一個copy assignment operator,來說明該operator在繼承下的行為:
inline Point&
Point::operator = (const Point& rhs)
{
_x = rhs._x;
_y = rhs._y;
return *this;
}
現在派生一個Point3d 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,編譯器就必須合成一個(因為前述第二項和第四項),合成的看起來像如下:
//C++ pseudo
//被合成的copy assignment operator
inline Point3d&
Point3d::operator = (Point3d* const this,const Point3d& p)
{
//呼叫base class的拷貝賦值函式
this->Point::operator(p);
//memberwise copy the derived class members
_z = p.z;
return *this;
}
copy assignment operator有一個不夠理想、不夠嚴謹的情況,就是它缺乏一個member assignment list(不能像建構函式那樣member initialization list),因為不能寫:
//C++ pseudo,以下性質不支援
inline Point3d&
Point3d::operator = (const Point3d& p3d)
:Point(p3d),z(p3d._z)
{}
//必須寫成以下的兩種形式,
//才能呼叫base class 的copy assignment operator
Point::operator = (p3d);
//或者
(*(Point)this) = p3d;
缺少copy assignment list,編譯器就無法壓抑上一層base class的copy operators被呼叫:
//class Vertex: virtual public Point
inline Vertex&
Vertex::operator = (const Vertex& v)
{
this->Point::operator(v);
_next = v._next;
return *this;
}
//從Point3d和Vertex中派生出Vertex3d
inline Vertex3d&
Vertex3d::operator = (const Vertex3d& v)
{
this->Point::operator(v);
this->Point3d::operator(v);
this->Vertex::operator(v);
//...
}
事實上,copy assignment operator在虛擬繼承情況下行為不佳,需要小心的設計和說明。許多編譯器甚至並不嘗試取得正確語意,它們在每一箇中間的copy assignment operator中呼叫每一個base class instance,於是造成virtual base class copy assignment operator的多個例項被呼叫。
有一種方法可保證most_derived class會引發virtual base class subobject的copy行為,就是在derived class的copy assignment oper函式例項的最後,顯示呼叫那個operator:
inline Vertex3d&
Vertex3d::operator = (const Vertex3d& v)
{
this->Point3d::operator(v);
this->Vertex::operator(v);
//must place this last if your
//compiler does not suppress
//intermediate class invocations
this->Point::operator(v);
/...
}
這並不能省略subobjects的多重拷貝,但卻可以保證語意正確。另一個方法就是把virtual subobject拷貝到一個分離的函式中,並根據call path條件化的呼叫它。
作者的建議是不要允許virtual baes class的拷貝操作,甚至不要再任何virtual base class中宣告資料。