C++11常规特性之其他一些新特性

C++11有很多的新特性,这里只记录一下一些比较会常用到的新特性,有:auto和decltype、nullptr、noexcept、lambda、基于范围的for语句、初始化器列表、右值引用和move语义、constexpr、override和final、强类型枚举(Strong-type enums)、智能指针(Smart Pointers)、非成员begin()和end()以及static_assert和type traits

nullptr

以前都是用0来表示空指针的,但由于0可以被隐式类型转换为整形,这就会存在一些问题。关键字nullptr是std::nullptr_t类型的值,用来指代空指针。nullptr和任何指针类型以及类成员指针类型的空值之间可以发生隐式类型转换,同样也可以隐式转换为bool型(取值为false)。但是不存在到整形的隐式类型转换。但是为了向前兼容,0仍然是个合法的空指针值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void foo(int* p) {}

void bar(std::shared_ptr<int> p) {}

int* p1 = NULL;
int* p2 = nullptr;
if(p1 == p2)
{
}

foo(nullptr);
bar(nullptr);

bool f = nullptr;
int i = nullptr; // error: A native nullptr can only be converted to bool or, using reinterpret_cast, to an integral type

Range-based for loops (基于范围的for循环)

为了在遍历容器时支持”foreach”用法,C++11扩展了for语句的语法。用这个新的写法,可以遍历C类型的数组、初始化列表以及任何重载了非成员的begin()和end()函数的类型。

1
2
3
4
5
6
7
8
9
10
for (const auto x : { 1,2,3,5,8,13,21,34 }) cout << x << '\n';

void f(vector<double>& v)
{

for (auto x : v) cout << x << '\n';
for (auto& x : v) ++x; // using a reference to allow us to change the value
}

int arr[] = {1,2,3,4,5};
for(int& e : arr) {e=e*e;};

constexpr和const

const并未区分出编译期常量和运行期常量
constexpr限定在了编译期常量

1
2
3
constexpr int mf = 20; // 常量表达式
constexpr int limit = mf + 1; // 常量表达式
constexpr int sz = size(); // 如果size()是常量表达式则编译通过,否则报错

constexpr修饰的函数,返回值不一定是编译期常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <array>
using namespace std;

constexpr int foo(int i)
{

return i + 5;
}

int main()
{

int i = 10;
std::array<int, foo(5)> arr; // OK

foo(i); // Call is Ok

// But...
std::array<int, foo(i)> arr1; // Error

}

constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。不过,我们不必因此而写两个版本,所以如果函数体适用于constexpr函数的条件,可以尽量加上constexpr。

而检测constexpr函数是否产生编译时期值的方法很简单,就是利用std::array需要编译期常值才能编译通过的小技巧。这样的话,即可检测你所写的函数是否真的产生编译期常值了。

以上内容来自知乎

通常,我们希望编译时期计算可以保护全局或者名字空间内的对象,对名字空间内的对象,我们希望它保存在只读空间内。
对于那些构造函数比较简单,可以成为常量表达式(也就是可以使用constexpr进行修饰)的对象可以做到这一点

1
2
3
4
5
6
7
8
struct Point {
int x,y;
constexpr Point(int xx, int yy) : x(xx), y(yy) { }
};
constexpr Point origo(0,0);
constexpr int z = origo.x;
constexpr Point a[] = {Point(0,0), Point(1,1), Point(2,2) };
constexpr int x = a[1].x; // x becomes 1

  1. const的主要功能是修饰一个对象而不是通过一个接口(即使对象很容易通过其他接口修改)。只不过声明一个对象常量为编译器提供了优化的机会。特别是,如果一个声明了一个对象常量而他的地址没有取到,编译器通常可以在编译时对他进行初始化(尽管这不是肯定的)保证这个对象在他的列表里而不是把它添加到生成代码里。
  2. constexpr的主要功能可以在编译时计算表达式的值进行了范围扩展,这是一种计算安全而且可以用在编译时期(如初始化枚举或者整体模板参数)。constexpr声明对象可以在初始化编译的时候计算出结果来。他们基本上只保存在编译器的列表,如果需要的话会释放到生成的代码里。

以上内容来自知乎

override和final

我总觉得C++中虚函数的设计很差劲,因为时至今日仍然没有一个强制的机制来标识虚函数会在派生类里被改写。vitual关键字是可选的,这使得阅读代码变得很费劲。因为可能需要追溯到继承体系的源头才能确定某个方法是否是虚函数。为了增加可读性,我总是在派生类里也写上virtual关键字,并且也鼓励大家都这么做。即使这样,仍然会产生一些微妙的错误。看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
class B
{
public:
virtual void f(short) {std::cout << "B::f" << std::endl;}
};

class D : public B
{
public:
virtual void f(int) {std::cout << "D::f" << std::endl;}
};

D::f 按理应当重写 B::f。然而二者的声明是不同的,一个参数是short,另一个是int。因此D::f(原文为B::f,可能是作者笔误——译者注)只是拥有同样名字的另一个函数(重载)而不是重写。当你通过B类型的指针调用f()可能会期望打印出D::f,但实际上则会打出 B::f 。

另一个很微妙的错误情况:参数相同,但是基类的函数是const的,派生类的函数却不是。

1
2
3
4
5
6
7
8
9
10
11
class B
{
public:
virtual void f(int) const {std::cout << "B::f " << std::endl;}
};

class D : public B
{
public:
virtual void f(int) {std::cout << "D::f" << std::endl;}
};

同样,这两个函数是重载而不是重写,所以你通过B类型指针调用f()将打印B::f,而不是D::f。

幸运的是,现在有一种方式能描述你的意图。新标准加入了两个新的标识符(不是关键字)::

  1. override,表示函数应当重写基类中的虚函数。
  2. final,表示派生类不应当重写这个虚函数。
    第一个的例子如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class B
    {
    public:
    virtual void f(short) {std::cout << "B::f" << std::endl;}
    };

    class D : public B
    {
    public:
    virtual void f(int) override {std::cout << "D::f" << std::endl;}
    };

现在这将触发一个编译错误(后面那个例子,如果也写上override标识,会得到相同的错误提示):

1
'D::f' : method with override specifier 'override' did not override any base class methods

另一方面,如果你希望函数不要再被派生类进一步重写,你可以把它标识为final。可以在基类或任何派生类中使用final。在派生类中,可以同时使用override和final标识。

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


class D : public B
{
public:
virtual void f(int) override final {std::cout << "D::f" << std::endl;}
};

class F : public D
{
public:
virtual void f(int) override {std::cout << "F::f" << std::endl;}
};

被标记成final的函数将不能再被F::f重写。

Strongly-typed enums 强类型枚举

传统的C++枚举类型存在一些缺陷:它们会将枚举常量暴露在外层作用域中(这可能导致名字冲突,如果同一个作用域中存在两个不同的枚举类型,但是具有相同的枚举常量就会冲突),而且它们会被隐式转换为整形,无法拥有特定的用户定义类型。

在C++11中通过引入了一个称为强类型枚举的新类型,修正了这种情况。强类型枚举由关键字enum class标识。它不会将枚举常量暴露到外层作用域中,也不会隐式转换为整形,并且拥有用户指定的特定类型(传统枚举也增加了这个性质)。

1
2
enum class Options {None, One, All};
Options o = Options::All;

在标准C++中,枚举类型不是类型安全的。枚举类型被视为整数,这使得两种不同的枚举类型之间可以进行比较。C++03 唯一提供的安全机制是一个整数或一个枚举型值不能隐式转换到另一个枚举别型。 此外,枚举所使用整数类型及其大小都由实现方法定义,皆无法明确指定。 最后,枚举的名称全数暴露于一般范围中,因此C++03两个不同的枚举,不可以有相同的枚举名。 (好比 enum Side{ Right, Left }; 和 enum Thing{ Wrong, Right }; 不能一起使用。)

1
2
3
4
5
6
7
enum class Enumeration
{
Val1,
Val2,
Val3 = 100,
Val4 /* = 101 */,
};

此种枚举为类型安全的。枚举类型不能隐式地转换为整数;也无法与整数数值做比较。 (表示式 Enumeration::Val4 == 101 会触发编译期错误)。

枚举类型所使用类型必须显式指定。在上面的示例中,使用的是默认类型 int,但也可以指定其他类型:

1
enum class Enum2 : unsigned int {Val1, Val2};

枚举类型的语汇范围(scoping)定义于枚举类型的名称范围中。 使用枚举类型的枚举名时,必须明确指定其所属范围。 由前述枚举类型 Enum2 为例,Enum2::Val1是有意义的表示法, 而单独的 Val1 则否。

此外,C++11 允许为传统的枚举指定使用类型:

1
enum Enum3 : unsigned long {Val1 = 1, Val2};

枚举名 Val1 定义于 Enum3 的枚举范围中(Enum3::Val1),但为了兼容性, Val1 仍然可以于一般的范围中单独使用。

在 C++11 中,枚举类型的前置声明 (forward declaration) 也是可行的,只要使用可指定类型的新式枚举即可。 之前的 C++ 无法写出枚举的前置声明,是由于无法确定枚举参数所占的空间大小, C++11 解决了这个问题:

1
2
3
4
5
enum Enum1;                     // 不合法的 C++ 與 C++11; 無法判別大小
enum Enum2 : unsigned int; // 合法的 C++11
enum class Enum3; // 合法的 C++11,列舉類別使用預設型別 int
enum class Enum4: unsigned int; // 合法的 C++11
enum Enum2 : unsigned short; // 不合法的 C++11,Enum2 已被聲明為 unsigned int

static_assert和 type traits

static_assert提供一个编译时的断言检查。如果断言为真,什么也不会发生。如果断言为假,编译器会打印一个特殊的错误信息。是在编译的时候进行断言,所以在实时编译的环境下编辑代码的时候,如果断言为假的话,就会直接提示错误

1
2
3
4
5
6
const int sj = 4;
static_assert(sj<9, "Size is too small"); //OK and assert is true

int a = 4;
// error 因为是在编译期的断言,所以在编译期必须能够对断言的内容进行确定,由于a是运行时动态确定的,所以这里编译错误
static_assert(a<9, "Size is too small");

static_assert和type traits一起使用能发挥更大的威力。type traits是一些class,在编译时提供关于类型的信息。在头文件中可以找到它们。这个头文件中有好几种class: helper class,用来产生编译时常量。type traits class,用来在编译时获取类型信息,还有就是type transformation class,他们可以将已存在的类型变换为新的类型。

下面这段代码原本期望只做用于整数类型。

1
2
3
4
5
template <typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
return t1 + t2;
}

但是如果有人写出如下代码,编译器并不会报错

1
2
std::cout << add(1, 3.14) << std::endl;
std::cout << add("one", 2) << std::endl;

程序会打印出4.14和”e”。但是如果我们加上编译时断言,那么以上两行将产生编译错误。

1
2
3
4
5
6
7
8
template <typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
static_assert(std::is_integral<T1>::value, "Type T1 must be integral");
static_assert(std::is_integral<T2>::value, "Type T2 must be integral");

return t1 + t2;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
error C2338: Type T2 must be integral
see reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled
with
[
T2=double,
T1=int
]
error C2338: Type T1 must be integral
see reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled
with
[
T1=const char *,
T2=int
]