2017年3月27日 星期一

C++11 lambda 實際用處與實現

C++11 lambda 實際用處與實現

tags: C++ Concept
C++開始才有的新語法,lambda匿名函式,看起來可能與原本的函式差不多,實際上有個很大的用處,用一句話形容
精簡呼叫方法
而且這種精簡是傳統副程式做不到的精簡

示例

如果你有一個函式
int fun(int a, int b){
    return a+b;
}
你希望它們做陣列的相加你必須這麼輸入
cout << fun(arr[1], arr[2]) << endl;
可是每次都要打arr實在有點擾人,我就只想對arr不對別人了
auto l = [&](int a, int b){
    return fun(arr[a], arr[b]);
};
使用這樣的語意實現副函式無法實現的精簡呼叫方法
cout << l(1, 2) << endl;
你可以省略每次都打arr的麻煩
如果用副函式實現還要輸入arr的指標
int arr_add(int* p, int a, int b){
    return p[a]+p[b];
}
// 使用副程式實現(無法完全精簡還是有個arr)
cout << arr_add(arr, 1, 2) << endl;

優勢

除了可以完全精簡之外,比如說如文中還要導入arr的指標進去,出現在類別的封裝個是惱人,為了這個必須寫一個與資料成員毫無相關的函式。
也可以減少副程式名稱對環境的汙染,也省去了花時間去想函式名字的煩惱,有時候要想個好名稱真的很費時間。

代碼

/*****************************************************************
Name : 
Date : 2017/03/27
By   : CharlotteHonG
Final: 2017/03/27
*****************************************************************/
#include <iostream>
using namespace std;

int fun(int a, int b){
    return a+b;
}

int arr_add(int* p, int a, int b){
    return p[a]+p[b];
}
/*==============================================================*/
int main(int argc, char const *argv[]){
    int arr[]{1, 2, 3, 4, 5};
    // 每次都要打 arr有點擾人
    cout << fun(arr[1], arr[2]) << endl;

    // lambda
    auto l = [&](int a, int b){
        return fun(arr[a], arr[b]);
    };
    // 現在你不用打那麼長了,也不用為了不汙染環境而想函式名字想破腦
    cout << l(1, 2) << endl;

    // 使用副程式實現(無法完全精簡)
    cout << arr_add(arr, 1, 2) << endl;
    return 0;
}
/*==============================================================*/

參考

2017年3月26日 星期日

重載 operator 各項技巧與範例 - 目錄

operator=() 進階重載2 同時實現深度拷貝與淺度拷貝功能

operator=() 進階重載2 同時實現深度拷貝與淺度拷貝功能

tags: operator

補充知識:

如果一個物件成員具有指針,那麼複製時候將存在兩種方式,深度拷貝與淺度拷貝,將兩種拷貝另用其他函式實現在介面使用時並不怎麼好
解決方法,可以利用右值引用來區分等號呼叫的函式,利用此一特性可以區別等號的兩種拷貝方式,而不用額外在重寫
將逐一介紹如實現該功能,以及可能遇到的坑,並提供完整的參考代碼

std::拷貝函式

std提供一個拷貝函式, 會拷貝指針所指之處的值,也就是深度拷貝。
vector<int> v{0, 1, 2}, v2(3);
std::copy(v.begin(), v.begin()+v.size(), v2.begin());
分別需要輸入的是
std::copy(起始指針, 結束指針, 對象起始指針);

重載深度拷貝複製建構函式

深度拷貝的複製建構函式透過 std::copy() 實現
// 複製建構子
List(List const & rhs): len(rhs.len), list(new T[rhs.len]){
    std::copy(rhs.list, rhs.list + len, this->list);
}
需要做的是初始資源空間與長度

重載深度拷貝複製函式

複製函式與複製建構函式的差異於,資源已經事先建立好了,需要先清除本地資源再重建資源,然後再進行賦值。然後也要注意是否會發生將自己複製給自己的情況,不應該浪費效能重新複製。
List & operator=(List const & rhs){
    cout << "copy" << endl;
    // 相同則離開
    if(this != &rhs){
        // 清除本地資源
        this->~List();
        // 重建資源
        this->list = new T[len];
        this->len = rhs.len;
        // 深度拷貝
        std::copy(rhs.list, rhs.list + len, this->list); 
    }
    return (*this);
}

重載移動函式

與複製函式一樣,本地資源已經不需要可以先釋放他,然後再將來源指針複製到本地指針。
需要注意的點是,這樣複製完之後會產生同時兩個指針指向同一個地址,這在釋放時會被釋放兩次,必須將來源指針歸零。
// 移動函式
List & operator=(List && rhs){
    cout << "Move" << endl;
    // 相同則離開
    if(this != &rhs){
        // 清除本地資源
        this->~List();
        // 淺度拷貝
        this->len = rhs.len;
        this->list = rhs.list;
        // 清空來源地址
        rhs.list = nullptr;
        rhs.len = 0;
    }
    return (*this);
}

重載移動建構函式

比較特別的是,由於不需要要求空間,可以直接套用移動函式來完成實作,藉此省去多餘的代碼。
要注意的是這裡也需要使用 std::move() ,否則右值引入的同時,他就具備名字了,會變成左值。
// 移動建構子
List(List && rhs): len(0), list(nullptr){
    (*this) = std::move(rhs);
}

範例代碼

/*****************************************************************
Name : operator 移動語意的實現
Date : 2017/03/25
By   : CharlotteHonG
Final: 2017/04/07
*****************************************************************/
#include <iostream>
#include <iomanip>
#include <numeric>
#include <vector>
#include <initializer_list>
#include <algorithm>
using namespace std;

template <typename T>
class List{
public:
    // 建構子
    List(initializer_list<T> n): len(n.size()), list(new T[len]){
        std::copy(n.begin(), n.end(), this->list);
    }
    // 複製建構子
    List(List const & rhs): len(rhs.len), list(new T[rhs.len]){
        std::copy_n(rhs.list, len, this->list);
    }
    // 移動建構子
    List(List && rhs): len(0), list(nullptr){
        (*this) = std::move(rhs);
    }
    // 解構子
    ~List(){
        if(this->list != nullptr) {
            delete [] this->list;
        }
    }
public:
    List & pri(string name=""){
        if(name != "")
            cout << name << endl;
        for(unsigned i = 0; i < this->len; ++i) {
            cout << setw(3) << (*this).list[i];
        } cout << endl;
        return (*this);
    }
public:
    // 複製函式
    List & operator=(List const & rhs){
        cout << "copy" << endl;
        // 相同則離開
        if(this != &rhs){
            // 清除本地資源
            this->~List();
            // 重建資源
            this->list = new T[len];
            this->len = rhs.len;
            // 深度拷貝
            std::copy_n(rhs.list, len, this->list); 
        }
        return (*this);
    }
    // 移動函式
    List & operator=(List && rhs){
        cout << "Move" << endl;
        // 相同則離開
        if(this != &rhs){
            // 清除本地資源
            this->~List();
            // 淺度拷貝
            this->len = rhs.len;
            this->list = rhs.list;
            // 清空來源地址
            rhs.list = nullptr;
            rhs.len = 0;
        }
        return (*this);
    }
public:
    size_t len;
    T* list;
};
/*==============================================================*/
int main(int argc, char const *argv[]){
    List<int> a{3, 2, 1};
    List<int> b{1, 2, 3};
    a.pri();
    // 移動函式
    a = move(b);
    a.pri();
    // 移動建構函式
    List<int> c = std::move(a);
    c.pri();
    return 0;
}
/*==============================================================*/

2017年3月25日 星期六

移動語意 std::move() 的真正用途與原則

移動語意 std::move() 的用途與原則

tags: operator

用一句話概括
用同一個等號,把對方的資源搶過來。
struct ImgData{
    int size;
    int* data;
    ...
};
ImgData img1, img2;

img1 = std::move(img2);  // 搶對方指標納為已用,並設置對方指向null (copy 8byte)
img1 = img2;             // 陣列多長就複製多長 (copy len * type byte)

std::move()

協助你呼叫淺度拷貝函式
用來區分什麼時候要移動,什麼時候要複製

部分知識補充


兩種拷貝方法

我們要先介紹兩個名詞深度拷貝與淺度拷貝,這兩種拷貝會發生在一個成員具有指標的物件,這時候拷貝可以有兩種情況
  • 深度拷貝:要求新空間且實際複製指針所指之處的內容
  • 淺度拷貝:只複製指針容器內的指針
淺度拷貝後A物件被更動,B物件也會跟著被改變,因為他們指向同一個位址

移動語意

如名字所敘移動語意是指移動行為,而既照語意來說就是,移動的過程中不會產生物件複製行為,進一步來說就是
在不發生深度拷貝的前提下把資源從 a 處弄到 b 處
淺度拷貝與此語意相符僅複製指標過去而不複製物件本身。

比較容易讓人產生誤解的是 std::move() 本身並不具備移動的函式,他所做的事情是
協助你呼叫淺度拷貝函式
把這一句話背下來,將協助你解決很多很多的困擾

如果 operator=() 同時重載了深度拷貝與淺度拷貝,我把它稱為具移動函式的物件;深度拷貝則稱為複製函式、淺度拷貝則稱為移動函式。

使用條件

既然是協助呼叫淺度拷貝函式,也就代表說
也要該物件具有淺度拷貝函式才能夠使用
如果一個物件不具有移動函式,那麼有沒有使用std::move()結果將沒有區別
因為 fun(const &T); 可以接收左右值,結果而言你所轉換的右值會呼叫該函式
Arr & operator=(Arr const & rhs){}

我們可以加載這個移動函式來區分左右值的引用
Arr & operator=(Arr && rhs){}
原先的左值會呼叫原先的函式,而被你標記為移動語意的物件將會呼叫移動函式
Arr a, b;

// 複製函式
a = b;

// 移動函式
a = std::move(b);

產生的問題

未指定行為

既然所謂的移動語意是淺層拷貝,那執行完上面的函式不就出問題了嗎?我更改a將會連動到b。
自己就要記清楚,這個 b 已經不要了,不可以再使用了
這麼想其實也很合理,不正是因為不要了才要移動嗎?如果還要該是呼叫複製函式對吧!然後經過 std::move() 之後也會做語法標記,被標記的物件雖然可以存取,但是會發生未指定行為。

轉發右值

右值有一個大原則他沒有名字
而右值引用的引用變數是帶有名字的
這在轉發的時候會出問題的,你所接收的右值在接收的那一個瞬間已經被轉成左值了
void fun(Arr const && i){
    Arr temp;
    temp = i;
}

void main(){
    Arr a;
    fun(std::move(a));
}
那個 a 雖然你正確的轉成右值輸入,但是當他變成 i 的時候他就有一個名字叫做 i 了,你將會呼叫複製函式而不是移動函式。
接收右值必須在std::move()一次,才能正確地使用移動函式
void fun(Arr const && i){
    Arr temp;
    temp = std::move(i);
}

void main(){
    Arr a;
    fun(std::move(a));
}

重複釋放同一個地址

當我們使用移動函式的時候,會把a物件的指針複製到b物件的指針,這樣會導致一個狀況
兩個物件同時指向同一個地址
同時指向還不是太大的問題,問題在於當他們生命周期結束時會個別呼叫個別的解構子,這樣會造成同一個地址被解構兩次,進而造成程式崩潰。
移動函式必須將,原移動對象歸零,指向nullptr

為什麼選用 非const版本 的引入參數?

左值參考

同樣的你可能也會好奇,為什麼複製函式是選用 const 版本的
因為結果很明確,不會動到來源
除此外還有一個特性,const可以接收兩者,但是 non-const,只能接收 non-const ,這種情況下你必須寫兩種版本的函式
Arr & operator=(Arr const & rhs){}
Arr & operator=(Arr & rhs){}
然而他們居然是做同一件事情,當然能整合寫一個就整合寫一個

右值參考

接下來你可能會覺的,那移動語意應該也該選擇 const 版本整合兩者不是嗎?
不~還有另一條,根據上面的規則,移動過後必須歸零來源!
如果你選了 const 那將妨礙到移動語意的操作,無法歸零來源

其次,這會導致複製函式的語意被 初始化規則否決,當存在以下兩條函式時
Arr & operator=(Arr const & rhs){} //原複製函式
Arr & operator=(Arr const && rhs){}
在接收右值的時候會優先執行下方的函式,複製函式將沒有機會實現,這會造成我們失去主動權,可以在主程式選擇 右值參考 要使用複製函式或是移動函式。
std::move() 的存在就是協助給予我們主動權
讓我們可以由決定該用哪一種方式執行程式。

不要以為返回 move 可以省下複製成本

函式的返回時會創建一個站存值,這個會造成一個複製成本
int fun(){
    int i;
    return i;
}
在函式內 int i 是新創值,當他返回到主程式的時候,又會複製一份新的站存值,如果不複製將會因為副程式內的 i 生命週期結束,主程式無法接收

於是你可能會想這麼做
int & fun(){
    int i;
    return i;
}
看起來是一個很好的解決方案,但是你要知道副程式執行完畢 i 就被解構了,返回的參考,將會參考到一個非法空間。

或許移動語意取代複製語意是個好方法?
int fun(){
    int i;
    return std::move(i);
}
實際上並不會,因為最初的做法編譯器會自動做優化(return value optimization),他會直接就地構造,差不多就相當於在主程式宣告,所以一開始說的多餘複製其實是不會發生的,除非主程式的物件已經存在。
當你使用 move() 語意將會破壞這個超級優化
/*****************************************************************
Name : 
Date : 2017/04/07
By   : CharlotteHonG
Final: 2017/04/07
*****************************************************************/
#include <iostream>
using namespace std;

struct A {
    A() = default;
    A(A && rhs){cout << "mctor" << endl;}
    ~A(){cout << "dctor" << endl;}
};

A fun(){
    A i;
    // return std::move(i);
    return i;
}

/*==============================================================*/
int main(int argc, char const *argv[]){
    A a = fun();
    return 0;
}
/*==============================================================*/
可以看出解構只執行一次,從頭到尾就只有創建一個。使用move語意後雖然會確實的執行move語意,但是會導致創建兩個實體,並沒有比較好。

參考