C++新语言

《C++11/14高级编程:Boost程序库探秘》笔记

####右值引用####
C++11/14标准使用“T&&"的形式表示右值引用,而原来的"T&"则表示左值引用。
对一个对象使用右值引用,意味着显式地标记这个对象是右值,可以被转移来优化,同时相当于为它添加了一个”临时的名字“,生命周期得到了延长,不会在表达式结束时消失,而是与右值引用绑定在了一起。

int&		r1 = ++x;	// 左值引用
int&&		r2 = x++;	// 右值引用,引用了自增后的临时对象xvalue
const int&	r3 = x++;	// 常量左值引用,也可以引用右值
const int&&	r4 = x++;	// 常量右值引用,无实际意义

/**
*	x = 0
*	r1 : 4
*	r2 : 1
*	r3 : 2
*	r4 : 3
**/

####转移语义####
右值对象通过延长临时对象的生命周期,可以被转移进而优化代码,所以C++11/14标准在头文件<utility>里专门定义了便捷函数std::move()来”转移“对象,它是一个模板函数,声明是:

template<class T>
typename remove_reference<T>::type&& move(T&& t) noexcept;

相当于:static_cast<T&&>(t) // 转型为右值引用

C++11/14标准为class新增加了参数类型为"T&&"的转移构造函数和转移赋值函数,只要类实现了这两个特殊函数就能利用右值对象”零成本“构造,这就是转移语义。它是现代C++语言里优化性能的重要手段,只要对象被move()标记为右值引用,那么就可以毫无损失地转移资源,再也不需要担心”深拷贝“的成本。
一个实现转移构造函数和转移赋值函数的简单类,可以使用转移语义。

class moveable
{
private:
	int x;
public:
	moveable() {}
	moveable(moveable&& other)	// 转移构造函数
	{
		std::swap(x, other.x);	// 需要使用某种方式“窃取”右值对象的内容
	}
	moveable& operator=(moveable&& other)	// 转移赋值函数
	{
		std::swap(x, other.x);
		return *this;
	}

public:
	static moveable create() //工厂函数创建对象
	{
		moveable obj;		// 函数栈上创建对象
		return obj;			// 返回临时对象,即右值,会引发转移语义
	}
};

moveable类里有一个工厂函数,它直接返回函数内部的局部变量obj,在函数返回时是一个临时对象,也就是右值,而moveable类也定义了转移构造函数,所以可以直接使用临时对象的值来创建对象,避免了拷贝的代价。示例:

moveable m1;
moveable m2(std::move(m1));
moveable m3 = movebale::create();

C++标准库里的string、vector、deque等组件都实现了转移构造函数和转移赋值函数,可以使用转移语义优化,所以现在在函数里返回一个大容器对象是非常高效的。

这些标准容器(std::array除外)还特别增加了emplace()系列函数,可以使用转移语义直接插入元素,进一步提高了运行性能。例如:

vector<complex<double>> v;	//标准序列容器
v.emplace_back(3,4);		//直接使用右值插入元素,无须构造再拷贝

map<string,string> m;		//标准映射容器
m.emplace("metroid","prime");	//直接使用右值插入元素,无须构造再拷贝

####完美转发####
标准头文件<utility>里还有一个函数std::forward(),用于在繁星编程时实现”完美转发“,可以把函数的参数原封不动地转发给其他函数,声明是:

template<class T> T&& forward(T& t) noexcept;
template<class T> T&& forward(T&& t) noexcept;
void check(int&)
{
	cout << "lvalue" << endl;
}

void check(int&&)
{
	cout << "rvalue" << endl;
}

template<typename T>
void print(T&& v)			// 参数是右值引用,会保持类型不变
{
	check(std::forward<T>(v));	// 完美转发,依据函数参数类型调用不同的函数
}
int x = 10;				// 一个左值对象
print(x);				// 传递左值引用,输出lvalue
print(std::move(x));	// 传递右值引用,输出rvalue

####自动类型推导####

  • auto
    对于复杂的变量类型,我们很难写出他们的名字,例如??? f = bind1st(std::less<int>(),2);然而事实上,编译器是知道这些表达式的类型的,auto关键字可将其用在赋值表达式里声明变量,并在编译期自动推倒出表达式的类型。
    使用auto能简化代码,让编译器去做”不必要“的类型声明:
auto x = 0L;				//推导为long类型
auto s = "zelda";			//推导为字符指针类型
auto iter = v.begin();		//推导为标准容器的迭代器类型
auto f = bind1st(std::less<int>(), 2);	//推导为正确的函数对象类型,很难直接写出

auto的用法注意事项

  • auto只能用于赋值语句里的类型推导,不能直接声明变量(因为无表达式供推导)

  • auto总是推断出值类型(非引用)

  • auto允许使用"const/volatile/&/*"等修饰,从而得到新的类型

  • auto&&总是推断出引用类型

  • decltype
    auto关键字能够在赋值语句里推导类型,但这只是C++语言里一种很少见的应用场景,decltype可以在任意场合下得到表达式的类型。
    decltype在技术上和用法上与sizeof非常相似,因为都需要编译器在编译期计算类型,但sizeof返回整数,decltype返回类型。
    decltype的形式很像函数调用:
    decltype(expression) // 获取表达式的类型,编译期计算
    decltype可以像auto一样用在赋值语句,但可以根据表达式的结果类别和表达式的性质推断出引用或非引用类型,能够更精确地控制类型:

int				x = 0;
const long		y = 100;
decltype(x)		d1 = x;		//类型是int
decltype(x)&	d2 = x;		//类型是int&
decltype(&x)	d3 = &x;	//类型是int*
decltype(x + y) d4 = x + y;	//类型是long
decltype(y)&	d5 = y;		//类型是const long&

decltype还可以用在变量声明、类型定义、函数参数列表、模板参数列表等任意地方。

decltype(std::less<int>()) functor;		//声明一个函数对象
decltype(0.0f) func(decltype(0L) x)		//用于函数返回值和参数声明
{
	return x*x;
}
typedef decltype(func)* func_ptr;		//简单地定义函数指针类型

vector<int> v;
decltype(v)::iterator iter;

template<typename T> class demo {};
demo<decltype(v)> obj;			//在模板参数列表里使用decltype

decltype用法注意事项:

  • decltype(e)的形式获得表达式计算结果的值类型
  • decltype((e))的形式获得表达式计算结果的引用类型,类似auto&&的效果
int				x = 0;
const volatile int	y = 0;
decltype(x)		d1 = x;		//值类型 int
decltype((x))	d2 = x;		//引用类型 int&
decltype(y)		d3 = y;		//值类型 const volatile int
decltype((y))	d4 = y;		//引用类型 const volatile int&
  • decltype(auto)
    C++14标准增加了两者结合起来的语法:
decltype(auto) x = 6;		//整数类型int
decltype(auto) y = 7L;		//整数类型long
decltype(auto) z = x + y;	//整数类型long

####面向过程编程####

  • 空指针
    C/C++语言里空指针用宏NULL表示,它的定义是#define NULL 0
    但NULL实际上是一个整数,不是一个真正的指针,有时候会造成语义混淆,C++11/14增加了新的关键字"nullptr",明确表示空指针的概念,可以隐式转化为任意类型的指针,也可以与指针进行比较运算,但不能转化为非指针的其他类型:
int*		p1 = nullptr;
vector<int>* p2 = nullptr;

assert(!p1 && !p2);
assert(nullptr == p1);		//执行指针比较运算
assert(10 >= nullptr);		//编译错误,不能与整数类型进行运算

nullptr是强类型,类型不是int或者void*,而是一个专用的类型nullptr_t,其定义用了关键字decltype:
typedef decltype(nullptr) nullptr_t;
可以使用nullptr_t任意定义与nullptr等价的空指针:

nullptr_t nil;
double* p3 = nil;
assert(nil == nullptr);
  • 初始化
    C++11/14统一使用花括号"{}"初始化变量,称为”列表初始化“
int			x{};
double		y{ 2.718 };
string		s{ "venom snake" };
complex<double>	c{ 1,1 };
int			a[] = { 1,2,3 };
vector<int>	v = { 4,5,6 };

set<int> get_set()
{
	return{ 2,4,6 };
}
  • 新式for循环
    C++11/14引入了一种更简单便捷的for循环形式,无须显式使用迭代器首尾位置,也无须解引用迭代器,就可以直接访问容器序列里所有元素,例如:
int a[] = { 2,3,5,7 };
for (auto x : a)
{
	cout << x << ",";
}

vector<int> v = { 253,874 };
for (const auto x : v)
{
	cout << x << ",";
}

在声明元素类型时使用auto将推导出值类型,有拷贝代价,也不能修改元素,所以可以为auto添加修饰,使用const auto&/auto&&避免拷贝,或者用auto&以修改元素的值。

for (auto& x : v)
{
	cout << ++x << ",";
}

新式for循环支持C++内建数组和所有的标准容器,对于其他类型,只要它具有begin()和end()成员函数,或者能够使用函数std::begin()和std::end()确定迭代范围的就可以应用于for。

  • 新式函数声明
    C++11/14增加了一种新的函数语法,允许返回值类型后置,基本形式是:
    auto func(...) -> type {...}
    返回值必须用auto来占位,函数名后面需要用”-> type“的形式来声明真正的返回值类型,这里的"type"可以是任意的类型:
auto func(int x) -> decltype(x)
{
	return x*x;
}

template<typename T,typename U>
auto calc(T t,U u) -> decltype(t+u)
{
	return t+u;
}

####面向对象编程####

  • default
    显示指定类的缺省构造/析构等特殊成员函数使用编译器的缺省实现
    用法:在成员函数后面使用"=default"就可以
  • delete
    禁用某些函数,通常是类的构造函数和拷贝构造函数,以阻止对象的拷贝
    用法:在成员函数后面使用"=delete"就可以
  • override
    显式地标记虚函数的重载,明确代码编写的意图。派生类的成员函数名后如果使用了override修饰,那么它必须是虚函数,而且签名也必须与基类的声明一致,否则导致编译错误。
  • final
    控制类的继承,也可以控制虚函数。
  • 在类名后使用final,显式地禁止类被继承,即不能再有派生类
  • 在虚函数后使用final,显示地禁止该函数在子类里再被重载

final可以与override混用,更好地标记类的继承体系和虚函数

struct interface			//基类,无final,可以被继承
{
	virtual void f() = 0;	//纯虚函数
	virtual void g() = 0;	//纯虚函数
};
struct abstract :public interface	//抽象类,无final,可以被继承
{
	void f() override final {}		//虚函数使用final,f()不能再重载
	void g() override {}			//仅使用override,派生类还能重载
};
struct last final : public abstract //类声明使用final,不能再被继承
{
	void f() override {}			//错误,f()不能重载
	void g() override {}			//g()仍然可以重载
}	
struct error :public last			//错误,last类不能被继承
{

};
  • 成员初始化
    允许类在声明时使用赋值或者花括号的方式直接初始化,无须在构造函数里特别指定。
class demo
{
public:
	int x = 0;
	string s = "hello";
	vector<int> v{1,2,3};
};

对于静态成员变量这种方法不适用,仍然要在类定义外初始化(需要分配实际且唯一的存储空间)
另外,这种赋值初始化的形式不能使用auto来推导变量类型。

  • 委托构造
    是一个概念,直接调用本类的其他构造函数,把对象的构造工作”委托“给其他构造函数来完成。

####泛型编程####

  • 类型别名
    C++11/14扩展了using关键字的能力,可以完成与typedef相同的工作,使用"using alias = type;"的形式为类型起别名,例如:
using int64 = long;
using llong = long long;

using可以结合template关键字为模板类声明”部分特化“的别名,例如:

template<typename T>
using int_map = std::map<int, T>;	//固定key类型为int
int_map<string> m;					//省略了一个模板参数

template<typename T,typename U>
class demo final {};

template<typename T>
using demo_long = demo<T, long>;

demo<char, int> d1;
demo_long<char> d2;
  • 编译器常量
    constexpr,相当于编译期的const,被其修饰的表达式编译器可以在编译期直接推导出来值。
    对比const,const有双重语义:变量只读,修饰常量。
    constexpr可以用于修饰常量表达式,包括普通函数、成员函数、构造函数等。在使用前,必须有对应的定义语句,同时整个函数的函数体中,不能出现非常量表达式之外的语句。
    constexpr可以修饰模板函数。

  • 静态断言
    C++11/14增加了新关键字static_assert作为编译期的断言,可以在编译期加入诊断信息,提前检查可能发生的错误。基本形式是:
    static_assert(condition,message) //要求编译期的条件必须成立,否则报错

  • 可变参数模板
    使用省略号"…"来声明不确定数量的参数。

template<typename ... T> class some_class {};	//模板类
template<typename ... T> void some_func() {};	//模板函数

"typename … T"声明了一个具有不确定数量参数的模板列表——模板参数包,数量可以是0,可以使1或更多,用sizeof…(T)的方式来得到具体数量。

template<typename ... T>
class variadic_class
{
	using type = x<T...>	//使用...解包,用于模板类
};

template<typename ... Args>
void variadic_func(Args... args)	//使用...解包,用于模板函数
{
	cout << sizeof...(Args) << endl;
}

####函数式编程####
函数式编程是与面向过程编程、面向对象编程、泛型编程并列的一种编程范式,它基于λ演算理论(lambda calculus),把计算过程视为数学函数的组合运算。

  • lambda表达式
    [](params){...} // lambda表达式的基本形式
    在这里,"[]"称为lambda表达式引出操作符,它之后的代码就是lambda表达式,形式如同一个标准的函数,圆括号里是函数的参数,而花括号内则是函数体。
    lambda表达式的类型成为”闭包“,无法直接写出,所以需要用auto的类型推导功能来存储。
auto f1 = [](int x,int y){
	return x < y;
};	//表达式结束,语句末尾需要分号
cout << f1(1,5) << endl;

vector<int> v = {1,3,5,7};
std::for_each(v.begin(),v.end(),
	[](int& x){
		if(x > 3) { x *= 2; }
	});

lambda表达式的返回值可以自动推导,也可以使用新的返回值后置语法:
[](params) -> return_type {...}

  • 捕获外部变量
    lambda表达式的完整声明语法:
    [captures](params) mutable -> type {...}
    []里的"captures"称为”捕获列表“,可以捕获表达式外部作用域的变量,在函数体内部直接使用,这是与普通函数或函数对象最大的不同。
    捕获列表规则:
  • : 无捕获,函数体内不能访问任何外部变量
  • [ =] : 以值(拷贝)的方式捕获所有外部变量,函数体内可以访问但不能修改
  • [ &] : 以引用的方式捕获所有外部变量,函数体内可以访问并修改
  • [ var] : 以值(拷贝)的方式捕获某个外部变量,函数体内可以访问但不能修改
  • [ &var] : 以引用的方式捕获某个外部变量,函数体内可以访问并修改
  • [ this] : 捕获this指针,可以访问类的成员变量和成员函数
  • [ =,&var] : 引用捕获变量var,其他外部变量使用值捕获
  • [ &,var] : 值捕获变量var,其他外部变量使用引用捕获

lambda表达式还可以用关键字mutable修饰,它为值方式捕获添加了一个例外情况,允许变量在函数体内也能修改,但这只是内部的拷贝,不会影响外部变量,例如:
auto f = [=]() mutable { return ++x; }; // x仅可以在内部修改,不影响外部变量

  • 类型转换
    lambda表达式可以转换为一个签名相同的函数指针,例如:
auto f = []() {};
typedef void(*func)();	//函数指针类型

func p1 = f;			//可以隐式转换为函数指针
func p2 = []() { cout << endl; };	//也可以直接赋值给函数指针

注意:转换为函数指针时lambda表达式必须是无捕获列表的。

  • 泛型的lambda表达式
    C++11标准lambda表达式函数参数必须是具体类型,而C++14则增加了泛型的功能:
    auto f = [](auto x) {..}; //泛型lambda表达式,函数参数使用auto推导

####并发编程####
C++11/14标准引入了新的关键字thread_local,它实现了线程本地存储(TLS,thread local storage),是一个与extern、static类似的变量存储指示标记。
线程本地存储是指变量在进程里拥有不止一个实例,每个线程都会拥有一个完全独立的、”线程本地化“的拷贝,多个线程对变量的读写互不干扰,完全避免了竞争、同步的麻烦。

extern		int x;		//外部变量,实体存储在外部,非本编译单元
static		int y = 0;	//静态变量,实体存在在本编译单元
thread_local int z = 0;	//线程局部存储,每个线程拥有独立的实体
auto f = [&]() {
	++y;
	++z;
	cout << y << "," << z << endl;	//在线程内输出
};

thread t1(f);
thread t2(f);

t1.join();
t2.join();

cout << y << "," << z << endl;	//主线程输出

运行结果:
1,1
2,1
2,0


####面向安全编程####

  • 无异常保证
    C++11/14正式废弃”异常规范“,但保留了”声明函数不会抛出任何异常“的功能,引入一个关键字noexcept来明确表述这个含义:
    void func() noexcept {} //函数绝对不会抛出异常

  • 内联名字空间
    C++使用名字空间来解决命名冲突问题,关键字namespace声明一个专有的作用域,使用时必须加上名字空间限定或者使用using打开名字空间。
    C++11/14增加了内联名字空间的概念,允许在一个名字空间声明前用inline关键字修饰,使外部同样可以无须限定直接访问这个名字空间内部的成员:

inline namespace temp{
	int xx = 0;
}
assert (xx == 0);

这个特性对于代码的版本化很有用,可以在多个子名字空间里实现不同版本的功能,而发布时用inline对外只暴露一个实现,很好地隔离版本差异:

namespace release{
	namespace v001{
		void func(){}
	}
	inline namespace v002{
		void func(){}
	}
}
//发布时,外界无须知道子名字空间的实现细节,直接使用release名字空间限定就可以调用正确的版本:
release::func();	//看不到子名字空间,直接使用父命字空间,调用v002的func
  • 强类型枚举
    早期C++里枚举是弱类型,相当于整数,而且枚举值直接暴露在外部名字空间,缺少限定,命名容易冲突,C++11/14为enum增加了安全性,可以用"enum class/struct"的形式声明强类型的枚举,它不能隐式转换为整数,而且必须要使用类型名限定访问枚举值:
enum class color {
	red = 1,
	green,
	blue
};

auto x = color::red;
auto y = red;				//error
auto z = color::red + 1;	//error

C++11/14还允许在枚举声明后用char、int等指示枚举使用的整数类型,更好控制空间占用:
enum class color:char {...}; //要求枚举使用char整数存储


####更多特性####

  • 语言版本号
    宏"__cplusplus"可以辨别当前编译器使用的标准的版本。
  • 未定义:不是C++编译器,而是C编译器

  • 199711L:C++98/03

  • 201103L:C++11

  • 201402L:C++14

  • 超长整型
    超长整数类型long long,至少有64位,可以用unsigned修饰,可以用后缀"LL/ll"或者"ULL/uLL/ull"来显示说明。

  • 原始字符串
    C++11/14提供了新的字符串书写方式——使用“R"(…)"”形式的原始字符串,以大写R开始,之后在括号内的所有字符都会原样保留。例如:

string s = R"(this is a "\string\")";
cout << s << endl;		//输出"this is a "\string\"",特殊符号不需要转义,而是直接保留在字符串里。

这种特性对于书写正则表达式特别有用。
原始字符串还有一种扩展形式,允许在圆括号两边加上最多16个字符——称为delimiter,更好地标记字符串:

auto b = R"***(BioShock Infinity)***";
auto d = R"===(Dark Souls)===";

括号两边的delimiter必须相同且不能用"@“”$“”"等特殊字符

  • 自定义字面值
    C++11/14增加了自定义字面值的功能,允许我们为字面值增加后缀(没开放前缀),定义函数来对字面值进行运算,决定字面值的实际类型。
    自定义字面值需要重载操作符""(两个连续的双引号),:
return_type operator"" _suffix (unsigned long long);
return_type operator"" _suffix (long double);
return_type operator"" _suffix (const char*,size_t);

_suffix为用户自定义的后缀,必须要以下划线开头,自定义字面值的函数的参数只能使用标准固定的几种形式,返回值类型可以返回任意的自定义类型。

long operator"" _kb(unsigned long long v)
{
	return v * 1024;
}

complex<double> operator"" _c(const char* s, size_t n)
{
	using namespace boost::xpressive;

	auto reg = cregex::compile(
		R"--(([0-9\.]+)\+([0-9\.]+)i)--"
	);
	cmatch what;

	auto ok = regex_match(s, what, reg);
	assert(ok);
	return complex<double>(stod(what[1]), stod(what[2]));
}

auto x = 2_kb;
assert(x == 2 * 1024);
auto c = "1.414+1.414i"_c;
cout << c << endl;

C++14标准增加了"h/min/s/ms"等新的字面值后缀,可以直接在代码书写时间单位

  • 其他
    右尖括号:允许模板参数列表里出现两个连续的右尖括号,会优先解释为模板参数列表的结束标记
    函数的默认模板参数:允许模板函数也可以使用默认参数
    增强的联合体
    二进制字面值:可以用“0b/0B”来直接书写二进制数字
    数字分位符:书写很长的数字时,C++14允许使用单引号’来将数字分组,以增强可读性

猜你喜欢

转载自blog.csdn.net/zuolj/article/details/78469056
今日推荐