14.C++11提高性能及操作硬件的能力
1. 常量表达式
1.1 运行时常量性和编译时常量性
//运行时常量
const int getConst(){return 1;}
大多数情况下, const 描述的都是一些 “运行时常量性”的概念,即具有运行时数据的不可更改性。不过有时候我们需要的却是编译时期的常量性,这是 const 无法保证的。
//c 风格的宏,简单粗暴
#define getConst 1
//c++11 中的常量表达式
constexpr getCosnt(){return 1;}
c 风格的宏定义和 c++11 提供的 constexpr 关键字,都可以在编译期对 getConst 进行计算。
1.2 常量表达式函数
1.3 常量表达式值
使用 constexpr 声明的数据最常被问起的问题是,下面两条语句有什么区别?
const int i = 1;
constexpr int j = 1;
事实上,两者在大多数情况下是没有区别的。不过有一点是肯定的,就是如果 i 在全局名字空间中,编译器一定会为 i 产生数据。而对于 j ,如果不是代码中显式地使用了它的地址,编译器可以选择不为它生成数据,而仅将其当做编译时期的值。
2. 变长模板
2.1 概述
在一些情况下,类也需要不定长度的模板参数。最为典型的就是 c++11 标准库中的 tuple 类模板。如果读者熟悉 c++98 中的 pair 类模板的话,那么理解 tuple 也就不困难了。具体来讲, pair 是两个不同类型的数据的集合。比如 pair<int, double> 就能够容纳 int 类型和 double 类型的两种数据。一些如 std::map 的标准库容器,其成员就需要时类模板 pair 的。在 c++11 中, tuple 是 pair 类的一种更为泛化的表现形式。比起 pair, tuple 是可以接受任意多个不同类型的元素的集合。比如我们可以通过:
std::tuple<double, char, std::string> collections;
来声明一个 tuple 模板类。该 collections 变量可以容纳 double、char、std::string 三种类型的数据。当然,读者还可以用更多的参数来声明 collection, 因为 tuple 可以接受任意多的参数。此外,和 pair 类似地,我们可以更为简单的使用 c++11 的模板函数 make_tuple 来创造一个 tuple 模板类型。
std::make_tuple(9.8, 'g', 'gravity');
3.原子类型与原子操作
3.1 并行编程、多线程与 c++11
常见的并行编程有多种模型,如共享内存、多线程、消息传递等。不过从实用性上讲,多线程模型往往具有较大的优势。多线程模型允许同一时间有多个处理器单元执行同一进程中的代码部分,而通过分离的栈空间和共享的数据区及堆栈空间,线程可以拥有独立的执行状态以及进行快速的数据共享。
c++11 标准的一个相当大的变化就是引入了多线程的支持。这使得 c/c++ 语言在进行线程编程时,不必依赖第三方库和标准。而 c/c++ 对线程的支持,一个最为重要的部分,就是在原子操作中引入原子类型的概念。
3.2 原子操作与 c++11 原子类型
所谓原子操作,就是多线程程序中 ”最小的且不可并行化的“ 操作。通常对一个共享资源的操作是原子操作的话,意味着多个线程访问该资源时,有且仅有唯一一个线程在对这个资源进行操作。那么从线程(处理器)的角度来看,其他线程就不能够在本线程对资源访问期间对资源进行操作,因此原子操作对于多线程而言,就不会发生有别于单线程程序的以外状况。
通常情况下,原子操作都是通过 “互斥” 的访问来保证的。实现互斥通常需要平台相关的特殊指令,这在 c++11 标准之前,常常意味着需要在 c/c++ 代码中嵌入内联汇编代码。对程序员来讲,就必须了解平台上与同步相关的汇编指令。当然,如果只是想实现粗粒度的互斥,借助 POSIX 标准的 phtread 库中的互斥锁(mutex)也可以做到(先定义一个 mutex,然后访问共享资源前加锁,访问完共享资源释放锁)。
c++11中,通过对并行编程更为良好的抽象,要实现同样的功能就简单了很多。
#include <thread>
#include <atomic>
#include <functional>
atomic_llong total{0};
long long total1{ 0 };
auto func1 = []()
{
for (long long i =0; i < 100000000LL; ++i)
{
total += i;
total1 += i;
}
};
void testAtomic()
{
cout << "enter testAtomic()...................................................................." << endl;
thread t1(func1);
thread t2(func1);
t1.join();
t2.join();
cout << "total: " << total << endl; //9999999900000000
cout << "total1: " << total1 << endl; // != 9999999900000000 并且每次运行结果不一致
cout << "return from testAtomic()...................................................................." << endl;
}
上例中,我们将变量 total 定义为一个 “原子数据类型”:atomic_llong, 该类型长度等同于 c++11 中的内置类型 long long。 在 c++11 中,程序员不需要为原子数据类型显式的声明互斥锁或调用加锁、解锁的 API,线程就能够对变量 total 互斥地进行访问。
相比于基于 c 以及过程编程的 pthread "原子操作 API" 而言, c++11 对于 ”原子操作“ 概念的抽象遵从了面向对象的思想 —— c++11 标准定义的都是所谓的 ”原子类型“。而传统意义上所谓的 ”原子操作“,则抽象为针对于这些原子类型的从操作(事实上,是原子类型的成员函数,稍后解释)。直观地看,编译器可以保证原子类型在线程间被互斥地访问。这样设计,从并行编程的角度看,是由于需要同步的总是数据而不是代码,因此 c++11 对数据进行抽象,会有利于产生行为更为良好的并行代码。而进一步地,一些琐碎的概念,比如互斥锁、临界区则可以被 c++11 的抽象所掩盖,因此并行代码的编写也会变得更加简单。