转向现代C++——在创建对象时注意区分()和{}

文章目录

在创建对象时注意区分()和{}

{}的使用注意

大多数情况尽量选择用**{}来创建对象**,对于{}主要有几个优势:

  1. 在 C++的三种初始化表达式写法中(()、{}和=),只有大括号适用于所有场合
  2. {}禁止内建型别之间进行**隐式窄化型别转换**。
  3. 免疫 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&&...)时:

  • 必须使用 () 来初始化内部的对象。
  • 只有当你明确知道这个模板只为 某种特定聚合类型服务、或者你显式地想提供列表初始化语义 时,才考虑在模板内用 {}
相关推荐
铅笔小新z1 小时前
【C语言】数组详解
c语言·开发语言
Tisfy1 小时前
VSCode Docker(Code Server)首次调试C++长时间下载debuginfo问题
c++·vscode·docker
摇滚侠1 小时前
Java 饿汉式 单例模式
java·开发语言·单例模式
lbb 小魔仙1 小时前
工业数据困局的破局者:DolphinDB 如何让海量时序数据真正“跑“出价值
开发语言·人工智能·python·langchain
枫叶丹41 小时前
【HarmonyOS 6.0】Device Security Kit安全审计阻断功能深度解析
开发语言·安全·华为·harmonyos
读书札记20221 小时前
C++ switch..case语句中变量跨域问题探讨及解决方法
开发语言·c++
一轮弯弯的明月1 小时前
Spring AOP编程
java·开发语言·spring boot·笔记·spring aop·学习心得
ch.ju1 小时前
Java程序设计(第3版)第四章——什么是对象
java·开发语言
努力努力再努力wz1 小时前
【Redis入门系列】Redis基础命令详解:从客户端连接到数据读写、key 管理与过期机制
c语言·开发语言·数据结构·数据库·c++·redis·缓存