初尝 C++ 11

我学的第一门语言便是C++,不过由于后期一直没有使用C++写过较大的项目,一直处于入门阶段。最近实习,第一个小项目便是搭建一个代理服务器,Manager处于对性能的要求,希望我用C++实现。当时比较懒,觉得用C++重写代理服务器进展会比较慢,便说服Manager让我使用Golang,配合一些现有的框架实现。现在看起来达到了预期效果,但也缺少了锻炼C++能力的机会。

现有的项目中,Android App几乎都是使用NDK进行开发的,近期我们有进行重构和性能优化的打算。恰逢其机,进哥在每周的Code Study上进行了 C++11 新标准的分享,我也借助周末的机会稍作整理。

概览

timeline

C++自98标准化后,变动不大。直到近年开始不断推出新的标准,我们现在似乎还处在不断改进的中期。

按照Manager的说法,改动可以被划分入如下几类:

  • 语法糖类。让代码更加简洁,易于理解。
  • 显式地进行语义申明。填之前留下的坑,并让代码更加符合最佳实践。
  • 其他语言的新特性。
  • 标准库的完善。

Manager之前是做编译器的,所以他觉得大部分修改是标准委员会拍拍大脑制定的。的确,语言的设计不是大杂烩,维持一个简洁规范的语法规则是很有必要的,C++由于需要向前兼容,看起来便不简洁了。

语法糖

这部分是Manager痛斥的,不过也是我们大家喜闻乐见的一些小改进。

Auto关键字

1
2
3
4
auto x = 144000000000000;
auto y = string("hello");
auto z = y + ", world";
auto a = someFunc();

这里主要的便利还是减少了类型申明的冗余,不过对于他人来说可读性会很差。比如auto x = 0;只会推断为int,可能会带来不必要的内存占用(比如范围只需要-128-127),或是溢出。如果需要使用STL的string也需要使用类名,不然会被推断为char数组。

最佳实践还是在能明显看出变量类型的时候使用,如容器的迭代器申明。

1
2
3
4
5

vector<int>::const_iterator ci = vi.begin();
for (auto i = vec.begin();i != vec.end();i++) {
std::cout << (*i) << std::endl;
}

不过还是有坑,配合新出的for loop时注意引用的问题,默认是一个拷贝。

1
2
3
for (auto &i : vec) {
i++
;

}

decltype自动化推导

1
2
3
typedef decltype(someFunc()) ITER
auto a = someFunc(); // 比如返回值为vector<int>::const_iterator
decltype(a) b; // 自动推导为和a一样的类型

看到定义函数指针的用法,比较实用

1
2
3
4
5
6
7
8
9
10
11
12
int myfunc(int a){
return a;
}
decltype(&myfunc) pfunc = myfunc;

int main(int argc, char *argv[])
{

std::cout << (*pfunc)(0) <<std::endl;
pfunc = [](int a){return a+1;};
std::cout << (*pfunc)(0) <<std::endl;
return 0;
}

初始化语法

1
2
3
4
5
6
7
8
9
C c {0,0}; //C++11 only. 相当于: C c(0,0);

int* a = new int[3] { 1, 2, 0 }; /C++11 only

class X {
int a[4];
public:
X() : a{1,2,3,4} {} //C++11, member array initializer
};

看完上面几个例子,似乎很多类型的初始化都可以使用{}来统一了。
不过Manager指出其实只是新增加了new int[3]{1,2,3};这一类的,其他的方法是为了大一统加上的。这样看来,初始化的方法的确比较混乱了。

1
2
void foo(C c){}
foo({0,0});

其实可以这样写,进行自动化推导后初始化传入的结构体。不过不太易于阅读。

1
2
3
4
vector<string> vs={ "first", "second", "third" };
map<string, string> singers =
{ {"Lady Gaga", "+1 (212) 555-7890"},
{"Beyonce Knowles", "+1 (212) 555-0987"}};

适合进行容器初始化。以前map的初始化不友好。

map和表达式配合,有一种Javascript的感觉。

1
2
3
4
5
6
7
8
9
10
11
12
void handler(int a){}
int main(int argc, char *argv[])
{

map<string, decltype(&handler)> handlers =
{
{"ori", [](int a){std::cout << a << std::endl;}},
{"plusone", [](int a){std::cout << a+1 << std::endl;}}
};

(*handlers["ori"])(1);
return 0;
}

明晰语义的改动

delete/default修饰

我们经常需要单例模式需要private构造函数,可以使用delete告诉编译器不生成默认的构造函数。
不过可以申明为private,似乎不是特别的必要。

下面例子是一个比较好的实践:

1
2
void f(int);
void f(double) = delete;

明晰了使用时不能编译器遇到传入double的情况,不会自动做类型转换了

nullptr,有类型的NULL

NULL在函数重载时会产生歧义,导致具体逻辑得看编译器实现了。

1
2
3
4
5
6
void f(int); //#1
void f(char *);//#2
//C++03
f(0); //二义性
//C++11
f(nullptr) //无二义性,调用f(char*)

final/override修饰

final的使用不用多说了。override我们当时讨论了很久其存在的必要性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A{
public:
virtual void f(int a){
std::cout << "in A" << std::endl;
}
};
class B : public A {
public:
virtual void f(int a) override {
std::cout << "in B" << std::endl;
}
};

int main(int argc, char *argv[])
{

A* p = new B();
p->f(1.1);
return 0;
}

上面例子中,最终调用到了B的f,不过如果A的f参数改为double,会由override产生编译错误。
如果不加的话,就会绕过B::f。

委托构造函数

1
2
3
4
5
6
7
8
class Student {
public:
Student(int i, string n): id(i), name(n){}
Student(): Student(0, "none"){}
private:
int id;
string name;
};

之前一直需要抽出公共的构造部分,有了委托后可以更加高效,因为在初始化列表中便初始化成员了。

新特性

Lambda表达式

其实在上文中已经使用过了一些,形如:

1
2
[闭包捕捉](参数列表) -> 返回值类型 {函数体}
[](int a, double b) -> double {return a+b;}

闭包是指在表达式中可以直接访问到表达式创建时的上下文中的变量,这样可以将特定的参数封在表达式内,调用时更加简洁。
在Javascript中,我们可以直接闭包。C++中区分了传值和传引用,所以我们必须通过[]来显式地捕捉外界的变量。
规则比较简单,[=]便是传值所有使用的外界变量;[&]传引用;[&,x]表示x传值,其他传引用;[=,&y]表示y传递,其他传值。

1
2
3
4
int a = 100, b = 10;
// auto 在这里很方便,->double也可以省去,可以被编译器自动推导
auto ff = [=](int x, double y) ->double {return a+b+x + y;};
printf("%f\n", ff(1, 2.2));

在使用标准库的高阶函数时,便可以使用函数表达式,省去了函数的申明和需要闭包的变量之前不方便传递的问题。

1
2
3
4
int sum = 0;
vector<int> nums{1,2,3,4,5};
for_each(nums.begin(), nums.end(), [&sum](int a){sum+=a;});
printf("%d\n", sum);

Javascript中非常常见的立即调用表达式。

1
2
[] { printf("Hi\n"); } ();
[](int i) { printf("Hi%d\n", i); } (100);

右值引用

详细的介绍可参考这里,写得很详细。

这个右值引用应该还是很好的特性。不过需要使用者明白自己究竟在做什么,主要是减少了同类中的一些冗余的拷贝过程。
不过我们当时的争论在于如果只是使用指针,也可以达到同样的效果,不过这里便没有类的封装性了,标准库也没办法通过move告知进行内部成员的移动。不过使用智能指针应该是可以达到同样的效果。
当时还认为如果内部变量是new获得的,应该自行管理不能传递,不过我认为这里的管理都是在同类型中间进行,传递也只是同类的实例传递给另外一个同类,所以管理是封闭在类的内部的,是符合自行管理new创造的对象的。

元组

可以用来返回多个返回值。

1
2
3
4
5
6
7
8
9
typedef std::tuple< int , double, string       > tuple_1;
typedef std::tuple< char, short , const char * > tuple_2;
int main(int argc, char const *argv[])
{

tuple_1 t1;
tuple_2 t2 {'X', 2, "Hola!"}; // ()或是{}初始化都可以,不过建议使用新的{}统一初始化
t1 = t2 ; // 第三个字串元素可由'const char *'隐式转换
return 0;
}

摘自Wikipedia。

增强的标准库

并发库(线程、锁、原子操作)

正则表达式库

通用智能指针

散列表

随机数生成

变长参数模板

多态函数对象包装器

其他

最近希望做到动态的、有不同函数签名的函数的动态调用,后来只能使用switch case实现。
看到C++17标准中有一个invoke,似乎是动态地通过参数列表调用一个callable对象,也许以后还会加入GC和reflect。

更多改动可参考Wikipedia
Coolshell