您的位置:首页 > 编程语言 > C语言/C++

C++ 学习笔记(16)模板、引用折叠、std::forward、可变参数模板、模板特例化

2018-02-19 19:25 597 查看

C++ 学习笔记(16)模板、引用折叠、std::forward、可变参数模板、模板特例化

参考书籍:《C++ Primer 5th》

API:模板

16.1 定义模版

16.1.1 函数模版

template <typename T> int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return -1;
return 0;
}

template <typename T, class U> calc (const T&, const U&);       // 每个模版参数都要带 typename 或 class 关键字

template <typename T> inline T func(const T&);      // inline 或 constexpr 说明符 放在模版参数列表之后


非类型模版参数:表示值而非类型。可以是整形,指向对象或函数的指针或左值引用。而绑定的实参必须是常量表达式,绑定到指针或引用实参必须在静态内存里。

// 比较两个字符串
template <unsigned M, unsigned N> int compare(const char(&p1)[M], const char(&p2)
)
{
retrun strcpy(p1, p2);
}


函数模版和类模版成员函数的定义通常放在头文件。

16.1.2 类模版

不同于函数模版,编译器无法为类模版推断模版参数类型。

template <typename T> A<T>::A() { }     // A类的函数在类外定义时,也需要指名模版参数


默认情况下,对于一个实例化了类模板,其成员只有在使用时才被实例化。

在类模版作用域内,编译器处理模板自身引用时可以省略模版参数。

template <typename T> class A
{
public:
// 定义时省略<T> 模版参数
A& operator++()
{
A a = this;     // 使用时也省略参数模版
return a;
}
};


别名模版

template<typename T> using twin = pair<T, T>;   // 使用别名
twin<int> ti;       // ti是 pair<int, int>


16.1.3 模版参数

使用类的类型成员,直接编写其成员,如果类型不包含该成员时,编译器会报错。

// 返回T的value_type成员,声明定义时如果没有该成员,编译时出错
template <typename T> typename T::value_type top(const T& c)
{
if (!c.empty())     // 执行该类的成员函数,(如果没有,运行错误)
return c.back();
else
return typename T::value_type();    // 若无,值初始化
}

void main()
{
vector<int> vi = { 3,4,5,2 };
cout << top<vector<int>>(vi);       // 输出2
}


// 可以使用默认实参,如下,默认比较函数使用标准库的less
template <typename T, typename F =less<T>> int compare(const T &v1, const T &v2, F f = F())
{
if (f(v1 < v2)) return -1;
if (f(v2 < v1)) return -1;
return 0;
}


16.1.4 成员模版

一个类可以包含本身是模版的成员函数,这个成员称为成员模版,不可以是虚函数。

struct Printer
{
std::ostream& os;
Printer(std::ostream& os) : os(os) {}
template<typename T> void operator()(const T& obj) { os << obj << ' '; }    // 成员模板
};

void main()
{
std::vector<int> v = { 1,2,3 };
std::for_each(v.begin(), v.end(), Printer(std::cout));  // 使用临时Printer对象
Printer pinter = Printer(std::cout);
std::string s = "abc";
std::for_each(s.begin(), s.end(), pinter);      // 使用Printer对象
}


16.1.5 控制实例化

模版被使用时才会实例化。多个相同模版就会生成多个实例,可以使用显示实例化避免这种开销,即使用
extern
声明实例,但也必须实例化定义。

实例化一个类模版时,所有成员都会实例化(包括内联成员函数)。

16.1.6 效率与灵活性

unique_ptr在编译时绑定删除器,避免间接调用删除器的运行时开学。

shared_ptr在运行时绑定删除器,使用户重载删除器更方便。

16.2 模版实参推断

16.2.1 类型转换与模版类型参数

const转换:可以将一个非const对象的引用(或指针)传递给const的形参。

数组或函数指针转换:函数形参不是引用类型时,可以用数组或函数类型做指针转换。(数组转指向第一个元素的指针,函数转指向该函数类型的指针)

传递数组实参时:

实参传递是引用时:数组大小不同时,类型不同。

实参传递非引用时:数组转换成指针,不在乎数组大小。

16.2.2 函数模版显式实参

template <typename T1, typename T2, typename T3> T1 sum1(T2, T3) {}
template <typename T1, typename T2, typename T3> T3 sum2(T2, T1) {}     // 糟糕的设计

void main()
{
// T1显示指定(必须指定),T2、T3从实参类型中推断。
auto v1 = sum1<int>(1.0, 2.0);  // int sum<int, double, double>(double, double)

// 因为T3才是返回值,所以必须要指定T1、T2。
auto v2 = sum2<float, float, int>(2, 4);    // 正确
auto v3 = sum3<int>(3, 5);      // 错误,返回值无法推断出来。
}


16.2.3 尾置返回类型与类型转换

使用标准库type_traits,实现类型转换。



// 指名返回值为参数类型的解引用,如果传入int序列,那就是返回int&
template <typename It> auto func1(It beg, It end) -> decltype(*beg) {}

// 使用标准库type_traits的模版,必须使用typename告诉编译器type是类型,传入int序列时,返回int类型值
template <typename It> auto func2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {}


16.2.5 模版实参推断和引用

右值引用参数:被传入左值时,类型是左值引用。

引用的引用:会被折叠成普通引用。

X& &
X& &&
X&& &
都会折叠成类型
X&


X&& &&
折叠成
X&&


引用折叠只能用于间接创建的引用的引用,如类型别名或模版参数。

模版参数类型为右值引用时(T&&):可以传递任意类型实参。且传递左值时,会被实例化成左值引用。

右值引用常用于:

模版转发实参。

模版重载。

template <typename T> void f(T&&) {}    // 右值形参

void main()
{
int i = 123;
const int ci = 345;
f(123);     // T为int。(传入右值)
f(i);       // T为int &。(传入左值)
f(ci);      // T为const int &。(传入左值)
}


16.2.6 理解std::move

参数为右值,所以可以传入任何实参(包括左值)。

remove_reference移除参数的左值引用(如果有)。

使用static_cast:将左值类型转换为右值引用。

template <typename T> typename remove_reference<T>::type && move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}


16.2.7 转发

使用std::forward保持实参传递的左值/右值属性。

配合传入模版参数类型为右值引用(T&&)时,forward会保持实参类型的所有细节。

// 调用函数f,按t2、t1顺序传入参数。
template <typename F, typename T1, typename T2> void flip(F f, T1 &&t1, T2 &&t2)
{
// 使用forward,传入实参的左值右值属性都会被保留。
f(std::forward<T2>(t2), std::forward<T1>(t1));
}


16.3 重载与模板

如果多个函数提供同样好的匹配:

只有一个是非模板函数,选择这个。

没有非模板函数,在多个最特例化的模版。

否则,调用有歧义。

cout << debug("hello") << endl;     // 传入const char *,调用 debug(T *)
// 下面三者都满足,调用优先级依次降低
// debug(T*),T为const char,比下面的更特例化
// debug(const T&),T为char[6]
// debug(const string&),从const char * 转换到 string,可以,但不是精确匹配


16.4 可变参数模板

可变数目的参数成为参数包

模板参数包:零个或多个模板参数。

函数参数包:零个或多个函数参数。

template <typename... Args> void foo(const Args &... rest)
{
cout << sizeof...(Args) << endl;    // 类型参数的数目
cout << sizeof...(rest) << endl;    // 函数参数的数目
}


16.4.1 编写可变参数函数模板

template <typename T,typename... Args> void print(ostream &os,const T& t, const Args &... rest)
{
os << t << ", ";
print(os, rest...);     // 递归调用,每次调用减少一个参数(t)
}

// 递归最内层的函数。非可变参数版本。必须有该函数,否则可变参数函数调用出错。
template <typename T> void print(ostream &os, const T& t)
{
os << t << "!! ";
}

void main()
{
// 输出:123, abc, 2.333!!
print(cout, 123, "abc", 2.333f);
}


16.4.2 包扩展

template <typename... Args> void print(ostream &os, const Args &... rest)
{
print(os, debug(rest) ...);     // 对每个参数都作为debug()函数的参数,递归调用
}


16.4.3 转发参数包

// 模仿构造函数的参数列表
template <class... Args> void emplace_back(Args&&...);


16.5 模板特例化

模板及其特例化版本应该声明在同一个头文件,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。

只能部分特例化类模板,不能部分特例化函数模板。

// 版本1:比较任意两个类型
template <typename T> int compare(const T &v1, const T &v2);

// 版本2:处理字符串字面常量,处理的是数组,不能是指针
template <unsigned M, unsigned N> int compare(const char(&p1)[M], const char(&p2)
);

// 版本3:模板特例化,可直接处理字符串(compare("sd","ab") )
template <> int compare(const char* const &p1, const char* const &p2);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: