在 C++ 中,為什麼通常使用複合賦值來實現算術運算符?
在 C++ Primer P497 看到這段話【如果類同時定義了算術運算符和相關的複合賦值運算符,則通常情況下應該使用複合賦值來實現算術運算符。】我的疑惑是,為什麼不用算術運算符來實現複合賦值運算符?
更新:使用符合賦值運算符來實現對應的算術運算符的顯著優點是代碼復用,對於無需修改的其他類成員只需要調用複製構造函數。然而這樣做的缺點是在某些情況下複製構造函數可能會帶來難以解決的問題,降低開發效率。所以如果直接使用複製構造函數不合適,而且單獨實現算術運算符不會產生大量重複代碼,分開實現算術運算符和複合賦值運算符是比較合適的選擇。-----------------------------------------------------------------------------------------------
因為複合賦值運算本身不需要創建一個新的對象,如果你用operator+來實現operator+=會不可避免地創建一個臨時對象,增加運行負擔。
舉個例子,有這樣一個類:class Plus
{
private:
int number;
public:
Plus() = default;
Plus(int num): number(num) {}
Plus operator+(const Plus A) const
{ return Plus(number + A.number); }
Plus operator+=(const Plus B); //如何依賴operator+實現?
看上去最簡單的方法就是構造一個新Plus對象然後替換掉原對象:
Plus Plus::operator+=(const Plus B)
{
*this = *this + B;
return *this;
}
*this + B調用operator+()創建一個臨時Plus對象複製(或移動)給*this,這導致一個臨時對象被創建,在C++11以前還要多算一份複製開銷。
---------------------------------------------------------------------------------------如果不依賴operator+實現operator+=的話:Plus Plus::operator+=(const Plus B)
{
number += B.number;
return *this;
}
這樣就只需要修改成員number,而不用創建一個新對象。
----------------------------------------------------------------------------------------@Milo Yip 提出的 「使用+=實現+會多出一次複製操作」 只是Plus這個類的特例。我用《C++ Primer》裡面的Sales_item類(即原題P.497裡面的示例類Sales_data的原型)再舉個例子://原Sales_item.h頭文件,與+和+=無關的部分已略去
#ifndef SALESITEM_H
#define SALESITEM_H
#include &
#include &
class Sales_item
{
private:
std::string bookNo;
unsigned units_sold;
double revenue;
public:
Sales_item() = default;
Sales_item(const std::string book):
bookNo(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream is) { is &>&> *this; }
//除默認構造函數外只有參數為const std::string或std::istream的構造函數
Sales_item operator+=(const Sales_item); //成員函數
};
Sales_item operator+(const Sales_item, const Sales_item); //非成員函數
Sales_item Sales_item::operator+=(const Sales_item rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
} //直接修改成員
Sales_item operator+(const Sales_item lhs, const Sales_item rhs)
{
Sales_item ret(lhs); //使用複製構造函數構造ret
ret += rhs; //調用operator+=()
return ret;
} //和P.497的例子幾乎一致
#endif
+或+=操作(假設ISBN一致)需要對units_sold和revenue兩個成員進行更改,而Sales_item類並沒有以這兩個成員為參數的構造函數。因此在這裡唯一合適的構造方法就是調用複製構造函數,如果不用operator+=來實現operator+的話是這個樣子:
//注意:這是個非成員函數
Sales_item operator+(const Sales_item lhs, const Sales_item rhs)
{
Sales_item ret(lhs);
ret.units_sold += rhs.units_sold;
ret.revenue += rhs.revenue;
return ret;
}
所以之前的return Plus(number + A.number);只是個省事的方法,放在Sales_item類完全不可行。
也就是說在通常情況下,不管是否依賴operator+=,operator+都要調用複製構造函數來創建那個需要被返回的對象。我認為兩種方式各有優缺點。我嘗試做一個 synthetic 的例子說明。
假設要實現一個類, 內含有一個 vector&
第一個版本:使用二元運算實現複合賦值
#include &
#include &
using namespace std;
typedef vector&
// Compound Assignment via Binary Operator
struct CAVBO {
CAVBO() { cout &<&< "CAVBO()" &<&< endl; }
~CAVBO() { cout &<&< "~CAVBO()" &<&< endl; }
CAVBO(const CAVBO src) : v(src.v) {
cout &<&< "CAVBO(const CAVBO)" &<&< endl;
}
explicit CAVBO(const V v) : v(v) {
cout &<&< "CAVBO(const V)" &<&< endl;
}
CAVBO operator+(const CAVBO rhs) const {
V result;
cout &<&< "reserve()" &<&< endl;
result.reserve(v.size() + rhs.v.size());
result.insert(result.end(), v.begin(), v.end());
result.insert(result.end(), rhs.v.begin(), rhs.v.end());
return CAVBO(std::move(result));
}
CAVBO operator+=(const CAVBO rhs) {
*this = *this + rhs;
return *this;
}
void print() const {
copy(v.begin(), v.end(), ostream_iterator&
cout &<&< endl;
}
vector&
};
int main() {
CAVBO a(V({1, 2}));
CAVBO b(V({3, 4, 5}));
CAVBO c(V({6, 7, 8, 9}));
cout &<&< endl;
cout &<&< "1. a += b;" &<&< endl;
a += b;
a.print();
cout &<&< endl;
cout &<&< "2. a + c;" &<&< endl;
CAVBO d = a + c;
d.print();
cout &<&< endl;
}
1. a += b;
reserve()
CAVBO(const V)
~CAVBO()
1,2,3,4,5,
2. a + c;
reserve()
CAVBO(const V)
1,2,3,4,5,6,7,8,9,
這個版本的缺點是,複合賦值時產生了臨時的 CAVBO 實例,當中調用了一個對 V 類的 move constructor,最後對臨時對象調用 destructor。
第二個版本:使用複合賦值實現二元運算(就是題目中的建議做法)
#include &
#include &
using namespace std;
typedef vector&
// Binary Operator via Compound Assignment
struct BOVCA {
BOVCA() { cout &<&< "BOVCA()" &<&< endl; }
~BOVCA() { cout &<&< "~BOVCA()" &<&< endl; }
BOVCA(const BOVCA src) : v(src.v) {
cout &<&< "BOVCA(const BOVCA)" &<&< endl;
}
explicit BOVCA(const V v) : v(v) {
cout &<&< "BOVCA(const V)" &<&< endl;
}
BOVCA operator+(const BOVCA rhs) const {
BOVCA result(*this);
result += rhs;
return result;
}
BOVCA operator+=(const BOVCA rhs) {
cout &<&< "reserve()" &<&< endl;
v.reserve(v.size() + rhs.v.size());
v.insert(v.end(), rhs.v.begin(), rhs.v.end());
return *this;
}
void print() const {
copy(v.begin(), v.end(), ostream_iterator&
cout &<&< endl;
}
V v;
};
int main() {
BOVCA a(V({1, 2}));
BOVCA b(V({3, 4, 5}));
BOVCA c(V({6, 7, 8, 9}));
cout &<&< endl;
cout &<&< "1. a += b;" &<&< endl;
a += b;
a.print();
cout &<&< endl;
cout &<&< "2. a + c;" &<&< endl;
BOVCA d = a + c;
d.print();
cout &<&< endl;
}
輸出:
BOVCA(const V)
BOVCA(const V)
BOVCA(const V)
1. a += b;
reserve()
1,2,3,4,5,
2. a + c;
BOVCA(const BOVCA)
reserve()
1,2,3,4,5,6,7,8,9,
這個版本解決了第一個版本的問題,複合賦值時不用再做 move constructor 及 destructor 了。但是,二元運算並不理想,它先用 copy constructor 複製左方對象,然後再做串接時 reserve() 可能需要重新分配內存(如果複製時並沒有留下足夠的額外空間)。這樣可能需要分配兩次內存。
我們若回顧第一個版本,會發現它的二元運算並不需要調用 copy constructor,而是在結束時調用 V類型的move constructor。
看到這個區別,我們可以得知,在這個場合下更好的方法是分別實現複合賦值及二元運算。
第三個版本:分別實現複合賦值及二元運算
#include &
#include &
using namespace std;
typedef vector&
// Binary Operator and Compound Assignment are independent
struct INDEP {
INDEP() { cout &<&< "INDEP()" &<&< endl; }
~INDEP() { cout &<&< "~INDEP()" &<&< endl; }
INDEP(const INDEP src) : v(src.v) {
cout &<&< "INDEP(const INDEP)" &<&< endl;
}
explicit INDEP(const V v) : v(v) {
cout &<&< "INDEP(const V)" &<&< endl;
}
INDEP operator+(const INDEP rhs) const {
V result;
cout &<&< "reserve()" &<&< endl;
result.reserve(v.size() + rhs.v.size());
result.insert(result.end(), v.begin(), v.end());
result.insert(result.end(), rhs.v.begin(), rhs.v.end());
return INDEP(std::move(result));
}
INDEP operator+=(const INDEP rhs) {
cout &<&< "reserve()" &<&< endl;
v.reserve(v.size() + rhs.v.size());
v.insert(v.end(), rhs.v.begin(), rhs.v.end());
return *this;
}
void print() const {
copy(v.begin(), v.end(), ostream_iterator&
cout &<&< endl;
}
V v;
};
int main() {
INDEP a(V({1, 2}));
INDEP b(V({3, 4, 5}));
INDEP c(V({6, 7, 8, 9}));
cout &<&< endl;
cout &<&< "1. a += b;" &<&< endl;
a += b;
a.print();
cout &<&< endl;
cout &<&< "2. a + c;" &<&< endl;
INDEP d = a + c;
d.print();
cout &<&< endl;
}
輸出:
1. a += b;
reserve()
1,2,3,4,5,
2. a + c;
reserve()
INDEP(const V)
1,2,3,4,5,6,7,8,9,
在這個版本中,複合賦值和二元運算都取得前兩個版本之長處。
這個問題對於一些含簡單 copy/move constructor 的類型來說,分別不是很大,編譯器在優化之後可能並無差異。但是對於一些較複雜、copy/move construction 有運行成本的類,僅實現一個複合賦值和二元運算,可能會造成不必要的性能開銷。這個時候,應該分別實現兩種運算。複合運算肯定是成員函數的,用複合運算來實現算求運算那麼算數運算就可以不申明為類的友元了。
more effective c++ 22條款有詳細解釋
+= 運算要比+運算更初級, 別看他叫複合運算符, 拿整型數據來說, +翻譯成彙編的代碼長度肯定長於+=
一個+ 比+=多運算多少次?一個後++比前++又多運算多少次?知道這個,你就明白了。
推薦閱讀:
※關於在C++ Primer一書中 「const是頂層的」的問題?
※新手自學C++ Primer(第五版)應該用什麼開發環境?
※c++ primer 中的這個習題是不是有錯誤?
※C++ 里刪delete指針兩次會怎麼樣?
※為什麼c++ primer 5 中拷貝賦值運算符的實現不直接用條件判斷自賦值 ?