Effective Modern C++:42条改善C++11和C++14代码的建议

深入理解 Effective Modern C++ 的核心思想,掌握现代C++最佳实践

引言

《Effective Modern C++》是 Scott Meyers 的经典著作,提供了 42 条改善 C++11 和 C++14 代码的具体建议。本文总结这些核心要点,帮助你写出更现代、更高效的 C++ 代码。

核心主题

  • 类型推导的陷阱与最佳实践
  • auto 的正确使用
  • 移动语义和完美转发
  • 智能指针的选择
  • Lambda 表达式的优化
  • 并发编程

第一章:类型推导

Item 1: 理解模板类型推导

模板类型推导有三种情况,取决于 ParamType 的形式。

规则总结

graph TD
    A[模板推导] --> B{ParamType类型}
    B -->|指针/引用| C[保留const]
    B -->|万能引用| D[左值/右值分别推导]
    B -->|按值传递| E[丢弃const和引用]

关键要点

  • 引用和指针不同时,const 被保留
  • 万能引用(T&&)区分左值和右值
  • 按值传递会忽略 const 和引用

Item 2: 理解 auto 类型推导

auto 与模板推导几乎完全相同,唯一例外是花括号初始化。

1
2
3
4
auto x1 = 27;     // int
auto x2(27); // int
auto x3 = {27}; // std::initializer_list<int> ❌陷阱!
auto x4{27}; // C++17: int, C++14: std::initializer_list<int>

要记住的事

  • auto 推导通常与模板推导相同
  • auto 假定花括号初始化代表 std::initializer_list
  • 函数返回值或 lambda 参数中的 auto 使用模板推导规则

Item 3: 理解 decltype

decltype 总是返回表达式的确切类型,不会丢失 const 或引用。

1
2
3
4
5
6
7
8
9
const int i = 0;
auto a = i; // int
decltype(i) d = i; // const int

// C++14 的 decltype(auto)
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) {
return std::forward<Container>(c)[i]; // 完美转发返回类型
}

括号陷阱

1
2
3
int x = 10;
decltype(x) t1; // int
decltype((x)) t2; // int& ⚠️ 危险!

Item 4: 学会查看类型推导结果

三种方法:

  1. IDE 编辑器:快速但可能不准
  2. 编译器诊断:利用编译错误
  3. 运行时输出:typeid 和 Boost.TypeIndex
1
2
3
4
5
// 最可靠的方法:故意制造编译错误
template<typename T>
class TD; // Type Displayer

TD<decltype(x)> xType; // 编译器会显示类型

第二章:auto

Item 5: 优先使用 auto 而非显式类型声明

优点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 避免未初始化变量
int x; // 未初始化
auto x = 0; // 必须初始化

// 2. 避免类型不匹配
std::unordered_map<std::string, int> m;

// ❌ 错误:每次迭代都拷贝
for (const std::pair<std::string, int>& p : m) { }

// ✓ 正确:零拷贝
for (const auto& p : m) { }

// 3. 简化复杂类型
std::function<bool(const std::unique_ptr<Widget>&,
const std::unique_ptr<Widget>&)> func;

// 简化为
auto func = [](const auto& lhs, const auto& rhs) {
return *lhs < *rhs;
};

性能优势

场景 显式类型 auto 性能
Lambda std::function auto auto 快 2-10倍
容器遍历 可能类型错误 总是正确 避免拷贝
闭包 无法表达 完美捕获 零开销

Item 6: 当 auto 推导出非预期类型时使用显式类型初始化

代理类陷阱

1
2
3
4
5
6
std::vector<bool> features(const Widget& w);

auto highPriority = features(w)[5]; // ❌ 返回代理对象
// highPriority 不是 bool,而是临时的代理类

bool highPriority = features(w)[5]; // ✓ 正确

解决方案:显式类型转换

1
auto highPriority = static_cast<bool>(features(w)[5]);

其他代理类场景

  • std::vector<bool>::reference
  • Matrix 表达式模板
  • 智能指针的代理

第三章:转向现代C++

Item 7: 创建对象时区分 () 和 {}

三种初始化语法

1
2
3
int x(0);     // 圆括号
int y = 0; // 等号
int z{0}; // 花括号(统一初始化)

花括号优点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 可用于任何初始化场景
struct Widget {
int x{0}; // ✓ 成员初始化
int y = 0; // ✓ 也可以
int z(0); // ❌ 不行
};

// 2. 禁止隐式窄化转换
double x = 3.14;
int y{x}; // ❌ 编译错误,禁止窄化
int z(x); // ⚠️ 允许,但丢失精度

// 3. 免疫最令人烦恼的解析
Widget w1(10); // 调用构造函数
Widget w2(); // ❌ 函数声明!
Widget w3{}; // ✓ 调用默认构造函数

陷阱:std::initializer_list 构造函数

1
2
std::vector<int> v1(10, 20);  // 10个元素,每个值为20
std::vector<int> v2{10, 20}; // 2个元素:10和20

Item 8: 优先使用 nullptr 而非 0 或 NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void f(int);
void f(bool);
void f(void*);

f(0); // 调用 f(int)
f(NULL); // 可能不编译,或调用 f(int)
f(nullptr); // 调用 f(void*)

// 模板中的优势
template<typename FuncType, typename PtrType>
decltype(auto) call(FuncType func, PtrType ptr) {
return func(ptr);
}

auto result1 = call(f, 0); // 错误:推导为 int
auto result2 = call(f, nullptr); // ✓ 正确

Item 9: 优先使用别名声明而非 typedef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// typedef
typedef std::unique_ptr<std::unordered_map<std::string, std::string>>
UPtrMapSS;

// using (更清晰)
using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;

// 模板别名 - typedef 无法做到
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;

MyAllocList<Widget> lw; // ✓ 简洁

// typedef 需要
template<typename T>
struct MyAllocList {
typedef std::list<T, MyAlloc<T>> type;
};

MyAllocList<Widget>::type lw; // ❌ 繁琐

Item 10-11: 优先使用限域 enum 和 deleted 函数

限域枚举

1
2
3
4
5
6
7
8
9
// C++98 enum:不限域
enum Color { black, white, red };
auto white = false; // ❌ 错误!white 已被定义

// C++11 enum class:限域
enum class Color { black, white, red };
auto white = false; // ✓ OK
Color c = Color::white; // 必须限定
auto c = Color::white; // 也可以

deleted 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 防止隐式转换
bool isLucky(int number);
bool isLucky(char) = delete; // 拒绝 char
bool isLucky(bool) = delete; // 拒绝 bool
bool isLucky(double) = delete; // 拒绝 double

// 禁止模板实例化
template<typename T>
void processPointer(T* ptr);

template<>
void processPointer<void>(void*) = delete;

template<>
void processPointer<char>(char*) = delete; // const char* 也被拒绝

Item 12-15: 特殊成员函数和优化

noexcept 的重要性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Widget {
public:
Widget(Widget&& rhs) noexcept // ✓ 推荐
: name(std::move(rhs.name)) {}

Widget& operator=(Widget&& rhs) noexcept {
name = std::move(rhs.name);
return *this;
}

private:
std::string name;
};

// noexcept 让 std::vector 使用移动而非拷贝
std::vector<Widget> vw;
vw.push_back(Widget()); // 如果没有 noexcept,会拷贝而非移动

第四章:智能指针

Item 18: 使用 std::unique_ptr 管理独占所有权资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Investment { };
class Stock : public Investment { };

// 工厂函数
auto makeInvestment() {
return std::make_unique<Stock>(); // C++14
}

// 自定义删除器
auto delInvmt = [](Investment* pInvestment) {
makeLogEntry(pInvestment);
delete pInvestment;
};

template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(Ts&&... params) {
std::unique_ptr<Investment, decltype(delInvmt)>
pInv(nullptr, delInvmt);

if (/* 创建 Stock */)
pInv.reset(new Stock(std::forward<Ts>(params)...));

return pInv;
}

特点

  • 零开销(与裸指针相同大小)
  • 独占所有权
  • 可转换为 shared_ptr

Item 19: 使用 std::shared_ptr 管理共享所有权资源

1
2
3
4
5
6
7
8
9
10
auto loggingDel = [](Widget *pw) {
makeLogEntry(pw);
delete pw;
};

std::unique_ptr<Widget, decltype(loggingDel)>
upw(new Widget, loggingDel); // 删除器是类型的一部分

std::shared_ptr<Widget>
spw(new Widget, loggingDel); // 删除器不是类型的一部分

引用计数机制

graph LR
    A[shared_ptr 1] --> C[控制块]
    B[shared_ptr 2] --> C
    C --> D[对象]
    C --> E[引用计数: 2]
    C --> F[弱引用计数]
    C --> G[删除器]

性能开销

  • 控制块动态分配
  • 引用计数原子操作
  • 虚函数调用(删除器)

Item 20: 使用 std::weak_ptr 解决悬空指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
auto spw = std::make_shared<Widget>();

std::weak_ptr<Widget> wpw(spw); // wpw 指向 Widget

spw = nullptr; // Widget 被销毁,wpw 悬空

if (spw == nullptr) { // ✓ 检测共享指针
}

if (wpw.expired()) { // ✓ 检测弱指针
}

// 原子检查并访问
std::shared_ptr<Widget> spw2 = wpw.lock(); // 如果 wpw 过期则返回 null
auto spw3 = wpw.lock();

应用场景

  1. 缓存
1
2
3
4
5
6
7
8
9
10
11
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id) {
static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;

auto objPtr = cache[id].lock(); // 尝试从缓存获取

if (!objPtr) { // 不在缓存中
objPtr = loadWidget(id);
cache[id] = objPtr;
}
return objPtr;
}
  1. 观察者模式
1
2
3
4
5
6
7
8
9
10
11
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
public:
void notify() {
for (auto& wo : observers) {
if (auto o = wo.lock()) { // 检查观察者是否存活
o->update();
}
}
}
};

Item 21: 优先使用 std::make_unique 和 std::make_shared

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ 不推荐
std::shared_ptr<Widget> spw(new Widget);

// ✓ 推荐
auto spw = std::make_shared<Widget>();

// 优点1: 异常安全
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());
// ⚠️ 可能泄漏:new Widget 可能在 computePriority() 抛异常后完成

processWidget(std::make_shared<Widget>(), computePriority());
// ✓ 安全

// 优点2: 性能更好
auto spw1 = std::shared_ptr<Widget>(new Widget); // 2次分配
auto spw2 = std::make_shared<Widget>(); // 1次分配

不能使用 make 函数的情况

1
2
3
4
5
6
7
8
9
// 1. 自定义删除器
auto deleter = [](Widget* pw) { delete pw; };
std::unique_ptr<Widget decltype(deleter)> upw(new Widget, deleter);

// 2. 花括号初始化
auto spv = std::make_shared<std::vector<int>>(10, 20); // 10个20
// 想要{10, 20}只能:
auto initList = {10, 20};
auto spv = std::make_shared<std::vector<int>>(initList);

第五章:右值引用、移动语义和完美转发

Item 23: 理解 std::move 和 std::forward

std::move 无条件转换为右值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Widget {
public:
Widget(Widget&& rhs)
: name(std::move(rhs.name)),
p(std::move(rhs.p)) {}

private:
std::string name;
std::shared_ptr<int> p;
};

// move 的实现(简化)
template<typename T>
decltype(auto) move(T&& param) {
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}

std::forward 条件转换

1
2
3
4
5
6
7
8
9
void process(const Widget& lvalArg);   // 处理左值
void process(Widget&& rvalArg); // 处理右值

template<typename T>
void logAndProcess(T&& param) {
auto now = std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param)); // 完美转发
}

对比

特性 std::move std::forward
用途 无条件转右值 条件转发
参数 通用引用 通用引用
使用场景 移动构造/赋值 完美转发

Item 24: 区分万能引用和右值引用

万能引用的判断标准

1
2
3
4
5
6
7
8
9
template<typename T>
void f(T&& param); // 万能引用(有类型推导)

auto&& var2 = var1; // 万能引用

void f(Widget&& param); // 右值引用(无类型推导)

template<typename T>
void f(std::vector<T>&& param); // 右值引用(不是 T&&)

Item 25: 对右值引用使用 std::move,对万能引用使用 std::forward

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
public:
Widget(Widget&& rhs)
: name(std::move(rhs.name)) {} // rhs 是右值引用

template<typename T>
void setName(T&& newName) {
name = std::forward<T>(newName); // newName 是万能引用
}

private:
std::string name;
};

错误示例

1
2
3
4
5
6
7
8
9
// ❌ 不要对右值引用使用 forward
Widget(Widget&& rhs)
: name(std::forward<Widget>(rhs).name) {} // 错误

// ❌ 不要对万能引用使用 move
template<typename T>
void setName(T&& newName) {
name = std::move(newName); // 可能移动左值!
}

Item 26: 避免重载万能引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ❌ 问题代码
std::multiset<std::string> names;

template<typename T>
void logAndAdd(T&& name) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}

logAndAdd(std::string("Persephone")); // ✓
logAndAdd("Patty Dog"); // ✓

std::string petName("Darla");
logAndAdd(petName); // ✓ 拷贝左值

short nameIdx = 22;
logAndAdd(nameIdx); // ❌ 问题:T 推导为 short&,不会转换为 string

Item 27-30: 熟悉完美转发失败的情况

完美转发失败的情况

  1. 花括号初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void f(const std::vector<int>& v);

f({1, 2, 3}); // ✓ OK

template<typename T>
void fwd(T&& param) {
f(std::forward<T>(param));
}

fwd({1, 2, 3}); // ❌ 错误:无法推导

// 解决方案
auto il = {1, 2, 3};
fwd(il); // ✓
  1. 0 或 NULL 作为空指针
1
2
3
fwd(NULL);  // ❌ 推导为整数
fwd(0); // ❌ 推导为整数
fwd(nullptr); // ✓
  1. 仅声明的 static const 成员变量
1
2
3
4
5
6
7
8
9
class Widget {
public:
static const std::size_t MinVals = 28; // 声明
};

std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals); // ✓

fwd(Widget::MinVals); // ❌ 链接错误
  1. 重载函数名和模板名
  2. 位域

第六章:Lambda表达式

Item 31: 避免默认捕获模式

按值捕获的问题

1
2
3
4
5
6
7
8
9
10
11
12
using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;

void addDivisorFilter() {
auto calc1 = computeSomeValue1();
auto calc2 = computeSomeValue2();
auto divisor = computeDivisor(calc1, calc2);

filters.emplace_back(
[=](int value) { return value % divisor == 0; } // ❌ 悬空引用
);
}

指针捕获的问题

1
2
3
4
5
6
7
8
9
10
11
class Widget {
public:
void addFilter() const {
filters.emplace_back(
[=](int value) { return value % divisor == 0; }
// ❌ 捕获的是 this 指针,不是 divisor
);
}
private:
int divisor;
};

正确做法

1
2
3
4
5
6
7
8
9
10
11
12
class Widget {
public:
void addFilter() const {
auto divisorCopy = divisor; // 拷贝数据成员

filters.emplace_back(
[divisorCopy](int value) { // ✓ 显式捕获副本
return value % divisorCopy == 0;
}
);
}
};

Item 32: 使用初始化捕获将对象移入闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C++14: 初始化捕获
auto pw = std::make_unique<Widget>();

auto func = [pw = std::move(pw)] { // 移动进闭包
return pw->isValidated() && pw->isArchived();
};

// C++11: 使用 bind 模拟
auto func = std::bind(
[](const std::unique_ptr<Widget>& pw) {
return pw->isValidated() && pw->isArchived();
},
std::make_unique<Widget>()
);

Item 33-34: Lambda 与 std::function

优先使用 auto 而非 std::function

1
2
3
4
5
// std::function
std::function<bool(int)> func1 = [](int x) { return x > 0; };

// auto
auto func2 = [](int x) { return x > 0; };

性能对比

特性 std::function auto
内存 固定大小,可能堆分配 闭包大小
内联 几乎不可能 容易内联
性能

第七章:并发API

Item 35: 优先使用基于任务而非基于线程的编程

1
2
3
4
5
6
7
// 基于线程
int doAsyncWork();
std::thread t(doAsyncWork); // ❌ 无法获取返回值

// 基于任务
auto fut = std::async(doAsyncWork); // ✓ 可以获取返回值
auto result = fut.get();

std::async 的优势

  • 自动管理线程
  • 可以获取返回值
  • 可以传播异常
  • 避免过度订阅

Item 36-40: 并发编程最佳实践

使用 std::atomic 而非 volatile

1
2
3
4
5
6
7
8
9
10
11
// ❌ volatile 不提供原子性
volatile int counter = 0;
void increment() {
++counter; // 不是原子的!
}

// ✓ atomic 提供原子性
std::atomic<int> counter(0);
void increment() {
++counter; // 原子操作
}

避免在 std::atomic 上使用复制操作

1
2
3
4
5
6
7
std::atomic<int> x(0);
auto y = x; // ❌ 错误:deleted
auto y = x.load(); // ✓ 显式读取

std::atomic<int> z(0);
z = x; // ❌ 错误:deleted
z.store(x.load()); // ✓ 显式存储

第八章:微调

Item 41: 对于可拷贝的形参,当移动成本低且总会被拷贝时,考虑按值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Widget {
public:
// 方案1: 重载左值和右值
void setName(const std::string& newName) {
name = newName;
}
void setName(std::string&& newName) {
name = std::move(newName);
}

// 方案2: 万能引用
template<typename T>
void setName(T&& newName) {
name = std::forward<T>(newName);
}

// 方案3: 按值传递(在某些情况下最优)
void setName(std::string newName) {
name = std::move(newName);
}

private:
std::string name;
};

性能对比

调用方式 重载 万能引用 按值传递
左值 1次拷贝 1次拷贝 1次拷贝+1次移动
右值 1次移动 1次移动 1次移动+1次移动

Item 42: 考虑使用置入而非插入

1
2
3
4
5
6
7
8
9
10
11
std::vector<std::string> vs;

// 插入
vs.push_back("xyzzy"); // 创建临时对象,然后移动

// 置入
vs.emplace_back("xyzzy"); // 直接在容器中构造,避免临时对象

// 性能对比
vs.push_back(std::string(50, 'x')); // 1个临时对象
vs.emplace_back(50, 'x'); // 无临时对象,直接构造

何时使用置入

  • 值被构造进容器,而非赋值
  • 传递的参数类型与容器元素类型不同
  • 容器不太可能拒绝新值(如 set)

总结

核心要点总结

graph TD
    A[Effective Modern C++] --> B[类型推导]
    A --> C[auto]
    A --> D[移动语义]
    A --> E[智能指针]
    A --> F[Lambda]
    A --> G[并发]
    
    B --> B1[模板推导3种情况]
    B --> B2[decltype陷阱]
    
    C --> C1[优先使用auto]
    C --> C2[注意代理类]
    
    D --> D1[move无条件转右值]
    D --> D2[forward条件转发]
    D --> D3[完美转发失败情况]
    
    E --> E1[unique_ptr独占]
    E --> E2[shared_ptr共享]
    E --> E3[weak_ptr观察]
    
    F --> F1[避免默认捕获]
    F --> F2[初始化捕获]
    F --> F3[优先auto]
    
    G --> G1[基于任务]
    G --> G2[atomic不是volatile]