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語意,但是會導致創建兩個實體,並沒有比較好。

參考

2017年3月24日 星期五

Android Studio 一鍵部署安裝到多台電腦的解決方案

Android Studio 一鍵部署解決方案


我拆分四個安裝檔案分別是
  1. Java JDK
  2. 軟體設定檔
  3. 主程式
  4. SDK
將他們用RAR封裝,並自動執行批次檔設定必要的環境變數與操作。

JAVA

比較有問題的是沒有經過官方的安裝程式安裝,除了環境變數要新增之外,還要新增 JAVA_HOME 的變數,這個變數只到JDK的資料夾
@echo off
Title JAVA_PATH By Charlotte.HonG& Color 1A

set str=%~dp0jdk1.8.0_101
setx /m JAVA_HOME "%str%"

set str=%PATH%;%~dp0jdk1.8.0_101\bin;
setx /m PATH "%str%"

exit
這個批次檔放在JDK的目錄內,利用rar封裝成exe解壓縮之後自動執行bat即可自動設定好路徑。

軟體設定檔

儲存在使用者文件內的 .android.AndroidStudio2.3 複製他們你可獲得完全相同的設定,而不用安裝之後還要進入程式內設定。
比較需要注意的是有一些檔案不能保留
.android 內的 avd 要清空,他沒有辦法被移植,必須重新建立
.AndroidStudio2.3 內的 system\caches 要清空讓目標電腦自己重新建立

主程式

一般裝在 C:\Program Files\Android 直接複製就可以用了

SDK

也是直接複製就可以用了,只是最好路徑要放到目標電腦一樣的位置,否則需要重新設定路徑。
這個容量很大沒辦法封裝成exe,我是壓縮成rar並利用命令解壓縮。
@Echo 解壓縮Android SDK檔
::設定環境變數
path=%path%;C:\Program Files\WinRAR;
::解壓縮檔案到指定位置
d:
::rar x "%~dp0AndroidSDK.rar" -r -o+

最後

將他們完整的複製到另一台電腦就可以直接移植了,我是將他們分別用rar封裝成exe自動解壓到目標位置,並設定成隱形模式利用一個批次檔依序執行4個壓縮檔。
Rem By CharlotteHonG
@Echo Off
Title Android Studio All Install & Color 1A
Rem 確認是否為管理員權限
call :IsAdmin
:: =================================================================
@Echo:**確認後請繼續**
Pause
@Echo 開始安裝...
start "" /wait "%~dp001_jdk1.8.0_101_AutoInstall.exe"
start "" /wait "%~dp002_Andrio Studio Setting.exe"
start "" /wait "%~dp003_Android Studio.exe"
@Echo 完成軟體安裝

@Echo 解壓縮Android SDK檔
::設定環境變數
path=%path%;C:\Program Files\WinRAR;
::解壓縮檔案到指定位置
d:
::rar x "%~dp0AndroidSDK.rar" -r -o+

@Echo 完成所有程序
set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_101
start "" "C:\Program Files\Android Studio\bin\studio64.exe"
Exit
:: =================================================================
:IsAdmin
Reg.exe query "HKU\S-1-5-19\Environment"
If Not %ERRORLEVEL% EQU 0 (
 Cls & 權限不足,請使用管理員全線重新開啟。
 Pause & Exit
)
Cls
goto:eof
:: =================================================================

例外設定

值得注意的是這樣子移植之後會有一個設定會跑掉,需要手動設定回來。

2017年3月23日 星期四

operator() 進階用法 繼承

operator() 進階用法 繼承

如果你有兩個以上的陣列類別,你就必須為他們個別重載所有的 operator 運算子,又他們很有可能都是完全一樣的,你只是想對那個陣列做運算不會動到其他成員,他們的差別只在於型態不一樣,彼此互相有不同的資料成員。
最好的解法就是把他們的陣列拉出來用template實作,並為他們重載需要的 operator 然後再用正確的型態繼承到個別的類別。
比如說父類別為
Arr<T>
子類別為
Arr_int
Arr_double
Arr_char
我們總不可能為了這三個個別寫了各自完全一樣,只有型態不行的運算子重載,一個 + 就需要用 +=+ ,如果還做了常數的相加
Arr_int a;
a=a+1;
a=1+a;
又需要多寫2個重載函式,相當於一個運算符號需要4個函式,總共有4個符號所以是16個,我們做了3個類別需要寫了16*3個。
繼承後會遇到一些問題,這裡附上可能發生的問題與他的解法

資料結構

這是我們的主類別
template <typename T>
class Arr {
    Arr(initializer_list<T> i): arr(i){}
    Arr & operator+=(Arr const& rhs);
protected:
    vector<T> arr;
};
Arr<T1> operator+(Arr<T1> const &lhs, Arr<T1> const &rhs){...}
這是繼承後的子類別
class Arr_int: public Arr<int>{
public:
    Arr_int(initializer_list<int> i): Arr<int>(i), i(-1){}
public:
    int i;
};

上下轉型的問題

加號產生的暫存父類別

這個問題發生在某些情況,或某些情況必然會遇到分別是
Arr_int a{0}, b{0};
a = b+b;
這裡因為我們的 + 函式的定義返還值為 Arr<int>Arr_int 不符合會導致
Arr_int = Arr<int>

讓他等於父類別

Arr<int> f{0};
Arr_int a{0};
a = f;

原因與解法

父類別型態出現在等號右邊,可是函式內預設只會建立
arr_int & arr_int::operator=(arr_int const & rhs);
他會找不到函式呀,我們需要多載正確的型態等號
Arr_int & operator=(Arr<int> const & rhs);
至於裡面的實作我們可以呼叫父類別來做
Arr_int & operator=(Arr<int> const & rhs){
   this->Arr<int>::operator=(rhs);
   // other ...

   return (*this);
}
這會導致子類別的多出來的成員不會被操作到,如果需要用上則在這裡補上。加號也是一樣如果需要的話則用這種方式減少代碼的重複。

繼承後如何轉名

這種做法會導致所有的子類別名稱都是
vector<T> arr;
我們可以透過參考的方式對他重新命名,取一個別名
public:
    // 更換別名
    decltype(arr) & mask = arr;
如此一來就可以自由地操作了。

實作範例

inhereit.hpp

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

template <typename T>
class Arr {
public:
    Arr(initializer_list<T> i): arr(i){}
    Arr & operator=(Arr<T> const & rhs) = default;
public:
    void pri(string name=""){
        cout << name << " = ";
        for(auto&& i : arr) {
            cout << i << ", ";
        }cout << endl;
    }
public:
    // 重載下標符號
    T & operator[](size_t idx){
        return const_cast<T&>(static_cast<const Arr&>(*this)[idx]);
    }
    const T & operator[](size_t idx) const{
        return arr[idx];
    }
    // 重載+=符號
    template <typename T1> friend
        Arr<T1> operator+(Arr<T1> const &lhs, Arr<T1> const &rhs);
    Arr & operator+=(Arr const& rhs){
        for(unsigned i = 0; i < arr.size(); ++i)
            (*this)[i] += rhs[i];
        return (*this);
    }
    Arr & operator+=(T const &rhs){
        for(unsigned i = 0; i < arr.size(); ++i)
            (*this)[i] += rhs;
        return (*this);
    }
protected:
    vector<T> arr;
};
// 重載+符號(全域函式)
template <typename T1>
Arr<T1> operator+(Arr<T1> const &lhs, Arr<T1> const &rhs){
    return Arr<T1>(lhs) += rhs;
}
//----------------------------------------------------------------
class Arr_int: public Arr<int>{
public:
    Arr_int(initializer_list<int> i): Arr<int>(i), i(-1){}
    Arr_int(Arr<int> const & rhs):Arr<int>(rhs), i(-1){}
    Arr_int & operator=(Arr<int> const & rhs){
        this->Arr<int>::operator=(rhs);
        return (*this);
    }
public:
    int i;
};

using uch = unsigned char;
class Arr_uch: public Arr<uch>{
public:
    // 更換別名
    decltype(arr) & mask = arr;
public:
    Arr_uch(initializer_list<uch> i): Arr<uch>(i){}
    Arr_uch(Arr<uch> const & rhs):Arr<uch>(rhs){}
    Arr_uch & operator=(Arr<uch> const & rhs){
        (*this).Arr<uch>::operator=(rhs);
        return (*this);
    }
};
//----------------------------------------------------------------

inhereit_main.cpp

/*****************************************************************
Name : 無損繼承 operator
Date : 2017/03/17
By   : CharlotteHonG
Final: 2017/03/17
*****************************************************************/
#include <iostream>
#include "inhereit.hpp"
using namespace std;
/*==============================================================*/
int main(int argc, char const *argv[]){
    Arr<int> a{0, 1, 2};
    Arr<int> b{0, 1, 2};
    b=b+a;
    b.pri("b+a");

    // 繼承運算符號
    Arr_int c{1, 1, 1};
    Arr_int c1{1, 1, 1};
    c.i=7;
    c1.i=8;
    c = c1+c1;
    cout << c.i << " || ";
    c.pri("c1+c1");

    // 繼承的 += 操作符號不會操作子類別成員
    c += c1;
    cout << c.i << " || ";
    c.pri("c+=c1");

    // 複製建構子
    Arr_int c2(c1);
    cout << c2.i << " || ";
    c2.pri("c2");
    Arr_int c3(c+c); // c+c 返還的是父類別
    cout << c3.i << " || ";
    c3.pri("c3");

    // 繼承運算符號2
    Arr_uch d{'0', '0', '0'};
    Arr<unsigned char> ch{'0', '0', '0'};
    d = d+d;
    d.pri("d+d");

    return 0;
}
/*==============================================================*/