《Effective Modern C++》第3章 Moving to Modern C++
一、区分圆括号 ()
与大括号 {}
(Item 7)
C++11 引入统一初始化(brace‑initialization),即使用 {}
来初始化对象,与传统的 ()
存在细微差别:
-
避免窄化转换(narrowing)
int x1(3.5); // x1 == 3(隐式截断) int x2{3.5}; // 编译错误,防止窄化
-
列表初始化优先级高于单参数构造
struct A { A(int); A(std::initializer_list<int>); }; A a1(1); // 调用 A(int) A a2{1}; // 调用 A(std::initializer_list<int>)
-
内置数组与聚合类型
std::vector<int> v1(5, 10); // 五个元素,每个值为 10 std::vector<int> v2{5, 10}; // 两个元素:5, 10
建议
- 对基本类型和聚合类型优先使用
{}
,以获得更严格的类型检查和一致的语法; - 对于只接受单一特定参数的构造,明确使用
()
避免调用错误的初始化列表构造函数。
二、优先使用 nullptr
而非 0
或 NULL
(Item 8)
-
问题
NULL
在不同平台下定义可能为0
或(void*)0
,带来类型模糊;- 使用整型
0
传给重载函数时,编译器难以区分指针重载与整数重载。
-
解决
void f(int); void f(char*);f(0); // 调用 f(int) f(nullptr); // 调用 f(char*)
建议
- 在所有指针上下文中使用
nullptr
,保证类型安全和重载解析明确。
三、使用别名声明(using
)替代 typedef
(Item 9)
-
typedef
限制- 语法晦涩,无法用于模板别名;
- 不易与模板参数一起阅读。
-
using
别名typedef std::map<std::string, std::vector<int>> MapType; // 改为 using MapType = std::map<std::string, std::vector<int>>;// 模板别名 template<typename K, typename V> using MapOf = std::map<K, V>;
建议
- 在新代码中一律采用
using
,既清晰又可与模板别名和别名模板配合使用。
四、优先使用作用域枚举(enum class
)(Item 10)
-
传统枚举问题
- 枚举常量位于所在命名空间,易与其他符号冲突;
- 默认可隐式转换为整型,丢失类型安全。
-
作用域枚举优势
enum Color { Red, Green, Blue }; // Red 与全局冲突 enum class Shape { Circle, Square }; // Shape::Circle,无冲突int i = Shape::Circle; // 错误,不能隐式转换
-
指定底层类型
enum class ErrorCode : uint8_t { OK = 0, Fail = 1 };
建议
- 新枚举定义一律使用
enum class
; - 如需与整型交互,可显式
static_cast
。
五、用已删除函数(= delete
)替代私有未定义函数(Item 11)
-
旧习惯
class NonCopyable { private:NonCopyable(const NonCopyable&);NonCopyable& operator=(const NonCopyable&); };
仅在不定义函数时会在链接期报错,且误报位置不直观。
-
现代做法
class NonCopyable { public:NonCopyable(const NonCopyable&) = delete;NonCopyable& operator=(const NonCopyable&) = delete; };
建议
- 对于不希望调用的函数,使用
= delete
,让编译器在编译期明确报错并指出源位置。
六、重写虚函数时声明 override
(Item 12)
-
风险
- 虚函数签名微小变动会导致意外重载而非重写,潜藏运行期错误。
-
加上
override
struct Base { virtual void f(int); }; struct Derived : Base {void f(int) override; // 正确重写void f(double) override; // 编译错误,函数签名不匹配 };
建议
- 所有重写基类虚函数的派生类函数都显式标注
override
。
七、优先使用 const_iterator
而非 iterator
(Item 13)
-
背景
在不需要修改容器元素时,应使用只读迭代器以保证不被意外改变。 -
示例
std::vector<int> v = {/*...*/}; for (auto it = v.cbegin(); it != v.cend(); ++it) {// it 为 const_iterator,无法通过 *it 进行写操作 }
建议
- 在遍历容器且不打算修改元素时,始终使用
cbegin()
/cend()
或手动指定const_iterator
。
八、声明不会抛出异常的函数为 noexcept
(Item 14)
-
好处
- 编译器可据此做更激进的优化;
- 在容器扩容时,若元素移动构造标记为
noexcept
,可避免回退到拷贝构造。
-
示例
void swap(Buffer& b1, Buffer& b2) noexcept {using std::swap;swap(b1.data, b2.data); }
建议
- 默认将不会抛出异常的函数标注
noexcept
; - 使用
noexcept(expr)
形式当抛出与否依赖于表达式。
九、尽可能使用 constexpr
(Item 15)
-
作用
- 在编译期间求值,提高性能;
- 构造常量对象、用作编译期上下文。
-
示例
constexpr int factorial(int n) {return n <= 1 ? 1 : (n * factorial(n - 1)); }static_assert(factorial(5) == 120, "错误");
建议
- 对所有能在编译期求值的函数或构造函数加上
constexpr
; - 在 C++14 及以后,
constexpr
函数可包含循环和更多语句。
十、使常量成员函数线程安全(Item 16)
-
问题
const
成员函数默认是线程安全的吗?不是。const
只是保证不修改成员表面状态,但底层可能修改缓存等。 -
做法
- 对内部缓存、延迟初始化等涉及可变状态的数据成员,使用
mutable
和适当的同步机制(如std::mutex
); - 或者在
const
函数中不使用可变共享状态。
- 对内部缓存、延迟初始化等涉及可变状态的数据成员,使用
示例
class Data {
public:int get() const {std::lock_guard<std::mutex> lg(m_);return cachedValue_;}
private:mutable std::mutex m_;int cachedValue_;
};
十一、理解特殊成员函数的生成规则(Item 17)
C++ 会在未显式声明时自动生成默认构造、拷贝/移动构造、拷贝/移动赋值、析构函数,规则复杂:
-
拷贝构造函数
- 如果显式声明了移动构造或拷贝赋值,拷贝构造会被阻塞(C++11);
-
移动构造函数
- 如果显式声明了拷贝构造、拷贝赋值或析构,移动构造会被阻塞;
-
析构函数
- 显式定义后,依然会生成,但会影响其他特殊成员函数生成。
建议
- 对于需要自定义移动或拷贝行为的类,最好同时声明并定义所有相关特殊成员函数(Rule of Five);
- 如无需移动,应显式
= delete
移动构造与移动赋值; - 利用
= default
保留自动生成版本,并在声明处表达意图。
通过对以上十一个细则的深入理解与实践,你将全面掌握现代 C++ 编程中的常见陷阱与最佳实践,为编写高性能、类型安全、可维护的代码奠定坚实基础。