顯示具有 operator 標籤的文章。 顯示所有文章
顯示具有 operator 標籤的文章。 顯示所有文章

2017年5月5日 星期五

operator 運算子的各式標準寫法與原因

operator 運算子的各式標準寫法與原因

tags: C++ Concept2
operator存在著許多應該遵守的公約,這裡會列出常見的operator如何撰寫標準公約,並說明為何要如此撰寫與舉例


宣告

/* 那些只有一行的函式可以直接上inline */

// +運算子
Arr & operator+=(Arr const &rhs);
friend Arr const operator+(Arr const &lhs, Arr const &rhs);
    return Arr(lhs) += rhs; // op+的定義

// 下標運算子
int & operator[](size_t idx){
    return const_cast<int&>(static_cast<const Arr&>(*this)[idx]);
}
const int & operator[](size_t idx) const;

// <<運算符號
friend ostream & operator<< (ostream& s, const Rati & r);

// 複製建構子
List(List const & rhs);
// 複製函式
List & operator=(List const & rhs);

// 移動建構子
List(List && rhs){
    (*this) = std::move(rhs);
}
// 移動函式
List & operator=(List && rhs);

// 取址
T* operator&();
// 取值(轉型)
operator T&();

// ++T
Arr & operator++();
// T++
Arr operator++(int);


公約

盡可能記住他,在大多數的可以幫你避開很多坑

參數

看一下以下的例子,這樣的寫法可以同時接收const與non-const
void fun(int const & num){
    cout << "num=" << num << endl;
}

int i=0;
fun(i);
  1. 避免寫兩個 fun() 函式
  2. 不小心修改到時編譯器會提醒你
參數能夠加上 const 就加上 const

返回的物件

假設一個沒有被加上const物件被返回可能會發生這種事情
Arr operator+(Arr const &lhs, Arr const &rhs);

Arr a, b;
a+a=b;
或許你不會這寫,但不能保證沒有人會這樣寫,嘗試對一個即將被解構的物件修改,其結果只是浪費效能。
返回的物件能夠加上 const 就加上 const

返回 (*this) 的參考

我們應該效仿基本的型別操作,int這些基礎型別是可以連續被指定的
int a, b, c, d;
a=b=c=d=1;
我們只要傳出 (*this) 的參考就可以做到這樣的操作,可以把他們拆解成原本的樣態,可以更容易理解
Arr a, b, c;
a=b=c;

a.operator=(b.operator=(c));

適當的使用全域函式

當我們使用 op+() 的時候要注意一種情況
Arr a;
a+1;
這把它還原看起來沒問題
a.operator+(1);
可是反過來就出問題
1.operator+(a);
這可以使用全域函式處理
Arr const operator+(int lhs, Arr const &rhs);
Arr const operator+(Arr const &lhs, int rhs);

適當的引用重複的函式

可以觀察到某些函式其實是具有重複性的
  • op+() 可以拆解為兩個已有的函式,複製後+=
  • 下標負號的 const 與 non-const
  • 移動建構子的內容與移動函式相等
  • 複製建構子與複製函式部分相同(額外拆一個函式呼叫)

拆解

一個相加的函式可以拆解為
int a=1, b=2;

int temp=a;
temp+=b;
下標符號比較特別需要特別為了 const 與 non-const 寫兩個完全一樣的函式
int & operator[](size_t idx);
const int & operator[](size_t idx) const;

Arr a;
const Arr b;

a[0]=1;
b[0];
解決方法長這個樣子
int & operator[](size_t idx){
    return const_cast<int&>(static_cast<const Arr&>(*this)[idx]);
}
簡單來說從右邊看回來
  1. 先加上const屬性
  2. 然後讓它去呼叫const的op[]()函式
  3. 再強制解除他的 const 屬性
順序不要反過來了把主代碼寫在 non-const (是可以正常運行的)
讓 const 去呼叫 non-const 的函式
這樣會導致函式運行的時候 const 屬性被解除可以被修改。
難保後面的人沒察覺到…(通常這個人是幾個月後的自己)

補上 friend 開放 private 的存取權限

某些函式寫在全域函式會比寫在成員函式還要來的好,可是一旦寫到全域函式去之後,相對的就會失去對private成員的存取權限
在類別的定義裡面補上該全域函式的 friend宣告,讓全域函式可以有應有的權限操作成員函式

待續

2017年4月12日 星期三

Operator 隱式轉換的重載,物件的轉出與轉入

Operator 隱式轉換的重載,物件的轉出與轉入

tags: operator2
你可以把一個新的物件當作一個型態看待,就像int是一個整數型態裡面什麼都沒有就只有一個成員int,這樣的概念,我們可以自訂一個型態。
這個型態你可以決定用什麼方式初始化他
class Opcast {
public: //建構子
    Opcast(int i): i(i) {}
private:
    int num;
};
比如說妳決定創建一個屬於自己的整數型態,可以這樣建立,且你可以從主程式初始化。
int main(int argc, char const *argv[]){
    Opcast a(int(1));
    return 0;
}
你也可以選擇輸入他類型的變數,比如說 double,在輸入的時候轉換成int讓引數符合初始化參數。static_cast<int>(num) 你可以把它看成 (int)num ,但前者是更好的寫法。
int main(int argc, char const *argv[]){
    double num = 1.0;
    Opcast a(static_cast<int>(num));
    return 0;
}
這裡我提一個自己的類比概念轉入,簡而言之就知把另一個型態轉入這個型態。還有一個概念是轉出,比如說這裡的 (int)num 以int的型態轉出給main使用。
你可能已經注意的到括號的使用右邊轉入,左邊轉出。
int i=0;
cout << Opcast(i) << endl;

Opcast a;
cout << (int)a << endl;
前者是把 i 導入,後者則是把 a 以 int 型態導出,這個以int形式導出的 行別轉換函式 是一個operator,這可能比較難想像。
函式的樣貌長這個樣子
Opcast::operator int() const{
    return i;
}
看你想重載導出什麼樣貌就寫什麼型態,後面的const是為了讓它可以同時接收兩種型態的呼叫
Opcast a(0);
const Opcast b(0);
這個樣子看起來一切很很美好,但實際上轉出與轉入它潛藏著不容易被發現的危機

轉入的隱式轉換

如果你有一個副程式接收了妳的自訂類別,而且那個自訂類別參數只有一個
Opcast::Opcast(int i): num(i) {}

void fun(Opcast a){
    cout << "fun" << endl;
}
我們試著呼叫他
fun(1);
理應是找不到定義為整數的方程式
void fun(int);
而實際上,編譯器會想辦一切辦法奏和出恰當的轉換,那個呼叫會變成
fun( Opcast(1) );
產生的結果可能不是你想要的,你可以透過關鍵字 explicit 避免這種編譯器自作主張的轉換
explicit Opcast::Opcast(int i): num(i) {}

傳統的解決方案(C11之前沒有這個關鍵字),可以使用替身類別來處理
class OpcastSize{
public:
    OpcastSize(int i): len(i){}
    operator int&(){return len;}
    int len;
};

Opcast::Opcast(OpcastSize i): num(i) {}


轉出的隱式轉換

如果你加載了型別轉換的函式要注意它有可能在你不需要的時候自作主張的呼叫
    Opcast a(1);
    cout << a << endl;
看起來因該會因為你沒有加載 << 運算子而導致呼叫失敗,實際上因為你加載了型別轉換函式,編譯器將會發現經過型別轉換後就可以正常印出了,而自作主張的幫你轉換。
這裡一樣可以使用關鍵字 explicit 來避免這種狀況
explicit operator double() const{...}
傳統的方案是乾脆不寫了,另外單獨使用一個普通函式來轉出,可以觀察 string 裡面的函式 c_str() 來進行型別的轉換。
這樣的寫法比較明確不會有其他可能的誤解,誤會成我是否可以將他轉成其他的任意型態,比如說 int、double。
其次也可以減少代碼的長度,C++的正確轉型方式為 static_cast<T>(t) 非常的長呢。


範例代碼

 /*****************************************************************
Name : Operator 隱式轉換的重載
Date : 2017/04/11
By   : CharlotteHonG
Final: 2017/04/11
*****************************************************************/
#include <iostream>
using namespace std;

class Opcast {
public:
    class OpcastSize{
    public:
        OpcastSize(int i): len(i){}
        operator int&(){return len;}
        int len;
    };
public: //建構子
    // ##轉入會被隱式轉換
    Opcast(int i): num(i) {}
    // 解決方法 1
    // explicit Opcast(int i): num(i) {}
    // 解決方法 2
    // Opcast(OpcastSize i): num(i) {}
public: // 轉型函式
    operator double() const{
        cout << "Cast to double. --> ";
        return num+1;
    }
    double to_dou(){
        cout << "Cast to double. --> ";
        return num+1;
    }
private:
    int num;
};

void fun(Opcast a){
    cout << "fun" << endl;
}
/*==============================================================*/
int main(int argc, char const *argv[]){
    Opcast a(1);
    // 轉入的隱式轉換
    cout << a << endl;
    // 轉出的隱式轉換
    fun(10);
    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;
}
/*==============================================================*/