在运行前声明、定义一个变量或常量、控制代码流、面向对象的函数、模板编程等操作可能发生在编写代码或编译器编译代码时。为此,我们通常谈论语言可用性,它指的是在运行时之前发生的语言行为。
2.1 Constants
2.1.1 nullptr
nullptr的用途似乎取代了NULL。 在某种意义上,传统的c++将NULL和0看作是一样的东西,这取决于编译器如何定义NULL,一些编译器将NULL定义为((void*)0),一些将直接定义为0。
c++不允许隐式地将void * 转换为其他类型。但是如果编译器试图将NULL定义为((void*)0),那么在下面的代码中:
char *ch = NULL;
没有void *隐式转换的c++必须将NULL定义为0。这仍然产生了一个新问题。将NULL定义为0将导致c++中的重载特性令人困惑。
考虑以下两个foo函数:
void foo(char*);
void foo(int);
然后foo(NULL)语句将调用foo(int),这将导致代码违反直觉。为了解决这个问题,c++ 11引入了nullptr关键字,它是专门用来区分空指针0的。nullptr的类型是nullptr_t,它可以被隐式地转换为任何指针或成员指针类型,并且可以与它们相等或不相等地进行比较。
You can try to compile the following code using clang++:
#include <iostream>
#include <type_traits>
void foo(char *);
void foo(int);
int main() {
if (std::is_same<decltype(NULL), decltype(0)>::value ||
std::is_same<decltype(NULL), decltype(0L)>::value)
std::cout << "NULL == 0" << std::endl;
if (std::is_same<decltype(NULL), decltype((void*)0)>::value)
std::cout << "NULL == (void *)0" << std::endl;
if (std::is_same<decltype(NULL), std::nullptr_t>::value)
std::cout << "NULL == nullptr" << std::endl;
foo(0); // will call foo(int)
// foo(NULL); // doesn't compile
foo(nullptr); // will call foo(char*)
return 0;
}
void foo(char *) {
std::cout << "foo(char*) is called" << std::endl;
}
void foo(int i) {
std::cout << "foo(int) is called" << std::endl;
}
输出:
foo(int) is called
foo(char*) is called
从输出中我们可以看到NULL不同于0和nullptr。所以,养成直接使用nullptr的习惯。
另外,在上面的代码中,我们使用了现代c++语法decltype和std::is_same。简单地说,decltype用于类型派生,std::is_same用于比较两种类型的相等性。我们将在稍后的decltype部分详细讨论它们。
2.1.2 constexpr
c++本身已经有了常量表达式的概念,比如1+ 2,3 *4。这样的表达式总是产生相同的结果而没有任何副作用。如果编译器能在编译时直接优化并将这些表达式嵌入到程序中,将会提高程序的性能。一个非常明显的例子是数组的定义阶段:
#include <iostream>
#define LEN 10
int len_foo() {
int i = 2;
return i;
}
constexpr int len_foo_constexpr() {
return 5;
}
constexpr int fibonacci(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}
int main() {
char arr_1[10]; // legal
char arr_2[LEN]; // legal
int len = 10;
// char arr_3[len]; // illegal
const int len_2 = len + 1;
constexpr int len_2_constexpr = 1 + 2 + 3;
// char arr_4[len_2]; // illegal, but ok for most of the compilers
char arr_4[len_2_constexpr]; // legal
// char arr_5[len_foo()+5]; // illegal
char arr_6[len_foo_constexpr() + 1]; // legal
// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
std::cout << fibonacci(10) << std::endl;
return 0;
}
在上面的例子中,char arr_4[len_2]可能会令人困惑,因为len_2被定义为一个常量。为什么char arr_4[len_2]仍然是非法的?这是因为数组的长度在c++标准必须是一个常量表达式,对len_2来说,这是一个const常数,常数表达式,因此,即使这种行为支持大多数编译器,但它是一种违法行为,我们需要使用constexpr特性介绍了c++ 11日将介绍下,为了解决这个问题;对于arr_5,在c++ 98之前,编译器无法知道len_foo()实际上返回一个常量a。
注意,大多数编译器现在都有自己的编译器优化。在编译器的优化下,许多非法行为变成了合法行为。如果需要重新生成错误,则需要使用旧版本的编译器。
c++ 11提供了constexpr让用户显式地声明函数或对象构造函数将在编译时成为常量表达式。这个关键字显式地告诉编译器它应该验证len_foo应该是一个编译时常量表达式。常数表达式。
此外,constexpr的函数可以使用递归:
constexpr int fibonacci(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}
从c++ 14开始,constexpr函数可以在内部使用简单的语句,如局部变量、循环和分支。例如,以下代码不能在c++ 11标准下编译:
constexpr int fibonacci(const int n) {
if(n == 1) return 1;
if(n == 2) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
为了做到这一点,我们可以写一个简化版本,像这样,使函数符合c++ 11标准:
constexpr int fibonacci(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}
2.2 Variables and initialization
2.2.1 if-switch
在传统的c++中,变量的声明可以声明临时变量int,它可以位于任何位置,甚至在for语句中,但是在if和switch语句中始终没有方法声明临时变量。例如:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4};
// after c++17, can be simplefied by using `auto`
const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 2);
if (itr != vec.end()) {
*itr = 3;
}
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4;
}
// should output: 1, 4, 3, 4. can be simplefied using `auto`
for (std::vector<int>::iterator element = vec.begin();
element != vec.end(); ++element)
std::cout << *element << std::endl;
}
在上面的代码中,我们可以看到itr变量是在整个main()的范围内定义的,这导致我们在一个变量需要再次遍历整个std::vector时重命名另一个变量。c++ 17消除了这个限制,所以我们可以在if(或switch)中这样做:
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4;
}
和Go类似吗?
2.2.2 Initializer list
初始化是一种非常重要的语言特性,最常见的是对象初始化的时候。在传统的c++中,不同的对象有不同的初始化方法,比如普通的数组,PODs(没有构造函数,析构函数和虚函数的类)或者结构类型可以用{}来初始化,这就是我们所说的初始化列表。对于类对象的初始化,需要使用copy构造,或者使用()。这些不同的方法是彼此特定的,不能是通用的。
c++ 11首先要解决这个问题,结合初始化列表的概念类型和调用std:: initializer_list,允许构造函数或其他功能使用初始化列表参数,初始化的类对象提供了一个统一的普通初始化数组和POD方法之间的桥梁、。例如:
#include <initializer_list>
#include <vector>
class MagicFoo {
public:
std::vector<int> vec;
MagicFoo(std::initializer_list<int> list) {
for (std::initializer_list<int>::iterator it = list.begin();
it != list.end(); ++it)
vec.push_back(*it);
}
};
int main() {
// after C++11
MagicFoo magicFoo = {1, 2, 3, 4, 5};
std::cout << "magicFoo: ";
for (std::vector<int>::iterator it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it)
std::cout << *it << std::endl;
}
这个构造函数被称为初始化列表构造函数,在初始化期间将特别关注这个构造函数的类型。
除了构造对象外,初始化列表还可以作为普通函数的形参,例如:
public:
void foo(std::initializer_list<int> list) {
for (std::initializer_list<int>::iterator it = list.begin(); it != list.end(); ++it)
vec.push_back(*it);
}
magicFoo.foo({6,7,8,9});
第二,c++ 11为初始化任意对象中还提供了一种统一的语法,例如:
Foo foo2 {3, 4};
2.2.3 Structured binding
结构化绑定提供的功能类似于其他语言提供的多个返回值。在容器这一章中,我们将了解到c++ 11添加了一个std::tuple 容器,用于构造包含多个返回值的元组。但缺陷是c++ 11/14不提供一个简单的方法,直接从元组定义元素的元组,虽然我们可以解压元组使用std::tie, 但是我们仍然要非常清楚这个元组包含多少个对象,每个对象是什么类型的,很麻烦的。
c++ 17完成了这个设置,结构化绑定让我们可以这样写代码:
#include <iostream>
#include <tuple>
std::tuple<int, double, std::string> f() {
return std::make_tuple(1, 2.3, "456");
}
int main() {
auto [x, y, z] = f();
std::cout << x << ", " << y << ", " << z << std::endl;
return 0;
}
auto类型派生在auto类型推断部分中进行了描述。
2.3 Type inference
在传统的C和c++中,参数的类型必须明确定义,这对我们快速编码没有帮助,特别是当我们面对大量复杂的模板类型时,我们必须明确指明变量的类型才能继续下去。随后的编码,不仅降低了我们的开发效率,而且还使代码变得冗长而糟糕。
c++ 11引入了两个关键字auto和decltype来实现类型派生,让编译器担心变量的类型。这使得c++与其他现代编程一样语言的方式提供的习惯不用担心变量类型。
2.3.1 auto
auto在c++中已经存在很长时间了,但它总是作为一种存储类型的指示符存在,与register共存。在传统的c++中,如果一个变量没有声明为寄存器变量,它就会被自动当作自动变量处理。
使用auto进行类型派生的最常见和最值得注意的示例之一是迭代器。您应该在前一节中看到用传统c++进行冗长的迭代编写:
// before C++11
// cbegin() returns vector<int>::const_iterator
// and therefore itr is type vector<int>::const_iterator
for(vector<int>::const_iterator it = vec.cbegin(); itr != vec.cend(); ++it)
当我们有auto:
#include <initializer_list>
#include <vector>
#include <iostream>
class MagicFoo {
public:
std::vector<int> vec;
MagicFoo(std::initializer_list<int> list) {
for (auto it = list.begin(); it != list.end(); ++it) {
vec.push_back(*it);
}
}
};
int main() {
MagicFoo magicFoo = {1, 2, 3, 4, 5};
std::cout << "magicFoo: ";
for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {
std::cout << *it << ", ";
}
std::cout << std::endl;
return 0;
}
其他一些常见用法:
auto i = 5; // i as int
auto arr = new auto(10); // arr as int *
注意:auto不能用于函数参数,因此以下内容无法编译(考虑到超载,我们应该使用模板):
int add(auto x, auto y);
此外,auto不能用于派生数组类型:
auto auto_arr2[10] = {arr}; // illegal, can't infer array type
2.3.2 decltype
decltype关键字用来解决auto关键字只能输入变量的缺陷。它的用法与typeof非常相似:
decltype(expression)
例如,有时我们可能需要计算表达式的类型:
auto x = 1;
auto y = 2;
decltype(x+y) z;
在前面的示例中,您已经看到decltype用于推断类型的用法。以下是确定上述变量x、y、z是否为同一类型的例子:
if (std::is_same<decltype(x), int>::value)
std::cout << "type x == int" << std::endl;
if (std::is_same<decltype(x), float>::value)
std::cout << "type x == float" << std::endl;
if (std::is_same<decltype(x), decltype(z)>::value)
std::cout << "type z == type x" << std::endl;
其中std::is_same<T, U>用于确定T和U两种类型是否相等。
输出是:
type x == int
type z == type x
2.3.3 tail type inference
您可能认为,在引入auto时,我们已经提到auto不能用作用于类型派生的函数参数。auto可以用来派生一个函数的返回类型吗?
仍然考虑一个添加函数的例子,我们必须用传统的c++编写:
template<typename R, typename T, typename U>
R add(T x, U y) {
return x+y
}
注意:模板参数列表中的typename和class之间没有区别。
在关键字typename出现之前,class用于定义模板参数。但是,当在模板中定义具有嵌套依赖类型的变量时,您需要使用typename来消除歧义。
这样的代码实际上非常难看,因为程序员在使用这个模板函数时必须显式地指明返回类型。但实际上,我们并不知道add()函数将执行何种操作,以及要获取何种返回类型。
这个问题在c++ 11中解决了。尽管您可能会立即对使用decltype派生x+y的类型做出反应,编写如下内容:
decltype(x+y) add(T x, U y)
但事实上,这种书写方式是无法编译的。这是因为当编译器读取decltype(x+y)时,x和y还没有定义。为了解决这个问题,c++ 11还引入了一个尾部返回类型,它使用auto关键字来发布返回类型:
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}
好消息是,从c++ 14可以直接推导出一个正常函数的返回值,所以下面的方法是合法的:
template<typename T, typename U>
auto add3(T x, U y){
return x + y;
}
您可以检查类型派生是否正确:
// after c++11
auto w = add2<int, double>(1, 2.0);
if (std::is_same<decltype(w), double>::value) {
std::cout << "w is double: ";
}
std::cout << w << std::endl;
// after c++14
auto q = add3<double, int>(1.0, 2);
std::cout << "q: " << q << std::endl;
2.3.4 decltype(auto)
decltype(auto)是c++ 14中稍微复杂一点的用法。要理解它,您需要了解c++中参数转发(parameter forwarding)的概念,我们将在语言运行时加强(Language Runtime Hardening)一章中详细介绍,稍后您可以回到本节的内容。
简单来说,decltype(auto)主要用于派生转发函数或包的返回类型,不需要我们显式指定decltype的参数表达式。考虑下面的例子,当我们需要包装以下两个函数:
std::string lookup1();
std::string& lookup2();
在C++11中:
std::string look_up_a_string_1() {
return lookup1();
}
std::string& look_up_a_string_2() {
return lookup2();
}
使用decltype(auto),我们可以让编译器做这个烦人的参数转发:
decltype(auto) look_up_a_string_1() {
return lookup1();
}
decltype(auto) look_up_a_string_2() {
return lookup2();
}
2.4 Control flow
2.4.1 if constexpr
正如我们在本章开头看到的,我们知道c++ 11引入了constexpr关键字,它将表达式或函数编译成常量结果。一个很自然的想法是,如果我们在条件判断中引入这个特性,让代码在编译时完成分支判断,它能使程序更高效吗?c++ 17在if语句中引入了constexpr关键字,允许您在代码中声明常量表达式的条件。考虑以下代码:
#include <iostream>
template<typename T>
auto print_type_info(const T& t) {
if constexpr (std::is_integral<T>::value) {
return t + 1;
} else {
return t + 0.001;
}
}
int main() {
std::cout << print_type_info(5) << std::endl;
std::cout << print_type_info(3.14) << std::endl;
}
在编译时,实际的代码表现如下:
int print_type_info(const int& t) {
return t + 1;
}
double print_type_info(const double& t) {
return t + 0.001;
}
int main() {
std::cout << print_type_info(5) << std::endl;
std::cout << print_type_info(3.14) << std::endl;
}
2.4.2 Range-based for loop
最后,c++ 11引入了一种基于范围的迭代方法,我们有能力编写像Python一样简洁的循环,我们可以进一步简化前面的例子:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4};
if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) *itr = 4;
for (auto element : vec)
std::cout << element << std::endl; // read only
for (auto &element : vec) {
element += 1; // writeable
}
for (auto element : vec)
std::cout << element << std::endl; // read only
}
2.5 Templates
c++模板一直是一种特殊的语言艺术,模板甚至可以作为一种新的语言独立使用。模板的原理是将所有可以在编译时处理的问题都扔到编译时,只在运行时处理那些核心动态服务,从而极大地优化运行时的性能。因此,模板也被许多人视为是一个c++的黑魔法。
2.5.1 Extern templates
在传统的c++中,模板只有在使用时才由编译器实例化。换句话说,只要在每个编译单元中编译的代码中遇到一个完整定义的模板(文件),它将被实例化。由于重复的实例化,这会导致编译时间的增加。另外,我们没有办法告诉编译器不要触发模板的实例化。
为此,c++ 11引入了一个外部模板,它扩展了原来的强制编译器的语法来在特定位置实例化一个模板,允许我们显式地告诉编译器何时实例化模板:
template class std::vector<bool>; // force instantiation
extern template class std::vector<double>; // should not instantiation in current file
2.5.2 The “>”
在传统的c++编译器中,>>总是被当作一个右移位运算符来处理。但实际上我们可以很容易地为嵌套模板编写代码:
std::vector<std::vector<int>> matrix;
这不是在传统的c++编译器,编译和c++ 11开始连续右尖括号,成为法律,可以编译成功。甚至以下文字也可以通过以下方式进行编译:
template<bool T>
class MagicType {
bool magic = T;
};
// in main function:
std::vector<MagicType<(1>2)>> magic; // legal, but not recommended
2.5.3 Type alias templates
在您理解类型别名模板之前,您需要了解它们之间的区别“模板”和“类型”。仔细理解这句话:模板是用来生成类型的。在传统的c++中,typedef可以为类型定义一个新名称,但无法为模板定义一个新名称。因为模板不是类型。例入:
template<typename T, typename U>
class MagicType {
public:
T dark;
U magic;
};
// not allowed
template<typename T>
typedef MagicType<std::vector<T>, std::string> FakeDarkMagic;
c++ 11使用using来引入如下的书写形式,同时支持与传统typedef相同的效果:
通常我们使用typedef来定义别名语法:typedef original name new name;,但是别名的定义语法如函数指针是不同的,这通常会给直接读取带来一定的难度:
typedef int (*process)(void *);
using NewProcess = int(*)(void *);
template<typename T>
using TrueDarkMagic = MagicType<std::vector<T>, std::string>;
int main() {
TrueDarkMagic<bool> you;
}
2.5.4 Default template parameters
我们可能已经定义了一个加法函数:
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
但是,在使用时,发现要使用add,每次都必须指定其模板参数的类型。
在c++ 11中可以方便地指定模板的默认参数:
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
2.5.5 Variadic templates
模板一直是c++独家的黑魔法之一。在传统的c++中,类模板和函数模板都只能接受指定的固定模板参数集;
c++ 11添加了一个新的表示,允许任意数量、任意类别的模板参数,并且在定义时不需要固定参数的数量。
template<typename... Ts> class Magic;
模板类Magic对象可以接受不限数量的typename作为模板的形式参数,如下面的定义:
class Magic<int,
std::vector<int>,
std::map<std::string,
std::vector<int>>> darkMagic;
因为它是任意的,所以一个数目为0的模板参数也是可能的:class Magic<> nothing;。
如果你不想生成0个模板参数,你可以手动定义至少一个模板参数:
template<typename Require, typename... Args> class Magic;
变量长度的模板参数也可以直接调整模板函数。传统C中的printf函数虽然也可以调用不定数量的形式参数,但它不是类安全的。除了定义类安全性的可变长度参数函数外,c++ 11还可以使类似printf的函数自然地处理非自包含的对象。除了…之外…在表示模板参数不定长度的模板参数中,函数参数也用同样的表示不定长度参数,这为我们简单地写出变长参数函数提供了方便的手段,例如
template<typename... Args> void printf(const std::string &str, Args... args);
然后我们定义可变长度模板参数,如何解包参数?
首先,我们可以使用sizeof…计算参数的数量:
#include <iostream>
template<typename... Ts>
void magic(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}
我们可以向这个神奇的函数传递任意数量的参数:
magic(); // 0
magic(1); // 1
magic(1, ""); // 2
其次,参数被解压缩。到目前为止还没有简单的方法来处理参数包,但是有两种经典的处理方法:
- Recursive template function
递归是一种非常容易想到的方法,也是最经典的方法。该方法连续递归传递模板参数给函数,从而达到递归遍历所有模板参数的目的:
#include <iostream>
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0;
}
- Variable parameter template expansion
您应该感到这是非常麻烦的。在c++ 17中增加了对变量参数模板扩展的支持,所以你可以在函数中编写printf:
template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0) printf2(t...);
}
实际上,有时我们使用可变参数模板,但不一定需要逐个遍历参数。我们可以利用std::bind和perfect forwarding的特性来实现函数和参数的绑定,从而获得调用的目的。
- Initialize list expansion
递归模板函数是一种标准实践,但其明显的缺点是必须定义终止递归的函数。
下面是对黑魔法的描述,使用初始化列表展开:
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>
{([&args] {std::cout << args << std::endl;}(), value)...};
}
在此代码中,c++ 11提供的初始化列表和Lambda表达式的属性
(下一节将提到)是额外使用的。通过初始化列表,(lambda表达式,值)…将会扩展。由于出现了逗号表达式,所以首先执行前面的lambda表达式,然后完成参数的输出。为了避免编译器警告,我们可以显式地将std::initializer_list转换为void。
2.5.6 Fold expression
在c++ 17中,将可变长度参数的这一特性进一步引入到表达式中,考虑如下例子:
#include <iostream>
template<typename ... T>
auto sum(T ... t) {
return (t + ...);
}
int main() {
std::cout << sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << std::endl;
}
2.5.7 Non-type template parameter deduction
我们上面主要提到的是模板形参的一种形式:type template parameters.
template <typename T, typename U>
auto add(T t, U u) {
return t+u;
}
模板参数T和U是特定的类型。但也有一种常见的模板形参形式,允许不同的 literals 作为模板形参,即非类型模板形参Non-type template parameter:
template <typename T, int BufSize>
class buffer_t {
public:
T& alloc();
void free(T& item);
private:
T data[BufSize];
}
buffer_t<int, 100> buf; // 100 as template parameter
2.6 Object-oriented
2.6.1 Delegate constructor
c++ 11引入了委托构造的概念,它允许一个构造函数调用同一个类中的另一个构造函数,从而简化了代码:
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // delegate Base() constructor
value2 = value;
}
};
2.6.2 Inheritance constructor
在传统的c++中,如果需要继承,构造函数需要一个接一个地传递参数,这会导致效率低下。c++ 11使用关键字using引入了继承构造函数的概念:
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // delegate Base() constructor
value2 = value;
}
};
class Subclass : public Base {
public:
using Base::Base; // inheritance constructor
};
int main() {
Subclass s(3);
std::cout << s.value1 << std::endl;
std::cout << s.value2 << std::endl;
}
2.6.3 Explicit virtual function overwrite
在传统的c++中,经常会意外地重载虚函数。
struct Base {
virtual void foo();
};
struct SubClass: Base {
void foo();
};
foo可能不是一个程序员试图重载一个虚函数,只是添加一个具有相同名称的函数。另一种可能的情况是,当基类的虚函数被删除时,子类拥有旧的函数,不再重载虚函数并将其转换为普通的类方法,这会产生灾难性的后果。
c++ 11引入了两个关键字override和final来防止这种情况发生。
- override
当重写一个虚函数时,引入override关键字会显式地告诉编译器重载,编译器会检查基函数是否有这样的虚函数,否则不会编译:
struct Base {
virtual void foo(int);
};
struct SubClass: Base {
virtual void foo(int) override; // legal
virtual void foo(float) override; // illegal, no virtual function in super class
};
- final
final是为了防止类被继续继承,并终止虚函数以继续被重载。
struct Base {
virtual void foo() final;
};
struct SubClass1 final: Base {
}; // legal
struct SubClass2 : SubClass1 {
}; // illegal, SubClass1 has final
struct SubClass3: Base {
void foo(); // illegal, foo has final
};
2.6.4 Explicit delete default function
在传统的c++中,如果程序员不提供它,编译器将默认生成对象的默认构造函数、复制构造函数、赋值操作符和析构函数。
c++还为所有类定义了诸如new delete之类的操作符。当程序员需要时,可以重写函数的这一部分。
这就提出了一些要求:无法控制精确控制默认函数生成的能力。例如,当禁止复制类时,复制构造函数和赋值操作符必须声明为private。尝试使用这些未定义的函数将导致编译或链接错误,这是一种非常非常规的方式。
此外,编译器生成的默认构造函数不能与用户定义的构造函数同时存在。如果用户定义了任何构造函数,编译器将不再生成默认构造函数,但有时我们希望同时拥有两个构造函数,这是很尴尬的。
c++ 11为上述需求提供了一个解决方案,允许显式声明接受或拒绝编译器提供的函数。
class Magic {
public:
Magic() = default; // explicit let compiler use default constructor
Magic& operator=(const Magic&) = delete; // explicit declare refuse constructor
Magic(int magic_number);
}
2.6.5 Strongly typed enumerations
在传统的c++中,枚举类型不是类型安全的,和枚举类型被当作整数,它允许两个完全不同的枚举类型直接相比(虽然编译器给出了检查,但不是全部),* 甚至不同的enum类型的枚举值名称相同的名称空间中不能相同的 *,这通常不是我们想看到的。
c++ 11引入了一个枚举类,并使用enum类的语法声明它:
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};
因此定义的枚举实现了类型安全。首先,它不能隐式转换为整数,也不能与整数进行比较,而且更不可能比较不同枚举类型的枚举值。但是如果指定的值在相同的枚举值之间是相同的,那么您可以比较:
if (new_enum::value3 == new_enum::value4) // true
在此语法中,枚举类型后跟一个冒号和一个type关键字,以指定枚举中枚举值的类型,这允许我们为枚举赋值(在未指定时,默认使用int)。
并且我们想要得到枚举值的值,我们将不得不显式类型转换,但我们可以重载<<运算符来输出,你可以收集以下代码片段:
#include <iostream>
template<typename T>
std::ostream& operator<<(typename std::enable_if<std::is_enum<T>::value,
std::ostream>::type& stream, const T& e)
{
return stream << static_cast<typename std::underlying_type<T>::type>(e);
}
Conclusion
本节介绍了现代c++对语言可用性的增强,我相信这是几乎每个人都需要知道和使用的最重要的特性:
1. auto type derivation
2. Scope for iteration
3. Initialization list
4. Variable parameter template