C++ 11中常用新特性

Jun 10, 2014


前言

C++ 11 标准出来已经很久了,距离上一个C++标准(C++03)已经快10年了,而这次C++新标准带来了许多令人期待的新特性。具体C++ 11标准带来哪些特性,这个就不用多说了,许多资料上都有说明。个人认为最权威的还是委员会提供的文档,当然,这里介绍一本书《深入理解C++11》(Michael Wong),这本书对C++11特性有较全面的说明,同时提供对每个特性的面向说明,作为一本查阅书籍还算是一本好书。

就标准发布这么久来说,C++ 11的很多内容在网上都可以找得到,写这个博客的原因无非就是自己学习的时候记录一下,别无其他。总的来说,C++ 11的更新内容还是很多的,但是有很多特性对于我们这种非库作者来说很少用到,所以对于新特性的支持我们先学习经常用到的即可。本文所涉及到C++ 特性:

  • auto关键字
  • 智能指针
  • using关键字
  • 空指针nullptr
  • lambda函数
  • std::thread线程与原子操作

auto关键字

我一直都认为auto关键出现的非常及时,在C++发展至今,很多namespace和类型变得越来越长,在写代码的时候需要写更多额外的东西,而这些东西又是相同的,就像下面这样:

std::vector<int>::iterator iter = vec.begin();
// 使用auto
auto iter = vec.begin();

看起来auto非常节省额外的时间,终于不用在写那么长的声明了。
auto是C++11中类型推导关键字。在lua中,对于一个变量的声明可以这样:

local str = "chencheng";
-- 或者
str = 123;

在这个过程中,str会自动推导为 string 类型和整形。还有python的方式:

name = "world"
print 'hello %s' % name

与这些动态语言相比,C++在变量使用上相当严格,必须先声明,值必须为所声明的类型。动态语言中对于变量的使用方式非常随性,大概这就是所谓“动态类型”的体验。严格来讲,静态类型与动态类型的主要区别在于对变量进行类型检查的时间点,静态类型检查变量类型主要发生在编译器,而动态类型的检查则发生在运行期,在运行期检查类型变量涉及到另外一个技术,即类型推导。在运行期推导出目的类型。auto推导最大的优势就是在拥有初始化表达式的复杂类型变量声明时得以简化代码。如下:

void loop(std::vector<std::string>& vs)
{
    for(auto iter = vs.begin(); iter != vs.end(); iter++)
    {
        // do anything...
    }
}
// 
class Person
{
public:
    Person()
    {
        name = "chencheng";
    }
};

auto* chencheng = new Person();
auto index = 12;
// auto还可以接受泛型推导
template<typename T1, typename T2>
double Sum(T1& t1, T2& t2)
{
    auto sum = t1 + t2;
    return s;
}

int a = 3;
float b = 5;
auto e = Sum<int, long>(a, b);
// 通过typeid的结果,sum在Sum中向上转换推导成long类型
// 由于返回值是double,所以存在了long->double的隐式转换

// auto还可以对指针或者引用进行类型推导
auto* pA = new int(12);
int bb = 13;
auto* pB = &bb;
auto& bB = bb;
// 此句将编译失败,Sum返回一个临时变量,无法对临时变量进行取地址操作
auto* pSum = &Sum<int, long>(a, b); 

那么auto在什么时候不能推导呢?

  1. 对于函数而言,auto不能作为形参,因此也就无法在参数中使用auto。
  2. 对于结构体而言,非静态成员变量的类型是不能使用auto推导的,同样,对class也是一样(原因很简单,类或者结构体在未实例化时是不存在的,auto是运行期检测,因为也就无法推导非静态成员)。
  3. 数组类型auto无法推导。
  4. 同样auto也不能推导模板参数。

智能指针

关于智能指针的话std里面来的比较迟,在这以前都是直接上boost了。智能指针的作用在此不表,std里面有三种智能指针:

  • shared_ptr,共享指针,可以与多个shared_ptr共享。
  • unique_ptr,唯一指针,不能与其他unique_ptr共享,保持对所指对象访问的唯一性。
  • weak_ptr,弱指针,它不含有引用计数,弱指针的出现主要是为了防止shared_ptr循环引用。

    class Me { public: void Name() { std::cout « “chencheng” « endl; } };

    std::shared_ptrpInt(new int(123)); std::cout << *pInt << endl;

    std::shared_ptrpInt2 = pInt; std::cout << *pInt << endl; std::cout << *pInt2 << endl;

    std::shared_ptrpMe(new Me); pMe->Name();

unique_ptr保持所指对象访问的唯一性,也就是说unique_ptr模板删除了拷贝函数:

std::unique_ptr<int> pInt(new int(123));
std::cout << *pInt << endl;

// 此句无法通过编译,unique\_ptr没有拷贝复制函数
// std::unique\_ptr<int> pInt2 = pInt;

// 此句可以正常编译,操作完成后pInt2拥有所指对象访问的唯一
// 使用std::move强制转换为右值
std::unique_ptr<int>pInt2 = std::move(pInt);

弱指针的使用也比较简单,主要是防止循环引用,关于弱指针可以看这里

std::shared_ptr<int>pInt(new int(123));
std::weak_ptr<int>pInt2 = pInt;
std::cout << *pInt << endl;
std::shared_ptr<int>pInt3 = pInt2.lock();
std::cout << *pInt3 << endl;

弱指针用到的地方比较少,最多的还是出现在使用shared_ptr的地方产生循环引用的问题,正因为weak_ptr没有引用计数,所以一旦shared_ptr计数为0时,再使用weak_ptr的时候就要特别注意了。

using关键字

using关键字的出现也是在一定程度上方便了写代码,当然也带来了技术上的展现。using主要有两个作用:

  1. 引入名称空间,一个是引入namespace,另外一个是在类中引入无法访问而又需要访问的成员或函数。
  2. 与typedef的作用类似,定义变量别称。

    //using 引入父类的的成员函数或者名称一般在特殊继承中使用 private 或者 protected 继承中。 class Base { public: Base() { n = 123; } std::size_t size() const { return n; } protected: std::size_t n; };

    // 这里继承类通过using之后可以正常访问基类的成员,using只会改变当前类的属性, // 而不会改变基类的属性。 class Derived : private Base { public: using Base::size; void Info() { std::cout « this->n « endl; } protected: using Base::n; };

    // 使用using取别名 typedef PCHAR char*; using DOUBLE = double;

    // using 还可以为模板取别名 using VEC = std::vector; VEC vc;

using主要是用在 private 或者 protected 继承中,打开相关命名空间和声明别名,在继承关系中使用using还是需要注意,一不小心会导致继承结构出错,当然用的最多还是打开命名空间。

空指针nullptr

先看一份代码:

void fun(char* c)
{
	std::cout << "Invoke fun(char* c)" << endl;
}

void fun(int i)
{
	std::cout << "Invoke fun(int i)" << endl;
}

int main()
{
	fun(0);
	fun(NULL);
	fun((char*)0);
}

上面的代码会输出:

Invoke fun(int i)
Invoke fun(int i)
Invoke fun(char* c)

显然输出的结果与预期的相悖。windows上,stddef.h文件中,将NULL定义0,编译期编译器就对NULL进行了隐式转换为0,也就导致了调用的是 fun(int i) 而不是 fun(char* c)。C++ 98标准中,常量0既可以是一个整形,也可以是 void* 指针,如果想调用 fun(char* c) 版本的话必须将0进行强制转换 (void*)0,如果程序员不注意这个问题,或许在某个地方就会出现类似的问题导致函数调用版本不一致。

C++ 11标准中提出了新的关键字,nullptr,它是一个指针空值常量,它的定义:

typedef decltype(nullptr) nullptr_t

如定义所言,nullptr只是一个值,但它与NULL等价,也就是说在使用NULL的地方都可以用nullptr替代而没有副作用。nullptr_t是空指针类型,意味着nullptr_t可以定义变量,但是不建议这么做。

class Me
{
public:
	// do anythings...
};

auto* pA = new Me;
if (pA == nullptr){}
// 上面的语句等价下面这句,但是上面的一句更加安全性。
if (pA == NULL){}

同样,nullptr也有很多地方需要注意:

  1. nullptr可以隐式转换到任何指针类型。
  2. 通过nullptr_t定义的数据都是等价的,且行为完全一致。
  3. nullptr_t类型数据不能转换为非指针类型,编译器不通过。
  4. nullptr_t类型数据无法用于算术运算表达式,但可以用于关系表达式。

lambda函数

其实lambda函数就是闭包,只不过C++的方式实现与其他语言有点不同。Javascript是这个样子的:

function A()
{
	return function()
	{}
}

在Lua中是这个样子的:

function A()
{
	local B = function()
	{}
	return B();
}

关于lambda函数,其实有几个要点如下:

  1. lambda函数如何捕获上下文环境。
  2. lambda函数如何调用。
  3. lambda函数如何转换。

一个个来解决上面的问题,由于lambda函数作为一个内部函数(也可以理解成一个局部变量,只不过这个变量类型是lambda类型),那么它肯定是要涉及到上下文环境的调用,即对于变量的调用,C++中的lambda函数在默认情况下是无法捕获上下文环境中的变量的,语言上面有下面的规则:

对于无参数无返回值的lambda函数:

auto noParam = []()
{
	std::cout << "Hello World" << endl;
};
noParam();

对于有返回值的lambda函数:

auto hasReturnVal = []()->int
{
	std::cout << "Hello World" << endl;
	return 132;
};

对于有参数有返回值的lambda函数:

int X = 123, Y = 456;
auto hasParamByValue = [](int a, int b)->bool
{
	if (a > b)
	{
		a = 789;
		return true;
	}
	return false;
};
std::cout << hasParamByValue(X, Y) << endl;
std::cout << X << endl;

从上面可以看出,实际上X和Y都是通过值传递进入lambda函数中的,在lambda只是修改了X的一份副本而已。对于引用传递的方式进入lambda函数中:

int X = 123, Y = 456;
auto hasParamByReference = [&X, &Y]()->bool
{
	if (X > Y)
	{
		X = 789;
		return true;
	}
	return false;
};

上面的代码就是以 & 的方式以引用传递让 X 和 Y 变量进入到lambda函数中,并且修改了 X 的值。看下lambda函数如何捕获 this 指针:

class Me
{
public:
	string hello = "Hello";
	void Print()
	{
		string world = "World";
		auto Hi = [this, world]()
		{
			std::cout << this->hello << ", " << world << endl;
		};

		Hi();
	}
};

最后一个问题,lambda函数的转换。在js和lua中,闭包函数都是一个局部变量,可以作为一个“值”进行传递,那么在C++中如何传递lambda函数呢,上面我们都是直接用 auto 推导的lambda函数,答案就是:lambda函数可以转换为一个函数指针。

typedef int (*FuncHasReVal) (void);
auto hasReturnVal = []()->int
{
	std::cout << "Hello World" << endl;
	return 132;
};

std::cout << hasReturnVal() << endl;
FuncHasReVal pFunc;
pFunc = hasReturnVal;
std::cout << (*pFunc)() << endl;

所有的lambda都可以转换成lambda函数指针。

在STL中的很多地方都用到了lambda函数,特别是STL中的算法部分,而且STL中的算法部分可以接受函数指针,仿函数,lambda函数等,使用STL算法更加容易。

class Com
{
public:
	void operator () (int x)
	{
		// do anything...
	}
};

void ComFunc(int x)
{
	// do anything...
}
typedef void (*Func) (int x);
std::vector<int>vec;

// 使用传统for循环
for (auto iter = vec.begin(); iter != vec.end(); ++iter)
{
	// do anything...
}

// 使用STL中的算法和仿函数
for_each(vec.begin(), vec.end(), Com());

// 使用STL中的算法和函数指针
Func pFun = ComFunc;
for_each(vec.begin(), vec.end(), pFun);

// 使用lambda函数
for_each(vec.begin(), vec.end(), [&]()->int
{
	// do anything...
});

在lambda之前,很多地方我们都是用的仿函数,直到lanbda出现之后改善了这一情况,但是还是lambda还是有不能替代仿函数的地方。

当你在lambda函数中使用 =& 的时,应该要明白,这里有陷阱。=是拷贝动作,也就意味着会拷贝上下文环境中的所有变量,一旦上下文环境中有比较大的数据,那么拷贝起来将是相当一部分的开销,这点就意味着你不能滥用 =

同样,当你在使用 & 时,你以为是不存在消耗,但是这里存在另外一个问题,异步操作问题。lambda在声明时会把上下文的变量以引用的方式输出到lambda函数环境中,但是在你用某个变量的时候,而变量被某一个线程修改了,如果这个时候不能确定上下文环境和lambda函数的关系,说不定就会得到和预期相悖的结果。

std::thread线程与原子操作

std中新增了std::thread这让我灰常高兴啊,终于不用再和CreateThread_beginthreadex打交道了,虽然说他们内部最终还是调用的CreateThread

以前在写程序的时候,一旦用到了线程一般都是用的CreateThread_beginthreadex与API直接打交道,麻烦的是你需要填很多参数进去,无意中增加了繁琐程度。以前因为会经常用到线程,还特意去封装了一个线程类使用,而现在std::thread提供了极度简便的方式给我们使用。

STL中的std::thread可以接受多种类型的回调函数:函数对象,函数指针,lambda函数,仿函数,而且std::thread对线程的相关使用都进行的封装,使用起来是相当方便。

void ThreadFunction()
{
	for (int i = 0; i < 10; i++)
	{
		std::cout << "i is add:" << i << endl;
	}
}

void FunctionAndBind()
{
	std::cout << "this is std::function<void ()> and std::bind!" << endl;
}

struct TestFunctor
{
	void operator() ()
	{
		std::cout << "this is TestFunctor " << endl;
	}
};

std::thread thread1(ThreadFunction);
thread1.join();

auto fn = [](){std::cout << "this is lambda!" << endl; };
std::thread thread2(fn);
thread2.join();

std::function<void()> fn1 = std::bind(FunctionAndBind);
std::thread thread3(fn1);
thread3.join();

std::thread thread4((TestFunctor()));
thread4.join();

线程是在join之后执行的,join函数的功能是等待线程返回,它会阻塞当前线程,同时带回了返回值。当然,当你想任由函数执行,可以使用thread.detach(),将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。

std::thread在使用上是灰常简单的,但是thread的内容肯定不只是这么一点的,由于std::thread是被新添加的内容,所以std::thread内容比较多。

joinable检查线程是否可被join。检查当前的线程对象是否表示了一个活动的执行线程,由默认构造函数创建的线程是不能被join的。另外,如果某个线程 已经执行完任务,但是没有被join的话,该线程依然会被认为是一个活动的执行线程,因此也是可以被join的。
swapSwap 线程,交换两个线程对象所代表的底层句柄(underlying handles)。
get_id获取线程 ID。
yield当前线程放弃执行,操作系统调度另一线程继续执行。
sleep_until线程休眠至某个指定的时刻(time point),该线程才被重新唤醒。
sleep_for线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能比sleep_duration所表示的时间片更长。
operator=比较特殊,thread对象通过operator=方式进行拷贝,如果发起拷贝的线程正在运行中,那么会调用std::terminate()终止正在运行的线程。

#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::thread t;
    std::cout << "before starting, joinable: " << t.joinable() << '\n';
 
    t = std::thread(foo);
    std::cout << "after starting, joinable: " << t.joinable() << '\n';
 
    t.join();
}

////////////////////////////////////////////////////////
#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::thread t1(foo);
    std::thread::id t1_id = t1.get_id();
 
    std::thread t2(foo);
    std::thread::id t2_id = t2.get_id();
 
    std::cout << "t1's id: " << t1_id << '\n';
    std::cout << "t2's id: " << t2_id << '\n';
 
    t1.join();
    t2.join();
}

////////////////////////////////////////////////////////
#include <iostream>
#include <chrono>
#include <thread>
 
void independentThread() 
{
    std::cout << "Starting concurrent thread.\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Exiting concurrent thread.\n";
}
 
void threadCaller() 
{
    std::cout << "Starting thread caller.\n";
    std::thread t(independentThread);
    t.detach();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Exiting thread caller.\n";
}
 
int main() 
{
    threadCaller();
    std::this_thread::sleep_for(std::chrono::seconds(5));
}

////////////////////////////////////////////////////////
#include <iostream>
#include <thread>
#include <chrono>
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
void bar()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::thread t1(foo);
    std::thread t2(bar);
 
    std::cout << "thread 1 id: " << t1.get_id() << std::endl;
    std::cout << "thread 2 id: " << t2.get_id() << std::endl;
 
    std::swap(t1, t2);
 
    std::cout << "after std::swap(t1, t2):" << std::endl;
    std::cout << "thread 1 id: " << t1.get_id() << std::endl;
    std::cout << "thread 2 id: " << t2.get_id() << std::endl;
 
    t1.swap(t2);
 
    std::cout << "after t1.swap(t2):" << std::endl;
    std::cout << "thread 1 id: " << t1.get_id() << std::endl;
    std::cout << "thread 2 id: " << t2.get_id() << std::endl;
 
    t1.join();
    t2.join();
}

这份代码的输出:

thread 1 id: 140151456622336
thread 2 id: 140151448229632
after std::swap(t1, t2):
thread 1 id: 140151448229632
thread 2 id: 140151456622336
after t1.swap(t2):
thread 1 id: 140151456622336
thread 2 id: 140151448229632

在传统的多线程编程中,一旦某几个线程用到了同一变量,就需要对这个变量进行同步操作。即多个线程在同一时间可以读取同一变量,但是写就不行了,某些情况会产生竞争,而且在某些情况下还需要固定多个线程的执行顺序,这都需要对线程及其资源做线程间同步。windows平台上针对线程同步有多种方式:关键段,信号量,事件,旋转锁等。

在STL中对于简单变量提供了原子类型,即“最小的且不可变行化”的操作。对一个共享资源的操作时原子操作的话,意味着多个线程访问该资源时,有且仅有唯一一个线程可以对这个资源进行操作。原子操作都是通过“互斥”的访问保证的,传统的原子操作可以用API实现:

CRITICAL_SECTION csLock;
::EnterCriticalSection(&csLock);
k = i;
::LeaveCriticalSection(&csLock);

而在新的<cstdatomic>头文件中,提供了很多基本原子类型,atomic_bool,atomic_int,atomic_char,atomic_long,atomic_short等,他们所对应的普通类型是bool,int,char,long,short。既然如此,原子类型在使用上与普通类型相差不多,只不过原子类型在多线程访问中是互斥的。

atomic_int i = 123;
// 在线程中的读
int temp = i.load();

// 在线程中的写
i.store(789);

由于原子类型的loadstore都是原子操作,因此可以避免线程间的抢夺。实际上atomic_bool是一个typedef:

typedef std::atomic_bool std::atomic<bool>;

也就是说所有原子类型实际上都是通过这么模板得来的,也就是说我们可以自己构造:

std::atomic<int> csInt = 123;
csInt.load();
csInt.store(789);

C++11中新增了原子类型又增加了便捷性,特别是在使用“标记变量”的时候,再也不用自己利用封装原子变量了。

本来还是想写下std::move移动语义的,但是发现移动语义里有太多的内容,比如左值右值,移动构造,完美转发等,每一项都需要对move有比较深刻的理解,真正要把move写完写的好感觉还是要假以时日吧,一来是自己在move方面用得少,二来也就导致理解不够深刻。索性就在这里挖个坑,等自己用到了move相关的东西之后再写吧。