C++高级特性
主要以C++11/C++14为主,整理了一下我在日常工作中经常用到的新特性。
1. 变量和基本类型
1.1 long long 类型
扩展精度浮点数,10位有效数字。
1.2 容器列表初始化
在我们实际编程中,我们经常会碰到变量初始化的问题,对于不同的变量初始化的手段多种多样,比如说对于一个数组我们可以使用 int arr[] = {1,2,3}的方式初始化,又比如对于一个简单的结构体:
struct A
{
int x;
int y;
}a={1,2};
这些不同的初始化方法都有各自的适用范围和作用,且对于类来说不能用这种初始化的方法,最主要的是没有一种可以通用的初始化方法适用所有的场景,因此C++11中为了统一初始化方式,提出了列表初始化(list-initialization)的概念。
在C++98/03中我们只能对普通数组和POD(plain old data,简单来说就是可以用memcpy复制的对象)类型可以使用列表初始化,在C++11中初始化列表被适用性被放大,可以作用于任何类型对象的初始化。如下:
class Foo
{
public:
Foo(int) {}
private:
Foo(const Foo &);
};
int _tmain(int argc, _TCHAR* argv[])
{
Foo a1(123); //调用Foo(int)构造函数初始化
Foo a2 = 123; //error Foo的拷贝构造函数声明为私有的,该处的初始化方式是隐式调用Foo(int)构造函数生成一个临时的匿名对象,再调用拷贝构造函数完成初始化
Foo a3 = { 123 }; //列表初始化
Foo a4 { 123 }; //列表初始化
int a5 = { 3 };
int a6 { 3 };
return 0;
}
由上面的示例代码可以看出,在C++11中,列表初始化不仅能完成对普通类型的初始化,还能完成对类的列表初始化,需要注意的是a3、a4都是列表初始化,私有的拷贝并不影响它,仅调用类的构造函数而不需要拷贝构造函数,a4、a6的写法是C++98/03所不具备的,是C++11新增的写法。
同时列表初始化方法也适用于用new操作等圆括号进行初始化的地方,如下:
int* a = new int { 3 };
double b = double{ 12.12 };
int * arr = new int[] {1, 2, 3};
在C++11中可以使用列表初始化方法对堆中分配的内存的数组进行初始化,而在C++98/03中是不能这样做的。此外,还有一些细节需要注意:
struct A
{
int x;
int y;
}a = {123, 321};
//a.x = 123 a.y = 321
struct B
{
int x;
int y;
B(int, int) :x(0), y(0){}
}b = {123,321};
//b.x = 0 b.y = 0
对于自定义的结构体A来说模式普通的POD类型,使用列表初始化并不会引起问题,x,y都被正确的初始化了,但看下结构体B和结构体A的区别在于结构体B定义了一个构造函数,并使用了成员初始化列表来初始化B的两个变量,因此列表初始化在这里就不起作用了,b采用的是构造函数的方式来完成变量的初始化工作。
C++11的列表初始化还有一个额外的功能就是可以防止类型收窄,也就是C++98/03中的隐式类型转换,将范围大的转换为范围小的表示,在C++98/03中类型收窄并不会编译出错,而在C++11中,使用列表初始化的类型收窄编译将会报错:
int a = 1.1; //OK
int b{ 1.1 }; //error
float f1 = 1e40; //OK
float f2{ 1e40 }; //error
const int x = 1024, y = 1;
char c = x; //OK
char d{ x };//error
char e = y;//error
char f{ y };//error
上面例子看出,用C++98/03的方式类型收窄并不会编译报错,但是将会导致一些隐藏的错误,导致出错的时候很难定位,而利用C++11的列表初始化方法定义变量从源头了遏制了类型收窄,使得不恰当的用法就不会用在程序中,避免了某些位置类型的错误,因此建议以后在实际编程中尽可能的使用列表初始化方法定义变量。
1.3 nullptr常量
C++11中新增nullptr常量,用于生成空指针,代替之前使用的NULL和0。目前有3种初始化空指针的方法:
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;
使用 nullptr 代替 0 或 NULL,能显著提高代码的清晰度,尤其是和 auto 连用时;还可以避免重载函数调用模糊的问题。尤其是在使用模板函数时,传入0会被推断为int型,与指针类型不匹配会直接报错。
1.4 constexpr变量
将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式;声明为constexpr的变量一定是一个常量,而且必须用常量表达式来初始化,比如说下面的情况则是不正确的:
int t = 10;
constexpr int q = t + 20; //t不是常量,报错
cout << "q" << q << endl;
需要将t声明为 const 才是正确的。一般来说,如果你认定变量是一个常量表达式,那就把它声明为constexpr类型;
constexpr也可以用于将函数声明为常量函数,需要遵从几项约定:
-
函数的返回类型以及所有形参的类型都是字面值类型(只能用它的值来称呼它);
-
函数体中必须有且只有一条return语句(C++14不再做要求);
-
必须非virtual
constexpr int func2() { return 10; } int main() { int arr[func2() + 10] = {0}; return 0; }
特别的,在类内,如果成员函数标记为
constexpr,则默认其是内联函数;如果变量声明为constexpr,则默认其是const。
1.5 类型别名声明
使用类型别名可以使复杂的类型名字变得更简单明了,易于理解和使用。现在有两种方法可以用来定义类型别名,一种是 typedef ,另一种则是新标准中的 using;
typedef double dnum;
typedef char *pstring;
using dnum2 = double;
using pstring2 = char*;
要特别注意,如果某个类型别名指代的是复合类型或者常量,那么就会产生意想不到的后果,比如:
typedef char *pstring;
const pstring cstr = 0;
我们这里使用pstring定义cstr,想要得到一个const char*,一个指向常量字符的指针,即指针可变,但是指针指向的内容不可变;但是实际上我们得到了一个char* const,一个指向字符的常量指针,即指针内容可变,但是指针不可变。在这里,使用using的效果也一样,要特别注意。
1.6 auto类型指示符
类型推导。auto类型从初始化表达式中推断出变量的数据类型,所以,其定义的变量必须要有初始值。从这个意义上讲,auto并非一种“类型”声明,而是一个类型声明时的“占位符”,编译器在编译时期会将auto替换为变量实际的类型。
auto c = 1, d = 1; // 正确
auto a = 1, b = 1.01; // 错误
可以添加*、&、&&修饰符,来定义auto类型的指针和引用。
我们也可以使用auto类型来简化一个函数的定义:
int (*test(int a,int n))[5]
{
return &a[n]; // 返回的是一个行指针
}
auto test(int a[][5],int n)
{
return &a[n]; // 同上
}
通过auto避免了复杂的类型声明。
1.7 decltype类型指示符
类型推导。decltype实际上有点像auto的反函数, auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到其类型。需要注意的是,decltype((variable))这样的用法,可以声明variable的引用,等效decltype(variable)&。
int i = 0;
decltype((i)) a; //报错,因为a类型为 int&,必须进行初始化
decltype(i)& b = i; //正确
decltype(i) c; //正确
左值(lvalue)和右值(rvalue)是从c继承过来的概念,在C++11之后,新标准基于这两个概念新增了部分特征(右值引用,用来解决移动和转发语义)。 我们平常使用的引用都是指左值引用。
在C++98中,临时量(术语为右值,因其出现在赋值表达式的右边)可以被传给函数,但只能被接受为const &类型。这样函数便无法区分传给const &的是真实的右值还是常规变量。而且,由于类型为const &,函数也无法改变所传对象的值。C++0x将增加一种名为右值引用的新的引用类型,记作typename &&。这种类型可以被接受为非const值,从而允许改变其值。这种改变将允许某些对象创建转移语义。比如,一个std::vector,就其内部实现而言,是一个C式数组的封装。如果需要创建vector临时量或者从函数中返回vector,那就只能通过创建一个新的vector并拷贝所有存于右值中的数据来存储数据。之后这个临时的vector则会被销毁,同时删除其包含的数据。有了右值引用,一个参数为指向某个vector的右值引用的std::vector的转移构造器就能够简单地将该右值中C式数组的指针复制到新的vector,然后将该右值清空。这里没有数组拷贝,并且销毁被清空的右值也不会销毁保存数据的内存。返回vector的函数现在只需要返回一个std::vector<>&&。如果vector没有转移构造器,那么结果会像以前一样:用std::vector<> &参数调用它的拷贝构造器。如果vector确实具有转移构造器,那么转移构造器就会被调用,从而避免大量的内存分配。
通俗来说,左值是等号左边的量,右值是等号右边的量,一般是将亡值,比如函数的返回值,等等。

关于如何详细准确的区别左值和右值,请参考cpp_reference–值类别。
1.9 universal引用(T&&)
(1)T&&的两种含义
①右值引用:当T是确定的类型时,T&&为右值引用。如int&& a;
②当T存在类型推导(模板)时,T&&为universal引用,表示一个未定的引用类型。如果被右值初始化,则T&&为右值引用。如果被左值初始化,则T&&为左值引用。
(2)引用折叠
①由于引用本身不是一个对象,C++标准不允许直接定义引用的引用。如“int& & a = b;”(注意两个&中间有空格,不是int&&)这样的语句是编译不过的。
②当类型推导时可能会间接地创建引用的引用,此时必须进行引用折叠。具体折叠规则如下:
A. X& &、X& &&和X&& &都折叠成类型X&。即凡是有左值引用参与的情况下,最终的类型都会变成左值引用。
B. 类型X&& &&折叠成X&&。即只有全部为右值引用的情况才会折叠为右值引用。
③引用折叠规则暗示我们,可以将任意类型的实参传递给T&&类型的函数模板参数。
(3)注意事项
①只有当发生自动类型推导时(如函数模板的类型自动推导或auto关键字),&&才是一个universal引用。当T的类型是确定的类型时,T&&为右值引用。
②当使用左值(类型为A)去初始化T&& t时,类型推导为A& &&,折叠会为A&,即t的类型为左值引用。而如果使用右值初始化T&&时,类型推导为A&&,一步到位无须折叠。
③universal引用仅仅在T&&下发生,任何一点附加条件都会使之失效,而变成一个普通的右值引用(const T&&被const修饰就成了右值引用)
1.10 std::move
move是一个右值相关的函数。它可以将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。如图所示是深拷贝和move的区别:

这种移动语义是很有用的,比如我们一个对象中有一些指针资源或者动态数组,在对象的赋值或者拷贝时就不需要拷贝这些资源了。在c++11之前我们的拷贝构造函数和赋值函数可能要这样定义:假设一个A对象内部有一个资源m_ptr;
A& A::operator=(const A& rhs)
{
// 销毁m_ptr指向的资源
// 复制rhs.m_ptr所指的资源,并使m_ptr指向它
}
同样A的拷贝构造函数也是这样。假设我们这样来用A:
A foo(); // foo是一个返回值为X的函数
A a;
a = foo();
最后一行有如下的操作:
-
销毁a所持有的资源
-
复制foo返回的临时对象所拥有的资源
-
销毁临时对象,释放其资源
上面的过程是可行的,但是更有效率的办法是直接交换a和临时对象中的资源指针,然后让临时对象的析构函数去销毁a原来拥有的资源。换句话说,当赋值操作符的右边是右值的时候,我们希望赋值操作符被定义成下面这样:
A& A::operator=(const A&& rhs)
{
// 仅仅转移资源的所有者,将资源的拥有者改为被赋值者
}
这就是所谓的move语义。再看一个例子,假设一个临时容器很大,赋值给另一个容器。
{
std::list< std::string > tokens;//省略初始化...
std::list< std::string > t = tokens;
}
std::list< std::string > tokens;
std::list< std::string > t = std::move(tokens);
如果不用std::move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。如果一个对象内部有较大的对内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。
1.11 std::forward
右值引用类型是独立于值的,一个右值引用参数作为函数的形参,在函数内部再转发该参数的时候它已经变成一个左值了,并不是它原来的类型了。因此,我们需要一种方法能按照参数原来的类型转发到另一个函数,这种转发被称为完美转发。所谓完美转发(perfect forwarding),是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。C++11中提供了这样的一个函数std::forward,它是为转发而生的,它会按照参数本来的类型来转发出去,不管参数类型是T&&这种未定的引用类型还是明确的左值引用或者右值引用。看看这个cpp_refenrence上的例子:
struct A {
A(int&& n) { std::cout << "rvalue overload, n=" << n << "\n"; }
A(int& n) { std::cout << "lvalue overload, n=" << n << "\n"; }
};
class B {
public:
template<class T1, class T2, class T3>
B(T1&& t1, T2&& t2, T3&& t3) :
a1_{std::forward<T1>(t1)},
a2_{std::forward<T2>(t2)},
a3_{std::forward<T3>(t3)}
{
}
private:
A a1_, a2_, a3_;
};
template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)));
}
template<class T, class... U>
std::unique_ptr<T> make_unique(U&&... u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)...));
}
int main()
{
auto p1 = make_unique1<A>(2); // rvalue
int i = 1;
auto p2 = make_unique1<A>(i); // lvalue
std::cout << "B\n";
auto t = make_unique<B>(2, i, 3);
}
//输出:
rvalue overload, n=2
lvalue overload, n=1
B
rvalue overload, n=2
lvalue overload, n=1
rvalue overload, n=3
如果在B的构造中不使用forward,那么将会调用3次A的左值构造函数。因为参数2和3作为右值引用传入B的构造函数,变成了具名变量t,右值变成了左值,将引起不必要的内存开销。
1.12 除法的舍入规则
新标准中,一律向0取整(直接切除小数部分),例:
double a = 14/5;
cout << a << endl;
输出结果为2,删掉了小数部分。
2.STL
2.1 范围for语句
for(declaration : expression) 其中,expression 部分是一个对象,用于表示一个序列。declaration 部分负责定义一个变量,该变量将用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素值。
例如:
string str("hello world");
for (auto c : str) {
cout << c;
}
可以遍历str。也可以遍历vector、map等STL容器。
2.2 cbegin()、cend()
C++11新增的STL成员函数,与之前的begin()、end()对应,返回const类型的迭代器。cbegin()返回指向容器首的const迭代,cend()返回指向容器尾的const迭代器。
2.3 std::tuple
C++11新增的容器,它是通用化的std::pair。pair只能有first和second两个元素,tuple可以将多个元素合并成一组。通常用于让函数返回多个值:
std::tuple<double, char, std::string>
这样就声明了一个元组。我们可以通过std::get、std::tie或结构化绑定(C++17起)获得元组中每个元素的值。
std::tuple<double, char, std::string> get_student(int id)
{
if (id == 0) return std::make_tuple(3.8, 'A', "Lisa Simpson");
if (id == 1) return std::make_tuple(2.9, 'C', "Milhouse Van Houten");
if (id == 2) return std::make_tuple(1.7, 'D', "Ralph Wiggum");
throw std::invalid_argument("id");
}
int main()
{
auto student0 = get_student(0);
//使用std::get获得元组中的元素
//用法:std::get<n>(tuple),n是tuple中元素的位置,从0开始,tuple是元组变量
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(student0) << ", "
<< "grade: " << std::get<1>(student0) << ", "
<< "name: " << std::get<2>(student0) << '\n';
double gpa1;
char grade1;
std::string name1;
// 使用std::tie获得元组中的元素
std::tie(gpa1, grade1, name1) = get_student(1);
std::cout << "ID: 1, "
<< "GPA: " << gpa1 << ", "
<< "grade: " << grade1 << ", "
<< "name: " << name1 << '\n';
//通过C++17结构化绑定获得元组中的元素
auto [ gpa2, grade2, name2 ] = get_student(2);
std::cout << "ID: 2, "
<< "GPA: " << gpa2 << ", "
<< "grade: " << grade2 << ", "
<< "name: " << name2 << '\n';
}
注意,C++17前,函数不能用初始化列表返回tuple:
std::tuple<int, int> foo_tuple()
{
return {1, -1}; // C++17 前错误
return std::make_tuple(1, -1); // 始终有效
}
特别的,这种写法也是错误的:
auto foo_tuple()
{
return {1, -1}; // 错误
}
这样没办法推导返回类型。但是可以通过尾置返回类型这样声明:
auto test() -> std::tuple<int, int>
{
return { 1, -1 }; // 正确
}
2.5 std::emplace
C++11中,针对顺序容器(如vector、deque、list),新标准引入了三个新成员:emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。
当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。
emplace函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配。emplace函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。
其它容器中,std::forward_list中的emplace_after、emplace_front函数,std::map/std::multimap中的emplace、emplace_hint函数,std::set/std::multiset中的emplace、emplace_hint,std::stack中的emplace函数,等emplace相似函数操作也均是构造而不是拷贝元素。
emplace相关函数可以减少内存拷贝和移动。在STL中,push_back左值,需要1次拷贝构造;push_back右值(std::move)需要一次移动构造,而且在调用之前还需要先构造元素本身;而使用emplace添加一个元素,这个元素原地构造,不需要触发拷贝构造和移动构造,而且调用形式更加简洁,直接根据参数初始化临时对象的成员,只调用1次构造函数。
int main()
{
std::map<std::string, std::string> m;
// 使用 pair 的移动构造函数
m.emplace(std::make_pair(std::string("a"), std::string("a")));
// 使用 pair 的转换移动构造函数
m.emplace(std::make_pair("b", "abcd"));
// 使用 pair 的模板构造函数
m.emplace("d", "ddd");
// 使用 pair 的逐片构造函数
m.emplace(std::piecewise_construct,
std::forward_as_tuple("c"),
std::forward_as_tuple(10, 'c'));
// C++17 起,能使用 m.try_emplace("c", 10, 'c');
for (const auto &p : m) {
std::cout << p.first << " => " << p.second << '\n';
}
}
2.6 STL列表初始化及列表返回值
可以通过初始化列表代替STL类型变量做函数返回值,也可以用参数列表初始化STL容器。
vector<int> test()
{
return { 1,2,3,4,5 };
}
vector<int> a{ 1,2,3,4,5 };
vector<int> a({ 1,2,3,4,5 });
vector<int> a = {1,2,3,4,5};
map<int, int> b{ (1,1),(2,2) };
也可以用参数列表初始化自定义类型(类或结构体):
struct test {
test(int a,int b) {}
};
int main()
{
test a{ 1,2 };
test a(1, 2);
test a = { 1, 2 };
return 0;
}
列表初始化实际上是由std::initializer_list<T>完成的。这是C++11及以后的一个模板类。如果我们这样vector<int> a({1,2,3,4,5})初始化一个vector,实际上是将std::initializer_list<double>作为vector构造函数的参数。
现在分析这样的一个列表初始化:
vector<int> a(10);
vector<int> b{10};
要注意,a被初始化了成了一个大小为10的空vector,而b初始化成了一个大小为1的vector,该元素的值是10。即,如果类接受std::initializer_list<T>作为其构造参数,那么语法{}将调用初始化列表的构造函数。假如vector没有初始化列表的构造函数,那么如上的两个语句其效果应该是相同的。涉及列表初始化类型转换的部分,已经在第一章中介绍,这里不再赘述。
2.7 std::swap
C++11起,定义在头文件 <utility>中。常量复杂度,交换两个变量的值。除了 array 外,swap不对任何元素进行拷贝、删除或者插入操作,因此可以保证常数时间内完成;swap 只是交换了容器内部数据结构,不会交换元素,因此,除string 外,指向容器的迭代器、引用和指针在 swap 操作后都不会失效。但是,对array的swap,会真正的交换它们的元素。
2.8 string的数值转换函数
新标准中,引入多个函数实现数值数据和标准库string之间的转换:
| 函数 | 描述 |
|---|---|
| to_string(val) | 返回任意算术类型val的字符串 |
| stoi(s, p, b) | int类型 |
| stol(s, p, b) | long类型 |
| stoul(s, p, b) | unsigned long类型 |
| stoll(s, p, b) | long long类型 |
| stoull(s, p, b) | unsigned long long类型 |
| stof(s, p, b) | float类型 |
| stod(s, p, b) | double类型 |
| stold(s, p, b) | long double类型 |
其中,s是字符串,p是开始转换的位置,默认是0,b是转换的底,默认是0。如果底是0,会自动检测数值进制:若前缀为0,则底为八进制,若前缀为 0x 或0X ,则底为十六进制,否则底为十进制。
2.9 shrink_to_fit
调用该函数要求 deque,vector,string 退回不需要的内存空间。它是减少 capacity()到 size()非强制性请求。请求是否达成依赖于实现。若发生重分配,则所有迭代器,包含尾后迭代器,和所有到元素的引用都被非法化。若不发生重分配,则没有迭代器或引用被非法化。
2.10 智能指针
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
智能指针有三种,分别是shared_ptr、unique_ptr以及weak_ptr。
2.10.1 shared_ptr
shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
-
初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的
-
拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
-
get()方法获取原始指针
-
注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
-
注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。
int main() { { int a = 10; std::shared_ptr<int> ptra = std::make_shared<int>(a); std::shared_ptr<int> ptra2(ptra); //copy std::cout << ptra.use_count() << std::endl; int b = 20; int *pb = &a; //std::shared_ptr<int> ptrb = pb; //error std::shared_ptr<int> ptrb = std::make_shared<int>(b); ptra2 = ptrb; //assign pb = ptrb.get(); //获取原始指针 std::cout << ptra.use_count() << std::endl; std::cout << ptrb.use_count() << std::endl; } }
2.10.2 unique_ptr
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
int main() { { std::unique_ptr<int> uptr(new int(10)); //绑定动态对象 //std::unique_ptr<int> uptr2 = uptr; //不能賦值 //std::unique_ptr<int> uptr2(uptr); //不能拷貝 std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權 uptr2.release(); //释放所有权 } //超過uptr的作用域,內存釋放 }
2.10.3 weak_ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
int main() {
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl;
std::weak_ptr<int> wp(sh_ptr);
std::cout << wp.use_count() << std::endl;
if(!wp.expired()){
std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl;
}
}
//delete memory
}
2.10.4 循环引用
考虑一个简单的对象建模——家长与子女:a Parent has a Child, a Child knows his/her Parent。在Java 里边很好写,不用担心内存泄漏,也不用担心空悬指针,只要正确初始化myChild 和myParent,那么Java 程序员就不用担心出现访问错误。一个handle 是否有效,只需要判断其是否non null。在C++ 里边就要为资源管理费一番脑筋。如果使用原始指针作为成员,Child和Parent由谁释放?那么如何保证指针的有效性?如何防止出现空悬指针?这些问题是C++面向对象编程麻烦的问题,现在可以借助智能指针把对象语义(pointer)转变为值(value)语义,shared_ptr轻松解决生命周期的问题,不必担心空悬指针。但是这个模型存在循环引用的问题,注意其中一个指针应该为weak_ptr。
首先考虑一下,采用原始指针如何实现这个设计:
class Child;
class Parent;
class Parent {
private:
Child* myChild;
public:
void setChild(Child* ch) {
this->myChild = ch;
}
void doSomething() {
if (this->myChild) {
}
}
~Parent() {
delete myChild;
}
};
class Child {
private:
Parent* myParent;
public:
void setPartent(Parent* p) {
this->myParent = p;
}
void doSomething() {
if (this->myParent) {
}
}
~Child() {
delete myParent;
}
};
int main() {
{
Parent* p = new Parent;
Child* c = new Child;
p->setChild(c);
c->setPartent(p);
delete c; //only delete one
}
return 0;
}
无论是delete c还是delete p,都只需要delete一次,且在delete后,没有被delete的指针变成了悬空指针,在编程中容易发生错误。
现在考虑用智能指针实现这个设计。如果在parent和child中都使用智能指针,则会产生循环引用,从而导致智能指针无法正确析构对象,结果就是内存泄漏:
class Child;
class Parent;
class Parent {
private:
std::shared_ptr<Child> ChildPtr;
public:
void setChild(std::shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
if (this->ChildPtr.use_count()) {
}
}
~Parent() {
}
};
class Child {
private:
std::shared_ptr<Parent> ParentPtr;
public:
void setPartent(std::shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
std::weak_ptr<Parent> wpp;
std::weak_ptr<Child> wpc;
{
std::shared_ptr<Parent> p(new Parent);
std::shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
std::cout << p.use_count() << std::endl; // 2
std::cout << c.use_count() << std::endl; // 2
}
std::cout << wpp.use_count() << std::endl; // 1
std::cout << wpc.use_count() << std::endl; // 1
return 0;
}
在这里,创建p,使得p的引用计数为1,再在子类中设置父类为p,使得p的引用计数为2,子类指针c也一样为2。在离开了作用域以后,引用计数减一为1,我们可以用weak_ptr观测引用计数得到这个结果。最后导致对象无法被析构,产生内存泄漏。
正确的使用方式应该是这样:
class Child;
class Parent;
class Parent {
private:
//std::shared_ptr<Child> ChildPtr;
std::weak_ptr<Child> ChildPtr;
public:
void setChild(std::shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
//new shared_ptr
if (this->ChildPtr.lock()) {
}
}
~Parent() {
}
};
class Child {
private:
std::shared_ptr<Parent> ParentPtr;
public:
void setPartent(std::shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
std::weak_ptr<Parent> wpp;
std::weak_ptr<Child> wpc;
{
std::shared_ptr<Parent> p(new Parent);
std::shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
std::cout << p.use_count() << std::endl; // 2
std::cout << c.use_count() << std::endl; // 1
}
std::cout << wpp.use_count() << std::endl; // 0
std::cout << wpc.use_count() << std::endl; // 0
return 0;
}
使用一个weak_ptr来进行引用,这样c的引用计数不会为2。离开作用域以后c被析构,c控制的指向p的智能指针也被析构,这样p的引用计数一次性减2,c和p都可以正常析构,不会产生内存泄漏。
正确使用智能指针,可以帮助我们减少开发中许多不必要的麻烦,增强安全性和便利性。
3.函数
3.1匿名函数lambda
Lambda表达式完整的声明格式如下:
[capture list](parameter list) mutable exception -> return type { function body }
各项具体含义如下:
-
capture list:捕获外部变量列表
-
params list:形参列表
-
mutable指示符:用来说用是否可以修改捕获的变量
-
exception:异常设定
-
return type:返回类型
-
function body:函数体
此外,我们还可以省略其中的某些成分来声明“不完整”的Lambda表达式,常见的有以下几种:
| 序号 | 格式 |
|---|---|
| 1 | [capture list] (params list) -> return type {function body} |
| 2 | [capture list] (params list) {function body} |
| 3 | [capture list] {function body} |
其中:
-
格式1声明了const类型的表达式,这种类型的表达式不能修改捕获列表中的值。
-
格式2省略了返回值类型,但编译器可以根据以下规则推断出Lambda表达式的返回类型: (1):如果function body中存在return语句,则该Lambda表达式的返回类型由return语句的返回类型确定; (2):如果function body中没有return语句,则返回值为void类型。
-
格式3中省略了参数列表,类似普通函数中的无参函数。
以下是lambda表达式的一个例子:
using namespace std; bool cmp(int a, int b) { return a < b; } int main() { vector<int> myvec{ 3, 2, 5, 7, 3, 2 }; vector<int> lbvec(myvec); sort(myvec.begin(), myvec.end(), cmp); // 旧式做法 cout << "predicate function:" << endl; for (int it : myvec) cout << it << ' '; cout << endl; sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; }); // Lambda表达式 cout << "lambda expression:" << endl; for (int it : lbvec) cout << it << ' '; }
在C++11之前,我们使用STL的sort函数,需要提供一个谓词函数。如果使用C++11的Lambda表达式,我们只需要传入一个匿名函数即可,方便简洁,而且代码的可读性也比旧式的做法好多了。
3.1.1 捕获外部变量
Lambda表达式可以使用其可见范围内的外部变量,但必须明确声明(明确声明哪些外部变量可以被该Lambda表达式使用)。那么,在哪里指定这些外部变量呢?Lambda表达式通过在最前面的方括号[]来明确指明其内部可以访问的外部变量,这一过程也称过Lambda表达式“捕获”了外部变量。
我们通过一个例子来直观地说明一下:
using namespace std; int main() { int a = 123; auto f = [a] { cout << a << endl; }; f(); // 输出:123 //或通过“函数体”后面的‘()’传入参数 auto x = [](int a){cout << a << endl;}(123); }
上面这个例子先声明了一个整型变量a,然后再创建Lambda表达式,该表达式“捕获”了a变量,这样在Lambda表达式函数体中就可以获得该变量的值。
类似参数传递方式(值传递、引入传递、指针传递),在Lambda表达式中,外部变量的捕获方式也有值捕获、引用捕获、隐式捕获。其中,隐式捕获指示编译器推断需要捕获的变量列表:
捕获形式 说明 [] 不捕获任何外部变量 [变量名, …] 默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符) [this] 以值的形式捕获this指针 [=] 隐式捕获,以值的形式捕获函数体用到的外部变量 [&] 隐式捕获,以引用形式捕获函数体用到的外部变量 [=, &x] 变量x以引用形式捕获,其余变量以传值形式隐式捕获 [&, x] 变量x以值的形式捕获,其余变量以引用形式隐式捕获
3.1.2 修改捕获变量
在Lambda表达式中,如果以传值方式捕获外部变量,则函数体中不能修改该外部变量,否则会引发编译错误。我们可以使用mutable关键字,该关键字用以说明表达式体内的代码可以修改值捕获的变量:
int main()
{
int a = 123;
auto f = [a]()mutable { cout << ++a; }; // 不会报错
cout << a << endl; // 输出:123
f(); // 输出:124
}
在Lambda表达式中传递参数还有一些限制,主要有以下几点:
-
参数列表中不能有默认参数
-
不支持可变参数
-
所有参数必须有参数名
3.2 尾置返回类型
一般用于和auto、decltype一起简化函数定义:
auto func(int) -> int(*)[10]
{
...
}
3.3 std::function
std::function是一个函数包装器模板,最早来自boost库,对应其boost::function函数包装器。在c++11中,将boost::function纳入标准库中。该函数包装器模板能包装任何类型的可调用元素(callable element),例如普通函数和函数对象。包装器对象可以进行拷贝,并且包装器类型仅仅只依赖于其调用特征(call signature),而不依赖于可调用元素自身的类型。
一个std::function类型对象实例可以包装下列这几种可调用元素类型:函数、函数指针、类成员函数指针或任意类型的函数对象(例如定义了operator()操作并拥有函数闭包)。std::function对象可被拷贝和转移,并且可以使用指定的调用特征来直接调用目标元素。当std::function对象未包裹任何实际的可调用元素,调用该std::function对象将抛出std::bad_function_call异常。
3.3.1 包装普通函数
using namespace std;
int g_Minus(int i, int j)
{
return i - j;
}
int main()
{
function<int(int, int)> f = g_Minus;
cout << f(1, 2) << endl; // -1
return 1;
}
3.3.2 包装模板函数
using namespace std;
template <class T>
T g_Minus(T i, T j)
{
return i - j;
}
int main()
{
function<int(int, int)> f = g_Minus<int>;
cout << f(1, 2) << endl; // -1
return 1;
}
3.3.3 包装lambda表达式
using namespace std;
auto g_Minus = [](int i, int j){ return i - j; };
int main()
{
function<int(int, int)> f = g_Minus;
cout << f(1, 2) << endl; // -1
return 1;
}
3.3.4 包装函数对象
非模板类型:
using namespace std;
struct Minus
{
int operator() (int i, int j)
{
return i - j;
}
};
int main()
{
function<int(int, int)> f = Minus();
cout << f(1, 2) << endl; // -1
return 1;
}
模板类型:
using namespace std;
template <class T>
struct Minus
{
T operator() (T i, T j)
{
return i - j;
}
};
int main()
{
function<int(int, int)> f = Minus<int>();
cout << f(1, 2) << endl; // -1
return 1;
}
3.3.5 包装类静态成员函数
非模板类型:
using namespace std;
class Math
{
public:
static int Minus(int i, int j)
{
return i - j;
}
};
int main()
{
function<int(int, int)> f = &Math::Minus;
cout << f(1, 2) << endl; // -1
return 1;
}
模板类型:
using namespace std;
class Math
{
public:
template <class T>
static T Minus(T i, T j)
{
return i - j;
}
};
int main()
{
function<int(int, int)> f = &Math::Minus<int>;
cout << f(1, 2) << endl; // -1
return 1;
}
3.3.7 包装类对象成员函数
非模板类型:
using namespace std;
class Math
{
public:
int Minus(int i, int j)
{
return i - j;
}
};
int main()
{
Math m;
function<int(int, int)> f = bind(&Math::Minus, &m, placeholders::_1, placeholders::_2);
cout << f(1, 2) << endl; // -1
return 1;
}
模板类型:
using namespace std;
class Math
{
public:
template <class T>
T Minus(T i, T j)
{
return i - j;
}
};
int main()
{
Math m;
function<int(int, int)> f = bind(&Math::Minus<int>, &m, placeholders::_1, placeholders::_2);
cout << f(1, 2) << endl; // -1
return 1;
}
3.3.8 std::bind
bind原先是boost中的方法,在C++11以前已经被广泛使用,从C++11开始被纳入std,定义在头文件、<functional>中。它的作用是生成一个函数f的转发调用包装器,调用此包装器等价于以一些绑定到 args 的参数调用 f 。有点类似函数式编程。例子如下:
void f(int n1, int n2, int n3, const int& n4, int n5)
{
std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << '\n';
}
int g(int n1)
{
return n1;
}
struct Foo {
void print_sum(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
int data = 10;
};
int main()
{
using namespace std::placeholders; // 对于 _1, _2, _3...
// 演示参数重排序和按引用传递
int n = 7;
// ( _1 与 _2 来自 std::placeholders ,并表示将来会传递给 f1 的参数)
auto f1 = std::bind(f, _2, _1, 42, std::cref(n), n);
n = 10;
f1(1, 2, 1001); // 1 为 _1 所绑定, 2 为 _2 所绑定,不使用 1001
// 进行到 f(2, 1, 42, n, 7) 的调用
// 嵌套 bind 子表达式共享占位符
auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
f2(10, 11, 12); // 进行到 f(12, g(12), 12, 4, 5); 的调用
// 常见使用情况:以分布绑定 RNG
std::default_random_engine e;
std::uniform_int_distribution<> d(0, 10);
std::function<int()> rnd = std::bind(d, e); // e 的一个副本存储于 rnd
for(int n=0; n<10; ++n)
std::cout << rnd() << ' ';
std::cout << '\n';
// 绑定指向成员函数指针
Foo foo;
auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
f3(5);
// 绑定指向数据成员指针
auto f4 = std::bind(&Foo::data, _1);
std::cout << f4(foo) << '\n';
// 智能指针亦能用于调用被引用对象的成员
std::cout << f4(std::make_shared<Foo>(foo)) << '\n'
<< f4(std::make_unique<Foo>(foo)) << '\n';
}
实际上在使用中,匿名函数lambda也可以取到和bind相同的效果,在写法上也比bind方便,看起来也更清晰,例如上面例子的auto f1 = std::bind(f, _2, _1, 42, std::cref(n), n);,如果用lambda来写就是auto f1 = [&n](int a, int b){f(b,a,42,n,7);};,可以看到用lambda实现起来更方便,也不需要cref这种帮助函数。调用它使用f1(1,2);得到的运行结果与使用bind完全相同。注意使用lambda做函数包装时,传值和传引用的区别:如果这里对n传值,那么下面的所有调用里面n的值都是定义的时候传入的10,相当于f(b,a,42,10,7);如果是传引用,后面n=7的赋值会影响到调用f1的结果。
lambda的简洁性,如果以3.3.8为例会更明显。使用bind的写法是这样的:auto f = bind(&Math::Minus<int>, &m, placeholders::_1, placeholders::_2);,使用lambda的写法是这样的:auto f = [&m](int a, int b){ return m.Minus(a,b);};。显然使用lambda包装要自然简洁得多。
4.线程支持库
C++11开始,提供了包含线程、互斥、条件变量和期货的内建支持。
4.1 线程
std::thread,用于定义一个线程,用法如下:
auto test(int a, int b)
{
return a + b;
}
int main()
{
thread t(test,1,2); // test(1,2)
t.join(); // 阻塞当前线程,直到子线程返回
//t.detach(); // 从 thread 对象分离执行的线程,允许执行独立地持续。
// 一旦线程退出,则释放所有分配的资源。
// 调用 detach 后,t不再占有任何线程。
return 0;
}
一个线程用函数和参数构造,然后这个线程就会根据参数去执行这个函数。要注意,thread是无法获取执行的函数的返回值的,它会忽略顶层函数的返回值。如过要获取返回值,可以通过共享变量或std::promise。
特别的,thread是支持swap的。你可以通过swap来交换两个thread对象所管理的线程句柄。
此外,thread还有获取线程ID的方法get_id()、休眠线程的方法sleep_for()、调度线程的方法yield()等。有关线程库的更多信息,请参照std::thread。
4.2 互斥
互斥算法避免多个线程同时访问共享资源。这会避免数据竞争,并提供线程间的同步支持。主要用于对发生竞争的变量加锁,这也导致了性能上的损失。
这里我只介绍以下用的最多的mutex与recursive_mutex,std::lock_guard与std::unique_lock。
此示例展示 mutex 能如何用于在保护共享于二个线程间的std::map:
std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex;
void save_page(const std::string &url)
{
// 模拟长页面读取
std::this_thread::sleep_for(std::chrono::seconds(2));
std::string result = "fake content";
std::lock_guard<std::mutex> guard(g_pages_mutex);
g_pages[url] = result;
}
int main()
{
std::thread t1(save_page, "http://foo");
std::thread t2(save_page, "http://bar");
t1.join();
t2.join();
// 现在访问g_pages是安全的,因为线程t1/t2生命周期已结束
for (const auto &pair : g_pages) {
std::cout << pair.first << " => " << pair.second << '\n';
}
}
使用mutex声明并定义一个锁变量,然后用lock_guard获得锁的所有权。创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。即函数返回后,lock_guard对象从栈中销毁,同时自动解锁,使用起来十分方便。
有的时候我们对于递归的函数也有上锁的需求,这个时候如果使用mutex则会造成死锁。我们可以使用递归锁recursive_mutex,它允许同一个线程多次上锁,但是解锁时,解锁的次数要和上锁的次数一致。其用法与mutex是一样的,这样就可以为递归函数上锁而不导致死锁。
至于unique_lock,它具有lock_guard的所有功能,但比其更加灵活。unique_lock对象以独占所有权的方式(unique owership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。与lock_guard只能给一个互斥量上锁不同,unique_lock可以同时锁定多个互斥量,这避免了多道加锁时的资源死锁问题。它的缺点是相比lock_guard空间和性能开销都要大一些。
这是互斥访问自定义类型的示例代码:
struct Box {
explicit Box(int num) : num_things{num} {}
int num_things;
std::mutex m;
};
void transfer(Box &from, Box &to, int num)
{
// 仍未实际取锁
std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
// 锁两个 unique_lock 而不死锁
std::lock(lock1, lock2);
from.num_things -= num;
to.num_things += num;
// 'from.m' 与 'to.m' 互斥解锁于 'unique_lock' 析构函数
}
int main()
{
Box acc1(100);
Box acc2(50);
std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
t1.join();
t2.join();
}
注意我们这里使用了std::defer_lock选项来初始化unique_lock,这样并不会立即加锁。
实际上有3种锁策略:
-
`defer_lock不获得互斥的所有权
-
try_to_lock尝试获得互斥的所有权而不阻塞 -
adopt_lock假设调用方线程已拥有互斥的所有权
如果没有定义任何锁策略,那么unique_lock也会像lock_guard一样立即上锁。除此之外,unique_lock也支持在其对象的生命周期内调用std::unlock主动解锁,还可以多次调用std::lock和std::unlock反复加解锁。这些功能都是lock_guard不具备的。
4.3 条件变量
std::condition_variable,线程间同步的一种方式,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量并通知 condition_variable ,配合mutex使用,例子如下:
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
// 等待直至 main() 发送数据
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return ready;});
// 等待后,我们占有锁。
std::cout << "Worker thread is processing data\n";
data += " after processing";
// 发送数据回 main()
processed = true;
std::cout << "Worker thread signals data processing completed\n";
// 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
lk.unlock();
cv.notify_one();
}
int main()
{
std::thread worker(worker_thread);
data = "Example data";
// 发送数据到 worker 线程
{
std::lock_guard<std::mutex> lk(m);
ready = true;
std::cout << "main() signals data ready for processing\n";
}
cv.notify_one();
// 等候 worker
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});
}
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
}
注意,每执行一次notify_one(),就会有一个wait()被唤醒。这个程序的工作流程是这样的。如果子线程先得到锁m,那么在cv.wait()的回调中因为ready的值为false被阻塞,之后主线程便可以获得锁m,设子线程的标志reday为true,然后通知cv上的一个等待线程(也可以通过notify_all()通知所有线程)。之后主线程获得锁,并调用cv.wait(),同样由于process是false而阻塞。主线程阻塞后,子线程获得锁并继续执行,修改process标志,解锁,并通知等待线程。此时主线程的cv.wait()由于process=true而不再被阻塞,至此程序执行完毕。如果主线程先获得锁m,那么在执行完std::cout << "main() signals data ready for processing\n";后,主线程解锁,子线程获得锁,但被wait阻塞。主线程通知以后,子线程不再阻塞,主线程锁m失败,被阻塞。子线程执行解锁后,子线程完成任务,主线程继续执行,由于process已经被子线程修改,主线程获得锁后没有在cv.wait()阻塞,之后也能正常运行至结束。可见无论谁先获得锁,结果都是一样的。
因此无论执行顺序如何,无论是谁先获得锁,输出都是:
main() signals data ready for processing
Worker thread is processing data
Worker thread signals data processing completed
Back in main(), data = Example data after processing
要注意,使用条件变量时,线程上锁需要使用unique_lock,不能使用lock_guard。
如果进程间需要同步,条件变量仍然是最合适的方式,它比进行循环判断效率更高,不会浪费CPU时间。我们还可以通过条件变量的思路来获得线程执行函数的返回值,只是有些麻烦。接下来我会介绍用std::promise获取返回值的方法。
4.4 期货
这里只介绍std::promise与std::future,用于获取一个线程的返回值,用例如下:
//std::cout std::endl
//std::thread
//std::future std::promise
//std::ref
//std::chrono::seconds
void initiazer(std::promise<int> &promiseObj){
std::cout << "Inside thread: " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
promiseObj.set_value(35);
}
int main(){
std::promise<int> promiseObj;
std::future<int> futureObj = promiseObj.get_future();
std::thread th(initiazer, std::ref(promiseObj));
std::cout << futureObj.get() << std::endl;
th.join();
return 0;
}
这样我们就可以获取th的返回值。如果主线程已经运行到get(),而子线程还没有set_value(),那么主线程就会被阻塞。这样获取返回值,比使用条件变量+互斥量要更简洁高效。
5.原子操作库
在多线程开发中,为了确保数据安全性,经常需要对数据进行加锁、解锁处理。C++11中添加了原子操作库,实现无锁并发编程。涉及同一对象的每个原子操作,相对于任何其他原子操作是不可分的。原子对象不具有数据竞争,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。原子操作库定义在头文件<atomic>中。
5.1 支持类型
atomic是一个结构体模板,可以用各种内置类型进行实例化。原子操作库在C++11中支持类型如下:
| 类型别名 | 定义 |
|---|---|
| std::atomic_bool | std::atomic<bool> |
| std::atomic_char | std::atomic<char> |
| std::atomic_schar | std::atomic<signed char> |
| std::atomic_uchar | std::atomic<unsigned char> |
| std::atomic_short | std::atomic<short> |
| std::atomic_ushort | std::atomic<unsigned short> |
| std::atomic_int | std::atomic<int> |
| std::atomic_uint | std::atomic<unsigned int> |
| std::atomic_long | std::atomic<long> |
| std::atomic_ulong | std::atomic<unsigned long> |
| std::atomic_llong | std::atomic<long long> |
| std::atomic_ullong | std::atomic<unsigned long long> |
| std::atomic_char16_t | std::atomic<char16_t> |
| std::atomic_char32_t | std::atomic<char32_t> |
| std::atomic_wchar_t | std::atomic<wchar_t> |
| std::atomic_int8_t | std::atomic<std::int8_t> |
| std::atomic_uint8_t | std::atomic<std::uint8_t> |
| std::atomic_int16_t | std::atomic<std::int16_t> |
| std::atomic_uint16_t | std::atomic<std::uint16_t> |
| std::atomic_int32_t | std::atomic<std::int32_t> |
| std::atomic_uint32_t | std::atomic<std::uint32_t> |
| std::atomic_int64_t | std::atomic<std::int64_t> |
| std::atomic_uint64_t | std::atomic<std::uint64_t> |
| std::atomic_int_least8_t | std::atomic<std::int_least8_t> |
| std::atomic_uint_least8_t | std::atomic<std::uint_least8_t> |
| std::atomic_int_least16_t | std::atomic<std::int_least16_t> |
| std::atomic_uint_least16_t | std::atomic<std::uint_least16_t> |
| std::atomic_int_least32_t | std::atomic<std::int_least32_t]> |
| std::atomic_uint_least32_t | std::atomic<std::uint_least32_t> |
| std::atomic_int_least64_t | std::atomic<std::int_least64_t> |
| std::atomic_uint_least64_t | std::atomic<std::uint_least64_t> |
| std::atomic_int_fast8_t | std::atomic<std::int_fast8_t> |
| std::atomic_uint_fast8_t | std::atomic<std::uint_fast8_t> |
| std::atomic_int_fast16_t | std::atomic<std::int_fast16_t> |
| std::atomic_uint_fast16_t | std::atomic<std::uint_fast16_t> |
| std::atomic_int_fast32_t | std::atomic<std::int_fast32_t> |
| std::atomic_uint_fast32_t | std::atomic<std::uint_fast32_t> |
| std::atomic_int_fast64_t | std::atomic<std::int_fast64_t> |
| std::atomic_uint_fast64_t | std::atomic<std::uint_fast64_t> |
| std::atomic_intptr_t | std::atomic<std::intptr_t> |
| std::atomic_uintptr_t | std::atomic<std::uintptr_t> |
| std::atomic_size_t | std::atomic<std::size_t> |
| std::atomic_ptrdiff_t | std::atomic<std::ptrdiff_t> |
| std::atomic_intmax_t | std::atomic<std::intmax_t> |
| std::atomic_uintmax_t | std::atomic<std::uintmax_t> |
注意: std::atomic_int*N*_t 、 std::atomic_uint*N*_t 、 std::atomic_intptr_t 及atomic_uintptr_t 分别若且唯若定义了 std::int*N*_t 、 std::uint*N*_t 、 std::intptr_t 及std::uintptr_t 才有定义。
上表来自<atomic>头文件中的定义。
此外,atomic还支持所有的指针类型std::atomic<T*>,且自C++20起,原子操作库为 std::shared_ptr 和 std::weak_ptr 提供部分特化std::atomic<std::shared_ptr<T>> 和 std::atomic<std::weak_ptr<T>> 。细节见 std::atomic 和 std::atomic 。atomic模板还支持浮点类型float 、 double 和 long double。
5.2 Trivially Copyable
这是C++的具名要求,翻译过来就是“平凡可复制”。atomic模板要求类型T是可平凡复制的。
可平凡复制要求:
-
每个复制构造函数为平凡或被删除
-
每个移动构造函数为平凡或被删除
-
每个复制赋值运算符为平凡或被删除
-
每个移动赋值运算符平凡或被删除
-
至少一个复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符未被删除
-
平凡而未被删除的析构函数
复制/移动构造函数和复制/移动赋值运算符的平凡意味着:
-
它不是用户提供的(即它是隐式定义或设为默认的);
-
T无虚成员函数; -
T无虚基类; -
为
T每个基类选择的复制/移动构造函数、复制/移动赋值运算符是平凡的; -
为每个
T类类型(或类类型数组)非静态成员选择的复制/移动构造函数、复制/移动赋值运算符是平凡的;
由此可见,标量类型和可平凡复制对象的数组,还有这些类型的 const 限定(但非 volatile限定)版本,也是可平凡复制的。由于这些限制,一般atomic模板只能对内置类型,也就是我们上面提到过的类型进行实例化,也可以对自定义简单结构体实例化。显然,面向对象编程离不开虚函数,我们自己编写的对象也很难使用默认的构造函数和析构函数,因此我们的自定义类对象很少有可以使用atomic模板实例化的。atomic一般也就是使用在内置类型上,避免加锁。
5.3 使用
使用原子变量还是很方便的,与使用普通变量没有区别,以下是一段示例代码:
atomic_long total(0);
void click()
{
for(int i=0; i<1000000;++i)
{
// 仅仅是数据类型的不同而以,对其的访问形式与普通数据类型的资源并无区别
total += 1;
}
}
此外,也像普通内置类型一样支持自增自减、与、或及异或的操作。
5.4 性能
根据我在网上查找到的一些资料,原子操作库相比线程支持库中可以实现同样功能的互斥量,快了70%以上。但mutex不受变量类型限制,功能上要更强。
6.模版与泛型编程
所谓泛型编程就是以独立于任何特定类型的方式编写代码。泛型编程与面向对象编程一样,都依赖于某种形式的多态性。面向对象编程中的多态性在运行时应用于存在继承关系的类。我们能够编写使用这些类的代码,忽略基类与派生类之间类型上的差异。
在泛型编程中,我们所编写的类和函数能够多态地用于跨越编译时不相关的类型。一个类或一个函数可以用来操纵多种类型的对象。面向对象编程所依赖的多态性称为运行时多态性,泛型编程所依赖的多态性称为编译时多态性或参数式多态性。模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。
6.1 类模板
定义一个类模板的语法如下:
template < parameter-list > class-declaration (1)
export template < parameter-list > class-declaration (2) (C++11 前)
类模板自身不是类型、对象或任何其他实体。不会从从仅含模板定义的源文件生成任何代码。必须实例化模板以令任何代码出现:必须提供模板实参,使得编译器能生成实际的类(或从函数模板生成函数)。
类模板的实例化有两种方式,显式实例化和隐式实例化。
6.1.1 显式实例化
显式实例化定义强制实例化其所指代的 class 、 struct 或 union 。它可以出现在程序中模板定义后的任何位置。而对于给定的实参列表,只允许它在整个程序中出现一次。
namespace N {
template<class T> class Y { void mf() { } }; // 模板定义
}
// template class Y<int>; // 错误:类模板 Y 在全局命名空间不可见
using N::Y;
// template class Y<int>; // 错误:显式实例化在模板的命名空间外
template class N::Y<char*>; // OK :显式实例化
template void N::Y<double>::mf(); // OK :显式实例化
类、函数、变量和成员模板特化能从其模板显式实例化。成员函数、成员类和类模板的静态数据成员能从其成员定义显式实例化。若同一组模板实参的显式特化出现于显式实例化之前,则显式实例化无效果。
特别注意,若以显式实例化定义显式实例化函数模板、变量模板、成员函数模板或类模板的成员函数或静态数据成员,则模板定义必须存在于同一翻译单元中。
在有多个cpp文件的情况下,一般来说我们需要将模板的定义和声明放在头文件中,然后让所有的cpp文件include这个头文件,然后在cpp中隐式实例化模板。通过显式实例化,我们只需要在头文件中声明模板,然后在某个cpp中定义模板并显式实例化,其他的cpp文件就可以直接使用显式实例化好的模板。这就是”模版声明实现分离”。
6.1.2 隐式实例化
在要求完整定义的类型的语境中,或当类型的完整性影响代码,而尚未显式实例化此特定类型时,出现隐式实例化。例如在构造此类型的对象时,但非在构造指向此类型的指针时。
这适用于类模板的成员:除非在程序中使用该成员,否则不实例化它,并且不要求定义:
template<class T> struct Z {
void f() {}
void g(); // 决不定义
}; // 模板定义
template struct Z<double>; // 显式实例化 Z<double>
Z<int> a; // 隐式实例化 Z<int>
Z<char>* p; // 此处不实例化任何内容
p->f(); // 隐式实例化 Z<char> 而 Z<char>::f() 出现于此。
// 决不需要且决不实例化 Z<char>::g() :不必定义它
若已经声明但未定义类模板,则实例化在实例化点产生不完整类类型:
template<class T> class X; // 声明,非定义
X<char> ch; // 错误:不完整类型 X<char>
6.2 函数模板
函数模板定义一族函数,可以是成员函数。
其定义如下:
template < parameter-list > function-declaration (1)
template < parameter-list > requires constraint function-declaration (2) (C++20 起)
function-declaration-with-placeholders (3) (概念 TS)
export template < parameter-list > function-declaration (4) (C++11 前)
6.2.1 显式实例化
函数模板的显式实例化有多种语法。函数模板特化或成员函数模板特化的显式实例化中,尾随的模板实参可以保留未指定,若它能从函数参数推导:
template<typename T>
void f(T s)
{
std::cout << s << '\n';
}
template void f<double>(double); // 实例化 f<double>(double)
template void f<>(char); // 实例化 f<char>(char) ,推导出模板实参
template void f(int); // 实例化 f<int>(int) ,推导出模板实参
有默认参数的函数模板的显式实例化定义不使用该参数,且不会试图实例化之:
char* p = 0;
template<class T> T g(T x = &p) { return x; }
template int g<int>(int); // OK 即使 &p 不是 int 。
6.2.2 隐式实例化
代码在要求函数定义存在的语境中指涉函数,且此特定函数未被显式实例化时,隐式实例化发生。若模板实参列表能从语境推导,则不必提供它。
template<typename T>
void f(T s)
{
std::cout << s << '\n';
}
int main()
{
f<double>(1); // 实例化并调用 f<double>(double)
f<>('a'); // 实例化并调用 f<char>(char)
f(7); // 实例化并调用 f<int>(int)
void (*ptr)(std::string) = f; // 实例化 f<string>(string)
}
6.2.3 实参推导
为实例化函数模板,必须知道每个模板实参,但并非必须指定每个模板实参。在可能时,编译器会从函数实参推导缺失的模板实参。这发生于尝试函数调用时及取函数模板的地址时。
template<typename To, typename From> To convert(From f);
void g(double d)
{
int i = convert<int>(d); // 调用 convert<int,double>(double)
char c = convert<char>(d); // 调用 convert<char,double>(double)
int(*ptr)(float) = convert; // 实例化 convert<int, float>(float)
}
此机制使得使用模板运算符可行,因为没有异于重写做函数调用表达式的语法为运算符指定模板实参。
int main()
{
std::cout << "Hello, world" << std::endl;
// operator<< 经由 ADL 查找为 std::operator<<,
// 然后推导出 operator<<<char, std::char_traits<char>>
// 同时推导 std::endl 为 &std::endl<char, std::char_traits<char>>
}
模板实参推导发生后于函数模板名称查找(可能涉及参数依赖查找),先于重载决议。
6.2.4 重载与特化
为编译到函数模板的调用,编译器必须在非模板重载、模板重载和模板重载的特化间决定。
template< class T > void f(T); // #1 :模板重载
template< class T > void f(T*); // #2 :模板重载
void f(double); // #3 :非模板重载
template<> void f(int); // #4 : #1 的特化
f('a'); // 调用 #1
f(new int(1)); // 调用 #2
f(1.0); // 调用 #3
f(1); // 调用 #4
注意只有非模板和初等模板重载参与重载决议。特化不是重载,且不受考虑。只有在重载决议选择最佳匹配初等函数模板后,才检验其特化以查看何为最佳匹配。
template< class T > void f(T); // #1 :所有类型的重载
template<> void f(int*); // #2 :为指向 int 的指针特化 #1
template< class T > void f(T*); // #3 :所有指针类型的重载
f(new int(1)); // 调用 #3 ,即使通过 #1 的特化会是完美匹配
即重载的优先级要高于特化。
关于模板函数重载的更多内容,参考function_template。
6.3 别名模板
类型别名是指代先前定义类型的名称(同 typedef ),别名模版是指代一族类型的名称。
template<class T>
struct Alloc { };
template<class T>
using Vec = vector<T, Alloc<T>>; // type-id 为<T, Alloc<T>>
Vec<int> v; // Vec<int> 同 vector<int, Alloc<int>>
using Vec1 = vector<int, Alloc<int>>;
Vec1 v1; // 同 Vec<int> v
也可以用typedef进行别名模板的定义。
// 类型别名,等同于
// typedef std::ios_base::fmtflags flags;
using flags = std::ios_base::fmtflags;
// 名称 'flags' 现在指代类型:
flags fl = std::ios_base::dec;
// 类型别名,等同于
// typedef void (*func)(int, int);
using func = void (*) (int, int);
// 名称 'func' 现在指代指向函数的指针:
void example(int, int) {}
func f = example;
// 别名模板
template<class T>
using ptr = T*;
// 名称 'ptr<T>' 现在是指向 T 指针的别名
ptr<int> x;
// 用于隐藏模板形参的别名模版
template<class CharT>
using mystring = std::basic_string<CharT, std::char_traits<CharT>>;
mystring<char> str;
// 能引入成员 typedef 名的别名模版
template<typename T>
struct Container { using value_type = T; };
// 可用于泛型编程
template<typename Container>
void g(const Container& c) { typename Container::value_type n; }
// 用于简化 std::enable_if 语法的类型别名
template<typename T>
using Invoke = typename T::type;
template<typename Condition>
using EnableIf = Invoke<std::enable_if<Condition::value>>;
template<typename T, typename = EnableIf<std::is_polymorphic<T>>>
int fpoly_only(T t) { return 1; }
struct S { virtual ~S() {} };
int main()
{
Container<int> c;
g(c); // Container::value_type 将在此函数为 int
// fpoly_only(c); // 错误: enable_if 禁止它
S s;
fpoly_only(s); // OK : enable_if 允许它
}
6.4 变量模板
C++14起提供支持。变量模板定义一族变量或静态数据成员。
template<class T>
constexpr T pi = T(3.1415926535897932385); // 变量模板
template<class T>
T circular_area(T r) // 函数模板
{
return pi<T> * r * r; // pi<T> 是变量模板实例化
}
6.5 可变参数模板
模板参数包是接受零或更多模板实参(非类型、类型或模板)的模板形参。函数模板形参报是接受零或更多函数实参的函数形参。
模板参数包的形式如下:
type ... Args(可选) (1) (C++11 起)
typename|class ... Args(可选) (2) (C++11 起)
template < parameter-list > typename(C++17)|class ... Args(可选) (3) (C++11 起)
函数参数包的形式如下:
Args ... args(可选) (4) (C++11 起)
模板参数展开(出现于变参数模板体中):
pattern ... (5) (C++11 起)
其中:
-
带可选名称的非类型模板参数包
-
带可选名称的类型模板参数包
-
带可选名称的模板模板参数包
-
带可选名称的函数模板参数包
-
模板参数包展开:展开成零或更多
pattern的逗号分隔列表。模式必须包含至少一个形式参数包。
至少有一个参数包的模板被称作可变参数模板。
6.5.1 可变参数模板函数
可变参数模板函数的定义如下:
template<typename ...T>
void f(T ...args)
{
cout << sizeof...(args) << endl;//打印可变参的个数
}
f(); //0
f(1, 2); //2
f(1, 2.3, ""); //3
可以使用sizeof...获取参数包的大小。
可以使用lambda捕获参数包:
template<class ...Args>
void f(Args... args) {
auto lm = [&, args...] { return f(args...); };
lm();
}
展开可变模版参数函数的方法一般有两种:一种是通过递归函数来展开参数包,另外一种是通过逗号表达式来展开参数包。
using namespace std;
//递归终止函数
template<typename T>
void myprint(T end)//递归到最后一次,调用单参数函数
{
cout << "parameter " << end << endl;
}
//展开函数
template<typename T,class ...Args>
void myprint(T head, Args... rest)
{
cout << "parameter " << head << endl;
myprint(rest...);
}
int main()
{
myprint(1, 2, 3, 4);
return 0;
}
如上是一个简单的可变模板参数函数,它打印所有的参数。这里采用了递归的方式来展开参数包。通过递归函数展开参数包,需要提供一个参展开函数和一个递归终止函数。参数包Args…在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当参数包展开到最后一个参数时,则调用单参数的函数终止递归过程。
递归调用过程如下:
myprint(1,2,3,4);
myprint(2,3,4);
myprint(3,4);
myprint(4);
其实也可以把递归终止函数定义为一个空函数,例如:
void myprint()
{
}
在这种情况下,递归过程如下:
myprint(1,2,3,4);
myprint(2,3,4);
myprint(3,4);
myprint(4);
myprint();
这说明一点,如果参数包为空,也可以用参数包传参,这个时候就是调用空参数的函数重载。要注意,由于可变模板参数函数都是在编译器确定函数重载的,因此递归终止函数必须定义在展开函数前,否则会编译报错(因为无法匹配)。
这是递归展开参数包的另一个例子,很有参考价值:
template<typename T>
T sum(T t)
{
return t;
}
template<typename T, typename ...Types>
auto sum(T first, Types... rest)
{
return first + sum(rest...);
}
int main()
{
cout << sum(1, 2.1, 3.2, 4) << endl; //10.3
return 0;
}
注意到这里的sum(rest...)是之前介绍过的实参推导,通过参数包推导first的类型,这里必须要这样隐式实例化。同时函数的返回类型要写auto而不是T。
假如T为返回类型,如果第一个参数是整数,就会导致返回值只能为整数。如果是auto,那么作为int的first在和作为float的rest做运算时会返回浮点数,这样才是正确结果。如果给定参数sum<T>调用sum则会导致后面传入的浮点类型被隐式转换为整数,这些都是错误的。因此我们在展开参数包的时候一定要特别注意类型定义的细节。
此外还有一点,不像上一个例子,我们可以用空函数作为终止函数。由于需要返回值,我们必须用模板函数来作为递归终止函数,并编写相应逻辑。
接下来介绍通过逗号表达式展开参数包,这种情况通常发生在我们需要在一层逻辑中用到不止一个参数包中的参数的情况。
逗号表达式是C中的语法,可能平时使用比较少,这里先简单介绍下逗号表达式:
d = (a = b, c);
类似这样的式子,其中(a=b,c)就是一个逗号表达式。逗号表达式会按照顺序执行逗号分隔的表达式1、表达式2、表达式3…等等。最后逗号表达式会返回最后一个表达式的值。因此d的值为c。
这是一个用逗号表达式展开参数包的例子:
template<typename T>
void printarg(T t)
{
cout << t << endl;
}
template<typename ...Args>
void myexpand(Args... args)
{
int arr[] = { (printarg(args), 0)... };
}
int main()
{
myexpand(1, 2, 3, 4);
//myexpand('a', 'b', 'c', 'd'); //myexpand 里面的int数组和它的参数无关
return 0;
}
这个例子将分别打印1,2,3,4四个数字。这种展开参数包的方式,不需要通过递归终止函数,是直接在myexpand()函数体中展开的,printarg()不是一个递归终止函数,只是一个处理参数包中的每一个参数的函数。这里使用到了C++11的列表初始化,通过列表表达式初始化变长数组arr。在初始化过程中,{(printarg(args), 0)…}将会展开成{((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… )}。根据逗号表达式的性质,arr最后变成了一个元素值全0的数组,其大小就是参数包的大小。在构造arr的过程中,函数printarg()被执行。数组arr并没有什么实际的用途。
这是另一个例子:
template<typename F,typename ...Args>
void myexpand(const F &f, Args &&...args)
{
//使用了完美转发
initializer_list<int>{(f(std::forward< Args>(args)), 0)...};
}
//调用
myexpand([](int i){cout << i << endl; }, 1, 2, 3); //打印 1 2 3
通过lambda表达式实现和上一个例子同样的效果,这样的好处是可以少写一个模板函数(实际上是在lambda的函数体中完成了逻辑)。myexpand的第一个参数F实际上传了一个由lambda实现的函数包装器,std::initializer_list之前在STL中已经介绍,是列表初始化的模板类。所以这里实际上用参数(f(std::forward< Args>(args)), 0)...构造了一个初始化列表。用逗号表达式调用了函数包装器F,并把参数包传入F。在列表初始化的过程中,参数包被展开。
假如是C++14,由于泛型lambda表达式的存在,还有功能更强的写法:
myexpand([](auto i) {cout << i << endl; }, 1, 2+2, "text",9.213,'z');
这样myexpand就可以接受int以外的参数了。
6.5.2 可变参数模板类
可变参数模板类是一个带可变模板参数的模板类,比如第二章中介绍的std::tuple就是一个可变参数模板类:
std::tuple<> tp;
std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, “”);
tuple的初始化就可以接受任意个参数。
可变参数模板类的参数包的展开的方式和可变参数模板函数的展开方式不同。可变参数模板类的参数包展开需要通过模板特化和继承方式去展开,展开方式比可变参数模板函数要复杂。
先介绍如何通过模板特化展开参数包:
//向前声明
template<typename ...Args>
struct Sum;
//基本定义
template<typename First,typename ...Rest>
struct Sum<First, Rest...>
{
enum{value=Sum<First>::value+Sum<Rest...>::value};
};
//递归终止
template<typename Last>
struct Sum<Last>
{
enum{value=sizeof(Last) };
};
这个Sum类的作用是在编译期计算出参数包中参数类型的size之和。例如,通过sum< int,double,short >::value就可以获取这3个类型的size之和为14。
可以看到一个基本的可变参数模板应用类由三部分组成:
第一部分是:
template<typename ...Args>
struct Sum;
它是前向声明,声明这个sum类是一个可变参数模板类;
第二部分是类的定义:
template<typename First,typename ...Rest>
struct Sum<First, Rest...>
{
enum{value=Sum<First>::value+Sum<Rest...>::value};
};
它定义了一个部分展开的可变模参数模板类,告诉编译器如何递归展开参数包。
第三部分是特化的递归终止类:
template<typename Last>
struct Sum<Last>
{
enum{value=sizeof(Last) };
};
这个前向声明要求sum的模板参数至少有一个,因为可变参数模板中的模板参数可以有0个,有时候0个模板参数没有意义,就可以通过上面的声明方式来限定模板参数不能为0个。
上面的这种三段式的定义也可以改为两段式的,可以将前向声明去掉,这样定义:
template<typename First, typename... Rest>
struct Sum
{
enum { value = Sum<First>::value + Sum<Rest...>::value };
};
template<typename Last>
struct Sum<Last>
{
enum{ value = sizeof(Last) };
};
上面的方式只要一个基本的模板类定义和一个特化的终止函数就行了,而且限定了模板参数至少有一个。
递归终止模板类可以有多种写法,比如上例的递归终止模板类还可以这样写:
template<typename... Args> struct sum;
template<typename First, typenameLast>
struct sum<First, Last>
{
enum{ value = sizeof(First) +sizeof(Last) };
};
即在展开到最后两个参数时终止。
还可以在展开到0个参数时终止:
template<>struct sum<> { enum{ value = 0 }; };
接下来介绍如何通过继承类来展开参数包:
//整型序列的定义
template<int...>
struct IndexSeq{};
//继承方式,开始展开参数包
template<int N, int... Indexes>
struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...> {};
// 模板特化,终止展开参数包的条件
template<int... Indexes>
struct MakeIndexes<0, Indexes...>
{
typedefIndexSeq<Indexes...> type;
};
int main()
{
using T = MakeIndexes<3>::type;
cout <<typeid(T).name() << endl;
return 0;
}
其中MakeIndexes的作用是为了生成一个可变参数模板类的整数序列。最终输出的类型是:struct IndexSeq<0,1,2>。 MakeIndexes继承于自身的一个特化的模板类,这个特化的模板类同时也在展开参数包,这个展开过程是通过继承发起的,直到遇到特化的终止条件展开过程才结束。
MakeIndexes<1,2,3>::type的展开过程是这样的:
MakeIndexes<3> : MakeIndexes<2, 2>{}
MakeIndexes<2, 2> : MakeIndexes<1, 1, 2>{}
MakeIndexes<1, 1, 2> : MakeIndexes<0, 0, 1, 2>
{
typedef IndexSeq<0, 1, 2> type;
}
通过不断的继承递归调用,最终得到整型序列IndexSeq<0, 1, 2>。
如果不希望通过继承方式去生成整形序列,则可以通过下面的方式生成:
template<int N, int... Indexes>
struct MakeIndexes3
{
using type = typename MakeIndexes3<N - 1, N - 1, Indexes...>::type;
};
template<int... Indexes>
struct MakeIndexes3<0, Indexes...>
{
typedef IndexSeq<Indexes...> type;
};
6.5.3 应用
我们通常可以用可变参数模板来消除大量重复代码及实现一些高级功能。
C++11之前如果要写一个泛化的工厂函数,这个工厂函数能接受任意类型的入参,并且参数个数要能满足大部分的应用需求的话,我们不得不定义很多重复的模版定义,比如下面的代码:
template<typename T>
T* Instance()
{
return new T();
}
template<typename T, typename T0>
T* Instance(T0 arg0)
{
return new T(arg0);
}
template<typename T, typename T0, typename T1>
T* Instance(T0 arg0, T1 arg1)
{
return new T(arg0, arg1);
}
template<typename T, typename T0, typename T1, typename T2>
T* Instance(T0 arg0, T1 arg1, T2 arg2)
{
return new T(arg0, arg1, arg2);
}
template<typename T, typename T0, typename T1, typename T2, typename T3>
T* Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3)
{
return new T(arg0, arg1, arg2, arg3);
}
template<typename T, typename T0, typename T1, typename T2, typename T3, typename T4>
T* Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
return new T(arg0, arg1, arg2, arg3, arg4);
}
struct A
{
A(int){}
};
struct B
{
B(int,double){}
};
A* pa = Instance<A>(1);
B* pb = Instance<B>(1,2);
可以看到这个泛型工厂函数存在大量的重复的模板定义,并且限定了模板参数。用可变模板参数可以消除重复,同时去掉参数个数的限制,代码很简洁, 通过可变参数模版优化后的工厂函数如下:
template<typename… Args>
T* Instance(Args&&… args)
{
return new T(std::forward<Args>(args)…);
}
A* pa = Instance<A>(1);
B* pb = Instance<B>(1,2);
6.6 模板特化
有的时候我们必须针对某个模板的实参定制特化的模板代码,这个时候我们就需要用到模板特化。其语法如下:
template <> declaration
任何下列者可以完全特化:
-
函数模板
-
类模板
-
(C++14 起)变量模板
-
类模板的成员函数
-
类模板的静态数据成员
-
类模板的成员类
-
类模板的成员枚举
-
类或类模板的成员类模板
-
类或类模板的成员函数模板
例如:
template<typename T> // 初等模板
struct is_void : std::false_type
{
};
template<> // 对 T = void 显式特化
struct is_void<void> : std::true_type
{
};
int main()
{
// 对于任何异于 void 的类型 T ,类导出自 false_type
std::cout << is_void<char>::value << '\n';
// 但当 T 为 void 时类导出自 true_type
std::cout << is_void<void>::value << '\n';
}
特化函数模板时,可忽略其实参,若模板实参推导能从函数参数提供它们:
template<class T> class Array { /*...*/ };
template<class T> void sort(Array<T>& v); // 初等模板
template<> void sort(Array<int>&); // 对 T = int 的特化
// 不需要写
// template<> void sort<int>(Array<int>&);
不能在函数模板、成员函数模板,及在隐式实例化类时的类模板的成员函数的显式特化中指定默认函数参数。显式特化不能是友元声明。
在类体外定义显式特化的类模板的成员时,不使用 template <> 语法,除非它是作为类模板特化的显式特化的成员类模板的成员,因为其他情况下,语法会要求这种定义以嵌套模板所要求的 template<parameters> 开始:
template< typename T>
struct A {
struct B {}; // 成员类
template<class U> struct C { }; // 成员类模板
};
template<> // 特化
struct A<int> {
void f(int); // 特化的成员函数
};
// template<> 不用于特化的成员
void A<int>::f(int) { /* ... */ }
template<> // 成员类的特化
struct A<char>::B {
void f();
};
// template<> 亦不用于特化的成员类的成员
void A<char>::B::f() { /* ... */ }
template<> // 成员类模板的的定义
template<class U> struct A<char>::C {
void f();
};
// template<> 在作为类模板定义显式特化的成员类模板时使用
template<>
template<class U> void A<char>::C<U>::f() { /* ... */ }
模板的静态数据成员的显式特化是定义,若声明包含初始化器;否则,它是声明。这些定义对于默认初始化必须用花括号:
template<> X Q<int>::x; // 静态成员的声明
template<> X Q<int>::x (); // 错误:函数声明
template<> X Q<int>::x {}; // 静态成员的默认初始化定义
类模板的成员或成员模板可对于类模板的隐式实例化显式特化,即使成员或成员模板定义于类模板定义中。
template<typename T>
struct A {
void f(T); // 成员,声明于初等模板
void h(T) {} // 成员,定义于初等模板
template<class X1> void g1(T, X1); // 成员模板
template<class X2> void g2(T, X2); // 成员模板
};
// 成员的特化
template<> void A<int>::f(int);
// 成员特化 OK ,即使定义于类中
template<> void A<int>::h(int) {}
// 类外成员模板定义
template<class T>
template<class X1> void A<T>::g1(T, X1) { }
// 成员模板特化
template<>
template<class X1> void A<int>::g1(int, X1);
// 成员模板特化
template<>
template<> void A<int>::g2<char>(int, char); // 对于 X2 = char
// 同上,用模板实参推导 (X1 = char)
template<>
template<> void A<int>::g1(int, char);
成员或成员模板可嵌套于多个外围类模板中。在这种成员的显式特化中,对每个显式特化的外围类模板都有一个 template<> 。
template<class T1> class A {
template<class T2> class B {
void mf();
};
};
template<> template<> class A<int>::B<double>;
template<> template<> void A<char>::B<char>::mf();
在这种嵌套声明中,某些层次可保留不特化(除了若其外围类不特化,则不能特化类成员模板)。对于每个这种层次,声明需要 template<arguments> ,因为这种特化自身是模板:
template <class T1> class A {
template<class T2> class B {
template<class T3> void mf1(T3); // 成员模板
void mf2(); // 非模板成员
};
};
// 特化
template<> // 对于特化的 A
template<class X> // 对于不特化的 B
class A<int>::B {
template <class T> void mf1(T);
};
// 特化
template<> // 对于特化的 A
template<> // 对于特化的 B
template<class T> // 对于不特化的 mf1
void A<int>::B<double>::mf1(T t) { }
// 错误: B<double> 被特化而且是成员模板,故其外围的 A 也必须特化
template<class Y>
template<> void A<Y>::B<double>::mf2() { }
7.Strongly-typed enums 强类型枚举
在C++11以前的枚举类型中,枚举类型的名字都在其父作用域空间可见的。例如:
enum Type { General, Light, Medium, Heavy };
enum Category{ General, Pistol, MachineGun, Cannon };
由于Category中的General和Type中的General都是全局的名字,因此编译器会报错。另外一个缺陷是传统枚举值总是被隐式转换为整形,用户无法自定义类型。我们通常使用的变量个数都不超过255个,也就是说用一个字节存储就足够了。但是,枚举变量却是按整形来存储的。我们多么希望可以指定存储类型,对于小于255的enum变量,要是可以指定用char来存储就好了。C++11中的强类型枚举解决了这些问题。
声明强类型枚举很简单,只需要在原有的enum后加上关键字class即可。
enum class Type { General, Light, Medium, Heavy };
这样,就声明了一个强类型枚举。强类型枚举有以下几点优势:
-
强作用域,强类型枚举成员的名称不会被输出到其父作用域空间。
-
转换限制,强类型枚举成员的值不可以与整型隐式地相互转换。
-
可以指定底层存储类型,强类型枚举默认的底层类型为
int,但也可以显式地指定底层存储类型,具体的做法就是在枚举名称后面加上冒号和类型,该类型可以是除wchar_t之外的任何整形类型。比如:enum class Type : char { General, Light, Medium, Heavy };
8.面向对象程序设计
8.1 类的列表初始化
一个类(class struct union)是否可以使用列表初始化来完成初始化工作,取决于类是否是一个聚合体(aggregate),首先看下C++中关于类是否是一个聚合体的定义:
-
无用户自定义构造函数。
-
无私有或者受保护的非静态数据成员
-
无基类
-
无虚函数
-
无{}和=直接初始化的非静态数据成员
8.2 构造函数初始化列表
与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。从概念上来讲,构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段。
class foo
{
public:
foo(string s, int i):name(s), id(i){} ; // 初始化列表
private:
string name ;int id ;
};
必须在类初始化列表中初始化的几种情况:
-
类成员为const类型
-
类成员为引用类型
-
类成员为没有默认构造函数的类类型
-
如果类存在继承关系,派生类必须在其初始化列表中调用基类的构造函数
8.3 虚函数的override和final指示符
override 可以帮助程序员的意图更加的清晰的同时让编译器可以为我们发现一些错误,其只能用于覆盖基类的虚函数;final 使得任何尝试覆盖该函数的操作都将引发错误,并不特指虚函数。这些修饰符均出现在形参列表(包括任何const或者引用限定符)以及尾置返回类型之后。
struct A
{
virtual void foo();
void bar();
};
struct B : A
{
void foo() const override; // 错误: B::foo 不覆写 A::foo
// (签名不匹配)
void foo() override; // OK : B::foo 覆写 A::foo
void bar() override; // 错误: A::bar 非虚
};
struct Base
{
virtual void foo();
};
struct A : Base
{
void foo() final; // A::foo 被覆写且是最终覆写
void bar() final; // 错误:非虚函数不能被覆写或是 final
};
struct B final : A // struct B 为 final
{
void foo() override; // 错误: foo 不能被覆写,因为它在 A 中是 final
};
struct C : B // 错误: B 为 final
{
};
8.4 继承构造函数
在C++继承中,我们可能会遇到下面这个例子:
class Base
{
public:
Base(int va)
:m_value(va)
{
}
Base(char c)
:m_c(c)
{
}
private:
int m_value;
char m_c;
};
class Derived :public Base
{
private:
public:
//假设派生类只是添加了一个普通的函数
void display()
{
}
//那么如果我们在构造B的时候想要拥有A这样的构造方法的话,就必须一个一个的透传各个接口,那么这是很麻烦的
Derived(int va)
:Base(va)
{
}
Derived(char c)
:Base(c)
{
}
};
上面过程是很麻烦的,但是呢C++11中推出了继承构造函数,使用using来声明继承基类的构造函数,我们可以这样写:
class Base1
{
public:
Base1(int va)
:m_value(va)
{
}
Base1(char c)
:m_c(c)
{
}
private:
int m_value;
char m_c;
};
class Derived1 :public Base1
{
private:
int m_d{0};
public:
//假设派生类只是添加了一个普通的函数
void display()
{
}
//使用继承构造函数
using Base1::Base1;
};
而且,更神奇的是,C++11标准继承构造函数被设计为跟派生类中的各个类默认函数(默认构造,析构,拷贝构造等)一样是隐式声明的。那么这就意味着如果一个继承构造函数不被相关代码使用,编译器就不会产生真正的函数代码,这样比透传更加节省了空间。
要注意以下几点:
-
继承构造函数只会初始化基类的成员变量,对于派生类的成员变量就无能为力
-
基类的构造函数可能会有默认值,但是对于继承构造函数来讲,参数的默认值是不会被继承的。
-
私有构造是不会被继承的
-
在多继承的情况下,可能出现冲突的情况
8.5 委派构造函数
c++11的委派构造函数是在构造函数的初始化列表位置进行构造的,委派的:
class Info
{
private:
void Init()
{
/*一些初始化操作*/
}
int type = 3;
char c = 'D';
public:
Info()
{
Init();
}
Info(int i)
:type(i)
{
Init();
}
Info(char cc)
:c(cc)
{
Init();
}
};
这样我们三个构造函数,都调用了Init初始化,这样很麻烦,我们可以利用委托构造函数改写:
class Info1
{
private:
void Init()
{
/*一些初始化操作*/
}
int type = 3;
char c = 'D';
public:
Info1()
{
}
Info1(int i)
:Info1()
{
type = i;
}
Info1(char cc)
:Info1()
{
c = cc;
}
};
这样的版本就比上面简单多了。上面的Init()函数被称为目标构造函数,其它两个构造函数被称为委派构造函数。要注意,不能同时使用委派构造函数和初始化列表。
9.其他
到此,我已经列出了我在工作中曾经了解并使用过的C++11新特性,有关更多C++11及之后的高级特性,请参考cpp_reference
