CppCon 2017 学习:10 Core Guidelines You Need to Start Using Now

C.45: 不要定义一个仅仅初始化成员变量 的默认构造函数,而是使用类内成员初始化器

如果你有一个默认构造函数,它的唯一作用是给成员变量赋默认值(如 1、2、3),这更清晰、简单的方法是直接在成员变量声明时使用类内初始化器(in-class initializers)

C.48: 优先使用类内初始化器,而不是在构造函数中初始化常量初值

解释:

当初始值是固定的(比如总是初始化为 1、2、3),直接放在成员定义里更清楚、更少重复。

✳ 重构你的代码

cpp 复制代码
class Simple {
public:
    Simple() = default;
    Simple(int aa, int bb, int cc = -1) : a(aa), b(bb), c(cc) {}
    Simple(int aa) {
        a = aa;
        b = 0;
        c = 0;
    }
private:
    int a{1};
    int b{2};
    int c{3};
};

优点:

  • a{1}b{2}c{3} 让默认初值更直观。
  • 默认构造函数 Simple() = default; 是自动生成的,不用你写。
  • 更少代码,更清晰意图。

原代码问题:

cpp 复制代码
Simple() : a(1), b(2), c(3) {}

这只是在做初始化,跟写在类内没区别,应该删掉改为类内初始化器

总结一句话:

如果成员有固定初始值,不要在构造函数里写,直接在类内初始化。构造函数就用来处理变化的参数。这样代码更清晰、更易维护。

你提供的这两段代码展示了**使用类内成员初始化器(in-class initializers)**的好处,结合后面列出的 Benefits(好处),我们来逐条解释。

示例代码含义解析

cpp 复制代码
class Simple {
public:
    Simple()  {}  // 用户定义的默认构造函数
    Simple(int aa, int bb, int cc) : a(aa), b(bb), c(cc) {}
    Simple(int aa) : a(aa) {}
private:
    int a = -1;
    int b = -1;
    int c = -1;
};
cpp 复制代码
class Simple {
public:
    Simple() = default;  // 编译器自动生成默认构造函数
    Simple(int aa, int bb, int cc) : a(aa), b(bb), c(cc) {}
    Simple(int aa) : a(aa) {}
private:
    int a = -1;
    int b = -1;
    int c = -1;
};

在这两个版本中:

  • 类内初始值 int a = -1; 是默认值,会被任何没特别赋值的构造函数所使用。
  • 第二个版本通过 = default,使默认构造函数由编译器自动生成,而不是手写空函数体。

优点(Benefits)详解

No arguing about "equivalent" ways to do it

不再争论各种"等价"的初始化写法。

统一使用类内初始化器可以避免这样的问题:

cpp 复制代码
Simple() : a(-1), b(-1), c(-1) {}   // 和
int a = -1; int b = -1; int c = -1; // 哪个更好?

→ 统一方式更清晰,减少团队代码风格争议。

May prevent some bugs

可以防止某些初始化遗漏的 bug。

比如:

cpp 复制代码
Simple(int aa) : a(aa) {}  // 如果 b 和 c 没在构造函数中初始化

→ 它们就会用类内初始值 -1,不会变成未定义值或垃圾值。

May put you back in "compiler generates constructors" land

有可能让你回到"让编译器自动生成构造函数"的美好世界。

你就可以用 = default 自动生成默认构造函数,不必手写:

cpp 复制代码
Simple() = default;

→ 更少代码,更少出错。

Potentially marginally faster in some circumstances

在某些情况下,这种做法可能更快(边际提升)。

尤其在使用 std::vector<Simple> 等容器时,类内初始化器有时能让构造路径更优化,因为编译器可能内联或避免重复初始化。

总结一句话:

类内初始化器 + 默认构造函数 = 更简单、更安全、更一致的 C++ 代码。

这符合现代 C++(C++11 起)的最佳实践,尤其在构造函数中不重复写相同的默认值。

你提供的是 C++ 核心准则中的一条重要建议:

F.51:如果可以选择,优先使用默认参数而不是函数重载

示例比较

使用重载实现默认行为(冗余):

cpp 复制代码
class Reactor {
public:
    double Offset(double a, double b, double ff);
    double Offset(double a, double b);  // 重载一份只是为了给 ff 默认值
};
double Reactor::Offset(double a, double b, double ff) {
    // 复杂计算
    return whatever;
}
double Reactor::Offset(double a, double b) {
    return Offset(a, b, 1.0);  // 手动添加默认值
}

使用默认参数(简洁):

cpp 复制代码
class Reactor {
public:
    double Offset(double a, double b, double ff = 1.0);
};
double Reactor::Offset(double a, double b, double ff /* = 1.0*/) {
    // 复杂计算
    return whatever;
}

为什么默认参数更好?(Benefits)

No arguing about "equivalent" ways to do it

避免对两种"看似等效"的实现方式的争论。

不用再争论是要重载两个版本,还是默认参数好------直接默认参数就行。

Will not forget to make same change to both copies

修改参数逻辑时,不会忘记同步另一个版本。

例如改成使用 ff = 0.95,只要改一处:

cpp 复制代码
double Offset(double a, double b, double ff = 0.95);

→ 避免因为忘改重载函数导致不一致或 bug。

Difference between the two "versions" is crystal clear

不再出现"两个版本"之间的模糊区别,调用者一目了然。

cpp 复制代码
Offset(10, 20);         // 用默认 ff = 1.0
Offset(10, 20, 0.75);   // 显式给出 ff

相比重载版本:

cpp 复制代码
Offset(10, 20);         // 哪个版本?(看签名)
Offset(10, 20, 0.75);   // 不明显差异

总结一句话:

用默认参数,写得更少,错得更少,读得更清楚

这体现了现代 C++ 的风格倾向:减少重复代码,提升可维护性与表达力

C.47:定义和初始化成员变量时,应按照它们在类中声明的顺序

为什么这很重要?

在 C++ 中,即使你在构造函数的初始化列表中按照你喜欢的顺序 写初始化语句,编译器实际上仍然会按成员在类中声明的顺序来初始化

示例:存在隐患的代码

cpp 复制代码
class Wrinkle {
public:
    Wrinkle(int i) : a(++i), b(++i), x(++i) {}
private:
    int a;
    int x;
    int b;
};

初始化顺序 实际上是:a → x → b

但你写的是:a → b → x

这样会导致:

  • 成员变量 b 被初始化时,依赖了 i 的值(可能和你想的不一样)
  • 代码看起来正确,但行为会出错或让人困惑
  • 编译器可能发出警告:"warning: field 'x' will be initialized after field 'b'"

更清晰的正确写法

cpp 复制代码
class Wrinkle {
public:
    Wrinkle(int i) : a(++i), x(++i), b(++i) {}
private:
    int a;
    int x;
    int b;
};

更真实的例子

假设:

cpp 复制代码
class Person {
public:
    Person(string first, string last) 
        : firstName(first), lastName(last), fullName(first + " " + last) {}
private:
    string firstName;
    string lastName;
    string fullName;
};

fullName 依赖于 firstNamelastName但必须确保它在它们后面声明,否则会使用未初始化的值!

谁可能打乱顺序?

  • **"热心的新人"**试图按字母顺序排列变量
  • 工具可能自动整理字段
  • 有人想按"逻辑分组"整理变量,却不看构造函数顺序

建议(总结)

  • 始终按类中声明的顺序编写构造函数初始化列表
  • 不要依赖初始化顺序之外的副作用(比如 ++i
  • 如果成员间存在初始化依赖,应在声明顺序上表达清晰的意图

好处

  • 避免初始化顺序 bug
  • 不需要每个开发者都记住 C++ 的这个"怪癖"
  • 鼓励你重新思考类的设计,减少成员之间的耦合依赖

一句话总结:

在初始化列表中改变顺序没用 ------ 编译器会按声明顺序来初始化。为了安全和可读性,让你的初始化列表和成员声明保持一致的顺序。

你提到的内容是 C++ 核心准则中的一条设计建议:

I.23: Keep the number of function arguments low

I.23:尽量减少函数参数数量

举例说明:

糟糕的设计(太多参数):
cpp 复制代码
int area(int x1, int y1, int x2, int y2);
int a = area(1, 1, 11, 21);
  • 参数太多,难记忆,容易出错。
  • 没有抽象,含义不清晰(哪个是左上角?哪个是右下角?)。
更好的设计(引入抽象):
cpp 复制代码
int area(Point p1, Point p2);
int a = area({1, 1}, {11, 21});
  • 使用 Point 类型,更清晰地表达意图。
  • 减少调用者负担,不需要记位置。
  • 抽象可复用。

进一步示例:构造 Customer

糟糕设计:
cpp 复制代码
Customer(string pfirst, string plast, string pph,
         string sfirst, string slast, string sph, string sid);
  • 多达 7 个字符串参数,难维护。
  • 非常容易传错。
改进设计:
cpp 复制代码
class Customer {
    Person details;
    Salesrep rep;
public:
    Customer(Person p, Salesrep s);
};
  • 将数据封装进合适的结构(如 Person, Salesrep)。
  • 调用清晰,代码更易维护。

核心好处

优点 说明
更低的认知负担 用户不用记住那么多参数顺序
更清晰的意图表达 结构化参数让含义更明确
抽象可以复用 Point, Person 可以在别处使用
降低未来代码变更影响 只需改结构体,函数签名不动

小结一句话:

函数参数越少越好,如果超过 3-4 个,应该考虑把它们组合进结构体或类里。

ES.50: Don't cast away const

不要去除 const 限定符(不要"cast away const")

背景问题

我们有一个 Stuff 类,其中包含一个缓存机制 cachedValue,希望在 getValue() 中使用它。

getValue()const 函数,不能修改任何成员变量。

错误做法:
cpp 复制代码
int Stuff::getValue() const {
    if (!cacheValid) {
        cachedValue = LongComplicatedCalculation();  //  编译错误,修改了成员
        cacheValid = true;
    }
    return cachedValue;
}

如果你想让它编译,有人可能会写:

cpp 复制代码
int Stuff::getValue() const {
    auto self = const_cast<Stuff*>(this);  //  cast away const!
    if (!self->cacheValid) {
        self->cachedValue = LongComplicatedCalculation();
        self->cacheValid = true;
    }
    return self->cachedValue;
}

这是不推荐的做法!

为什么不能去掉 const

  • 违反接口契约getValue() 承诺不修改对象状态,却偷偷修改了。
  • 让头文件成为谎言:你声称不变,其实在改。
  • 代码难以维护 :别人调用你的 const 函数,会以为它是线程安全的、无副作用的,但其实不是。
  • 容易出 bug,尤其是涉及优化、多线程、拷贝等。

正确做法:使用 mutable

cpp 复制代码
class Stuff {
private:
    int number1;
    double number2;
    int LongComplicatedCalculation() const;
    mutable int cachedValue;
    mutable bool cacheValid;
public:
    Stuff(int n1, double n2)
        : number1(n1), number2(n2), cachedValue(0), cacheValid(false) {}
    bool Service1(int x);
    bool Service2(int y);
    int getValue() const;
};
int Stuff::getValue() const {
    if (!cacheValid) {
        cachedValue = LongComplicatedCalculation();
        cacheValid = true;
    }
    return cachedValue;
}

为什么 mutable 是好的解决方案?

优点 说明
保持 const 函数语义 只有缓存变量能被修改,接口保持诚实
可读性好 一眼能看出哪些成员可能在 const 函数中被改
编译器优化友好 保留 const 语义,有助于优化和静态分析
更安全 避免了错误地修改非缓存成员的风险

小结:

当你需要在 const 函数中修改内部缓存状态时,请使用 mutable,而不是 const_cast。

永远不要 cast away const 除非你非常清楚代价,并且这是最后手段。
mutable 是安全地修改缓存的标准做法。

如果你还想探讨 mutable 的使用边界或缓存设计模式,可以继续问我。

你提到的是 C++ 核心准则中非常重要的一条资源管理原则:

I.11: Never transfer ownership by a raw pointer (T*)

永远不要用裸指针(T*)传递资源所有权

违反规则的错误示例:

cpp 复制代码
Policy* SetupAndPrice(args) {
    Policy* p = new Policy{...};  // 手动分配内存
    // ...
    return p;                     // 通过裸指针传递所有权
}
  • 🔺 谁来 delete? 不清楚。
  • 🔺 极易造成 内存泄漏
  • 🔺 调用方不知道是否需要释放。
  • 🔺 所有权不明确,违反了现代 C++ 的资源管理理念。

更安全的替代方案:

1. 返回值传递(by value)

cpp 复制代码
Policy SetupAndPrice(args);  // 编译器可优化掉复制(RVO/NRVO)
  • 简洁。
  • 现代编译器通常会 自动省略拷贝
  • 如果不担心复制代价,这是首选方式。

2. 使用非 const 引用传入已有对象

cpp 复制代码
void SetupAndPrice(Policy& policy);  // 调用方自己拥有 policy
  • 函数不会创建资源,只修改它。
  • 最适用于:调用前就已有对象。

3. 返回智能指针(推荐!)

cpp 复制代码
std::unique_ptr<Policy> SetupAndPrice(args);
  • 明确表示"我拥有这个对象"。
  • 调用方拿到智能指针后,对象会自动销毁。
  • 避免忘记 delete。

4. 使用 gsl::owner<T*>

cpp 复制代码
gsl::owner<Policy*> SetupAndPrice(args);
  • 并不自动管理内存。
  • 作用是标记:"这个裸指针的所有权转移了"
  • 更易被工具分析/被人理解。
cpp 复制代码
template <class T, class = std::enable_if_t<std::is_pointer<T>::value>>
using owner = T;

小结:

做法 安全性 所有权是否清晰
裸指针返回 T* 高风险 不明确
返回值 T 安全 明确
智能指针 unique_ptr<T> 安全 明确
引用参数 T& 安全 明确
gsl::owner<T*> 辅助作用 明确但不自动

核心观点:

内存管理太重要,不能只靠记忆。
不要手动管理内存,应该:

  • 用值语义(复制或移动)
  • 用智能指针管理所有权
  • 或至少用 owner<T*> 明确所有权

F.21: To return multiple "out" values, prefer returning a tuple or struct

返回多个"输出"值时,优先返回 tuplestruct,不要用输出参数(out-params)

传统写法:输出参数

cpp 复制代码
int foo(int inValue, int& outValue) {
    outValue = inValue * 2;
    return inValue * 3;
}
int main() {
    int number = 4;
    int answer = foo(5, number);
    return 0;
}

问题

  • number 是隐含的输出值,看起来像输入。
  • 函数返回值和"副作用输出"分开,阅读困难。
  • 不符合现代 C++ 倡导的值语义风格。

更好方式 1:自定义 struct

cpp 复制代码
struct twoNumbers {
    int value1;
    int value2;
};
twoNumbers fooStruct(int inValue) {
    return twoNumbers{ inValue * 2, inValue * 3 };
}
int main() {
    twoNumbers result = fooStruct(6);
    int number = result.value1;
    int answer = result.value2;
    return 0;
}

优点

  • 清晰表达含义(用字段名说明含义)
  • 编译器可优化拷贝(RVO)
  • 接口干净,没有输出引用

更好方式 2:返回 std::tuple

cpp 复制代码
std::tuple<int, int> fooTwo(int inValue) {
    return std::make_tuple(inValue * 2, inValue * 3);
}
使用 std::tie 拆解:
cpp 复制代码
int number, answer;
std::tie(answer, number) = fooTwo(9);
使用结构化绑定(C++17 起):
cpp 复制代码
auto [answer, number] = fooTwo(9);

优点

  • 表达力强,代码简洁
  • 结构化绑定让读取 tuple 更方便
    但 tuple 缺点是:字段无名称,不够语义化。如果语义重要,还是 struct 更好。

可选方式:std::optional<T>

如果你只是返回一个对象和一个"是否有效"的布尔值,可以用 std::optional<T>

cpp 复制代码
std::optional<int> maybeDivide(int a, int b) {
    if (b == 0) return std::nullopt;
    return a / b;
}
int main() {
    auto result = maybeDivide(10, 0);
    if (result)
        std::cout << "Result: " << *result << '\n';
    else
        std::cout << "Division failed.\n";
}

为什么要避免输出参数?

问题 原因
不清晰 输出参数隐藏在函数签名中
副作用 函数修改了外部变量,阅读成本高
可读性差 调用者必须准备变量来传入修改
无法组合 输出参数难以用于链式表达式或惰性计算

总结:建议优先顺序

目的 推荐做法
返回多个有语义的值 自定义 struct
返回多个简单值 std::tuple + 结构化绑定
返回可选单值 std::optional<T>
不建议 用引用参数 (int& out) 作为输出

你提到的是现代 C++ 中推荐使用的 enum class(作用域枚举),这是 C++11 引入的一项重要特性。下面逐条解释你贴出的内容,并说明其意义。

使用 enum class 的好处

cpp 复制代码
enum class Error { OK, FileNotFound, OutOfMemory };
enum class Ratings { Terrible, OK, Terrific };
enum oldStyle { OH, OK, OR };

传统的 enum(如 oldStyle)的问题:

cpp 复制代码
oldStyle Oklahoma = OK;
  • 你可以直接写 OK,没有作用域前缀。
  • 名字冲突 :多个枚举如果都有 OK,只能有一个能叫 OK。
  • 自动转换为 int,可能造成隐式错误:
cpp 复制代码
int x = OK;  // 自动转 int,危险

enum class 的优势

cpp 复制代码
Error result = Error::OK;
Ratings stars = Ratings::OK;
int r = static_cast<int>(result);
  • 名字必须加作用域限定,例如 Error::OK,防止冲突。
  • 不会自动转换为 int,必须显式转换:
cpp 复制代码
int r = static_cast<int>(result);
  • 可以在不同枚举里重复名字(每个都有自己的作用域)

更强类型、更安全

特性 enum enum class
作用域限定
隐式转为 int
可以重名(如都叫 OK)
类型安全(可当作独立类型)
推荐 旧风格 强烈推荐

可指定底层类型(C++11 起)

cpp 复制代码
enum class Error : uint8_t { OK, FileNotFound, OutOfMemory };
  • 默认底层类型是 int,但可以用更小(或大)的类型。
  • 适用于节省空间或做序列化通信协议。

实践建议

  • 永远使用 enum class,除非你明确需要与 C API 兼容。
  • 避免老式的 enum,尤其是放在头文件里的(容易污染命名空间)。
  • 使用 static_cast<int>(e) 明确转为整型。

示例总结:

cpp 复制代码
enum class Error { OK, FileNotFound, OutOfMemory };
enum class Ratings { Terrible, OK, Terrific };
Error result = Error::OK;
Ratings stars = Ratings::OK;
// Cannot do this:
// int x = result;   错误
// Must be explicit:
int x = static_cast<int>(result);  // 

你提供的内容出自 C++ Core Guidelines (由 Kate Gregory 和 Bjarne Stroustrup 等人推动),主题是提高代码的安全性、可读性和意图表达。我们逐条来解释并 翻译理解这些条目。

I.12: 使用 not_null 明确指针不能为空

cpp 复制代码
Service s(1);
Service* ps = &s;
i = ps->DoSomething();
ps = nullptr;          //  潜在空指针异常
i = ps->DoSomething(); // 崩溃

使用 GSL(Guidelines Support Library)中的 not_null

cpp 复制代码
#include <gsl/gsl>
gsl::not_null<Service*> ps = &s;
ps = nullptr; //  编译失败或运行时断言

好处

  • 防止空指针解引用
  • 提升性能 (不需要反复检查指针是否为 nullptr
  • 表达意图:这个指针不能为 null,不是"可能为 null"

避免不安全的类型转换(ES.46)

C++ 中隐式转换可能丢失信息,例如:

cpp 复制代码
int x = 300;
char c = x; //  隐式缩窄,char 只有 8 位,丢失数据

使用 GSL 中的 narrownarrow_cast

cpp 复制代码
#include <gsl/gsl>
int x = 300;
char c = gsl::narrow<char>(x);       //  抛异常(值改变了)
char c2 = gsl::narrow_cast<char>(x); //  允许丢失数据,但开发者明确知道这事

narrow vs narrow_cast 区别

功能 narrow<T>(x) narrow_cast<T>(x)
类型转换
运行时检查 抛出异常 不检查
有信息丢失时 报错 安静执行
使用目的 安全性第一(调试优先) 性能优先,但我知道后果

总结:为什么要用这些工具

工具 目的 帮助
gsl::not_null<T*> 明确一个指针绝不能是 null 编译或运行时强制检查
gsl::narrow<T> 类型转换必须安全 运行时防止隐式精度丢失
gsl::narrow_cast<T> 允许转换但表达开发者意图 编译时不报错,清晰表达风险

最终目标

  • 编译器和工具 帮你发现错误
  • 表达清晰的意图,让别人 看得懂你的代码
  • 提前发现 bug,减少运行时崩溃
相关推荐
大明者省19 分钟前
pycharm2020.2版本给项目选择了虚拟环境解释器,项目文件都运行正常,为什么terminal文件路径的前面没有虚拟解释器的名称
开发语言·python
岸边的风1 小时前
JavaScript篇:【前端必备】数组方法大全:从‘会写’到‘玩出花’,你掌握几个?
开发语言·前端·javascript
HoroMin1 小时前
在Spring Boot中自定义JSON返回日期格式的指南
java·开发语言·spring boot·注解
汪洪墩1 小时前
使用Mars3d加载热力图的时候,出现阴影碎片
开发语言·前端·javascript·vue.js·cesium
木木黄木木1 小时前
使用Three.js创建炫酷的3D玻璃质感动态效果
开发语言·javascript·3d
wen__xvn1 小时前
基础数据结构第03天:顺序表(实战篇)
数据结构·c++·算法
八一考研数学竞赛1 小时前
第十七届全国大学生数学竞赛初赛模拟试题
学习·数学·latex·全国大学生数学竞赛
@小红花1 小时前
Python从入门到精通
开发语言·python
androidwork2 小时前
Kotlin实现文件上传进度监听:RequestBody封装详解
android·开发语言·kotlin
爱装代码的小瓶子2 小时前
字符操作函数续上
android·c语言·开发语言·数据结构·算法