C++ 学习笔记
Posted 腾讯技术工程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 学习笔记相关的知识,希望对你有一定的参考价值。
作者:readywang(王玉龙)
template 是 c++ 相当重要的组成部分,堪称 c++语言的一大利器。在大大小小的 c++ 程序中,模板无处不在。c++ templates 作为模板学习的经典书籍,历来被无数 c++学习者所推崇。第二版书籍覆盖了 c++ 11 14 和 17 标准,值得程序猿们精读学习,特此整理学习笔记,将每一部分自认为较为重要的部分逐条陈列,并对少数错误代码进行修改
一、函数模板
1.1 函数模板初探
1.模板实例化时,模板实参必须支持模板中类型对应的所有运算符操作。
template <typename T>
T max(const T &a, const T &b)
return a > b? a : b;
class NdGreater
;
int main()
NdGreater n1, n2;
::max(n1, n2); // 不支持 > 编译报错
2.模板编译时会进行两阶段检查
a.模板定义时,进行和类型参数无关的检查,如未定义的符号等。
b.模板实例化时,进行类型参数相关的检查。
template<typename T>
void foo(T t)
undeclared(); // 如果 undeclared()未定义,第一阶段就会报错,因为与模板参数无关
static_assert(sizeof(T) > 10, "T too small"); //与模板参数有关,只会在第二阶段报错
3.根据两阶段检查,模板在实例化时要看到完整定义,最简单的方法是将实现放在头文件中。
1.2 模板参数推断
1.函数模板的模板参数可以通过传递的函数参数进行推断。
2.函数推断时会用到参数类型转换,规则如下:
a.如果函数参数是按引用传递的,任何类型转换都不被允许。(此处有疑问,const 转换还是可以的)
b.如果函数参数是按值传递的,可以进行退化(decay)转换:const(指针或者引用只有顶层 const 可以被忽略) 和 volatile 被忽略;引用变为非引用;数组和函数变为对应指针类型。
template <typename T>
void RefFunc(const T &a, const T &b);
template <typename T>
void NoRefFunc(T a, T b);
int main()
int *const ic = nullptr;
const int *ci = nullptr;
int *p = nullptr;
RefFunc(p, ic); // ok 顶层const可以被忽略 T 为 int *
RefFunc(p, ci); // error 底层const不可以忽略
NoRefFunc(p, ci); // error 底层const不可以忽略
int i = 0;
int &ri = i;
NoRefFunc(i, ri); // ok ri从int &转换为int
int arr[4];
NoRefFunc(p, arr); // ok arr 被推断为int *
NoRefFunc(4, 5.0); // error T 可以推断为int或double
3.上文的最后一句调用,类型推断具有二义性,无法正确实例化。可以通过以下方式解决
a.类型转换:
b.显式指定模板实参:
NoRefFunc(static_cast<double>(4), 5.0); // ok 类型转换
NoRefFunc<int>(4, 5.0); // 显式指定
4.函数模板无法通过默认参数推断模板参数。如果函数模板只有一个函数参数,且函数参数提供了默认值的情况,应该为模板类型参数 T 也提供和函数参数默认值匹配的默认类型。
template <typename T>
void Default(T t = 0);
Default(); // error 无法推断为int
template <typename T = int>
void Default(T t = 0);
Default(); // ok 默认类型为int
1.3 多模板参数
1.当函数返回类型不能或不便由函数参数类型直接推断时,可以在函数模版中新增模板参赛指定返回类型。
2.c++11 之后,可以通过 auto + decltype +尾后返回类型 推断函数模板返回类型。当函数参数为引用类型时,返回类型应该为非引用。而decltype 会保留引用,因此还需通过 decay 进行类型退化。
3.c++14 之后,可以通过 auto 直接推断函数模板返回类型,前提是函数内部的多个返回语句推断出的返回类型要一致。auto 会自动对类型进行 decay。
4.c++11 之后,可以通过 common_type 返回多个模版类型参赛的公共类型,common_type 返回的类型也是 decay 的。
#include<type_traits>
// 单独通过RT指定返回类型
template <typename RT, typename T1, typename T2>
RT max1(const T1& a, const T2& b) return a > b ? a : b;
// auto c++11支持 通过decay 进行类型退化 typename 用于声明嵌套从属名称 type 为类型而不是成员
template <typename T1, typename T2>
auto max2(const T1& a, const T2& b) -> typename std::decay<decltype(a > b ? a : b)>::type return a > b ? a : b;
// auto c++14支持
template <typename T1, typename T2>
auto max3(const T1& a, const T2& b) return a > b ? a : b;
// common_type c++11支持 max4(5, 7.3) max4(7.4, 5) 的返回类型均被推断为double
template <typename T1, typename T2>
typename std::common_type<T1, T2>::type max4(const T1& a, const T2& b) return a > b ? a : b;
1.4 默认模板参数
1.可以给模板参数指定默认值。
// 默认模板参赛 因为RT需要T1 T2推断,所以放在最后
template <typename T1, typename T2, typename RT = typename std::common_type<T1, T2>::type>
RT max5(const T1& a, const T2& b) return a > b ? a : b;
1.5 函数模板重载
1.一个非模板函数可以和同名的函数模板共存,并且函数模板可实例化为和非模板函数具有相同类型参数的函数。函数调用时,若匹配度相同,将优先调用非模板函数。但若显式指定模板列表,则优先调用函数模板。
2.函数模板不可以进行类型自动转换,非模板函数可以。
#pragma once
template <typename T>
T max(T a, T b)
return a > b ? a : b;
template <typename RT, typename T1, typename T2>
RT max(T1 a, T2 b)
return a > b ? a : b;
int max(int a, int b)
return a > b ? a : b;
int main()
::max(6, 8); // 调用非模板函数
::max<>(6, 8); // 调用函数模板 max<int>
::max('a', 'b'); // 调用函数模板 max<char>
::max(4, 4.0); // 通过类型转换调用非模板函数
::max<double>(4, 4.0); //指定了返回类型 调用max<double,int,double>
3.调用函数模板时,必须保证函数模板已经定义。
int max(int a, int b)
return a > b ? a : b;
template <typename T>
T max(T a, T b, T c)
return max(max(a,b),c); //T为int时,并不会调用max<int> 而是调用非模板函数
template <typename T>
T max(T a, T b)
return a > b ? a : b;
max(1, 2, 3); // 最终调用非模板函数比较
max("sjx", "wyl", "shh"); // error 找不到二元的max<const char *>
二、类模板
2.1 stack 类模板实现
1.类模板不可以定义在函数作用域或者块作用域内部,通常定义在 global/namespace/类作用域。
#include<vector>
#include<iostream>
template <typename T>
class Stack
public:
void push(const T& value);
void pop();
T top();
int size() const elem_.size(); ;
bool empty() const return elem_.empty(); ;
void print(std::ostream & out) const;
protected:
std::vector<T> elem_;
;
template <typename T>
void Stack<T>::push(const T &value)
elem_.push_back(value);
template <typename T>
void Stack<T>::pop()
elem_.pop_back();
template <typename T>
T Stack<T>::top()
return elem_.back();
template <typename T>
void Stack<T>::print(std::ostream &out) const
for (auto e : elem_)
out << e << std::endl;
2.2 stack 类模板使用
1.直到 c++17,使用类模板都需要显式指定模板参数。
2.类模板的成员函数只有在调用的时候才会实例化。
2.3 部分使用类模板
1.类模板实例化时,模板实参只需要支持被实例化部分所有用到的操作。
int main()
// 只会实例化类模板中的push 和 print函数
Stack<int> s;
s.push(3);
s.print(std::cout);
// Stack<int>未重载<<运算符,实例化print函数时失败
Stack<Stack<int>> ss;
ss.push(s);
ss.print(std::cout);
return 0;
2.c++11 开始,可以通过 static_assert 和 type_traits 做一些简单的类型检查
template <typename T>
class C
static_assert(std::is_default_constructible<T>::value, "class C requires default contructible");
;
2.4 友元
2.5 模板特化
1.可以对类模板的一个参数进行特化,类模板特化的同时需要特化所有的成员函数,非特化的函数在特化后的模板中属于未定义函数,无法使用。
// stringle类型特化
template <>
class Stack<std::string>
public:
void push(const std::string& value);
/* 特化其他成员函数*/
;
2.6 模板偏特化
1.类模板特化时,可以只特化部分参数,或者对参数进行部分特化。
// 指针类型特化
template <typename T>
class Stack<T *>
public:
void push(T *value);
void pop();
T* top();
int size() const elem_.size(); ;
bool empty() const return elem_.empty(); ;
protected:
std::vector<T *> elem_;
;
template <typename T>
void Stack<T*>::push(T *value)
elem_.push_back(value);
template <typename T>
void Stack<T*>::pop()
elem_.pop_back();
template <typename T>
T* Stack<T*>::top()
return elem_.back();
2.7 默认类模板参数
1.类模板也可以指定默认模板参数。
template <typename T, typename COND = std::vector<T> >
class Stack
public:
void push(const T& value);
void pop();
T top();
int size() const elem_.size(); ;
bool empty() const return elem_.empty(); ;
protected:
COND elem_;
;
template <typename T, typename COND>
void Stack<T, COND>::push(const T &value)
printf("template 1\\n");
elem_.push_back(value);
template <typename T, typename COND>
void Stack<T, COND>::pop()
elem_.pop_back();
template <typename T, typename COND>
T Stack<T, COND>::top()
return elem_.back();
2.8 类型别名
1.为了便于使用,可以给类模板定义别名。
typedef Stack<int> IntStack;
using DoubleStack = Stack<double>;
2.c++11 开始可以定义别名模板,为一组类型取一个方便的名字。
template <typename T>
using DequeStack = Stack<T, std::deque<T>>;
3.c++14 开始,标准库使用别名模板技术,为所有返回一个类型的 type_trait 定义了快捷的使用方式。
// stl库定义
namespace std
template <typename T>
using add_const_t = typename add_const<T>::type;
typename add_const<T>::type; //c++ 11 使用
std::add_const_t<T>; //c++14使用
2.9 类模板类型推导
1.c++17 开始,如果构造函数能够推断出所有模板参数的类型,那么不需要指定参数类型了。
template <typename T>
class Stack
public:
Stack() = default;
Stack(T e): elem_(e);
protected:
std::vector<T> elem_;
;
Stack intStack = 0; //通过构造函数推断为int
2.类型推导时,构造函数参数应该按照值传递,而非按引用。引用传递会导致类型推断时无法进行 decay 转化。
Stack strStack = "sjx";
//若构造函数参数为值传递,则T为const char *,引用传递时则为const char[4]
3.c++ 17 支持提供推断指引来提供额外的推断规则,推断指引一般紧跟类模板定义之后。
// 推断指引,传递字符串常量时会被推断为string
Stack<const char *> -> Stack<std::string>
2.10 聚合类的模板化
1.聚合类:没有显式定义或继承来的构造函数,没有非 public 的非静态成员,没有虚函数,没有 virtual,private ,protected 继承。聚合类也可以是模板。
template <typename T>
struct ValueWithComment
T val;
std::string comment;
;
ValueWithComment<int> vc;
vc.val = 42;
vc.comment = "sjx";
三、非类型模板参数
3.1 类模板的非类型模板参数
1.模板参数不一定是类型,可以是数值,如可以给 Stack 指定最大容量,避免使用过程元素增删时的内存调整。
template <typename T, int MAXSIZE>
class Stack
public:
Stack():num_(0);
void push(const T& value);
void pop();
T top();
int size() const return num_; ;
bool empty() const return num_ == 0; ;
protected:
T elem_[MAXSIZE];
int num_;
;
template <typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::push(const T &value)
printf("template 1\\n");
assert(num_ < MAXSIZE);
elem_[num_++] = value;
template <typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::pop()
assert(num_ > 0);
--num_;
template <typename T, int MAXSIZE>
T Stack<T, MAXSIZE>::top()
assert(num_ > 0);
return elem_[0];
3.2 函数模板的非类型模板参数
1.函数模板也可以指定非类型模板参数。
template<typename T, int VAL>
T addval(const T &num)
return num + VAL;
int main()
std::vector<int> nums = 1, 2, 3, 4, 5;
std::vector<int>nums2(nums.size(),0);
std::transform(nums.begin(),nums.end(),nums2.begin(),addval<int,5>);
for(auto num : nums2)
printf("%d\\n",num);
3.3 非类型模板参数限制
1.非类型模板参数只能是整形常量(包含枚举),指向 objects/functions/members 的指针,objects 或者 functions 的左值引用,或者是 std::nullptr_t(类型是 nullptr),浮点数和类对象不能作为非类型模板参数。
2.当传递对象的指针或者引用作为模板参数时,对象不能是字符串常量,临时变量或者数据成员以及其它子对象。
3.对于非类型模板参数是 const char*的情况,不同 c++版本有不同限制
a. C++11 中,字符串常量对象必须要有外部链接
b. C++14 中,字符串常量对象必须要有外部链接或内部链接
c. C++17 中, 无链接属性也可
4.内部链接:如果一个名称对编译单元(.cpp)来说是局部的,在链接的时候其他的编译单元无法链接到它且不会与其它编译单元(.cpp)中的同样的名称相冲突。例如 static 函数,inline 函数等。
如果一个名称对编译单元(.cpp)来说不是局部的,而在链接的时候其他的编译单元可以访问它,也就是说它可以和别的编译单元交互。
6.非类型模板参数的实参可以是任何编译器表达式,不过如果在表达式中使用了 operator >,就必须将相应表达式放在括号里面,否则>会被作为模板参数列表末尾的>,从而截断了参数列表。
#include<string>
template < double VAL > // error 浮点数不能作为非类型模板参数
double process(double v)
return v * VAL;
template < std::string name > // error class对象不能作为非类型模板参数
class MyClass
;
template < const char * name >
class Test
;
extern const char s01[] = "sjx"; // 外部链接
const char s02[] = "sjx"; // 内部链接
template<int I, bool B>
class C;
int main()
Test<"sjx"> t0; // error before c++17
Test<s01> t1; // ok
Test<s02> t2; // since c++14
static const char s03[] = "sjx"; // 无链接
Test<s03> t3; // since c++17
C<42, sizeof(int)> 4 > c; //error 第一个>被认为模板参数列表已经结束
C<42, (sizeof(int)> 4) > c; // ok
3.4 用 auto 作为非模板类型参数的类
1.从 C++17 开始,可以不指定非类型模板参数的具体类型(代之以 auto),从而使其可以用于任意有效的非类型模板参数的类型。
template <typename T, auto MAXSIZE>
class Stack
public:
using size_type = decltype(MAXSIZE); // 根据MAXSIZE推断类型
public:
Stack():num_(0);
void push(const T& value);
void pop();
T top();
size_type size() const num_; ;
bool empty() const num_ == 0; ;
protected:
T elem_[MAXSIZE];
size_type num_;
;
template <typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::push(const T &value)
printf("template 1\\n");
assert(num_ < MAXSIZE);
elem_[num_++] = value;
template <typename T, int MAXSIZE>
void Stack<T, MAXSIZE>::pop()
assert(num_ > 0);
--num_;
template <typename T, int MAXSIZE>
T Stack<T, MAXSIZE>::top()
assert(num_ > 0);
return elem_[0];
int main()
Stack<int,20u> s1; // size_type为unsigned int
Stack<double,40> s2; // size_type为int
四、可变参数模板
4.1 可变参数模板
1.c++11 开始,模板可以接收一组数量可变的参数。
#include <iostream>
void print(); // 递归基
template<typename T, typename ...Types>
void print(T firstArg, Types ... args)
std::cout << firstArg << std::endl;
print(args...);
int main()
print(7.5, "hello", 10); // 调用三次模板函数后再调用普通函数
当两个函数模板的区别只在于尾部的参数包的时候,会优先选择没有尾部参数包的函数模板。
// 使用模板函数的递归基,最后只剩一个参数时会优先使用本模板
template<typename T>
void print(T arg)
std::cout << arg << std::endl;
3.c++11 提供了sizeof...运算符来统计可变参数包中的参数数目
template<typename T, typename ...Types>
void print(T firstArg, Types ... args)
std::cout << sizeof...(Types) << std::endl;
std::cout << sizeof...(args) << std::endl;
std::cout << firstArg << std::endl;
print(args...);
4.函数模板实例化时会将可能调用的函数都实例化。
#include <iostream>
template<typename T, typename ...Types>
void print(T firstArg, Types ... args)
std::cout << firstArg << std::endl;
if(sizeof...(args) > 0)
print(args...); // error 缺少递归基。即使只在参数包数目>0时调用,args为0个参数时的print函数
int main()
print(7.5, "hello", 10); // 调用三次模板函数后再调用普通函数
4.2 折叠表达式
1.C++17 提供了一种可以用来计算参数包(可以有初始值)中所有参数运算结果的二元运算符 ...
template<typename ...T>
auto sum(T ...s)
return (... + s); // ((s1+s2)+s3)...
4.3 可变参数模板的使用
4.4 变参类模板和变参表达式
可变参数包可以出现在数学表达式中,用于表达式运算。
template<typename ...T>
void PrintDouble(const T &... args)
print(args + args...); // 将args翻倍传给print
template<typename ...T>
void addOne(const T & ...args)
print(args + 1 ...); // 1和...中间要有空格
int main()
PrintDouble(7.5, std::string("hello"), 10); // 等价调用print(7.5 + 7.5, std::string("hello") + std::string("hello"), 10 + 10);
addOne(1, 2, 3); // 输出 2 3 4
可变参数包可以出现在下标中,用于访问指定下标的元素。
可变参数包可以为非类型模板参数。
template<typename C, typename ...Index>
void printElems(const C &coll, Index ...idx)
print(coll[idx]...);
template<typename C, std::size_t ...Idx> // 参数包为非类型模板参数
void printIndex(const C &coll)
print(coll[Idx]...);
int main()
std::vector<std::string> coll = "wyl", "sjx", "love";
printElems(coll, 2, 0, 1); //相当于调用print(coll[2], coll[0], coll[1]);
printIndex<std::vector<std::string>, 2, 0, 1>(coll); // 等同于以上调用
可变参数包也可以作为类模板参数,用于表示数据成员的类型,或对象可能的类型等含义。
template <typename... T>
class Tuple;
Tuple<int, char> t; // 可以表示数据成员的类型
template <typename... T>
class Variant;
Variant<int, double, bool, float> v; // 可以表示对象可能的类型
5.推断指引也可以是可变参数的。
namespace std
// std::array a42,43,44 会被推断为 std::array<int,3> a42,43,44
template<typename T, typename… U> array(T, U…) -> array<enable_if_t<(is_same_v<T, U> && …), T>, (1 + sizeof…(U))>;
五、基础技巧
5.1 typename 关键字
1.c++规定模板中通过域作用符访问的嵌套从属名称不是类型名称,为了表明该名称是类型,需要加上 typename 关键字。
template<typename T>
class MyClass
public:
void foo()
/* 若无typename SubType会被当做T中的一个static成员或枚举值
下方的表达式被理解为SubType 和 ptr的乘积 */
typename T::SubType *ptr;
//...
;
5.2 零初始化
1.c++中对于未定义默认构造函数的类型对象,定义时一般不会进行默认初始化,这时候对象的值将是未定义的。
2.在模板中定义对象时,为了避免产生未定义的行为,可以进行零初始化。
template<typename T>
void foo()
T x = T(); // 对x提供默认值
5.3 使用 this ->
1.若类模板的基类也是类模板,这时在类模板中不能直接通过名称调用从基类继承的成员,而应该通过 this-> 或 Base::。
template<typename T>
class Base
public:
void bar();
;
template<typename T>
class Derived:public Base<T>
public:
void foo()
bar(); //ERROR 无法访问基类bar成员,若global存在bar函数,则会访问全局bar函数
this->bar(); // ok
Base<T>::bar(); // ok
;
5.4 使用裸数组或字符串常量的模板
1.当向模板传递裸数组或字符串常量时,如果是引用传递,则类型不会 decay;如果是值传递,则会 decay。
2.也可以通过将数组或字符串长度作为非类型模板参数,定义可以适配不同长度的裸数组或字符串常量的模板。
template <typename T>
void foo(T t);
template <typename T>
void RefFoo(const T &t);
template<typename T, int N, int M>
bool less (T(&a)[N], T(&b)[M])
for (int i = 0; i<N && i<M; ++i)
if (a[i]<b[i]) return true; if (b[i]<a[i]) return false;
return N < M;
int main()
foo("hello"); // T 为const char *
RefFoo("hello"); // T 为const char[6]
less("sjx", "wyl"); // T 为const char, N,M为3
5.5 成员模板
1.不管类是普通类还是类模板,类中的成员函数都可以定义为模板。
2.若是将构造函数或者赋值运算符定义为模板函数,此时定义的模板函数不会取代默认的的构造函数和赋值运算符。下面定义的 operater = 只能用于不同类型的 stack 之间的赋值,若是相同类型,仍然采用默认的赋值运算符。
#include<deque>
template <typename T, typename COND = std::deque<T> >
class Stack
public:
void push(const T& value);
void pop();
T top();
int size() const elem_.size(); ;
bool empty() const return elem_.empty(); ;
//实现不同类型的stack之间的相互赋值
template <typename T2>
Stack<T,COND> & operator = (const Stack<T2>& s);
protected:
COND elem_;
;
template <typename T, typename COND>
template <typename T2>
Stack<T,COND> & Stack<T,COND>::operator = (const Stack<T2>& s)
if((void *)this == (void *)&s)
return *this;
Stack<T2> tmp(s);
elem_.clear();
while(!tmp.empty())
elem_.push_front(tmp.top());
tmp.pop();
return *this;
2.成员函数模板也可以被偏特化或者全特化。
#include<string>
class BoolString
private:
std::string value;
public:
BoolString (std::string const& s): value(s)
template<typename T = std::string>
T get() const
return value;
;
// 进行全特化, 全特化版本的成员函数相当于普通函数,
// 放在头文件中会导致重复定义,因此必须加inline
template<>
inline bool BoolString::get<bool>() const
return value == "true" || value == "1";
int main()
BoolString s1("hello");
s1.get(); // "hello"
s1.get<bool>(); // false
3.当通过依赖于模板参数的对象通过.调用函数模板时,需要通过 template 关键字提示<是成员函数模板参数列表的开始,而非<运算符。
#include<bitset>
#include<iostream>
template<unsigned int N>
void printBitSet(const std::bitset<N> &bs)
// bs依赖于模板参数N 此时为了表明to_string后是模板参数,需要加template
std::cout << bs.template to_string<char,std::char_traits<char>,std::allocator<char> >() << std::endl;
4.c++14 引入的泛型 lambda 是对成员函数模板的简化。
[](auto x, auto y)
return x + y;
// 编译将以上lamba转化为下方类
class SomeCompilerSpecificName
public:
SomeCompilerSpecificName();
template<typename T1, typename T2>
auto operator() (T1 x, T2 y) const
return x + y;
;
5.6 变量模板
1.c++14 开始,可以通过变量模板对变量进行参数化。
2.变量模板的常见应用场景是定义代表类模板成员的变量模板。
3.c++17 开始,标准库用变量模板为其用来产生一个值(布尔型)的类型萃取定义了简化方式。
#include<iostream>
template <typename T = double>
constexpr T pi3.1415926;
std::cout<< pi<> <<std::endl; // <>不可少,输出3.1415926
std::cout<< pi<int> <<std::endl; // 输出3
template<typename T>
class MyClass
public:
static constexpr int max = 1000; // 类静态成员
;
// 定义变量模板表示类静态成员
template <typename T>
int myMax = MyClass<T>::max;
// 使用更方便
auto i = myMax<int>; // 相当于auto i = MyClass<int>::max;
// 萃取简化方式,since c++17
namespace std
template<typename T>
constexpr bool is_const_v = is_const<T>::value;
5.7 模板模板参数
1.当非类型模板参数是一个模板时,我们称它为模板模板参数。
2.实例化时,模板模板参数和实参的模板参数必须完全匹配。
#include <deque>
#include <vector>
// 错误定义 deque 中的模板参数有两个:类型和默认参数allocator
// 而模板模板参数Cont的参数只有类型Elem
template<typename T,
template<typename Elem> class Cont = std::deque>
class Stack
private:
Cont<T> elems; // elements
public:
void push(T const&); // push element
void pop(); // pop element
T const& top() const; // return top element
bool empty() const // return whether the stack is empty
return elems.empty();
;
// 正确定义
template<typename T, template<typename Elem, typename =
std::allocator<Elem>> class Cont = std::deque>
class Stack
private:
Cont<T> elems; // elements
public:
void push(T const&); // push element
void pop(); // pop element
T const& top() const; // return top element
bool empty() const // return whether the stack is empty
return elems.empty();
;
// 使用
Stack<int> iStack;
Stack<double,std::vector> dStack;
六、移动语义和 enable_if<>
6.1 完美转发
1.c++11 引入了引用折叠:创建一个引用的引用,引用就会折叠。除了右值引用的右值引用折叠之后还是右值引用之外,其它的引用全部折叠成左值引用。
2.基于引用折叠和 std::forward,可以实现完美转发:将传入将被参数的基本特性(是否 const,左值、右值引用)转发出去。
#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 variable
X const c; // create constant
f(v); // f() for variable calls f(X&) => calls g(X&)
f(c); // f() for constant 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 move-enabled variable calls f(X&&)=>calls g(X&&)
6.2 特殊成员函数模板
1.当类中定义了模板构造函数时:
a.定义的模板构造函数不会屏蔽默认构造函数。
b.优先选用匹配程度高的构造函数
c.匹配程度相同时,优先选用非模板构造函数
#include <utility>
#include <string>
#include <iostream>
class Person
private:
std::string name;
public:
// generic constructor for passed initial name:
template<typename STR>
explicit Person(STR&& n) : name(std::forward<STR>(n))
std::cout << "TMPL-CONSTR for ’" << name <<std::endl;
// 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()
Person p1("tmp"); // ok 调用模板函数n 被推断为const char [4]类型,可以赋值给string
Person p2(p1); // error 模板函数相比Person (Person const& p)构造函数更匹配,n 被推断为Person &类型,最终将Person对象赋值给string
Person p3(std::move(p1)); // ok 同等匹配程度时,优先调用Person (Person&& p)
6.3 通过 std::enable_if 禁用模板
1.c++11 提供了辅助模板 std::enable_if,使用规则如下:
a.第一个参数是布尔表达式,第二个参数为类型。若表达式结果为 true,则 type 成员返回类型参数,此时若未提供第二参数,默认返回 void。
b.若表达式结果为 false,根据替换失败并非错误的原则,包含 std::enable_if 的模板将会被忽略。
2.c++14 提供了别名模板技术(见 2.8 节),可以用 std::enable_if_t<>代替 std::enable_if<>::type.
3.若不想在声明中使用 std::enable_if,可以提供一个额外的、有默认值的模板参数。
#include <type_traits>
template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
foo1()
// std::enable_if为true时等同下方函数,未提供第二参数默认返回void; false时函数模板被忽略
// void foo1();
template<typename T>
typename std::enable_if<(sizeof(T) > 4), bool>::type
foo2()
// std::enable_if为true时等同下方函数,false时函数模板被忽略
// bool foo2();
// 提供额外默认参数进行类型检查
template<typename T, typename = typename std::enable_if<(sizeof(T) > 4)>::type>
void foo3()
// std::enable_if为true时等同下方函数模板,false时函数模板被忽略
// template<typename T, typename = void>
// void foo3();
6.4 使用 std::enable_if
1.通过 std::enable_if 和标准库的类型萃取 std::is_convertiable<FROM, TO>可以解决 6.2 节构造函数模板的问题。
class Person
private:
std::string name;
public:
// 只有STR可以转换为string时才有效
template<typename STR, typename = typename std::enable_if<std::is_convertible<STR, std::string>::value>::type>
explicit Person(STR&& n) : name(std::forward<STR>(n))
std::cout << "TMPL-CONSTR for ’" << name <<std::endl;
// 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";
;
6.5 使用 concept 简化 enable_if<>表达式
1.c++20 提出了 concept 模板可以进行编译期条件检查,大大简化了 enable_if
template <typename T>
concept convert_to_string = std::is_convertible<STR, std::string>::value;
class Person
private:
std::string name;
public:
// 只有STR可以转换为string时才有效
template<convert_to_string STR>
explicit Person(STR&& n) : name(std::forward<STR>(n))
std::cout << "TMPL-CONSTR for ’" << name <<std::endl;
// 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";
;
七、按值传递还是按引用传递
7.1 按值传递
1.当函数参数按值传递时,原则上所有参数都会被拷贝。
2.当传递的参数是纯右值时,编译器会优化,避免拷贝产生;从 c++17 开始,要求此项优化必须执行。
3.按值传递时,参数会发生 decay,如裸数组退化为指针,const、volatile 等限制符会被删除。
#include <string>
template <typename T>
void foo(T t);
int main()
std::string s("Sjx");
foo(s); // 进行拷贝
foo(std::string("sjx")); // 避免拷贝
foo("sjx"); // T decay为const char *
7.2 按引用传递
1.当函数参数按照引用传递时,不会被拷贝,并且不会 decay。
2.当函数参数按照引用传递时,一般是定义为 const 引用,此时推断出的类型 T 中无 const。
3.当参数定义为非常量引用时,说明函数内部可以修改传入参数,此时不允许将临时变量传给左值引用。此时若传入参数是 const 的,则类型 T 被推断为 const,此时函数内部的参数修改将报错.
template <typename T>
void print(const T &t);
template <typename T>
void out(T &t);
template <typename T>
void modify(T &t)
t = T();
;
int main()
print("sjx"); // arg为const char[3], T 为char[3]
std::string s("Sjx");
out(s); // ok
out(std::string("sjx")); // error 不能将临时变量传给左值引用
const std::string name = "sjx";
modify(name); //error T 被推断为 const std::string, 此时modify不能修改传入参数
4.对于给非 const 引用参数传递 const 对象导致编译失败的情形,可以通过 static_assert std::enable_if 或者 concept 等方式进行检查。
// static_assert 触发编译期错误
template <typename T>
void modify(T &t)
static_assert(!std::is_const<T>::value, "param is const");
t = T();
;
// std::enable_if禁用模板
template <typename T, typename = typename std::enable_if<!std::is_const<T>::value> >
void modify(T &t)
t = T();
;
// concept禁用模板
template <typename T>
concept is_not_const = !std::is_const<T>::value;
template <is_not_const T>
void modify(T &t)
t = T();
;
5.当完美转发时,会将函数参数定义为右值引用。此时若在函数内部用 T 定义未初始化的变量,会编译失败。
template<typename T>
void passR(T &&t)
T x;
std::string s("Sjx");
passR(s); // error T 推断为string &,初始化时必须绑定到对象
7.3 使用 std::ref()和 std::cref()
1.c++11 开始,若模板参数定义为按值传递时,调用者可以通过 std::cref 或 std::ref 将参数按照引用传递进去。
2.std::cref 或 std::ref创建了一个 std::reference_wrapper<>的对象,该对象引用了原始参数,并被按值传递给了函数模板。std::reference_wrapper<>对象只支持一个操作:向原始对象的隐式类型转换。
template<typename T>
void foo(T arg)
T x;
int main()
std::string s = "hello";
foo(s); // T 为std::string
foo(std::cref(s)); // error T 为std::reference_wrapper<std::string> 只支持向std::string的类型转换
7.4 处理字符串常量和裸数组
1.字符串常量或裸数组传递给模板时,如果是按值传递,则会 decay;如果是按照引用传递,则不会 decay。实际应用时,可以根据函数作用加以选择,若要比较大小,一般是按照引用传递;若是比较参数类型是否相同,则可以是按值传递。
7.5 处理返回值
1.函数返回值也可以是按值返回或按引用返回。若返回类型为非常量引用,则表示可以修改返回对象引用的对象。
2.模板中即使使用 T 作为返回类型,也不一定能保证是按值返回。
template<typename T>
T retR(T &&p)
return T();
template<typename T>
T retV(T t)
return T();
int main()
int x;
retR(x); // error T 被推断为int &
retV<int &>(x); // error T推断为int &
3.为保证按值返回,可以使用 std::remove_reference<>或 std::decay<>。
7.6 关于模板参数声明的推荐方法
1.一般通常按值传递,如有特殊需要,可以结合实际按引用传递。
2.定义的函数模板要明确使用范围,不要过分泛化。
八、编译期编程
8.1 模板元编程
1.模板元编程:在编译期通过模板实例化的过程计算程序结果。
/* 定义用于编译期判断素数的模板 */
template<unsigned p ,unsigned d>
struct DoIsPrime
// 从p%d !=0 开始依次判断 p%(d-1)!=0,p%(d-2)!=0,...
static constexpr bool value = (p % d != 0) && DoIsPrime<p, d-1>::value;
;
// 递归基
template<unsigned p>
struct DoIsPrime<p ,2>
static constexpr bool value = (p % 2 != 0);
;
// 提供给用户调用的模板
template<unsigned p>
struct IsPrime
static constexpr bool value = DoIsPrime<p, p / 2>::value;
;
// 对于p/2<2的情形进行特化
template<>
struct IsPrime<0>
static constexpr bool value = false;
;
template<>
struct IsPrime<1>
static constexpr bool value = false;
;
template<>
struct IsPrime<2>
static constexpr bool value = false;
;
template<>
struct IsPrime<3>
static constexpr bool value = false;
;
int main()
IsPrime<9>::value; // 编译期通过层层递归实例化最后得到结果为false
8.2 通过 constexpr 进行计算
1.c++11 提出了 constexpr 关键字可以用于修饰函数返回值,此时该函数为常量表达式函数,编译器可以在编译期完成该函数的计算。
2.c++11 中规定常量表达式函数在使用前必须要知道完整定义,不能仅仅是声明,同时函数内部只能有一条返回语句。
3.c++14 中移除了常量表达式函数只能有一条返回语句的限制。
4.编译器可以在编译期完成该函数计算,但是是否进行还取决于上下文环境及编译器。
// constexpr函数只能有一个return语句
constexpr bool doIsPrime(unsigned p, unsigned d)
return d != 2 ? (p % d != 0) && doIsPrime(p, d - 1) : (p % 2 != 0);
constexpr bool isPrime(unsigned p)
return p < 4 ? !(p < 2) : doIsPrime(p, p / 2);
int main()
constexpr bool ret = isPrime(9); // constexpr 限定必须在编译期得到结果
8.3 通过模板偏特化进行路径选择
1.可以通过模板偏特化在不同的实现方案之间做选择。
// 根据是否是素数判断是否喜欢
template <unsigned int SZ, bool = isPrime(SZ)>
struct IsLove;
// 特化两个版本处理
template< unsigned int SZ>
struct IsLove<SZ, false>
static constexpr bool isLove = false;
;
template< unsigned int SZ>
struct IsLove<SZ, true>
static constexpr bool isLove = true;
;
int main()
IsLove<520>::isLove; // 编译期就知道了520不是喜欢
8.4 SFINAE(替换失败不是错误)
SFINAE:当函数调用的备选方案中出现函数模板时,编译器根据函数参数确定(替换)函数模板的参数类型及返回类型,最后评估替换后函数的匹配程度。替换过程中可能失败,此时编译器会忽略掉这一替换结果。
替换和实例化不同,替换只涉及函数函数模板的参数类型及返回类型,最后编译器选择匹配程度最高的函数模板进行实例化。
#include<vector>
#include<iostream>
// 返回裸数组长度的模板,只有用裸数组替换时才能成功
template<typename T, unsigned N>
std::size_t len (T(&)[N])
return N;
// 只有含有T::size_type的类型才能替换成功
template<typename T>
typename T::size_type len (T const& t)
return t.size();
// ... 表示为可变参数,匹配所有类型, 但匹配程度最差
std::size_t len(...)
return 0;
int main()
int a[10];
std::cout << len(a) <<std::endl; // 匹配裸数组
std::cout << len("sjx") <<std::endl; // 匹配裸数组
std::vector<int> v;
std::cout << len(v) <<std::endl; // T::size_type
int *p;
std::cout << len(p) <<std::endl; // 函数模板均不匹配,最后调用变参函数
std::allocator<int> x;
/* std::allocator 定义了size_type,所以匹配T::size_type和变参函数,
前者匹配程度更高,因此选择该模板。但在实例化时会发现allocator不存在size成员 */
std::cout << len(x) <<std::endl; //error
我们可以通过 SFINAE 原理在一些情形下忽略掉该模板。一种简单用法是 6.3 6.4 中的 std::enable_if;对于上例,则可以采用 auto + 尾后返回的方式,使得在替换时就知道 T 中必须含有 size 成员的限制。
/* decltype中采用逗号表达式,只有若T中不存在size成员,则替换失败。
替换成功时,才会将逗号表达式最后一句作为返回类型 */
template<typename T>
auto len (T const& t) -> decltype((void)(t.size()) , T::size_type)
return t.size();
8.5 编译期 if
除了前面介绍的忽略模板的方法,c++17 还提供了编译期的条件判断语法 if constexpr(...)。
// 修改4.1中的print函数
template<typename T, typename… Types>
void print (T const& firstArg, Types const&… args)
std::cout << firstArg << std::endl; // print只有一个参数时,只会编译这一部分
if constexpr(sizeof…(args) > 0)
print(args…); // 只有args不为空的时候才会继续递归实例化
C++17)
九、实践中使用模板
9.1 包含模式
模板在编译期会进行实例化,实例化时需要提供模板的定义,所以对于模板相关代码,正确用法是将声明和定义均置于头文件中。
9.2 模板和 inline
函数模板全特化后和普通函数相同,但函数模板一般定义在头文件中,为了避免在多个模块 include 时出现重复定义的错误,一般将全特化后的函数模板定义为 inline。
template<typename T>
void foo(const T& t);
// 全特化函数
template<>
inline void foo<int>(const int& i);
9.3 预编译头文件
预编译头文件不在 c++标准要求中,具体由编译器实现。
预编译头文件:如果多个代码文件的前 n 行均相同,编译器就可以先对前 n 行进行编译,再依次对每个文件从 n+1 行进行编译。
为了充分利用预编译头文件功能,实践中建议文件中#include 的顺序尽量相同。
9.4 破译大篇幅的错误信息
预编译头文件不在 c++标
十、模板基本术语
10.1 “类模板”还是“模板类”
10.2 替换,实例化和特例化
替换:在用模板实参去查找匹配的模板时,会尝试用实参去替换模板参数,见 8.4 节。
实例化:查找到最匹配的模板后,根据实参从模板创建出常规类或函数的过程。
特例化:对模板中的部分或全部参数进行特化,定义新模板的过程。
10.3 声明和定义
声明:将一个名称引入 c++作用域内,并不需要知道名称的相关细节。
定义:如果在声明时提供了细节,声明就变成了定义。
// 声明
class A;
extern int v;
void f();
// 定义
class A;
int v = 1;
void f();
10.4 唯一定义法则
唯一定义法制(ODR)
a.常规(比如非模板)非 inline 函数和成员函数,以及非 inline 的全局变量和静态数据成员,在整个程序中只能被定义一次.
b. Class 类型(包含 struct 和 union),模板(包含部分特例化,但不能是全特例化),以及 inline 函数和变量,在一个编译单元中只能被定义一次,而且不同编译单元间的定义 应该相同.
10.5 模板参数和模板实参
1.模板参数:模板定义中模板参数列表中的参数。
模板实参:实例化模板参数时传入的参数。
十一、泛型库
11.1 可调用对象
c++可调用对象类型
a.函数指针
b. 仿函数
c. 存在一个函数指针或者函数引用的转换函数的 class 类型
#include <iostream>
#include <vector>
template<typename Iter, typename Callable>
void foreach (Iter current, Iter end, Callable op)
while (current != end) //as long as not reached the end
op(*current); // call passed operator for current element
++current; // and move iterator to next element
// a function to call:
void func(int i)
std::cout << "func() called for: " << i << std::endl;
// a function object type (for objects that can be used as functions):
class FuncObj
public:
void operator() (int i) const //Note: const member function
std::cout << "FuncObj::op() called for: " << i << std::endl;
;
int main()
std::vector<int> primes = 2, 3, 5, 7, 11, 13, 17, 19;
foreach (primes.begin(), primes.end(), func); // function as callable (decays to pointer)
foreach(primes.begin(), primes.end(), &func); // function pointer as callable
foreach(primes.begin(), primes.end(), FuncObj()); // function object as callable
foreach(primes.begin(), primes.end(), [] (int i) //lambda as callable
std::cout << "lambda called for: " << i << std::endl;);
11.2 其它实现泛型库的工具
标准库中提供了丰富多样的 type traits 工具,使用时一定要注意类型萃取的精确定义。
#include <type_traits>
template<typename T>
class C
// ensure that T is not void (ignoring const or volatile):
static_assert(!std::is_same_v<std::remove_cv_t<T>,void>,
"invalid instantiation of class C for void type");
public:
template<typename V>
void f(V&& v)
if constexpr(std::is_reference_v<T>) … // special code if T is a reference type
if constexpr(std::is_convertible_v<std::decay_t<V>,T>) … // special code if V is convertible to T
if constexpr(std::has_virtual_destructor_v<V>) … // special code if V has virtual destructor
;
std::addressof<>()会返回一个对象或者函数的准确地址,即使一个对象重载了取地址运算符&也是这样。
函数模板 std::declval()可以被用作某一类型的对象的引用的占位符。
// 避免在调用运算符?:的时候不得不去调用 T1 和 T2 的(默认)构造函数,这里使用了std::declval
#include <utility>
template<typename T1, typename T2,
typename RT = typename std::decay_t< decltype(true ? std::declval<T1>() : std::declval<T2>())> >
RT max (T1 a, T2 b)
return b < a ? a : b;
11.3 完美转发临时变量
使用 auto &&可以创建一个可以被转发的临时变量。
template<typename T>
void foo(T x)
auto&& val = get(x);
// perfectly forward the return value of get() to set():
set(std::forward<decltype(val)>(val));
11.4 其它实现泛型库的工具
模板参数 T 的类型可能被推断为引用类型,此时可能会引起意料之外的错误。
template<typename T, T Z = T>
class RefMem
private:
T zero;
public:
RefMem() : zeroZ
;
int null = 0;
int main()
RefMem<int> rm1, rm2;
rm1 = rm2; // OK
RefMem<int&> rm3; // ERROR: 无法用int & 对Z进行默认初始化
RefMem<int&, 0> rm4; // ERROR: 不能用0 初始化int & 对象zero
RefMem<int&,null> rm5, rm6;
rm5 = rm6; // ERROR: 具有非static引用成员的类,默认赋值运算符会被删掉
2.如果需要禁止引用类型进行实例化,可以使用 std::is_reference 进行判断。
11.5 推迟计算
可以通过模板来延迟表达式的计算,这样模板可以用于不完整类型。
template<typename T>
class Cont
private:
T* elems;
public:
;
struct Node
std::string value;
Cont<Node> next; // only possible if Cont accepts incomplete types
;
十二、深入模板基础
12.1 参数化声明
C++ 目前支持四种基本类型的模板:类模板、函数模板、变量模板和别名模板。这些模板均可定义在全局作用域或者类作用域中。
template<typename T> // a namespace scope class template
class Data
public:
static constexpr bool copyable = true;
;
template<typename T> // a namespace scope function template
void log (T x)
template<typename T> // a namespace scope variable template (since C++14)
T zero = 0;
template<typename T> // a namespace scope variable template (since C++以上是关于C++ 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章