ROOT
ROOT
文章目录
  1. Item 7: 使用 () 和{}创建对象的区别
    1. Things to Remember
  2. Item 8: 优先考虑使用 nullptr 代替 0 或者 NULL
    1. Things to Remember
  3. Item 9: 优先考虑使用 using 代替 typedef 声明
    1. Things to Remember

《Effective Modern C++》第三章笔记:Moving to Modern C++

这一章介绍了 C++11/14 的最新的基本特性。

Item 7: 使用 () 和{}创建对象的区别

C++ 有以下几种初始化对象的方法:

int x(0); // 圆括号初始化
int y = 0; // 等于号初始化
int z {0}; // 花括号初始化
int z = {0}; // 同上,和花括号初始化一样

需要注意的是,自定义类使用 = 的时候,可能会调用拷贝构造函数或者赋值 (operator=) 函数。

在 C++98 中,无法为 STL 容器初始化多个值;而在 C++11 中,引进一个叫做通用初始化 uniform initialization 的东西,这个东西基于 {} 初始化,可以用在很多地方,所以也称为花括号初始化braced initialization

于是,可以这么表达初始化多个值:

vector<int> {1, 2, 3};

在 C++11 中,可以为类内非静态成员初始化默认值,用 ={}都可以,但是用 () 不行:

class Widget {
	private:
		int x{0}; // 可以这么初始化
		int y = 0; // 同上
		int z(0); // 不能这么初始化
}

而对一些不可复制的对象(如 std::atomics)就不能使用= 来初始化了,而 (){}没问题。

通过以上例子说明了为啥 {} 为通用初始化了,C++ 有 3 种初始化方法,而 {} 都能用上。

{}还有一个特性就是,不能进行隐式窄类型转换,否则编译器会报错;然而 = 因为需要兼容老代码所以不会报错。

double x, y, z;
int sum1{x + y}; // error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
int sum2 = x + y; // 没问题
int sum3(x + y); // 没问题

还有一个问题,使用 () 初始化可能会产生混淆,例如创建一个无参默认对象的时候:

Widget w2(); // 声明一个 w2 返回 Widget 的函数

这时候使用 {} 初始化,就不会出现这个问题了:

Widget w3{}; // 创建一个 w3 对象

综上所述,{}初始化有以下优点:

  • 使用场景多
  • 防止隐式窄类型转换
  • 防止混淆

但是 {} 也有缺点的,主要是和 initializer_list 与构造函数重载有关。前面 Item 2 也说过了,auto 类型推导会将花括号初始化看做 std::initializer_list 类型。

当构造函数没有使用 initializer_list 的时候,使用 (){}初始化是一样的。若重载构造函数使用了 initializer_list 的时候,使用 {} 初始化,编译器会尽可能使用 initializer_list 版本的构造函数,哪怕其他构造函数更加合适(不过书上也给出了例外情况)。

若既有默认构造函数,又有 initializer_list 版本的构造函数,那么当使用 {} 不带参数初始化的时候,将使用的是默认构造函数而不是 initializer_list 版本的了。如果想要表达的是,默认值是空的 initializer_list 的话,就用 ({}) 或者 { {} } 来初始化。

Things to Remember

  • {}初始化应用范围广
  • 在重载构造函数中,{}初始化会尽可能使用 initializer_list 版本的构造函数,哪怕其他构造函数更加合适
  • 创建 vector 对象的时候,使用 (){}初始化的作用各不相同。
  • 在模板中创建对象,使用 () 还是 {} 初始化需要仔细考虑。

Item 8: 优先考虑使用 nullptr 代替 0 或者 NULL

因为 0 在 C++ 中是一个 int 类型,而不是一个指针类型,只有在指针环境下才会把 0 看做空指针。同样 NULL 也不是指针类型,而是数值类型(可能是 int 也可能是 long,依赖于实现),也就是说,0 和 NULL 都不是指针类型。

假如有一下重载函数

void f(int);
void f(bool);
void f(void*);

f(0); // 调用的是 f(int),而不是 f(void*)
f(NULL); // 可能会有歧义,因为 NULL 可能是 long 也可能是 int,假设是 long,那么既可以转换为 int,也可以转换为 bool,还可以转换为 void*

也就是说,我想调用的是f(void*),然而实际调用的确实数值类型的。所以程序员一般避免重载指针类型和数值类型,其实当 nullptr 出现后,这种情况就可以解决了,而且也可以使得代码更加清晰。

nullptr 的优势是它不是一个数值类型,其实也不是指针类型,但是可以把它看做是所有类型的指针。因为 nullptr 是 std::nullptr_t 类型的实例,而 std::nullptr_t 类型可以隐式转换成指针类型。

这在模板中非常有用,因为模板会推导类型,如果使用 0 或者 NULL 的话,将会被推导成数值类型,那么有可能就无法初始化指针了。

Things to Remember

  • 优先考虑使用 nullptr 代替 0 或者 NULL
  • 避免重载指针类型和数值类型

Item 9: 优先考虑使用 using 代替 typedef 声明

在 C++98 中可以使用 typedef 为一个类型起名:

typedef
	std::unique_ptr<std::unordered_map<std::string, std::string>>
	UPtrMapSS;

C++11 中提供了一个别名声明alias declarations,即使用 using:

using UPtrMapSS =
	std::unique_ptr<std::unordered_map<std::string, std::string>>;

这两个都能达到一样的效果,那么 using 有什么优势呢?使用 using 会比较清晰:

typdef void (*FP)(int, const std::string&); // FP 为函数指针
using FP = void(*)(int, const std::string&); // 同上

其实还有比较重要的一点就是,using 可以用来作为模板别名(alias templates),而 typedef 不行。

templates<class T>
using MyAllocList = std::list<T, MyAlloc<T>>; // MyAllocList<T> 就是 std::list<T, MyAlloc<T>>

如果要使用 typedef 来达到同样效果,那么可以外套一个结构体:

template<class T>
struct MyAllocList {
	typdef std::list<T, MyAlloc<T>> typde;
}; // MyAllocList<T>::type 就是 std::list<T, MyAlloc<T>>

如果我想在一个模板类内使用 MyAllocList<T>::type 链表来存储数据的话,那么情况更复杂:

template<class T>
class Widget {
	typename MyAllocList<T>::type list; // 需要使用 typdename 前缀
	...
};

MyAllocList<T>::type表明是一个依赖模板参数的类型 (dependent type),必须要加一个typdename 前缀来表明它是一个依赖类型(否则编译器无法区分 ::type 是类型还是数据成员)。

如果使用 using 别名声明的话,就不用那么麻烦了:

template<class T>
class Widget {
	MyAllocList<T> list; // 省略了 typename 前缀,也省略了::type 后缀
	...
};

也因为 using 表明 MyAllocList<T> 就是一个类型的别名,所以无需 typdename 来说明。

虽然 C++11 就支持 using 了,但是这里有必要提一个东西,那就是 STL 的type traits transformation(std::transformation<T>::type),可以给类型加修饰符的东西,但是它们是用 typedef 来搞的,例如

template<class T> struct remove_const<const T> {typedef T type;}; // 去掉 const 的修饰符,const T -> T
template<class T> struct remove_reference<T&> {typedef T type;}; // 去掉引用,T& -> T

也就是说,如果在模板中使用这些东西,需要加 typdename 前缀。由于历史原因,直到 C++14,才使用 using 来定义这些type traits transformation(std::transformation_t<T>):

std::remove_const<T>::type // C++11: const T -> T
std::remove_const_t<T> // 同上

将 typedef 的类型转换成用 using 定义,也很简单:

template<class T>
using remove_const_t = typename remove_reference<T>::type;

Things to Remember

  • typedef 不支持模板化,而 using 别名声明可以
  • 使用 using 别名声明可以避免 ::type 后缀,在模板中,能避免 typename 前缀
  • C++14 提供 using 版本的 type traits transformation 来代替 C++11 的 typedef 版本
支持一下
扫一扫,支持Netcan
  • 微信扫一扫
  • 支付宝扫一扫