文章目录
在创建对象时注意区分()和{}
{}的使用注意
大多数情况尽量选择用**{}来创建对象**,对于{}主要有几个优势:
- 在 C++的三种初始化表达式写法中(()、{}和=),只有大括号适用于所有场合。
- {}禁止内建型别之间进行**隐式窄化型别转换**。
- 免疫 C++的**最令人苦恼之解析语法**。
在 C++11 之前,不同的数据类型有着**截然不同的初始化方式**。而 {} 的出现,终结了这种混乱,做到了"一招鲜,吃遍天"。
cpp
#include <vector>
#include <string>
class Widget {
public:
Widget(int x, int y) {}
};
int main() {
// ---- 优势:可以用同一种语法初始化所有对象 ----
// 1. 基础内置类型
int a{10};
// 2. 容器/聚合类型
std::vector<int> v{1, 2, 3, 4, 5};
// 3. 自定义类对象
Widget w1{10, 20};
// 4. 用于设定类成员的默认初始化值(C++11 非静态成员初始化)
class Player {
int health{100}; // 正确:可以使用 {} 或 =,但不能用 ()
// int mana(50); // 错误!编译失败
};
// 5. 用于函数返回值(动态构建并返回)
// return {x, y}; // 无需写出类名,直接返回花括号
}
使用 () 或 = 初始化时,C++ 会极其宽容(甚至说是放任)地**允许高精度类型隐式转换为低精度类型**,这往往是 Bug 的温床。而 {} 则是严格的"守门员"。
cpp
int main() {
double x = 7.5;
double y = 3.3;
// ---- 使用 = 或 () ----
int total1 = x + y; // 隐式转换为 int,total1 变成 10 (精度丢失,编译器可能只给警告)
int total2(x + y); // 同上,悄悄地截断了小数部分
// ---- 使用 {} ----
// int total3{x + y}; // 错误!编译直接报错!
// error: type 'double' cannot be narrowed to 'int' in initializer list
// 如果你明确知道要截断,必须显式转型,逼你写出安全的代码:
int total4{static_cast<int>(x + y)}; // 正确
}
C++ 有一条历史悠久的看门狗法则:"任何可以被编译器解析为函数声明的东西,都会被解析为函数声明 "。这导致用 () 初始化无参对象时,经常被误判为函数声明。
cpp
class Widget {
public:
Widget() {}
Widget(int x) {}
};
void doSomething() {
// ---- 苦恼的起源:你想调用默认构造函数 ----
Widget w1();
// 警惕!这【不是】一个叫 w1 的 Widget 对象!
// 它被 C++ 解析为一个名为 w1、不接收参数、返回值是 Widget 的【函数声明】!
// ---- 导致连锁崩溃 ----
// w1.doSomething(); // 错误:对函数求成员(编译报错)
// ---- 救星:使用 {} ----
Widget w2{};
// 正确!这明确无误地告诉编译器:我要调用默认构造函数,创建一个 w2 对象。
}
{}同样有缺点,最明显的在于 std::initializer_list,在初始化对象时创建的可能不是原类型而是 std::initializer_list,这是之前讨论过的。并且, 如果一个类同时存在普通构造函数 和带有 std::initializer_list 的构造函数 ,只要你用了 {},编译器就会极度偏心地优先匹配后者,哪怕类型不完美匹配也会硬转。
编译器在看到 {} 时,会优先考虑 std::initializer_list。只要大括号里的元素能够隐式转换为 `initializer_list 声明的底层类型(这里是long double),编译器就会毫不犹豫地抛弃那些原本完美匹配的普通构造函数。
cpp
#include <iostream>
#include <initializer_list>
class Widget {
public:
// 普通构造函数 1
Widget(int i, bool b) {
std::cout << "调用了 Widget(int, bool)\n";
}
// 普通构造函数 2
Widget(int i, double d) {
std::cout << "调用了 Widget(int, double)\n";
}
// std::initializer_list 构造函数
Widget(std::initializer_list<long double> il) {
std::cout << "调用了 Widget(std::initializer_list)\n";
}
};
int main() {
// 1. 使用小括号 ()
Widget w1(10, true); // 完美匹配 Widget(int, bool)
Widget w2(10, 5.0); // 完美匹配 Widget(int, double)
std::cout << "-------------------\n";
// 2. 使用花括号 {} ------ 陷阱爆发
Widget w3{10, true}; // 强行调用 Widget(std::initializer_list)!
// int(10) 和 bool(true) 被悄悄提升/转换为 long double
Widget w4{10, 5.0}; // 强行调用 Widget(std::initializer_list)!
// int(10) 和 double(5.0) 被强行转换为 long double
}
如果偏心遇到了规则冲突,编译器宁可选择编译报错,也不愿意回头去调用那个完美的普通构造函数。
cpp
#include <initializer_list>
class Widget {
public:
Widget(int i, double d) {} // 普通构造函数
Widget(std::initializer_list<int> il) {} // 底层类型改为 int
};
int main() {
// 如果用小括号,完美运行
Widget w1(10, 5.0);
// 如果用花括号:
// Widget w2{10, 5.0};
/*
【编译报错!】
编译器的内心活动:
1. 发现了 {},我必须优先匹配 std::initializer_list<int>。
2. 列表里有 10 (int) 和 5.0 (double)。
3. 糟糕,把 double 转换为 int 是"窄化转换"(Narrowing Conversion),在 {} 中是被禁止的!
4. 那我要不要回头去调用 Widget(int, double) 呢?不,老子偏不!直接报错!
报错信息类似于:error: type 'double' cannot be narrowed to 'int' in initializer list
*/
}
只有当大括号里的类型无论如何都绝对无法转换成 initializer_list` 的底层类型时,编译器才会死心,不情愿地去调用普通构造函数。
cpp
#include <initializer_list>
#include <string>
#include <iostream>
class Widget {
public:
Widget(int i, double d) { std::cout << "普通构造\n"; }
// 底层类型是 std::string
Widget(std::initializer_list<std::string> il) { std::cout << "List 构造\n"; }
};
int main() {
// 无论是 int 还是 double,都无法隐式转换为 std::string
// 编译器终于放弃了 initializer_list
Widget w{10, 5.0}; // 输出:普通构造
}
如果写 Widget w{};,它到底是调用空列表的 initializer_list,还是调用默认构造函数?
语言规定,在这种情形下应该执行**默认构造函数**。
cpp
class Widget {
public:
Widget() { std::cout << "默认构造\n"; }
Widget(std::initializer_list<int> il) { std::cout << "List 构造\n"; }
};
int main() {
Widget w1{}; // 输出:默认构造 (C++ 规定空花括号代表没有任何参数,执行默认初始化)
// 如果非要调用"空的 initializer_list" 构造函数,你必须这么写:
Widget w2{{}}; // 输出:List 构造 (外层花括号是初始化,内层空花括号代表空列表)
}
泛型编程中疑惑
在泛型编程(模板)中,() 和 {} 的选择会直接决定你的模板是变成一个通用的神器 ,还是变成一个随机炸弹。
因为模板在编写时,你根本无法预知未来的用户会传入什么类型(T)以及什么参数(Args...)。如果我们在模板内部盲目使用 {},那么当用户传入的类型恰好带有 std::initializer_list` 构造函数时,模板的行为就会彻底偏离预期。
假设我们要写一个类似于 std::make_unique 的工厂函数,它接收任意参数,并在堆上构造对象。
cpp
#include <memory>
#include <vector>
#include <iostream>
#include <utility>
// ==================== 错误示范 ====================
// 内部使用了花括号 {} 进行完美转发
template<typename T, typename... Args>
std::unique_ptr<T> bad_make_unique(Args&&... args) {
// 灾难根源:使用了 { std::forward<Args>(args)... }
return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
}
// ==================== 正确示范 ====================
// 标准库采用的做法,内部使用小括号 ()
template<typename T, typename... Args>
std::unique_ptr<T> good_make_unique(Args&&... args) {
// 安全:使用的是 ( std::forward<Args>(args)... )
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
如果用户传入的类没有 std::initializer_list 构造函数,bad 和 good 都能正常工作。
cpp
class Widget {
public:
Widget(int x, int y) { std::cout << "Widget 构造成功\n"; }
};
int main() {
auto p1 = good_make_unique<Widget>(10, 20); // 正常:调用 Widget(int, int)
auto p2 = bad_make_unique<Widget>(10, 20); // 正常:调用 Widget(int, int)
}
当用户想要通过工厂函数创建std::vector<int> 时,由于std::vector内部对() 和 {}有着完全不同的解释,就会产生致命的 bug。
在 bad_make_unique 内部,代码被展开成了 new std::vector<int>{10, 20}。、花括号强行劫持了控制权,导致它去调用了 std::initializer_list 构造函数。这样就无意间改变了用户的语义。
cpp
int main() {
// 用户的真实意图:我想创建一个含有 10 个元素,每个元素都是 20 的 vector
// 1. 使用正确的工厂(内部是 ())
auto v_good = good_make_unique<std::vector<int>>(10, 20);
std::cout << "good vector 大小: " << v_good->size() << "\n";
// 输出: 10 (符合预期!)
std::cout << "-------------------------------------\n";
// 2. 使用错误的工厂(内部是 {})
auto v_bad = bad_make_unique<std::vector<int>>(10, 20);
std::cout << "bad vector 大小: " << v_bad->size() << "\n";
// 输出: 2 (崩盘!)
// 此时 vector 内部只有两个元素:[10, 20]
}
如果用户传入的参数根本不是为了 initializer_list 准备的,但因为你用了 {},编译器强行去匹配,结果就是直接编译失败。
cpp
int main() {
// 用户的意图:创建一个容量为 10 的 vector,元素默认初始化
// 1. 使用 () 的工厂
auto v_good = good_make_unique<std::vector<int>>(10); // 成功!大小为 10
// 2. 使用 {} 的工厂
// auto v_bad = bad_make_unique<std::vector<int>>(10);
/*
【编译报错!】
为什么?因为在 bad_make_unique 内部展开为了:new std::vector<int>{10}
你可能会想:这不就是创建一个包含元素 10 的 vector 吗?为什么报错?
因为:有些编译器/库在处理只有一个元素的 T{x} 模板转发时,
由于完美转发的引用类型推导(如 int&),在特定复杂的嵌套模板中,
{} 匹配 std::initializer_list 可能会引发由于窄化检查或显式构造函数导致的推导失败。
更典型的报错发生在:
auto v = bad_make_unique<std::vector<std::string>>(10);
意图:创建 10 个默认字符串的 vector。
内部展开:new std::vector<std::string>{10} -> 试图把 10 转换为 std::string 填入 initializer_list,直接报语法错误!
*/
}
💡 核心铁律
在编写可重用的模板函数(特别是涉及完美转发 Args&&...)时:
- 必须使用
()来初始化内部的对象。 - 只有当你明确知道这个模板只为 某种特定聚合类型服务、或者你显式地想提供列表初始化语义 时,才考虑在模板内用
{}。