2017年2月23日 星期四

C++ 重載 operator + , += 重複打兩次及效能問題

重載 operator+,+= 符號

tags: C++ Concept
如何合併++=的代碼,避免寫兩份一樣的代碼

類別的定義

class Arr{
public:
    Arr(size_t len=3, int value=1): num(len) {...}
    vector<int> num;
};

1. 常數項無法呼叫函式成員

首先如果把+寫進成員函式,讓他與常數做相加會遇到一些問題。
Arr & Arr::operator+(int value){...}
Arr p1;
p1=p1+1; // ok
p1=1+p1; // error
問題在於常數沒辦法呼叫成員函式 operator+
所以必須把她寫在外面來用全域函式
Arr operator+(int value, Arr const &rhs){...}
Arr operator+(Arr const &lhs, int value){...}
如此一來就可以任意將常數放置在前與後了。

注意

如果你把該函式放置在底下,會找不到該函式的定義,在類別宣告內加入
friend Arr operator+(Arr const &lhs, Arr const &rhs);

2. + 多建一個物件

觀察一下程式代碼 + 通常都是帶等於的
Arr p1, p2;
p1 = p1+p2;
p1+p2; // 不會出錯但貌似沒作用...
可以推論出
  1. 你不能改變 p1 與 p2
  2. 你必須返回他們相加的結果
這會導致一個問題
Arr operator+(Arr const &lhs, Arr const &rhs){...}
裡面的lhs rhs你都不能動他,但又要返回一個新值
你只能在裡面創一個暫存用來接收他們的相加值
Arr operator+(Arr const &lhs, Arr const &rhs){
    Arr temp;
}

又物件的建立與複製非常消耗效能
你會在函式內建構一個新的,然後在賦值
Arr temp;
temp.num[i]=lhs.num[i]+rhs.num[i];

到了外面主程式又複製一次
然後會解構兩次p1一次是函式內的暫存,一次是函式傳出的
p1 = p1+p2;
如果有三個會解構4次p1,因為每次只能2個2個來(每個2次)

補充效能問題

從以上可以觀察到,這樣子的代碼會比較拖效能
(多呼叫一次=的複製函式,還有p1會解構2次)
p1 = p1+p2;
這樣的子的代碼能夠比較節省效能
p1 += p2;
所以如果有多項要相加
p1 += p2;
p1 += p3;
代碼拆多一點,效能會好一點
實測p=p1+p2+p3+p4;會解構6次p1,拆開寫0次

3. 合併 +, += 代碼

為了避免物件建立後才給值的狀況你可以依靠+=來規避
同時也可以合併++=的代碼,使他成為一份主代碼(才不用改2次)
+=本身並不會多複製物件
Arr & Arr::operator+=(Arr const &rhs){
    for(unsigned i = 0; i < num.size(); ++i)
        this->num[i] += rhs.num[i];
    return *this;
}
operator+ 去呼叫 +=
(注意這裡的+是全域函式)
Arr operator+(Arr const &lhs, Arr const &rhs){
    cout << "lhs+rhs : ";
    return Arr(lhs) += rhs;
}
這樣做會函式內會優化成直接建構一個有初值的物件;
而不是先建構,建好了再賦值。

已知條件
  • operator+ 會產生暫存值(複製行為)
  • operator+= 不會產生暫存直(沒有複製行為)
如果你讓 operator+= 去呼叫 operator+ 是不是就變成
兩個都有複製行為了,不應該如此浪費效能才是。

參考代碼

/*****************************************************************
Name : 重載 operator+ 與 += 函式
Date : 2017/02/22
By   : CharlotteHonG
Final: 2017/03/05
*****************************************************************/
#include <iostream>
#include <vector>
using namespace std;

class Arr{
public:
    Arr(size_t len=3, int value=1): num(len) {
        // cout << "Constructor" << endl;
        for(auto&& i : num)
            i=value;
    }
    ~Arr(){
        // cout << "Destructor" << endl;
    }
    void pri(){
        for(auto&& i : num) {
            cout << i << ", ";
        }cout << endl;
    }
public:
    Arr & operator+=(Arr const &rhs){
        for(unsigned i = 0; i < num.size(); ++i)
            this->num[i] += rhs.num[i];
        return *this;
    }
    Arr & operator+=(int value){
        for(unsigned i = 0; i < num.size(); ++i)
            this->num[i] += value;
        return *this;
    }
private:
    vector<int> num;
};

Arr operator+(Arr const &lhs, Arr const &rhs){
    cout << "lhs+rhs : ";
    return Arr(lhs) += rhs;
}
Arr operator+(int value, Arr const &rhs){
    cout << "rhs+    : ";
    return Arr(rhs) += value;
}
Arr operator+(Arr const &lhs, int value){
    cout << "lhs+    : ";
    return Arr(lhs) += value;
}
/*==============================================================*/
int main(int argc, char const *argv[]){
    Arr p;
    p=p+p;
    p.pri();
    p=1+p;
    p.pri();
    p=p+1;
    p.pri();
    return 0;
}
/*==============================================================*/

更新 20190511

關於這一份代碼
Arr operator+(Arr const &lhs, Arr const &rhs){
    cout << "lhs+rhs : ";
    return Arr(lhs) += rhs;
}
參考自 cpp ref 網站的 type 是
https://en.cppreference.com/w/cpp/language/operators
Arr operator+(Arr lhs, Arr const &rhs){
    cout << "lhs+rhs : ";
    return lhs += rhs;
}
兩個是等價的
上面的是把copy寫在Return的時候,那時候才創建一個新類別並複製。
下面的是傳入的時候就發生copy,那時候就創建一個新類別了。
我建議你用下面的版本,因為那是官方給的XDD
能想到的差別是,如果你的op+不是常規的方式,有自訂什麼特別功能,不像文中的功能只是呼叫op+=()就完事了,在定義內有複雜的運算,這時候上面的寫法,lhs這個變數就不能動他,因為傳進來的時候帶const,這會造成很大困擾的歐!
如果是常規狀態,如文內範例,就是等價的沒什麼差~
內文就不修改了,用補充的方式添加,修改還要校正QQ
沒校正怕哪邊失誤多一個字少一個字之類的出錯~~

參考:

  1. [心得] operator+ 與 operator+= 的設計
  2. 使用 friend 函式重載運算子

沒有留言:

張貼留言