现代c++编程c++11/14/17/20:Chapter 02: Language Usability Enhancements

本文详细介绍了C++11及其后续版本中的一些关键语言特性,包括 nullptr、constexpr、变量初始化、类型推断、控制流程改进、模板扩展以及面向对象编程的增强。例如,nullptr 提供了安全的空指针常量,constexpr 支持编译时常量表达式,auto 和 decltype 实现类型推断,范围基础的 for 循环简化了迭代,以及模板的改进如默认参数和折叠表达式。此外,还提到了委托构造函数、继承构造函数和强类型枚举等面向对象的增强。这些特性提高了代码的清晰度、效率和安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在运行前声明、定义一个变量或常量、控制代码流、面向对象的函数、模板编程等操作可能发生在编写代码或编译器编译代码时。为此,我们通常谈论语言可用性,它指的是在运行时之前发生的语言行为。

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

其次,参数被解压缩。到目前为止还没有简单的方法来处理参数包,但是有两种经典的处理方法:

  1. 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;
}
  1. 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的特性来实现函数和参数的绑定,从而获得调用的目的。

  1. 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来防止这种情况发生。

  1. 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
};
  1. 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值