?第 5 章 基礎技巧
5.1 typename 關鍵字
????????關鍵字typename在C++標準化過程中被引入進來,用來澄清模板內部的一個標識符代表的
是某種類型,而不是數據成員。考慮下面這個例子:
template<typename T>
class MyClass {
public:void foo() {typename T::SubType* ptr;
}
};
????????其中第二個 typename 被用來澄清 SubType 是定義在 class T 中的一個類型。因此在這里 ptr
是一個指向 T::SubType 類型的指針。
????????如果沒有 typename 的話,SubType 會被假設成一個非類型成員(比如 static 成員或者一個枚舉常量,亦或者是內部嵌套類或者 using 聲明的 public 別名)。這樣的話,表達式
T::SubType* ptr 會被理解成 class T 的 static 成員 SubType 與 ptr 的乘法運算,這不是一個錯誤,因為對 MyClass<>的某些實例化版本而言,這可能是有效的代碼。
????????通常而言,當一個依賴于模板參數的名稱代表的是某種類型的時候,就必須使用 typename。
13.3.2 節會對這一內容做進一步的討論。
????????使用 typename 的一種場景是用來聲明泛型代碼中標準容器的迭代器:
// print elements of an STL container
template<typename T>
void printcoll(T const& coll)
{typename T::const_iterator pos; // iterator to iterate over colltypename T::const_iterator end(coll.end()); // end positionfor (pos = coll.begin(); pos != end; ++pos) {std::cout << *pos << "";}std::cout << "\n";
}int main()
{std::string test = "hello";printcoll(test);return 0;
}
5.2零初始化
????????對于基礎類型,比如int,double以及指針類型,由于它們沒有默認構造函數,因此它們不
會被默認初始化成一個有意義的值。比如任何未被初始化的局部變量的值都是未定義的:
void foo()
{int x; // x has undefined valueint* ptr; // ptr points to anywhere (instead of nowhere)
}
????????因此在定義模板時,如果想讓一個模板類型的變量被初始化成一個默認值,那么只是簡單的
定義是不夠的,因為對內置類型,它們不會被初始化:
template<typename T>
void foo()
{T x; // x has undefined value if T is built-in type
}
? ? ? ? 正確做法
void foo()
{int x{}; // x has undefined valueint* ptr{}; // ptr points to anywhere (instead of nowhere)std::cout << x << " " << ptr;
}
????????出于這個原因,對于內置類型,最好顯式的調用其默認構造函數來將它們初始化成 0(對于
bool 類型,初始化為 false,對于指針類型,初始化成 nullptr)。通過下面你的寫法就可以
保證即使是內置類型也可以得到適當的初始化:
template<typename T>
void foo()
{T x{}; // x is zero (or false) if T is a built-in type
}
????????這種初始化的方法被稱為“值初始化(value initialization)”,它要么調用一個對象已有的
構造函數,要么就用零來初始化這個對象。即使它有顯式的構造函數也是這樣。
????????
????????對于用花括號初始 化的情況,如果沒有可用的默認構造函數,它還可以使用列表初始化構造函數(initializer-list constructor)。
????????
從 C++11 開始也可以通過如下方式對非靜態成員進行默認初始化:
template<typename T>
class MyClass {
private:
T x{}; // zero-initialize x unless otherwise specified …
};
模版參數默認值
template<typename T>
void foo(T p = T{}) { //OK (must use T() before C++11) …
}
5.3 使用 this->
對于類模板,如果它的基類也是依賴于模板參數的,那么對它而言即使 x 是繼承而來的,使
用 this->x 和 x 也不一定是等效的。比如:
template<typename T>
class Base {
public:void bar();
};template<typename T>
class Derived : Base<T> {
public:void foo() {bar(); // calls external bar() or error}
};
????????Derived 中的 bar()永遠不會被解析成 Base 中的 bar()。因此這樣做要么會遇到錯誤,要么就
是調用了其它地方的 bar()(比如可能是定義在其它地方的 global 的 bar())。
????????13.4.2 節對這一問題有更詳細的討論。目前作為經驗法則,建議當使用定義于基類中的、依
賴于模板參數的成員時,用 this->或者 Base<T>::來修飾它。
5.4 使用裸數組或者字符串常量的模板
5.5 成員模板
Stack<int> intStack1, intStack2; // stacks for ints
Stack<float> floatStack; // stack for floats…intStack1 = intStack2; // OK: stacks have same type
floatStack = intStack1; // ERROR: stacks have different types
template<typename T>
class Stack {
private:std::deque<T> elems; // elements
public:void push(T const&); // push elementvoid pop(); // pop elementT const& top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();}// assign stack of elements of type T2template<typename T2>Stack& operator= (Stack<T2> const&);
};
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{Stack<T2> tmp(op2); // create a copy of the assigned stackelems.clear(); // remove existing elementswhile (!tmp.empty()) { // copy all elementselems.push_front(tmp.top());tmp.pop();}return *this;
}
?
成員模板的特例化
?成員函數模板也可以被全部或者部分地特例化。比如對下面這個例子:
// testtemplate.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
//#include <iostream>
#include <deque>class BoolString {
private:std::string value;
public:BoolString(std::string const& s): value(s) {}template<typename T = std::string>T get() const { // get value (converted to T)return value;}template<>inline bool get<bool>() const {return value == "true" || value == "1" || value == "on";}
};int main()
{std::cout << std::boolalpha;BoolString s1("hello");std::cout << s1.get() << "\n"; //prints hellostd::cout << s1.get<bool>() << "\n"; //prints falseBoolString s2("on");std::cout << s2.get<bool>() << "\n"; //prints truereturn 0;
}
特殊成員函數的模板
template 的使用
#include <bitset>template<unsigned long N>
void printBitset(std::bitset<N> const& bs) {std::cout << bs.template to_string<char,std::char_traits<char>,std::allocator<char>>();
}
泛型 lambdas 和成員模板
在 C++14 中引入的泛型 lambdas,是一種成員模板的簡化。對于一個簡單的計算兩個任意類
型參數之和的 lambda:
[] (auto x, auto y) {return x + y;
}
編譯器會默認為它構造下面這樣一個類:
class SomeCompilerSpecificName {
public:SomeCompilerSpecificName(); // constructor only callable by compilertemplate<typename T1, typename T2>auto operator() (T1 x, T2 y) const {return x + y;}
};
5.6 變量模板
用于數據成員的變量模板
template<typename T>
class MyClass {
public:static constexpr int max = 1000;
};
namespace std {
template<typename T>
class numeric_limits {public: …static constexpr bool is_signed = false; …
};
}
類型萃取 Suffix_v
5.7 模板參數模板
#include<deque>template<typename T,template<typename Elem> class Cont = std::deque>
class Stack {
private:Cont<T> elems; // elements
public:void push(T const&); // push elementvoid pop(); // pop elementT const& top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();} …
};



?模板參數模板的參數匹配
template<typename T, template<typename Elem,
typename Alloc = std::allocator<Elem>> class Cont = std::deque>
class Stack {
private:Cont<T> elems; // elements
…
};
#include <iostream>
#include <deque>
#include <cassert>
#include <memory>
#include <vector>template<typename T, template<typename Elem, typename =std::allocator<Elem>> class Cont = std::deque>class Stack {private:Cont<T> elems; // elementspublic:void push(T const&); // push elementvoid pop(); // pop elementT const& top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();}// assign stack of elements of type T2template<typename T2, template<typename Elem2,typename = std::allocator<Elem2> >class Cont2>Stack<T, Cont>& operator= (Stack<T2, Cont2> const&);// to get access to private members of any Stack with elements of type T2 :template<typename, template<typename, typename>class>friend class Stack;
};template<typename T, template<typename, typename> class Cont>
void Stack<T, Cont>::push(T const& elem)
{elems.push_back(elem); // append copy of passed elem
}
template<typename T, template<typename, typename> class Cont>
void Stack<T, Cont>::pop()
{assert(!elems.empty());elems.pop_back(); // remove last element
}
template<typename T, template<typename, typename> class Cont>
T const& Stack<T, Cont>::top() const
{assert(!elems.empty());return elems.back(); // return copy of last element
}
template<typename T, template<typename, typename> class Cont>
template<typename T2, template<typename, typename> class Cont2>
Stack<T, Cont>&
Stack<T, Cont>::operator= (Stack<T2, Cont2> const& op2)
{elems.clear(); // remove existing elementselems.insert(elems.begin(), // insert at the beginningop2.elems.begin(), // all elements from op2op2.elems.end());return *this;
}int main()
{Stack<int> iStack; // stack of intsStack<float> fStack; // stack of floats// manipulate int stackiStack.push(1);iStack.push(2);std::cout << "iStack.top(): " << iStack.top() << "\n";// manipulate float stack:fStack.push(3.3);std::cout << "fStack.top(): " << fStack.top() << "\n";// assign stack of different type and manipulate againfStack = iStack;fStack.push(4.4);std::cout << "fStack.top(): " << fStack.top() << "\n";// stack for doubless using a vector as an internal containerStack<double, std::vector> vStack;vStack.push(5.5);vStack.push(6.6);std::cout << "vStack.top(): " << vStack.top() << "\n";vStack = fStack;std::cout << "vStack: ";while (!vStack.empty()) {std::cout << vStack.top() << " ";vStack.pop();}std::cout << "\n";return 0;
}
? 第 6 章 移動語義和 enable_if<>
6.1 完美轉發(Perfect Forwarding)?
????????假設希望實現的泛型代碼可以將被傳遞參數的基本特性轉發出去:
- ?可變對象被轉發之后依然可變。
- Const 對象被轉發之后依然是 const 的。
- ?可移動對象(可以從中竊取資源的對象)被轉發之后依然是可移動的。
????????不使用模板的話,為達到這一目的就需要對以上三種情況分別編程。比如為了將調用f()時傳遞的參數轉發給函數 g():
// test111.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
//#include <iostream>
#include <windows.h>
using namespace std;#include <utility>
#include <iostream>
class X {};
void g(X&) {std::cout << "g() for variable\n";
}
void g(X const&) {std::cout << "g() for constant\n";
}
void g(X&&) {std::cout << "g() for movable object\n";
}
// let f() forward argument val to g():
void f(X& val) {g(val); // val is non-const lvalue => calls g(X&)
}
void f(X const& val) {g(val); // val is const lvalue => calls g(X const&)
}void f(X&& val) {g(std::move(val)); // val is non-const lvalue => needs ::move()tocall g(X&&)
}
int main() {X v; // create variableX const c; // create constantf(v); // f() for nonconstant object calls f(X&) => calls g(X&)f(c); // f() for constant object calls f(X const&) => calls g(X const&)f(X()); // f() for temporary calls f(X&&) => calls g(X&&)f(std::move(v)); // f() for movable variable calls f(X&&) => callsg(X&&)
}
????????這里定義了三種不同的 f(),它們分別將其參數轉發給 g()
????????注意其中針對可移動對象(一個右值引用)的代碼不同于其它兩組代碼;它需要用std::move() 來處理其參數,因為參數的移動語義不會被一起傳遞。雖然第三個 f()中的val 被聲明成右值引用,但是當其在 f()內部被使用時,它依然是一個非常量左值(參考附錄B),其行為也將和第一個 f()中的情況一樣。因此如果不使用 std::move()的話,在第三個f()中調用的將是g(X&) 而不是 g(X&&)。
????????這個模板只對前兩種情況有效,對第三種用于可移動對象的情況無效。基于這一原因,C++11 引入了特殊的規則對參數進行完美轉發(perfect forwarding)。實現這一目的的慣用方法如下:
template<typename T>
void f(T&& val) {g(std::forward<T>(val));
}
????????注意 std::move 沒有模板參數,并且會無條件地移動其參數;而 std::forward<>會跟據被傳遞參數的具體情況決定是否“轉發”其潛在的移動語義。
???????? 不要以為模板參數 T 的 T&&和具體類型 X 的 X&&是一樣的。雖然語法上看上去類似,但是它們適用于不同的規則:
- ?具體類型 X 的 X&&聲明了一個右值引用參數。只能被綁定到一個可移動對象上(一個prvalue,比如臨時對象,一個 xvalue,比如通過 std::move()傳遞的參數,更多細節參見附錄 B)。它的值總是可變的,而且總是可以被“竊取”。
- 模板參數 T 的 T&&聲明了一個轉發引用(亦稱萬能引用)。可以被綁定到可變、不可變(比如 const)或者可移動對象上。在函數內部這個參數也可以是可變、不可變或者指向一個可以被竊取內部數據的值。
????????注意 T 必須是模板參數的名字。只是依賴于模板參數是不可以的。對于模板參數T,形如typename T::iterator&&的聲明只是聲明了一個右值引用,不是一個轉發引用。
????????因此,一個可以完美轉發其參數的程序會像下面這樣:
#include <utility>
#include <iostream>
class X {};
void g(X&) {std::cout << "g() for variable\n";
}
void g(X const&) {std::cout << "g() for constant\n";
}
void g(X&&) {std::cout << "g() for movable object\n";
}template<typename T>
void f(T&& val) {g(std::forward<T>(val));
}int main() {X v; // create variableX const c; // create constantf(v); // f() for nonconstant object calls f(X&) => calls g(X&)f(c); // f() for constant object calls f(X const&) => calls g(X const&)f(X()); // f() for temporary calls f(X&&) => calls g(X&&)f(std::move(v)); // f() for movable variable calls f(X&&) => callsg(X&&)
}
6.2 特殊成員函數模板
????????特殊成員函數也可以是模板,比如構造函數,但是有時候這可能會帶來令人意外的結果。
????????考慮下面這個例子
#include <utility>
#include <string>
#include <iostream>
class Person {private:std::string name;public:// constructor for passed initial name:explicit Person(std::string const& n) : name(n) {std::cout << "copying string-CONSTR for ’" << name << "’\n";}explicit Person(std::string&& n) : name(std::move(n)) {std::cout << "moving string-CONSTR for ’" << name << "’\n";}// copy and move constructor:Person(Person const& p) : name(p.name) {std::cout << "COPY-CONSTR Person ’" << name << "’\n";}Person(Person&& p) : name(std::move(p.name)) {std::cout << "MOVE-CONSTR Person ’" << name << "’\n";}
};int main() {std::string s = "sname";Person p1(s); // init with string object => calls copying string - CONSTRPerson p2("tmp"); // init with string literal => calls movingstring-CONSTRPerson p3(p1); // copy Person => calls COPY-CONSTRPerson p4(std::move(p1)); // move Person => calls MOVE-CONSTreturn 0;
}
????????例子中 Person 類有一個 string 類型的 name 成員和幾個初始化構造函數。為了支持移動語義,重載了接受 std::string 作為參數的構造函數:
????????現在將上面兩個以 std::string 作為參數的構造函數替換為一個泛型的構造函數,它將傳入的參數完美轉發(perfect forward)給成員 name:?
#include <utility>
#include <string>
#include <iostream>
class Person {private:std::string name;public:template<typename T>explicit Person(T&& str) : name(std::forward<T>(str)) {std::cout << "template for ’" << name << "’\n";}// copy and move constructor:Person(Person const& p) : name(p.name) {std::cout << "COPY-CONSTR Person ’" << name << "’\n";}Person(Person&& p) : name(std::move(p.name)) {std::cout << "MOVE-CONSTR Person ’" << name << "’\n";}
};int main() {std::string s = "sname";Person p1(s); // init with string object => calls templatePerson p2("tmp"); // init with string literal => calls template
// Person p3(p1); // build errorPerson p4(std::move(p1)); // move Person => calls MOVE-CONSTreturn 0;
}
????????問題出在這里:根據 C++重載解析規則(參見 16.2.5 節),對于一個非const 左值的Personp,成員模板
template Person(STR&& n)
通常比預定義的拷貝構造函數更匹配:
Person (Person const& p) 這里 STR 可以直接被替換成 Person&,
但是對拷貝構造函數還要做一步const 轉換。額外提供一個非 const 的拷貝
6.3 通過 std::enable_if<>禁用模板?
?????????從 C++11 開始,通過 C++標準庫提供的輔助模板 std::enable_if<>,可以在某些編譯期條件下忽略掉函數模板。
???????? 比如,如果函數模板 foo<>的定義如下:
#include <utility>
#include <string>
#include <iostream>template<typename T>
typename std::enable_if < (sizeof(T) > 4) >::type
foo() {
}int main() {foo<double>();// build success//foo<bool>();// build error “std::enable_if<sizeof(T)>4,void>::type foo(void)”的顯式 模板 參數無效return 0;
}
???????這一模板定義會在 sizeof(T) > 4 不成立的時候被忽略掉。如果 sizeof > 4 成立,函數模板會展開成:
template<typename T>
void foo() {
}
????????也就是說 std::enable_if<>是一種類型萃取(type trait),它會根據一個作為其(第一個)模板參數的編譯期表達式決定其行為:
- 如果這個表達式結果為 true,它的 type 成員會返回一個類型:-- 如果沒有第二個模板參數,返回類型是 void。 -- 否則,返回類型是其第二個參數的類型。
- 如果表達式結果 false,則其成員類型是未定義的。根據模板的一個叫做SFINAE(substitute failure is not an error,替換失敗不是錯誤,將在 8.4 節進行介紹)的規則,這會導致包含 std::enable_if<>表達式的函數模板被忽略掉。
????????由于從 C++14 開始所有的模板萃取(type traits)都返回一個類型,因此可以使用一個與之對應的別名模板 std::enable_if_t<>,這樣就可以省略掉 template 和::type 了。如下
template<typename T>
std::enable_if_t < (sizeof(T) > 4) >
foo() {
}
?????????如果給 std::enable_if<>或者 std::enable_if_t<>傳遞第二個模板參數
template<typename T>
std::enable_if_t < (sizeof(T) > 4), T >
foo() {return T();
}
????????那么在 sizeof(T) > 4 時,enable_if 會被擴展成其第二個模板參數。因此如果與T 對應的模板參數被推斷為 MyType,而且其 size 大于 4,那么其等效于
MyType foo()
6.4 使用 enable_if<>
????????通過使用 enable_if<>可以解決 6.2 節中關于構造函數模板的問題。
????????我們要解決的問題是:當傳遞的模板參數的類型不正確的時候(比如不是std::string 或者可以轉換成 std::string 的類型),禁用如下構造函數模板:
explicit Person(STR && n): name(std::forward<STR>(n)) {std::cout << "TMPL-CONSTR for ’" << name << "’\n";}
????????為了這一目的,需要使用另一個標準庫的類型萃取,std::is_convertiable。在C++17中,相應的構造函數模板的定義如下:
template<typename STR, typename =
std::enable_if_t<std::is_convertible_v<STR, std::string>>>
Person(STR&& n);
如果 STR 可以轉換成 std::string,這個定義會擴展成:
?
template<typename T,typename = void>Person(STR&& n);
????????否則這個函數模板會被忽略。
????????這里同樣可以使用別名模板給限制條件定義一個別名:
using EnableIfString =std::enable_if_t<std::is_convertible_v<T, std::string>>;
現在完整 Person 類如下?
#include <utility>
#include <string>
#include <iostream>
#include <type_traits>template<typename T>
using EnableIfString =std::enable_if_t<std::is_convertible_v<T, std::string>>;
class Person {private:std::string name;public:// generic constructor for passed initial name:template<typename STR, typename = EnableIfString<STR>>explicit Person(STR && n): name(std::forward<STR>(n)) {std::cout << "TMPL-CONSTR for ’" << name << "’\n";}// copy and move constructor:Person(Person const& p) : name(p.name) {std::cout << "COPY-CONSTR Person ’" << name << "’\n";}Person(Person&& p) : name(std::move(p.name)) {std::cout << "MOVE-CONSTR Person ’" << name << "’\n";}
};int main() {std::string s = "sname";Person p1(s); // init with string object => calls TMPL-CONSTRPerson p2("tmp"); // init with string literal => calls TMPL-CONSTRPerson p3(p1); // OK => calls COPY-CONSTRPerson p4(std::move(p1)); // OK => calls MOVE-CONSTreturn 0;
}
?禁用某些成員函數
????????注意我們不能通過使用 enable_if<>來禁用 copy/move 構造函數以及賦值構造函數。這是因為成員函數模板不會被算作特殊成員函數(依然會生成默認構造函數),而且在需要使用copy 構造函數的地方,相應的成員函數模板會被忽略掉。因此即使像下面這樣定義類模板:
#include <utility>
#include <string>
#include <iostream>
#include <type_traits>class C {public:C() = default;template<typename T>C(T const&) {std::cout << "tmpl copy constructor\n";}
};int main() {C x;C y{ x }; // still uses the predefined copy constructor (not the membertemplate)return 0;
}
?C y{ x };? 并不會調用模板,調用默認拷貝構造函數
????????但是也有一個辦法:可以定義一個接受 const volatile 的 copy 構造函數并將其標示為delete。這樣做就不會再隱式聲明一個接受 const 參數的 copy 構造函數。在此基礎上,可以定義一個構造函數模板,對于 nonvolatile 的類型,它會優選被選擇(相較于已刪除的copy 構造函數):
class C {public:C() = default;C(C const volatile&) = delete;// implement copy constructor template with better match:template<typename T>template<typename T>C(T const&) {std::cout << "tmpl copy constructor\n";}
};
這樣即使對常規 copy,也會調用模板構造函數:
C x;
C y{x}; // uses the member template
????????于是就可以給這個模板構造函數添加 enable_if<>限制。比如可以禁止對通過int 類型參數實例化出來的 C<>模板實例進行 copy:
#include <utility>
#include <string>
#include <iostream>
#include <type_traits>template<typename T>
class C {public:C() = default;C(C const volatile&) = delete;// if T is no integral type, provide copy constructor templatewith better match:template < typename = std::enable_if_t < !std::is_integral<T>::value >>C(C<T> const&) {std::cout << "tmpl copy constructor\n";}
};int main() {C<double> x;C y{ x }; // still uses the predefined copy constructor (not the membertemplate)return 0;
}
6.5 使用 concept 簡化 enable_if<>表達式
????????即使使用了模板別名,enable_if 的語法依然顯得很蠢,因為它使用了一個變通方法:為了達到目的,使用了一個額外的模板參數,并且通過“濫用”這個參數對模板的使用做了限制。這樣的代碼不容易讀懂,也使模板中剩余的代碼不易理解。?
????????原則上我們所需要的只是一個能夠對函數施加限制的語言特性,當這一限制不被滿足的時候,函數會被忽略掉。
????????這個語言特性就是人們期盼已久的 concept,可以通過其簡單的語法對函數模板施加限制條件。不幸的是,雖然已經討論了很久,但是 concept 依然沒有被納入C++17 標準。一些編譯器目前對 concept 提供了試驗性的支持,不過其很有可能在 C++17 之后的標準中得到支持(目前確定將在 C++20 中得到支持)。通過使用 concept 可以寫出下面這樣的代碼
template<typename STR>
requires std::is_convertible_v<STR,std::string>
Person(STR&& n) : name(std::forward<STR>(n)) { …
}
6.6 總結?
- 在模板中,可以通過使用“轉發引用”(亦稱“萬能引用”,聲明方式為模板參數T加&&)和 std::forward<>將模板調用參完美地數轉發出去。
- 將完美轉發用于成員函數模板時,在 copy 或者 move 對象的時候它們可能比預定義的特殊成員函數更匹配。
- 可以通過使用 std::enable_if<>并在其條件為 false 的時候禁用模板。
- 通過使用 std::enable_if<>,可以避免一些由于構造函數模板或者賦值構造函數模板比隱式產生的特殊構造函數更加匹配而帶來的問題。
- 可 以 通 過 刪 除 對 const volatile 類 型 參 數 預 定 義 的 特 殊 成 員函數,并結合使用std::enable_if<>,將特殊成員函數模板化。
- 通過 concept 可以使用更直觀的語法對函數模板施加限制。
?