C++ template-2

?第 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 使用裸數組或者字符串常量的模板

????????當向模板傳遞裸數組或者字符串常量時,需要格外注意以下內容:
????????第一,如果參數是按引用傳遞的,那么參數類型不會退化(decay )。也就是說當傳遞 ”hello”
作為參數時,模板類型會被推斷為 char const[6] 。這樣當向模板傳遞長度不同的裸數組或者
字符串常量時就可能遇到問題,因為它們對應的模板類型不一樣。只有當按值傳遞參數時,
模板類型才會退化( decay ),這樣字符串常量會被推斷為 char const * 。相關內容會在第 7
章進行討論。

5.5 成員模板

????????類的成員也可以是模板,對嵌套類和成員函數都是這樣。這一功能的作用和優點同樣可以通
Stack<> 類模板得到展現。通常只有當兩個 stack 類型相同的時候才可以相互賦值( stack
的類型相同說明它們的元素類型也相同)。即使兩個 stack 的元素類型之間可以隱式轉換,
也不能相互賦值:
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
????????默認的賦值運算符要求等號兩邊的對象類型必須相同,因此如果兩個 stack 之間的元素類型
不同的話,這一條件將得不到滿足。
????????但是,只要將賦值運算符定義成模板,就可以將兩個元素類型可以做轉換的 stack 相互賦值。
新的 Stack<> 定義如下:
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&);
};
以上代碼中有如下兩點改動:
1. 賦值運算符的參數是一個元素類型為 T2 stack
2. 新的模板使用 std::deque<> 作為內部容器。這是為了方便新的賦值運算符的定義。
新的賦值運算符被定義成下面這樣:
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;
}
下面先來看一下成員模板的定義語法。在模板類型為 T 的模板內部,定義了一個模板類型為
T2 的內部模板
????????在模板函數內部,你可能希望簡化 op2 中相關元素的訪問。但是由于 op2 屬于另一種類型
(如果用來實例化類模板的參數類型不同,那么實例化出來的類的類型也不同),因此最好
使用它們的公共接口。這樣訪問元素的唯一方法就是通過調用 top() 。這就要求 op2 中所有
元素相繼出現在棧頂,為了不去改動 op2 ,就需要做一次 op2 的拷貝。由于 top() 返回的是
最后一個被添加進 stack 的元素,因此需要選用一個支持在另一端插入元素的容器,這就是
為什么選用 std::deque<> 的原因,因為它的 push_front() 方法可以將元素添加到另一端。
為了訪問 op2 的私有成員,可以將其它所有類型的 stack 模板的實例都定義成友元:

?

????????當然,這樣的賦值就不會改變 floatStack 的類型,也不會改變它的元素的類型。在賦值之后, floatStack 存儲的元素依然是 float 類型, top() 返回的值也依然是 float 類型。
????????看上去這個賦值運算符模板不會進行類型檢查,這樣就可以在存儲任意類型的兩個 stack
間相互賦值,但是事實不是這樣。必要的類型檢查會在將源 stack (上文中的 op2 或者其備
temp )中的元素插入到目標 stack 中的時候進行: elems . push_front ( tmp . top ());
????????比如如果將存儲 string stack 賦值給存儲 int stack ,那么在編譯這一行代碼的時候會遇
到如下錯誤信息:不能將通過 tmp.top() 返回的 string 用作 elems.push_front() 的參數(不同編
譯器產生的錯誤信息可能會有所不同,但大體上都是這個意思):

成員模板的特例化

?成員函數模板也可以被全部或者部分地特例化。比如對下面這個例子:

// 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;
}
其成員函數模板 get()進行全特例化:bool get<bool>()
????????注意我們不需要也不能夠對特例化的版本進行聲明;只能定義它們。由于這是一個定義于頭
文件中的全實例化版本,如果有多個編譯單 include 了這個頭文件,為避免重復定義的錯誤,
必須將它定義成 inline 的。
????????

特殊成員函數的模板

????????如果能夠通過特殊成員函數 copy 或者 move 對象,那么相應的特殊成員函數( copy 構造函
數以及 move 構造函數)也將可以被模板化。和前面定義的賦值運算符類似,構造函數也可
以是模板。但是需要注意的是,構造函數模板或者賦值運算符模板不會取代預定義的構造函
數和賦值運算符。成員函數模板不會被算作用來 copy 或者 move 對象的特殊成員函數。在
上面的例子中,如果在相同類型的 stack 之間相互賦值,調用的依然是默認賦值運算符。
這種行為既有好處也有壞處:
(1)某些情況下,對于某些調用,構造函數模板或者賦值運算符模板可能比預定義的
copy/move 構造函數或者賦值運算符更匹配,雖然這些特殊成員函數模板可能原本只打
算用于在不同類型的 stack 之間做初始化。詳情請參見 6.2 節。
(2)想要對 copy/move 構造函數進行模板化并不是一件容易的事情,比如該如何限制其存
在的場景。詳情請參見 6.4 節。

template 的使用

????????某些情況下,在調用成員模板的時候需要顯式地指定其模板參數的類型 。這時候就需要使用
關鍵字 template 來確保符號 < 會被理解為模板參數列表的開始,而不是一個比較運算符。考
慮下面這個使用了標準庫中的 bitset 的例子:
#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>>();
}
????????對于 bitset 類型的 bs ,調用了其成員函數模板 to_string() ,并且指定了 to_string() 模板的所有
模板參數。如果沒有 .template 的話,編譯器會將 to_string() 后面的 < 符號理解成小于運算符,
而不是模板的參數列表的開始。 這一這種情況只有在點號前面的對象依賴于模板參數的時候
才會發生。在我們的例子中, bs 依賴于模板參數 N
.template 標識符(標識符 ->template ::template 也類似)只能被用于模板內部,并且它前
面的對象應該依賴于模板參數。詳情請參見 13.3.3

泛型 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 變量模板

C++14 開始,變量也可以被某種類型參數化。稱為變量模板。
例如可以通過下面的代碼定義 pi ,但是參數化了其類型:
????????template < typename T >
????????constexpr T pi { 3.1415926535897932385 };
注意,和其它幾種模板類似,這個定義最好不要出現在函數內部或者塊作用域內部。
????????在使用變量模板的時候,必須指明它的類型。比如下面的代碼在定義 pi<> 的作用域內使用了
兩個不同的變量:
????????std :: cout << pi < double > << \n ;
????????std :: cout << pi < float > << \n ;

用于數據成員的變量模板

????????變量模板的一種應用場景是,用于定義代表類模板成員的變量模板。比如如果像下面這樣定
義一個類模板:
template<typename T>
class MyClass {
public:static constexpr int max = 1000;
};
那么就可以為 MyClass<> 的不同特例化版本定義不同的值:
????????template < typename T >
????????int myMax = MyClass < T >:: max ;
應用工程師就可以使用下面這樣的代碼:
????????auto i = myMax < std :: string >;
而不是:
????????auto i = MyClass < std :: string >:: max ;
這意味著對于一個標準庫的類:
namespace std {
template<typename T>
class numeric_limits {public: …static constexpr bool is_signed = false; …
};
}
可以定義:
template < typename T >
constexpr bool isSigned = std :: numeric_limits < T >:: is_signed ;
這樣就可以用:
isSigned < char >
代替:
std :: numeric_limits < char >:: is_signed

類型萃取 Suffix_v

C++17 開始,標準庫用變量模板為其用來產生一個值(布爾型)的類型萃取定義了簡化方
式。比如為了能夠使用:
std :: is_const_v < T > // since C++17
而不是:
std :: is_const < T >:: value //since C++11
標準庫做了如下定義:
namespace std {
????????template < typename T >
???????????????constexpr bool is_const_v = is_const < T >:: value ;
}

5.7 模板參數模板

????????如果允許模板參數也是一個類模板的話,會有不少好處。在這里依然使用 Stack 類模板作為
例子。
????????對 5.5 節中的 stack 模板,如果不想使用默認的內部容器類型 std::deque ,那么就需要兩次指
stack 元素的類型。也就是說為了指定內部容器的類型,必須同時指出容器的類型和元素
的類型:
????????Stack < int , std :: vector < int >> vStack ; // integer stack that uses a vector
????????使用模板參數模板,在聲明 Stack 類模板的時候就可以只指定容器的類型而不去指定容器中
元素的類型:
????????Stack < int , std :: vector > vStack ; // integer stack that uses a vector
????????為此就需要在 Stack 的定義中將第二個模板參數聲明為模板參數模板。可能像下面這樣:

#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 Elem > class Cont
默認類型也從 std::deque<T> 變成 std::deque 。這個參數必須是一個類模板,它將被第一個模
板參數實例化:
????????Cont < T > elems ;
????????用第一個模板參數實例化第二個模板參數的情況是由 Stack 自身的情況決定的。實際上,可
以在類模板內部用任意類型實例化一個模板參數模板。
????????和往常一樣,聲明模板參數時可以使用 class 代替 typename 。在 C++11 之前, Cont 只能被某 個類模板的名字取代。
????????從 C++11 開始,也可以用別名模板( alias template )取代 Cont ,但是直到 C++17 ,在聲明模 板參數模板時才可以用 typename 代替 class:????????

????????這兩個變化的目的都一樣:用 class 代替 typename 不會妨礙我們使用別名模板( alias
template )作為和 Cont 對應的模板參數。
????????由于模板參數模板中的模板參數沒有被用到,作為慣例可以省略它(除非它對文檔編寫有幫
助):
成員函數也要做相應的更改。必須將第二個模板參數指定為模板參數模板。比如對于 push()
成員,其實現如下:
template < typename T , template < typename > class Cont >
void Stack < T , Cont >:: push ( T const & elem )
{
????????elems . push_back ( elem ); // append copy of passed elem
}
注意,雖然模板參數模板是類或者別名類( alias templates )的占位符,但是并沒有與其對
應的函數模板或者變量模板的占位符

?模板參數模板的參數匹配

如果你嘗試使用新版本的 Stack ,可能會遇到錯誤說默認的 std::deque 和模板參數模板 Cont
不匹配。這是因為在 C++17 之前, template<typename Elem> typename Cont = std::deque
的模板參數必須和實際參數( std::deque )的模板參數匹配(對變參模板有些例外,見 12.3.4
節)。而且實際參數( std::deque 有兩個參數,第二個是默認參數 allocator )的默認參數也
要被匹配,這樣 template<typename Elem> typename Cont = std::dequ 就不滿足以上要求(不
過對 C++17 可以)。
作為變通,可以將類模板定義成下面這樣:
template<typename T, template<typename Elem,
typename Alloc = std::allocator<Elem>> class Cont = std::deque>
class Stack {
private:Cont<T> elems; // elements
…
};
其中的 Alloc 同樣可以被省略掉。???????
因此最終的 Stack 模板會像下面這樣(包含了賦值運算符模板
#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 總結?

  1. 在模板中,可以通過使用“轉發引用”(亦稱“萬能引用”,聲明方式為模板參數T加&&)和 std::forward<>將模板調用參完美地數轉發出去。
  2. 將完美轉發用于成員函數模板時,在 copy 或者 move 對象的時候它們可能比預定義的特殊成員函數更匹配。
  3. 可以通過使用 std::enable_if<>并在其條件為 false 的時候禁用模板。
  4. 通過使用 std::enable_if<>,可以避免一些由于構造函數模板或者賦值構造函數模板比隱式產生的特殊構造函數更加匹配而帶來的問題。
  5. 可 以 通 過 刪 除 對 const volatile 類 型 參 數 預 定 義 的 特 殊 成 員函數,并結合使用std::enable_if<>,將特殊成員函數模板化。
  6. 通過 concept 可以使用更直觀的語法對函數模板施加限制。

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/696874.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/696874.shtml
英文地址,請注明出處:http://en.pswp.cn/news/696874.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【代碼隨想錄算法訓練營Day09】28.實現 strStr(); 459.重復的子字符串

文章目錄 Day 9 第四章 字符串part0228. 實現 strStr() &#xff08;本題可以跳過&#xff09;KMP 思路KMP 代碼 459.重復的子字符串 &#xff08;本題可以跳過&#xff09;字符串總結雙指針回顧 Day 9 第四章 字符串part02 今日任務 28.實現 strStr(); 459.重復的子字符串; 字…

題目:C++快速找到未知長度單鏈表的中間節點。普通方法和高級方法2種解題思路解析。

在數據結構的面試中&#xff0c;經常會出現這樣的問題&#xff1a;如何快速找到未知長度單鏈表的中間節點&#xff1f;通常&#xff0c;面試官會期待你提供兩種解法&#xff1a;一種是最基本的普通方法&#xff0c;另一種是更高效的 advanced 方法。本文將詳細介紹這兩種方法。…

Nginx -2

接著上文寫 5.4.7 驗證模塊 需要輸入用戶名和密碼 模塊名稱&#xff1a;ngx_http_auth_basic_module 訪問控制基于模塊 ngx_http_auth_basic_module 實現&#xff0c;可以通過匹配客戶端資源進行限制 語法&#xff1a; Syntax: auth_basic string | off; Default: auth_ba…

威爾金森功分器基本原理學習筆記

威爾金森功分器基本原理 威爾金森功率分配器的功能是將輸入信號等分或不等分的分配到各個輸出端口&#xff0c;并保持相同輸出相位。環形器雖然有類似功能&#xff0c;但威爾金森功率分配器在應用上具有更寬的帶寬。微帶形功分器的電路結構如圖所示&#xff0c;其中&#xff0…

【OpenAI Sora】何時開放使用?付費課程已上線(sora什么時候開放使用 )

Sora何時開放使用 根據提供的信息&#xff0c;Sora目前還未對廣大用戶開放。OpenAI在2024年2月15日展示了Sora的視頻&#xff0c;但沒有設立等待名單或提供API訪問。Sora仍在開發中&#xff0c;正在接受安全測試&#xff0c;并且尚未向公眾開放使用。 付費課程已上線 根據最…

Vue圖片瀏覽組件v-viewer,支持旋轉、縮放、翻轉等操作

Vue圖片瀏覽組件v-viewer&#xff0c;支持旋轉、縮放、翻轉等操作 之前用過viewer.js&#xff0c;算是市場上用過最全面的圖片預覽。v-viewer&#xff0c;是基于viewer.js的一個圖片瀏覽的Vue組件&#xff0c;支持旋轉、縮放、翻轉等操作。 基本使用 安裝&#xff1a;npm安裝…

費舍爾FISHER金屬探測器探測儀維修F70

美國FISHER LABS費舍爾地下金屬探測器&#xff0c;金屬探測儀等維修&#xff08;考古探金銀銅探寶等儀器&#xff09;。 費舍爾F70視聽目標ID金屬探測器&#xff0c;Fisher 金屬探測器公司成立于1931年&#xff0c;在實驗條件很艱苦的情況下&#xff0c;研發出了地下金屬探測器…

【Python】實現一個類似于Glass2k的Windows窗口透明化軟件

一 背景說明 網上看到一款Windows下的窗口透明化工具Glass2k&#xff08;Glass2k官網&#xff09;&#xff0c;可以簡單地通過快捷鍵實現任意窗口的透明化&#xff0c;還挺方便的&#xff0c;想用Python自己實現一下類似的功能。 軟件已經開源到&#xff1a;窗口透明化小工具開…

【Leetcode】889. 根據前序和后序遍歷構造二叉樹

文章目錄 題目思路代碼結果 題目 題目鏈接 給定兩個整數數組&#xff0c;preorder 和 postorder &#xff0c;其中 preorder 是一個具有 無重復 值的二叉樹的前序遍歷&#xff0c;postorder 是同一棵樹的后序遍歷&#xff0c;重構并返回二叉樹。 如果存在多個答案&#xff0c;…

CSS基礎屬性

【三】基礎屬性 【1】高度和寬度 &#xff08;1&#xff09;參數 width&#xff08;寬度&#xff09;&#xff1a;用于設置元素的寬度。可以使用具體的數值&#xff08;如像素值&#xff09;或百分比來指定寬度。 height&#xff08;高度&#xff09;&#xff1a;用于設置元…

Kubernetes 卷存儲 NFS | nfs搭建配置 原理介紹 nfs作為存儲卷使用

目錄 1、NFS介紹2、NFS服務部署2.1安裝nfs服務 (服務端配置)2.2啟動NFS服務2.3 服務檢查2.4 客戶端配置 3、nfs作為存儲卷使用3.1 nfs作為volume3.2 nfs存儲的缺點3.3 nfs作為PersistentVolum 4、nfs作為動態存儲提供5、總結 1、NFS介紹 NFS&#xff08;Network File System&a…

4.pom文件介紹Maven常用命令

1.pom.xml文件介紹. 1.1project標簽和modelVersion標簽介紹. pom.xml文件是maven的核心文件&#xff0c;POM(Project Object Model&#xff0c;項目對象模型)定義了項目的基本信息&#xff0c;用于描述如何構建&#xff0c;聲明項目依賴;&#xff1b; 1.2依賴坐標介紹. 依賴的…

得物面試:Kafka消息0丟失,如何實現?

得物面試&#xff1a;Kafka消息0丟失&#xff0c;如何實現&#xff1f; 尼恩說在前面 在40歲老架構師 尼恩的讀者交流群(50)中&#xff0c;最近有小伙伴拿到了一線互聯網企業如得物、阿里、滴滴、極兔、有贊、希音、百度、網易、美團的面試資格&#xff0c;遇到很多很重要的面…

新版Java面試專題視頻教程——多線程篇②

新版Java面試專題視頻教程——多線程篇② 0. 問題匯總0.1 線程的基礎知識0.2 線程中并發安全0.3 線程池0.4 使用場景 1.線程的基礎知識2.線程中并發鎖3.線程池3.1 說一下線程池的核心參數&#xff08;線程池的執行原理知道嘛&#xff09;3.2 線程池中有哪些常見的阻塞隊列Array…

高級語言期末2014級A卷

1.編寫函數 int delarr(int a[] ,int n)&#xff0c;刪除有n個元素的正整型數組a中所有素數&#xff0c;要求&#xff1a; 1&#xff09;數組a中剩余元素保持原來次序&#xff1b; 2&#xff09;將處理后的數組輸出&#xff1b; 3&#xff09;函數值返回剩余元素個數&#xff1…

MySQL索引面試題(高頻)

文章目錄 前言什么時候需要&#xff08;不需要&#xff09;)使用索引&#xff1f;有哪些優化索引的方法前綴索引優化索引覆蓋優化索引失效場景 總結 前言 今天來講一講 MySQL 索引的高頻面試題。主要是針對前一篇文章 MySQL索引入門&#xff08;一文搞定&#xff09;進行查漏補…

虛擬機的內存結構

一、摘要 熟悉 Java 語言特性的同學都知道&#xff0c;相比 C、C 等編程語言&#xff0c;Java 無需通過手動方式回收內存&#xff0c;內存中所有的對象都可以交給 Java 虛擬機來幫助自動回收&#xff1b;而像 C、C 等編程語言&#xff0c;需要開發者通過代碼手動釋放內存資源&…

MedicalGPT 訓練醫療大模型,實現了包括增量預訓練、有監督微調、RLHF(獎勵建模、強化學習訓練)和DPO(直接偏好優化)

MedicalGPT 訓練醫療大模型&#xff0c;實現了包括增量預訓練、有監督微調、RLHF(獎勵建模、強化學習訓練)和DPO(直接偏好優化)。 MedicalGPT: Training Your Own Medical GPT Model with ChatGPT Training Pipeline. 訓練醫療大模型&#xff0c;實現了包括增量預訓練、有監督微…

Linux第63步_為新創建的虛擬機添加必要的目錄和安裝支持linux系統移植的軟件

1、創建必要的目錄 1)、創建“/home/zgq/linux/”目錄 打開終端&#xff0c;進入“/home/zgq/”目錄 輸入“mkdir linux回車”&#xff0c;創建“/home/zgq/linux/”目錄 輸入“ls回車”&#xff0c;列舉“/home/zgq/”目錄的所有文件和文件夾 創建好“/home/zgq/linux/”…

EIS(防抖):meshflow算法 C++實現

視頻防抖的應用 對視頻防抖的需求在許多領域都有。 這在消費者和專業攝像中是極其重要的。因此&#xff0c;存在許多不同的機械、光學和算法解決方案。即使在靜態圖像拍攝中&#xff0c;防抖技術也可以幫助拍攝長時間曝光的手持照片。 在內窺鏡和結腸鏡等醫療診斷應用中&…