《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這個函式的宣告。。。
原因是因為編譯器不會去找模板基類的名稱,有三種辦法可以解決這個問題:
- 新增this指標
this->foo();
- 顯示呼叫
Base<T>::foo();
- 使用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++程式可能在每一方面都更高效:較小的可執行檔案、較短的執行期、較少的記憶體需求。
下面是一個計算
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;
}
當程式編譯成功的那一刻,
- Template metaprogramming(TMP,模板超程式設計)可將工作由執行期移往編譯期,因而得以實現早期錯誤偵測和更高的執行效率
- TMP可將用來生成“基於政策選擇組合”的客戶定製程式碼,也可用來避免生成對某些特殊型別並不適合的程式碼