C++整理-C++11重要特性
我们按照cppreference给出的顺序整理一下C++11的重要特性:
auto
auto
在C中本来是界定变量存储方式的关键字,在C++11之后,该关键字行为被改变为进行类型推导,最简单的方法是通过auto
声明变量:
auto bar = foo();
编译器会通过foo()
自动推导类型,我们也可以添加类型标记:
const auto bar = foo();
auto& bar = foo();
auto* bar = foo();
为什么auto
要有类型标记?这是因为某些情况下我们需要对类型进行进一步的解释,假如foo()
返回的是一个reference
,auto
的行为就会产生歧义,标记可以消除这种歧义:
auto bar = foo(); // 拷贝
auto& bar = foo(); // 引用
C++11的auto
也可以用来声明函数:
auto
foo() -> int
{
// ...
}
这是我们常说的trailing-return-type
,某些时候我们需要这个特性来引用参数类型,比如下面这个函数返回int
类型变量:
auto
foo(int a) -> decltype(a)
{
// ...
}
C++11的auto
关键字尚且十分稚嫩,我们会在后续章节介绍更新标准的auto
。
decltype
decltype
也是编译期类型推导的手段之一,它是编译器扩展typeof
的标准化,简单来说,我们可以用他来推导已经存在变量的类型:
decltype(1) bar; // int
同样,在C++11时它也没有什么特殊之处,不过值得一提的是,对于括号表达式,decltype
将其处理为左值引用,这个例子来自cppreference:
struct A { double x; };
const A* a;
decltype(a->x) y; // type of y is double (declared type)
decltype((a->x)) z = y; // type of z is const double& (lvalue expression)
default
和delete
以前,我们都知道编译器会帮助我们默认实现一些函数(析构、构造、拷贝等),现在我们可以显化这种默认行为:
class Foo {
public:
Foo() = default; // 默认实现构造函数
~Foo() = delete; // 删除析构函数实现
};
如果某个函数被标记为delete
,那么编译时,如果有人引用这个符号,编译将会失败。
final
和override
它们是面向对象工具的扩展,final
用来标记虚函数不应被子类重写、或者类不应被继承:
class Foo final : public Base {
public:
void bar() final;
};
override
用来标记函数由另一个虚函数覆盖而来,它的目的是保证函数是虚函数,且由父类继承而来
右值引用
左值和右值是一个相当庞大的话题,一般来说:左值是被绑定到具体符号上的变量,他们的生命周期和符号作用域绑定,右值是那些表达式结束之后就不会再存在的临时对象(比如函数的返回值、表达式的结果、非字符字面量)。
我们非常了解左值引用,它就是为已经存在的左值创建一个alias
,但右值引用是什么呢?在语义上,它代表将值的所有权进行转移,举个例子:
void
foo(std::vector<int>&& rval)
{
// ...
}
这往往代表着经过这个函数之后,rval
所指向的变量便走向了它的终结,所以,foo
这个函数获得了处分这个变量的权利,那么,明确地知道变量即将消失有什么好处吗?答案是我们可以通过不拷贝的方式转移对象,为了达成这个目的,我们有了移动构造函数:
class Foo {
public:
Foo(Foo&& other)
{
// 不必执行深拷贝
}
};
当然,我们也同时有了移动赋值函数,其二者大致相同,为了调用这样的函数,我们需要为其传入右值:
Foo a;
Foo(Foo());
Foo(std::move(a));
std::move
是一个工具函数,其将左值的引用转换为右值引用,含义是:我们不会再使用该左值,因此将其移交(给其他函数或者移动构造)
但是需要注意,在进入函数之后,右值引用参数就变成了左值:
void
foo(std::vector<int>&& rval)
{
// 从此开始,`rval`是左值
}
为什么是左值呢?因为现在的rval
在各种意义上都符合左值的定义:和符号绑定、生命周期和作用域相当、所有权位于当前作用域。
如何重新将其变为右值引用?要么我们再std::move一次,要么我们可以使用std::forward
,该函数将类型完美转发给其他函数:
void
foo(std::vector<int>&& rval)
{
bar(std::forward<std::vector<int>>(rval));
}
std::forward
利用了引用折叠机制,简单来说,我们的引用标记会被折叠,本例来源于cppreference:
typedef
typedef int& lref;
typedef int&& rref;
int n;
lref& r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref& r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&
了解了这个,我们再来观察最后一个现象,那就是万能引用:
template <typename T>
void foo(T&& val) {
// ...
}
为什么T
既能引用左值,也能引用右值?那是因为模板在展开时进行了引用折叠,如果推导为右值引用,仍然折叠为右值引用,如果推导为左值引用,仍然折叠为左值引用。
有命名空间的枚举
我们可以声明带有命名空间的枚举来隔离枚举符号:
enum class Color {
RED,
GREEN,
BLUE,
};
使用时,我们不能像C枚举一样直接使用RED
,而是需要使用Color::RED
,这样有效地隔离了枚举符号对命名空间的污染。
也可以使用
enum struct
constexpr
和字面类型
简单来说,constexpr
是对字面量的扩展,其表示变量和函数可以在编译期展开,利用这个关键字,部分操作可以直接在编译期完成,对于变量我们可以这么声明:
constexpr Foo value{};
在C++11时,能被声明为constexpr
的类型,其必须是字面量类型,任何函数想要声明为constexpr
,其参数和返回值类型必须是字面量类型。
关于字面量类型,请参考cppreference。
有关constexpr
的特性无比庞大,且每个版本都有一定程度的修订,不过如果读者围绕C++11或者C++17进行工作,并且不需要元编程特性,则constexpr
没有什么太大用处(除了给我们定义一些常量之外)
列表初始化
过去,我们自定义的列表类型不能直接兼容c-style
的数组初始化方式,C++11为了统一类型初始化,创造了列表初始化和std::initializer_list
。
列表初始化允许我们使用大括号调用类型的构造函数:
Foo obj{ arg1, arg2 }; // 直接初始化
Foo obj = { arg1, arg2 }; // 拷贝初始化
基于这个特性,配合std::initializer_list
,我们可以支持自定义列表类型的初始化:
std::vector<int> arr{ 1, 2, 3, 4 };
nullptr
c-style
的空指针NULL
一般被定义为:
#define NULL ((void*)0)
我们如果将void*
看作any
的话,实际上使用这个定义是极其危险的,因为NULL
实际上不仅可以是指针类型,也可以转换为任何类型,在拥有函数重载功能的C++中,这种性质很容易造成多义性。
nullptr
是std::nullptr_t
的字面量实例,它只能被隐式转换成任何指针类型,避免了我们对函数重载中指针和整形变量的误判。
using
using
是对typedef
的升级,其作用是对类型进行重命名:
using num_t = int;
using num_vec_t = std::vector<int>;
我们可以在重命名时保留模板:
template <typename T>
using vec_t = std::vector<T>;
可变参数模板
我们可以使用...
声明可变参数模板:
template <typename... Args>
void
foo(Args... args)
{
bar(&args...);
}
在作用域内部,我们可以使用...
解包可变参数,&args...
将参数包解包为指针,再传递给bar
,我们也可以使用[]
访问具体参数。
fold-expression
于C++17引入
lambda
表达式
lambda
表达式是源于数学的概念,在编程语言中一般代指嵌套函数声明,或者更为本质的函数对象。
在C++11后,我们可以通过如下方式声明lambda
表达式:
auto f = [](int a) { return a + 1; };
f(1); // 2
中括号标记的部分被称为捕获列表,可以将当前作用域中的符号以拷贝或引用的方式传递给lambda
表达式:
int base = 10;
auto f = [base](int a) { return a + base; };
f(1); // 11
一般来说,编译器实现lambda
表达式的方法是构造一个匿名类,并重写其operator()
,捕获列表则作为参数传递给类,这也是为什么我们不能写出具体的lambda
类型。
如果想要引用lambda
作为参数,需要使用模板参数,或者std::function
。
静态断言
static_assert
用来进行编译期断言,可以作为一种模板类型检查手段,例子来源于cppreference:
template<class T>
void swap(T& a, T& b) noexcept
{
static_assert(std::is_copy_constructible_v<T>,
"Swap requires copying");
static_assert(std::is_nothrow_copy_constructible_v<T> &&
std::is_nothrow_copy_assignable_v<T>,
"Swap requires nothrow copy/assign");
auto c = b;
b = a;
a = c;
}
更新日志
4bba0
-C++11于