《Effective Modern C++》学习笔记 - Item 7: 创建对象时区分使用小括号 () 和大括号 {}

  • C++11中有四种初始化方法:
int x(0); 		// 小括号 (parentheses) 初始化
int y = 0; 		// 等号初始化
int z{ 0 }; 	// 大括号 (braces) 初始化
int z = { 0 }; 	// 1、3结合
  • 其中方法4基本被视为与方法3相同,以下讨论中忽略。
  • C++98中的老规矩:声明时不加任何括号等号,调用默认构造函数(default constructor);声明时用等号或括号,调用复制构造函数(copy constructor);非声明时用等号,调用赋值运算符函数(operator=)。
Widget w1; 		// call default constructor
Widget w2 = w1; // not an assignment; calls copy ctor (易错点)
w1 = w2; 		// an assignment; calls copy operator=
  • C++98语法中表达不出一些语义:例如,将一个STL容器初始化为一些数值的集合(笔者:无比真实,大一早期写代码痛点之一)。为了解决存在多种初始化方法,每种又不能全部适用的问题,C++11提出了统一初始化(uniform initialization)。统一初始化是表达它可以用在任何地方表达任何事的概念;大括号初始化(braced initialization) 是表达它使用大括号的语法结构。使用它可以表达上面的语义:
std::vector<int> v{ 1, 3, 5 }; // v's initial content is 1, 3, 5
  • 大括号和等号初始化一样可以用于在类中初始化非静态成员变量(小括号不行):
class Widget {
private:
	int x{ 0 }; // fine, x's default value is 0
	int y = 0; 	// also fine
	int z(0); 	// error!
};
  • 大括号和小括号初始化一样可以用于初始化不可复制的对象(等号不行):
std::atomic<int> ai1{ 0 }; 	// fine
std::atomic<int> ai2(0); 	// fine
std::atomic<int> ai3 = 0;	// error!
  • 大括号的另一新特性:阻止内建类型的隐式的缩窄转换(narrowing conversion)。俗话说就是禁止了double转float,long转int,int转char这样,会直接报编译错误。而小括号和等号都是允许这样的隐式转换的。
double x, y, z;
int sum1{ x + y + z }; 	// error! sum of doubles may
						// not be expressible as int
int sum2(x + y + z); 	// okay (value of expression
 						// truncated to an int)
int sum3 = x + y + z; 	// ditto
  • 大括号初始化的问题:通常与 std::initializer_list 以及构造函数重载的解析相关,关于 auto 推导产生的问题已经Item 2中有所说明。调用构造函数时,如果没有重载含 std::initializer_list 参数的构造函数,那么大括号和小括号初始化的意义相同。但一旦出现,就会严重偏向于那样的版本:只要有任何方式能调用 std::initializer_list 参数版本的构造函数,编译器就会那样做。甚至复制和移动构造函数都会被“劫持”:
class Widget {
public:
	Widget(int i, bool b) {
		cout << "int bool ctor" << endl;
	}
	Widget(int i, double d) {
		cout << "int double ctor" << endl;
	}
	Widget(const Widget& w) {
		cout << "copy ctor" << endl;
	}
	Widget(Widget&& w) {
		cout << "move ctor" << endl;
	}
	Widget(std::initializer_list<long double> il) {
		cout << "ctor ini_list" << endl;
	}
	operator float() const {
		cout << "convert to float" << endl;
		return 1.f;
	}
};

int main()
{
	Widget w1(10, true);
	Widget w2{ 10, true };

	Widget w3(10, 5.0);
	Widget w4{ 10, 5.0 };

	Widget w5(w4);
	Widget w6{ w4 }; // 先由operator float()转为float再调用initializer_list版本

	Widget w7(std::move(w4));
	Widget w8(std::move(w5));
	return 0;
}

加入最后一个构造函数前:
在这里插入图片描述
注:这里笔者观察到了奇特的现象:加入initializer_list参数构造函数后,w2和w4的确都会调用该版本,但w6和w8,也就是复制和移动构造,VS和GCC的版本运行结果居然不同:GCC如作者所说调用该版本,而VS调用了原来的复制和移动版本,只有当明确对w4和w5调用 operator float() 转换后才会调用initializer_list版本。如有了解的朋友欢迎在评论区交流:)
在这里插入图片描述

VS 运行结果

在这里插入图片描述

GCC 运行结果

  • 甚至当initializer_list版本的构造函数转换失败时,编译器都选择报错而不是调用参数类型完全符合的其他版本构造函数:
Widget(int i, double d) {
	cout << "int double ctor" << endl;
}
Widget(std::initializer_list<bool> il) {
	cout << "ctor ini_list" << endl;
}

Widget w{10, 5.0};	// 直接报错:不能缩窄转换(double, int向bool)
  • 只有当大括号内的元素完全不可能转换成 std::initializer 模板的类型时,编译器才会重新调用原来的构造函数:
Widget(int i, bool b) {
	cout << "int bool ctor" << endl;
}
Widget(int i, double d) {
	cout << "int double ctor" << endl;
}
Widget(std::initializer_list<std::string> il) {
	cout << "ctor ini_list" << endl;
}

Widget w{10, true}; // 调用第一个构造函数
Widget w{10, 5.0}; 	// 调用第二个构造函数
  • 还有一个有趣的edge case。假设你用了一个空的大括号,那么调用的是默认构造函数还是内容为空的 std::initializer_list 参数构造函数?答案是前者。如果你想调用后者,则应该在大括号内再加一个空的大括号,代表空的 std::initializer_list。当然此时你也可以用小括号来调用该函数。
    在这里插入代码片
  • 关于大括号初始的语法到这里就差不多了。从这段讨论中,我们应该从这段讨论中吸收两件事:
    1. 作为一个类的编写者,你要意识到如果给一个原来没有 std::initializer_list 初始化函数的类添加一个,可能导致客户端的调用被大幅度改变,因此一定要谨慎。当前 std::vector 的设计就被认为是存在错误的(例如传入两个数10和20,小括号调用创建的是20个值为10的元素,而大括号调用创建的是2个元素分别为值10和20)。
    2. 作为一个类的使用者,你必须在创建对象时谨慎选择使用小括号还是大括号。二者没有绝对的优劣,重点是你自己要选择一个,保持一致性,并且时刻有着清晰的思路。

总结

  1. 大括号初始化是能使用场景最广的初始化语法,防止了隐式的缩窄转换。
  2. 在重载的构造函数选择中,大括号初始化极度倾向于调用 std::initializer_list 参数版本的构造函数。
  3. 小括号和大括号初始化差异巨大的一个例子是用两个参数创建 std::vector
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值