1. 程式人生 > >《effective c++》學習筆記(七)

《effective c++》學習筆記(七)

瞭解隱式介面和編譯期多型

檢視下面這段程式碼:

template<typename T>
bool foo(const T& lhs, const T& rhs) {
    if (lhs.bar() && rhs.bar()) {
        return true;
    }
    return false;
}

當呼叫foo()時,會根據引數的型別來例項化出函式,也就是我foo()這個語句,可以呼叫不同的函式,這就是編譯期多型。

而隱式介面則是規定呼叫foo的引數必須要有bar這個成員函式,否則就會編譯錯誤(例項化出函式後,編譯錯誤)

  • classes和template都支援介面和多型
  • 對classes而言介面是顯式的,以函式簽名為中心。多型則是通過virtual函式發生於執行期
  • 對template引數而言,介面是隱式的,奠基於有效表示式。多型則是通過template具現化和函式過載解析發生於編譯期

瞭解typename雙重含義

對於一個template宣告

template<typename T>
class Foo{
};

template<class T>
class Foo{
};

是一模一樣的,typename和class沒有任何區別

但考慮下面這份程式碼:

template<typename T>
class Foo{
public:
    typedef T value_type;
};

template<typename T>
void bar() {
    Foo<T>::value_type a;
}

int main() {
    bar<int>();
    return 0;
}

原因是template內出現的名稱如果相依於某個template引數,稱之為從屬名稱。如果從屬名稱在class內呈巢狀狀,稱之為巢狀從屬名稱。而巢狀從屬名稱,會被編譯器認為不是一個型別,而是其他東西(比如一個static成員變數),這個時候需要在前面加一個typename:

template<typename T>
class Foo{
public:
    typedef T value_type;
};

template<typename T>
void bar() {
    typename Foo<T>::value_type a;
}

int main() {
    bar<int>();
    return 0;
}

但這裡有一個例外就是,typename不可以出現在base classes list內的巢狀從屬名稱型別名稱之前,也不可以在member initialization list中作為base class修飾符

  • 宣告template引數時,字首關鍵字class和typename可互換
  • 請使用關鍵字typename標識巢狀從屬型別名稱:但不是在base class lists(基類列)或member initialization list內以它作為base class修飾符

學習處理模板化基類的名稱

考慮下面這份程式碼:

template<typename T>
class Base {
public:
    void foo() { }
};

template<typename T>
class Derived : Base<T> {
public:
    void bar() {
        foo();
    }
};

一切看起來都很正常,但編譯器會提示找不到foo這個函式的宣告。。。

原因是因為編譯器不會去找模板基類的名稱,有三種辦法可以解決這個問題:

  1. 新增this指標 this->foo();
  2. 顯示呼叫 Base<T>::foo();
  3. 使用using using Base<T>::foo;

-

  • 可在derived class templates內通過”this->”指涉base classes template內的成員名稱,或藉由一個明白寫出的“base class資格修飾符”完成

將與引數無關的程式碼抽離templates

考慮下面這份程式碼:

template<typename T, size_t n>
class Matrix{
public:
    Matrix() : _data(nullptr) {  }
private:
    shared_ptr<T> _data;
};

當我定義了許多個不同的Matrix時

Matrix<int, 1> m1;
Matrix<int, 2> m2;
Matrix<int, 3> m3;

這份class就會例項化出3份class,最後程式的程式碼段會很長,充斥著重複程式碼,實際上完全可以把size_t n當作一個成員變數來使用

template<typename T>
class Matrix{
public:
    Matrix(size_t size) : _data(nullptr), _size(size)  {  }
private:
    shared_ptr<T> _data;
    size_t        _size;
};
  • Templates生成多個classes和多個函式,所以任何template程式碼都不該與某個造成膨脹的template引數相依關係
  • 因非型別模板引數而造成的程式碼膨脹,往往可消除,做法是以函式引數或class成員變數替換template引數
  • 因型別引數而造成的程式碼膨脹,往往可降低,做法是讓帶有完全相同二進位制表述的具現型別共享實現碼

需要型別轉換時請為模板定義非成員函式

考慮下面這份程式碼

#include <bits/stdc++.h>
using namespace std;

template<typename T>
class Foo{
public:
    Foo() = default;
    Foo(int x) {  }
};

template<typename T>
const Foo<T> operator*(const Foo<T> &lhs, const Foo<T> &rhs) {
    Foo<T> ret;
    return ret;
}

int main() {
    Foo<int> f;
    2 * f;
    return 0;
}

我們的預期是在執行2 * f時,2可以隱式轉為Foo,然後和f相乘。但實際上這份程式碼不能通過編譯,原因是因為編譯器根據2推斷出T是int型別,而int不能接受一個Foo。如果寫成f * 2,結果也是一樣的,因為引數一個是Foo,一個是int,無法推斷出來T是什麼型別。

解決辦法是,要讓執行operator*時,函式已經被例項化了。做法是我們把operator*放在class內並宣告一個friend就可以了

#include <bits/stdc++.h>
using namespace std;

template<typename T>
class Foo{
    friend const Foo operator*(const Foo &lhs, const Foo &rhs) {
        Foo ret;
        return ret;
    }
public:
    Foo() = default;
    Foo(int x) {  }
};

int main() {
    Foo<int> f;
    2 * f;
    return 0;
}

這樣的話當class被例項化時,這個函式也就被例項化了,當呼叫operator*時就會自動調這個friend函式。

最後還有一個問題就是class內的函式隱式inline,如果想要避免程式碼膨脹,可以使用這個class內部的friend函式呼叫一個non-member operator*輔助函式即可。

  • 當我們編寫一個class template,而它所提供之“與此template相關的”函式支援“所有引數之隱式型別轉換”時,請將那些函式定義為“class template內部的friend函式”

請使用traits classes表現型別資訊

對於STL資料結構和演算法,你可以使用五種迭代器。下面簡要說明了這五種型別:

  • Input iterators 提供對資料的只讀訪問。
  • Output iterators 提供對資料的只寫訪問
  • Forward iterators 提供讀寫操作,並能向前推進迭代器。
  • Bidirectional iterators提供讀寫操作,並能向前和向後操作。
  • Random access iterators提供讀寫操作,並能在資料中隨機移動。

以std::advance為例,對於Random迭代器,我們可以直接進行+=操作,對於其他型別,只能一步一步的++操作,而在編譯時獲得模板型別可以使用traits

template<typename IterT, typename DistT>
void doAdvance( IterT& iter, DistT d, std::random_access_iterator_tag ){
  iter += d;
}
template<typename IterT, typename DistT>
void doAdvance( IterT& iter, DistT d, std::bidirectional_iterator_tag){
  if( d>=0 ) { while (d--) ++iter; }
  else { while( d++ ) --iter; }
}
template<typename IterT, typename DistT>
void doAdvance( IterT& iter, DistT d, std::input_iterator_tag){
  if( d<0 )
    throw std::out_of_range("Nagative Distance");
  while (d--) ++iter;
}

template<typename IterT, typename DistT>
void advance( IterT& iter, DistT d ){
  doAdvance( iter, d,
      typename std::iterator_traits<IterT>::iterator_category()
      );
}
  • Traits classes使得“在編譯期可用。它們以templates和”templates特化“來完成
  • 整合過載技術後,traits classes有可能在編譯期對型別執行if…else測試

認識template超程式設計

所謂模板超程式設計是以C++寫成、執行與C++編譯器內的程式。一旦TMP程式結束執行,其輸出,也就是從templates具現出來的若干C++原始碼,便會一如既往地被編譯。

由於執行於C++編譯期,因此可將工作從執行期轉移到編譯期。這導致的一個結果是,某些錯誤原本通常在執行期才能偵測到,現在可在編譯期找出來。另一個結果是,使用TMP的C++程式可能在每一方面都更高效:較小的可執行檔案、較短的執行期、較少的記憶體需求。

下面是一個計算N!的TMP程式:

template<size_t n>
struct Fact {
    enum {
        value = Fact<n - 1>::value * n
    };
};

template<>
struct Fact<0> {
    enum {
        value = 1
    };
};

int main(int argc, char const* argv[]) {
    cout << Fact<10>::value << endl;        //3628800
    return 0;
}

當程式編譯成功的那一刻,10!已經被計算出來了,多麼神奇的一件事情!

  • Template metaprogramming(TMP,模板超程式設計)可將工作由執行期移往編譯期,因而得以實現早期錯誤偵測和更高的執行效率
  • TMP可將用來生成“基於政策選擇組合”的客戶定製程式碼,也可用來避免生成對某些特殊型別並不適合的程式碼