条款7:在创建对象时注意区分( )和{ }
Effective Modern C++之条款7
·
1 各种初始化
- C++11中,可以使用()、= 或 { } 来指定初始化值。
int x(0);
int y = 0;
int z{ 0 };
int z = { 0 }; // 等价于 int z{ 0 };
- 对于像 int 这样的内置类型可能无所谓,但对于用户定义的类型,区分初始化和赋值是很重要的,因为涉及到不同的函数调用:
Widget w1; // 调用默认构造函数
Widget w2 = w1; // 不是赋值,调用拷贝构造函数
w1 = w2; // 是赋值; 调用拷贝赋值运算符(copy operator=)
- C++11之前,不能直接使用一组特定的值(例如,1、3 和 5)来创建一个 STL 容器。从概念上来说是“统一初始化”;从语法结构上来说是“大括号初始化”。
std::vector<int> v{ 1, 3, 5 }; // v 的初始化内容为:1, 3, 5
- C++11 中,可以使用{ }, = 对非静态数据成员进行初始化,但( ) 不行:
class Widget {
…
private:
int x{ 0 }; // 正确, x的默认值为0
int y = 0; // 同样正确
int z(0); // 错误!
};
- 不可复制的对象(例如,std::atomics)可以使用{ }或()进行初始化,但不能使用 = :
std::atomic<int> ai1{ 0 }; // 正确
std::atomic<int> ai2(0); // 正确
std::atomic<int> ai3 = 0; // 错误!
- { }初始化禁止内置类型之间的隐式窄化转换。
double x, y, z;
…
int sum1{ x + y + z }; // 错误! 双精度数的和可能无法表示为整数
int sum2(x + y + z); // 可以 (表达式的值被截断为整数)
int sum3 = x + y + z; // 同上
- { } 初始化不受 C++中令人烦恼的解析问题(任何可以被解析为声明的东西都必须被解释为声明)的影响。
Widget w1(10); // 调用 Widget 类的构造函数,并传入参数10
Widget w2(); // 令人烦恼的解析!声明了名为 w2 的函数,它返回一个 Widget!
Widget w3{}; // 无参数,调用构造函数
- { } 初始化的缺点是随之而来的有时令人惊讶的行为。
class Widget {
public:// 没有定义std::initializer_list 参数版本的ctor
Widget(int i, bool b);
Widget(int i, double d);
…
};
Widget w1(10, true); // 调用第一个 ctor
Widget w2{10, true}; // 还是调用第一个 ctor
Widget w3(10, 5.0); // 调用第二个 ctor
Widget w4{10, 5.0}; // 还是调用第二个 ctor
//在构造函数调用中,只要不涉及 std::initializer_list 参数,()和{ }的含义是相同的。
- 如果存在构造函数声明了类型为 std::initializer_list 的参数,那么使用{ }初始化语法的调用强烈倾向于采用接受 std::initializer_list 的重载。非常强烈!!!
class Widget {
public:
Widget(int i, bool b); // 和以前一样
Widget(int i, double d); // 和以前一样
Widget(std::initializer_list<long double> il); // 新加入
…
};
Widget w1(10, true); // 调用第一个ctor
Widget w2{10, true}; // 调用initializer_list ctor(10和true转换为long double)
Widget w3(10, 5.0); // 调用第二个ctor
Widget w4{10, 5.0}; // 调用initializer_list ctor(10和5.0转换为long double)
- std::initializer_list 构造函数还可能劫持拷贝和移动构造函数。
class Widget {
public:
Widget(int i, bool b); // 和以前一样
Widget(int i, double d); // 和以前一样
Widget(std::initializer_list<long double> il); // 和以前一样
operator float() const; // 转换为float
…
};
Widget w5(w4); // 调用 copy ctor
Widget w6{w4}; // 调用 std::initializer_list ctor
// (w4 转换为 float, float 转换为 long double)
Widget w7(std::move(w4)); // 调用 move ctor
Widget w8{std::move(w4)}; // 调用 std::initializer_list ctor(原因和w6一样)
- 即使匹配出来的 std::initializer_list 构造函数无法被调用,std::initializer_list 的构造函数也会占上风。
class Widget {
public:
Widget(int i, bool b); // 和以前一样
Widget(int i, double d); // 和以前一样
//元素类型现在为bool
Widget(std::initializer_list<bool> il);
};
Widget w{10, 5.0}; // 错误! 需要窄化转换
//注意:类里不存在隐式类型转换
- 只有当没有办法将{ }中的参数类型转换为 std::initializer_list 中的类型时,编译器才会回退到正常的重载解析。
class Widget {
public:
Widget(int i, bool b); // 和以前一样
Widget(int i, double d); // 和以前一样
// std::initializer_list 元素类型现在是 std::string
Widget(std::initializer_list<std::string> il);
… // 不存在隐式的类型转换
};
Widget w1(10, true); // 调用第一个 ctor
Widget w2{10, true}; // 调用第一个 ctor
Widget w3(10, 5.0); // 调用第二个 ctor
Widget w4{10, 5.0}; // 调用第二个 ctor
- 假设使用空{ }来构造一个支持默认构造且还支持std::initializer_list构造的对象。那么空括{ }意味着什么?
class Widget {
public:
Widget(); // default ctor
Widget(std::initializer_list<int> il); // std::initializer_list ctor
… // 不存在隐式转换
};
// 空{ }意味着没有参数,而不是空的 std::initializer_list
Widget w1; // default ctor
Widget w2{}; // default ctor
Widget w3(); // 难以理解的语法解析! 声明了一个函数!
// 将空{ }放在()或{ }内,表面传递一个空的 std::initializer_list
Widget w4({}); // 使用空列表,调用 std::initializer_list ctor
Widget w5{ {} }; // 同上
- std::vector有一个非std::initializer_list的构造函数,允许指定容器的初始大小以及初始值,但它也有一个接受std::initializer_list的构造函数,允许指定容器中的初始值列表。
std::vector<int> v1(10, 20); // 使用 non-std::initializer_list ctor
std::vector<int> v2{10, 20}; // 使用 std::initializer_list ctor
- 对于模板的作者来说,在创建对象时选择使用()还是{ }可能会很困难,因为通常无法确定谁更好。
template<typename T, // 需要创建的对象的类型
typename... Ts> // 参数列表的类型
void doSomeWork(Ts&&... params)
{
//使用params创建T类型的局部对象,下列方案二选一
T localObject(std::forward<Ts>(params)...); // 方案1
T localObject{std::forward<Ts>(params)...}; // 方案2
…
}
doSomeWork<std::vector<int>>(10, 20); // 方案1:10个元素,值都为20
// 方案2:2个元素,值为10和20
2 要点速记
- { } 初始化是最广泛可用的初始化语法,它防止窄化转换,并且不受令人烦恼的解析问题的影响。
- 在构造函数重载解析期间,如果可能的话,{ } 初始化表达式将与 std::initializer_list 参数匹配,即使其他构造函数提供了看似更好的匹配。
- ()和 { } 之间的选择可能产生重大影响,例如使用两个参数创建 std::vector<数值类型>。
- 在模板内部创建对象时,选择使用()还是 { } 可能很棘手。
更多推荐
所有评论(0)