介绍了三个现代 C++ 的实用工具或功能。下面是对每部分的解释和总结:
1. Auto()
宏
- 用途:在作用域结束时自动执行清理代码,类似于 RAII 的机制。
- 功能:你可以定义一段代码,在作用域结束时自动执行(不需要手动调用)。
- 典型应用场景:
- 自动关闭文件
- 自动解锁 mutex
- 自动释放资源(比如临时分配的内存、数据库连接等)
Auto([]{ std::cout << "Leaving scope...\n"; });
这段代码就会在作用域结束时自动输出一句话。
2. make_iterable()
和 iterator_range
- 用途:将一对迭代器(begin 和 end)转换成类似容器的对象,可以用于范围-based for 循环。
- 解决的问题:
- 通常,
std::pair<iterator, iterator>
不能直接用于范围 for 循环。 - 使用
make_iterable(begin, end)
可以更方便地遍历子范围。
- 通常,
auto v = std::vector<int>{1, 2, 3, 4, 5};
for (auto i : make_iterable(v.begin() + 1, v.end() - 1)) {std::cout << i << " ";
}
// 输出: 2 3 4
3. std::spaceship
操作符(三路比较符)
- 用途:对两个对象进行统一的比较,自动生成
<
,==
,>
,!=
,<=
,>=
等操作符。 - 特点:
- 使用
<=>
运算符,称为 三路比较运算符。 - 可以自定义返回值(比如
std::strong_ordering
,std::partial_ordering
等) - 在结构体中一行定义即可取代所有比较函数。
- 使用
struct Point {int x, y;auto operator<=>(const Point&) const = default;
};
这个例子中,Point 自动支持所有比较操作。
总结对照表:
功能名 | 用途 | 主要优点 |
---|---|---|
Auto() 宏 | 在作用域结束时自动运行清理代码 | 简化 RAII 风格,临时代码更方便 |
make_iterable() | 将一对迭代器包装为可遍历范围 | 支持范围 for 循环遍历子范围 |
<=> (spaceship) | 自动生成比较运算符 | 简洁高效,统一对象间的比较逻辑 |
这是关于 C++ 中作用域自动清理的一种改进方法,使用 Auto()
宏 来代替传统的手动清理逻辑,比如 OnScopeExit()
。
初始问题(传统做法)
void Mutate(State *state)
{state->DisableLogging(); // 关闭日志state->AttemptOperation(); // 执行某操作state->AttemptDifferentOperation(); // 执行另一操作state->EnableLogging(); // 恢复日志return;
}
问题:
- 如果
AttemptOperation()
或AttemptDifferentOperation()
抛出异常,EnableLogging()
就永远不会被执行。 - 手动恢复状态容易遗漏,尤其是代码路径复杂时。
改进方向:使用作用域清理机制(RAII)
使用像 Auto()
宏或 OnScopeExit()
这样的机制,可以确保在作用域退出时自动执行代码。
理想目标:改写为自动恢复的风格
void Mutate(State *state)
{state->DisableLogging();Auto([&] { state->EnableLogging(); }); // 作用域结束时自动启用日志state->AttemptOperation();state->AttemptDifferentOperation();return;
}
这个 Auto()
的行为:
- 作用域开始:注册一段清理代码(lambda)
- 作用域结束(正常 return 或异常):自动执行该 lambda
类似于:
struct OnExit {std::function<void()> func;~OnExit() { func(); }
};
这样做的优点:
优点 | 描述 |
---|---|
更安全 | 不怕中途 return 或异常跳过清理代码 |
更清晰 | 清理逻辑靠近资源分配逻辑,语义清楚 |
更简洁 | 避免写繁琐的 try/catch 或手动清理代码 |
RAII 风格一致 | 与 C++ 的资源管理惯例一致 |
你现在展示的是一个 改进版的作用域退出执行机制 ——使用 Auto(...)
宏,在作用域结束时自动执行一段代码(无论是正常返回、错误返回,还是抛出异常),从而确保资源释放或状态恢复不会遗漏。
原始代码的问题(没有清理)
bool Mutate(State *state)
{state->DisableLogging(); // 日志被关闭if (!state->AttemptOperation()) return false; // 可能提前 returnif (!state->AttemptDifferentOperation()) return false;state->EnableLogging(); // 可能不会执行!return true;
}
如果中途 return false
,EnableLogging()
就不会执行 —— 逻辑错误。
想要的写法(使用 Auto()
宏)
bool Mutate(State *state)
{state->DisableLogging();Auto(state->EnableLogging()); // 确保作用域退出时自动恢复日志if (!state->AttemptOperation()) return false;if (!state->AttemptDifferentOperation()) return false;return true;
}
无论函数是走完逻辑还是中途返回/异常,都保证执行 state->EnableLogging()
,从而让代码更安全、清晰。
实现分析
AtScopeExit
类
template <class Lambda> class AtScopeExit {Lambda& m_lambda;
public:AtScopeExit(Lambda& action) : m_lambda(action) {}~AtScopeExit() { m_lambda(); }
};
- 用于在作用域退出时执行传入的 lambda。
- RAII 原则:构造时捕获清理动作,析构时自动调用。
Auto()
宏展开过程(简化理解)
Auto(state->EnableLogging());
最终变成:
auto Auto_func_42 = [&]() { state->EnableLogging(); };
AtScopeExit<decltype(Auto_func_42)> Auto_instance_42(Auto_func_42);
__COUNTER__
确保生成的变量名唯一,防止宏冲突。Auto()
用于注入作用域清理代码。
总结优点
优点 | 描述 |
---|---|
安全 | 不会遗漏清理动作,无论发生什么提前退出 |
简洁 | 一行代码写清清理逻辑 |
通用性强 | 可用于文件关闭、锁释放、状态还原等场景 |
不依赖 C++17 | 在 C++11 及以上即可使用 |
如你所说,异常控制流也存在同样问题,而 Auto() 机制能一致地解决所有“作用域退出”问题,推荐用于关键资源的管理。 | |
是否还需要我: |
- 举例更多使用场景?
- 帮你实现跨多个资源清理?
- 对比 C++17 的
std::scope_exit
?
你展示的是一个用于 自动作用域退出清理 的 C++ 宏和模板机制 —— 本质是手动实现了 scope guard
(类似于 C++17 的 std::scope_exit
),并通过宏包装使使用更简洁。
目标:让你能写出一行这样的代码
Auto(state->EnableLogging());
自动在作用域结束时执行 state->EnableLogging()
,无论是正常退出还是提前 return
或抛出异常。
宏和类组件解析
① AtScopeExit
模板类
template <class Lambda>
class AtScopeExit {Lambda& m_lambda;
public:AtScopeExit(Lambda& action) : m_lambda(action) {}~AtScopeExit() { m_lambda(); } // 自动在作用域结束时执行 lambda
};
- 持有 lambda 引用,在析构时执行它
- 实现的是 RAII(Resource Acquisition Is Initialization)模式
- 核心行为:作用域结束 = 执行 cleanup 逻辑
② 宏魔法层层展开
Auto(...)
最终展开成:
Auto(...)
// expands into
Auto_INTERNAL2(__COUNTER__, ...)
// expands into
Auto_INTERNAL1(Auto_func_42, Auto_instance_42, ...)
// expands into:
auto Auto_func_42 = [&]() { ... };
AtScopeExit<decltype(Auto_func_42)> Auto_instance_42(Auto_func_42);
每一层负责一部分功能:
宏 | 功能 |
---|---|
TOKEN_PASTE(x, y) | 拼接 x##y ,生成唯一变量名 |
Auto_INTERNAL1(...) | 声明 lambda 和 AtScopeExit 对象 |
Auto_INTERNAL2(...) | 用 __COUNTER__ 自动生成唯一 ID |
Auto(...) | 用户接口,一行搞定 |
优点总结
特性 | 描述 |
---|---|
安全 | 避免忘记清理资源或状态恢复 |
通用 | 可用于锁、文件、事务、状态切换等 |
简洁 | 一行代码,无需显式手动 try/finally 或 catch |
无依赖 | 不依赖 C++17,兼容 C++11 |
宏唯一性 | __COUNTER__ 保证多个 Auto() 调用间不冲突 |
使用场景示例
void doSomething() {lock.lock();Auto(lock.unlock()); // 自动释放锁file.open();Auto(file.close()); // 自动关闭文件timer.start();Auto(timer.stop()); // 自动停止计时器
}
如果你使用 C++17 或更新标准,也可以使用标准库的:
#include <scope>
std::scope_exit guard{ [&] { state->EnableLogging(); } };
不过当前这种 Auto(...)
宏实现非常轻量、兼容性好,适用于 C++11 项目。
如果你还想:
- 加强这个宏对异常的捕捉能力?
- 为
Auto(...)
添加调试输出? - 抽象出多个资源同时清理?
完整地解释这一宏的全部展开过程,逐步拆解你写的:
#define Auto(...) Auto_INTERNAL2(__COUNTER__, __VA_ARGS__)
宏定义流程逐层解释
这是一组用于作用域退出自动执行的宏展开链。以下是从 Auto(...)
开始的完整过程:
用户写:
Auto(state->EnableLogging());
② 展开:
Auto_INTERNAL2(__COUNTER__, state->EnableLogging());
假设 __COUNTER__
当前是 42
,那么变成:
Auto_INTERNAL2(42, state->EnableLogging());
③ Auto_INTERNAL2
定义是:
#define Auto_INTERNAL2(ctr, ...) \Auto_INTERNAL1(TOKEN_PASTE(Auto_func_, ctr), \TOKEN_PASTE(Auto_instance_, ctr), __VA_ARGS__)
代入 ctr = 42
:
Auto_INTERNAL1(Auto_func_42, Auto_instance_42, state->EnableLogging());
④ Auto_INTERNAL1
定义是:
#define Auto_INTERNAL1(lname, aname, ...) \auto lname = [&]() { __VA_ARGS__; }; \AtScopeExit<decltype(lname)> aname(lname);
代入 lname = Auto_func_42
, aname = Auto_instance_42
, __VA_ARGS__ = state->EnableLogging()
:
auto Auto_func_42 = [&]() { state->EnableLogging(); };
AtScopeExit<decltype(Auto_func_42)> Auto_instance_42(Auto_func_42);
最终展开的 C++ 代码:
auto Auto_func_42 = [&]() { state->EnableLogging(); };
AtScopeExit<decltype(Auto_func_42)> Auto_instance_42(Auto_func_42);
这两行在当前作用域中创建了一个 lambda 和其管理对象。当当前作用域结束时(不管是正常 return、异常还是其他),Auto_instance_42
析构,自动调用 state->EnableLogging()
。
全套宏定义总结(完整)
#pragma once
template <class Lambda>
class AtScopeExit {Lambda& m_lambda;
public:AtScopeExit(Lambda& action) : m_lambda(action) {}~AtScopeExit() { m_lambda(); }
};
// 连接两个 token
#define TOKEN_PASTEx(x, y) x ## y
#define TOKEN_PASTE(x, y) TOKEN_PASTEx(x, y)
// 生成作用域退出对象
#define Auto_INTERNAL1(lname, aname, ...) \auto lname = [&]() { __VA_ARGS__; }; \AtScopeExit<decltype(lname)> aname(lname);
// 使用 __COUNTER__ 保证唯一性
#define Auto_INTERNAL2(ctr, ...) \Auto_INTERNAL1(TOKEN_PASTE(Auto_func_, ctr), \TOKEN_PASTE(Auto_instance_, ctr), __VA_ARGS__)
// 用户使用的简化宏
#define Auto(...) Auto_INTERNAL2(__COUNTER__, __VA_ARGS__)
让我们逐条深入理解这些概念,它们是构建 Auto()
宏的关键组成部分:
核心技术解说
1. Variadic macros & __VA_ARGS__
(可变参数宏)
#define LOG(...) printf(__VA_ARGS__)
__VA_ARGS__
允许宏接受不定数量的参数。- 在
Auto(...)
宏中,它传入要在作用域结束时执行的任意代码语句(如state->EnableLogging();
)。
用途:构建灵活、复用性高的宏。
2. Token pasting (##
)
#define COMBINE(x, y) x ## y
- 将两个 token 拼接为一个标识符。例如:
COMBINE(Auto_func_, 42)
→Auto_func_42
- 用于生成唯一变量名,避免命名冲突。
用途:宏生成局部变量名时防止重复。
3. Templates
template <typename Lambda>
class AtScopeExit {Lambda& m_lambda;~AtScopeExit() { m_lambda(); }
};
- 泛型类允许我们在编译时确定
lambda
类型。 - 相比虚函数,模板无运行时开销(零开销抽象)。
用途:为Auto()
提供泛型作用域结束行为执行器。
4. Lambdas (C++11)
auto f = [&]() { ... };
- 匿名函数对象,可捕获作用域变量。
- 在
Auto()
中,lambda 捕获局部上下文,确保代码在作用域结束时执行。
用途:简洁、高效的行为表达方式。
5. __COUNTER__
(非标准,但广泛支持)
- 每次展开宏时生成一个递增的整数字面量。
__COUNTER__ // → 0, 1, 2, ...
- 与 token pasting 结合可生成唯一变量名。
用途:自动避免宏多次展开导致的命名冲突。
6. #pragma once
(非标准,但实际标准)
#pragma once
- 防止头文件被多重包含。
- 非标准,但被所有主流编译器支持(GCC、Clang、MSVC)。
用途:更简洁的 include guard 替代方式。
风格问题(Style Points)
7. “Aren’t macros evil?”
Yes, but context matters.
- 宏缺少类型检查和作用域控制,会导致调试困难。
- 但像
Auto()
这种“局部作用域 + 宏生成 + 模板”组合,是 “好宏”的范例:- 不污染全局命名空间。
- 没有副作用或隐藏控制流。
- 在编译期展开并优化。
最佳实践:谨慎使用宏,能用constexpr
或模板就别用宏。但Auto()
这样功能清晰且不可替代的用途是 OK 的。
8. Why lambdas instead of std::function
?
Lambda 更轻量,性能更高。
std::function<void()>
是类型擦除,需要动态分配(在某些实现中),开销较大。- Lambda 可在编译期内联,没有额外开销。
AtScopeExit
只需要存储 lambda 本身,不需要多态性 → 模板就足够。
结论:使用 lambda 更快、更轻、更符合用途,尤其在作用域控制工具中非常合适。
总结
技术 | 用途 |
---|---|
__VA_ARGS__ | 灵活支持任意数量代码语句 |
## | 动态生成变量名,防止冲突 |
模板 | 泛型支持不同 lambda 类型,零开销抽象 |
Lambda | 高效、简洁地封装作用域结束动作 |
__COUNTER__ | 为变量生成唯一编号 |
#pragma once | 替代 include guard |
风格观点 | 此场景下宏设计是安全且清晰的 |
提供的内容讨论了两个非常实用但“非标准”的编译器特性:__COUNTER__
和 #pragma once
,并顺带解释了“幂等性(idempotence)”的含义。我们逐一理解:
__COUNTER__
的含义与用途
#define UNIQUE_NAME(x) x##__COUNTER__
__COUNTER__
是一个 每次展开都会递增的整数宏。- 非标准,但 所有主流编译器(如 GCC、Clang、MSVC)都支持。
- 主要用于 宏展开中自动生成唯一变量名,防止命名冲突。
例如:
int var_##__COUNTER__; // 展开为:int var_0;
int var_##__COUNTER__; // 展开为:int var_1;
你提到 __LINE__
作为替代:
确实,它也是预定义宏,每次展开为当前行号。
但如果多个宏定义在同一行,则 __LINE__
无法生成唯一标识符,而 __COUNTER__
可以 → 所以不等价也不可靠。
#pragma once
与幂等性
#pragma once
是防止头文件被多次包含的最简单方式。- 它使文件具有 幂等性(idempotence):无论包含多少次,只会生效一次。
替代方式是传统的:
#ifndef MY_HEADER_H
#define MY_HEADER_H
// content...
#endif
这种方式虽然标准,但容易出错(特别是宏名重复),也更冗长。
#pragma once
优点:
- 简洁
- 不需要命名唯一宏
- 编译器能更快处理
你提到:
“它是非标准的,但我说它是每个编译器都支持的。”
这是一个很常见的技术现实 —— 尽管不是 C++ 标准文档的一部分,但所有主流实现都支持,实际上已成“事实标准”。
幂等性(Idempotence)的定义与解释
“一个函数 f 是幂等的,当 f(f(x)) = f(x) 对所有 x 成立。”
- 在软件工程中,幂等性常用于描述 多次调用的副作用是否相同。
- 对于头文件,如果多次包含它的结果和只包含一次一样,那就具有幂等性。
#pragma once
保证头文件只处理一次 → 是构建幂等头文件的最有效方式。
小结
特性 | 是什么 | 是否标准 | 是否安全用 |
---|---|---|---|
__COUNTER__ | 编译期间递增的整数宏,用于唯一命名 | 是“事实标准” | |
#pragma once | 确保头文件只包含一次,实现幂等性 | 非常推荐 | |
幂等性 | 多次调用等价于一次调用 | 数学概念 | 理论支撑 |
如你想进一步了解: |
__COUNTER__
在大型宏系统中的实际应用(如测试框架)#pragma once
与 include guard 的性能对比- 或是幂等性在 REST API / 数据库操作中的作用
你正在理解一个非常典型的 性能优化决策:为什么 Auto()
宏背后的 AtScopeExit
类没有用 std::function
。
让我们一条条解释清楚:
问题核心:为什么不用 std::function
?
先看两种实现的区别:
我们使用的轻量实现:
template <class Lambda>
class AtScopeExit {Lambda& m_lambda;
public:AtScopeExit(Lambda& action) : m_lambda(action) {}~AtScopeExit() { m_lambda(); }
};
- 不依赖
<functional>
。 - 不涉及类型擦除。
- 没有动态分配(heap allocation)。
- 完全零成本抽象,编译器能 inline 展开整个 lambda 逻辑。
用 std::function<void(void)>
会怎样?
#include <functional>
class AtScopeExit {std::function<void()> m_lambda;
public:template <class Lambda>AtScopeExit(Lambda& action) : m_lambda(action) {}~AtScopeExit() { m_lambda(); }
};
为何不选它?理由如下:
🔹 1. 重量级头文件 <functional>
std::function
需要引入<functional>
,它本身非常庞大,带来不必要的编译时间和依赖开销。auto.h
被频繁包含、甚至被生成工具包含 → 需要保持轻量,避免拖慢整个构建系统。
🔹 2. 类型擦除 + 可能的堆分配
std::function
采用 类型擦除(type erasure)机制,把任意可调用对象封装成一个统一的函数对象。- 这通常意味着 内部堆分配(heap allocation),尤其是在闭包很大或不能放进 small-buffer-optimization 时。
这种开销在小函数作用域中使用时是 完全不值得的,而 lambda 原生形式在编译期就能优化为直接调用。
🔹 3. 不可 inline 展开,影响性能
std::function
的调用是虚拟调用(或函数指针间接调用)。- 编译器无法 inline 内部逻辑,导致性能下降。
反之,模板方式保留了具体类型信息,编译器可以把 lambda 完全 inline 展开,优化得更彻底(零开销)。
经验数据支持这个决策(即:“我们看到汇编更好”)
“Empirically, we get better code this way.”
他们查看了汇编输出,发现:
- 使用
std::function
:多了堆分配、间接调用,调用开销大。 - 使用模板 + lambda 引用:代码更短、更快,全程 inline。
小结:为什么不用 std::function
原因 | 解释 |
---|---|
1. 编译依赖大 | <functional> 是重量级头文件,会拖慢编译速度 |
2. 类型擦除和堆分配 | std::function 可能动态分配内存,带来运行时开销 |
3. 无法内联 | 编译器无法展开 std::function 中的 lambda,性能较差 |
4. 实际测试更慢 | 汇编验证显示模板写法生成的机器码更紧凑、更快 |
5. 要保持宏轻量 | Auto() 是通用宏,需要可插拔、轻量、高效 |
代码回顾:
#include <stdio.h>
#include "auto.h" // 假设提供了 Auto(...) 宏
extern void foo(); // 声明外部函数 foo()
int main() {if (true) {Auto(puts("two"));puts("one"); // 编译器知道这不会抛出异常}if (true) {Auto(puts("three"));foo(); // 可能抛出异常}
}
分析:
1. Auto(...)
是什么?
这是一个宏,看起来像是模拟 RAII(Resource Acquisition Is Initialization) 风格,用于自动执行某段代码,通常是在作用域结束时。
这类似于 C++ 的 defer
模拟,或者 Go 语言里的 defer
语句。C 本身不支持这一特性,但很多实现里通过宏和 __attribute__((cleanup))
机制或构造函数/析构函数模拟这一行为。
假设用途: Auto(expr)
会在当前作用域退出时执行 expr
。但这里,它看起来是立即执行的。
例如:
Auto(puts("two")); // 马上打印 "two",并注册一个作用域结束时的清理动作?
2. 实际执行顺序:
if (true) {Auto(puts("two")); // 立即打印 "two"puts("one"); // 然后打印 "one"
}
// 输出顺序:
// two
// one
if (true) {Auto(puts("three")); // 打印 "three"foo(); // 如果 foo 抛出异常(如 longjmp、signal),Auto 注册的清理动作可能不执行
}
注意这里对异常的含义较宽泛:C 本身没有异常处理,但某些 C 代码会使用 setjmp
/longjmp
、信号、甚至是某种内嵌 C++ 异常机制。
总结:
Auto(...)
可能是一个自定义的宏,模仿 RAII,用于作用域结束时自动执行某些操作。puts("two")
和puts("three")
是立即执行的,但也可能被注册为作用域退出时执行的动作(看具体宏定义)。- 如果
foo()
抛出异常或导致非正常跳出,Auto 注册的代码可能不会被执行,除非宏做了异常安全处理。
离开作用域执行
当然,我们全面解释一遍你提到的 std::function
的底层实现机制,以及为什么它性能较差。
std::function
是什么?
std::function<void()>
是一个可以存储任意可调用对象(函数指针、Lambda、函数对象、绑定表达式等)并调用它的通用类型。
它的本质是通过 类型擦除(Type Erasure) 实现的。
类型擦除的实现方式
你给出的示例代码基本就是 std::function
的简化实现版本。我们来逐步理解它的实现思想:
1. 一个抽象的接口 ContainerBase
struct ContainerBase {virtual void perform() = 0;virtual ~ContainerBase() = default;
};
这是一个抽象基类,用于统一接口,隐藏具体 Lambda 类型。
2. 对任意类型的 Lambda 进行包装
template <class Lambda>
struct Container : ContainerBase {Lambda m_lambda;Container(Lambda&& lambda) : m_lambda(std::move(lambda)) {}void perform() override { m_lambda(); }
};
这个模板类可以包装任何类型的 Lambda,继承 ContainerBase
,并实现了虚函数 perform()
。
3. 类似 std::function<void()>
的封装类
class function {ContainerBase* m_ctr;
public:template<class Lambda>function(Lambda lambda): m_ctr(new Container<Lambda>(std::move(lambda))) {}void operator()() { m_ctr->perform(); }~function() { delete m_ctr; }
};
- 构造时进行 类型擦除:不再关心 Lambda 的真实类型。
- 调用时通过
virtual
来执行perform()
。 - 内部使用
new
分配内存存放 Lambda。
为什么性能差?
这段代码展示了 std::function
性能瓶颈的 3 大核心来源:
1. 虚函数调用(Virtual Dispatch)
m_ctr->perform();
- 是一个间接调用,不能被内联。
- 会导致 CPU 分支预测失败,降低执行效率。
2. 堆内存分配(Heap Allocation)
new Container<Lambda>(...)
- 每次构造
function
时,都在堆上分配一个包装对象。 - 内存分配/释放很慢,尤其在频繁创建销毁的场景下。
3. 移动构造 Lambda
std::move(lambda)
- 虽然是“移动”,但也会调用 Lambda 的移动构造函数,进而移动其所有捕获变量。
- 如果 Lambda 捕获较多东西,移动成本仍然存在。
🛠 小函数优化(Small Function Optimization, SFO)
有些 std::function
实现会做小对象优化,即:
- 如果 Lambda 很小(比如 1-2 个指针大小),就直接存在
std::function
内部,不使用堆内存。 - 这样可以避免 heap allocation。
但并不是所有实现都有 SFO,或启用条件不同。
更高效的替代方案
你还提到了其他更高效的替代方式:
1. Alexandrescu 的 ScopeGuard
template<typename F>
struct ScopeGuard {F func;ScopeGuard(F f) : func(std::move(f)) {}~ScopeGuard() { func(); }
};
- 没有虚函数
- 没有堆内存
- 完全内联,高效!
用法:
auto guard = ScopeGuard([]{ puts("cleaning up"); });
2. Boost.ScopeExit / Google scope_exit
这些库提供更方便的宏或语法糖实现,比如:
BOOST_SCOPE_EXIT(...) { /* 资源释放代码 */ } BOOST_SCOPE_EXIT_END
或 C++17:
auto cleanup = gsl::finally([] { std::cout << "done\n"; });
这些方式都是基于模板、无虚函数、无堆分配,更适合资源管理、性能敏感的场景。
总结表格
特性 | std::function | ScopeGuard / finally |
---|---|---|
支持任意类型的函数 | ||
虚函数调用 | ||
堆内存分配 | ||
可内联(inline) | ||
适合频繁调用/构造 | ||
性能 | 一般 | 高 |
“Type Erasure in a nutshell”:
Type Erasure(类型擦除)简明理解
背景:
C++ 是静态类型语言,通常要求在编译时就知道对象的具体类型。但有时我们想要写出可以处理“任意类型”对象的代码,比如:
std::function
std::any
std::variant
typeid
/void*
等用途
为了实现这种“隐藏真实类型”的能力,我们引入了 类型擦除技术(Type Erasure)。
第一步:使用模板类封装任意类型对象
template<typename T>
class Container {T captured_object;
};
这个类可以持有任何类型的对象,比如:
Container<int> ci; // 存 int
Container<std::string> cs; // 存 string
Container<MyClass> cm; // 存自定义类型
问题:
虽然 Container<T>
是泛型的,但它每个类型都不同(Container<int>
≠ Container<std::string>
),我们不能把这些放进一个容器或指针中统一处理。
目标:创建一个可以“处理任意类型”的统一接口
比如:
std::vector<TypeErasedObject> v;
v.push_back(42);
v.push_back("hello");
v.push_back(MyClass());
第二步:为任意 Container 提供统一接口(通过继承虚函数)
你可以这么做:
struct ContainerBase {virtual void perform() = 0;virtual ~ContainerBase() = default;
};
template<typename T>
class Container : public ContainerBase {T captured_object;
public:Container(T obj) : captured_object(std::move(obj)) {}void perform() override {// 调用 T 的接口,如 operator()、print()、serialize() 等captured_object(); // 假设 T 是个可调用对象}
};
这时,虽然 Container<T>
是模板类,但它们都可以被转换成 ContainerBase*
指针:
ContainerBase* p1 = new Container<int>(42); // ok
ContainerBase* p2 = new Container<MyLambda>(...); // ok
你就完成了类型擦除的关键:
编译时知道类型 → 运行时隐藏类型
通过:
- 模板封装具体类型
- 虚函数统一访问接口
- 基类指针存储/调用
总结一句话
“类型擦除”就是:使用模板捕获任何类型,然后通过指向基类的指针在运行时操作它。
如果你想更深入了解:
std::function
就是这个套路的完整演示(可调用类型擦除)std::any
用类似原理做任意类型持有(带 RTTI)std::shared_ptr<void>
也利用了部分类型擦除思想
#pragma once
#include <utility>
struct ContainerBase {virtual void perform() = 0;virtual ~ContainerBase() = default;
};
template <class Lambda>
struct Container : ContainerBase {Lambda m_lambda;Container(Lambda&& lambda) : m_lambda(std::move(lambda)) {}virtual void perform() { m_lambda(); }
};
class function { // equivalent to std::function<void(void)>ContainerBase* m_ctr;
public:template <class Lambda>function(Lambda lambda) : m_ctr(new Container<Lambda>(std::move(lambda))) {}void operator()() { m_ctr->perform(); }~function() { delete m_ctr; }
};
目标回顾
我们想要一个 function
类,能够像 std::function<void()>
一样:
- 存储任意可调用对象(比如 lambda、函数、函数对象等)
- 调用统一接口
operator()()
- 释放时能正确析构
关键组件逐个解释
ContainerBase
:虚基类接口
struct ContainerBase {virtual void perform() = 0;virtual ~ContainerBase() = default;
};
作用: 所有持有对象都将派生自它,提供统一的 perform()
接口。
Container<T>
:包装任何可调用对象的模板类
template <class Lambda>
struct Container : ContainerBase {Lambda m_lambda;Container(Lambda&& lambda) : m_lambda(std::move(lambda)) {}virtual void perform() override { m_lambda(); }
};
说明:
- 这是一个模板类,支持包装任意类型
Lambda
(只要它是 callable) - 调用
perform()
时,它就执行m_lambda()
—— 即用户给的 lambda 或函数对象 m_lambda
是通过 移动构造 接收的,提高性能
function
:类型擦除封装器(相当于 std::function<void()>
)
class function {ContainerBase* m_ctr;
public:template<class Lambda>function(Lambda lambda): m_ctr(new Container<Lambda>(std::move(lambda))) {}void operator()() { m_ctr->perform(); }~function() { delete m_ctr; }
};
功能完整实现:
成员 | 作用 |
---|---|
m_ctr | 指向 ContainerBase ,实际类型为 Container<Lambda> |
构造函数 | 接收任意 Lambda,并用 new 创建对应 Container<Lambda> 实例 |
operator()() | 调用 perform() ,等效于执行原始 lambda |
析构函数 | 删除容器,释放内存 |
举个例子:
function f = [] { puts("Hello Type Erasure!"); };
f(); // 输出:Hello Type Erasure!
运行流程:
[] { puts(...) }
被作为Lambda
捕获new Container<Lambda>(...)
构建实际容器m_ctr->perform()
实际调用 lambda
类型擦除的关键要素(总结)
元素 | 解释 |
---|---|
模板类 | 捕获任意类型(如 Container<T> ) |
虚基类 | 提供统一接口(如 ContainerBase::perform ) |
基类指针 | 存储实际对象(如 ContainerBase* ) |
动态分配 | 避免模板类型“污染”使用方,靠堆上存储 |
虚函数 | 实现运行时行为调用 |
注意点:
- 每次使用
function
都会 堆分配 一个新对象 → 性能低于lambda
- 每次调用都要 虚函数调度 → 比内联调用慢
- 适用于需要 类型擦除 的场景,如:回调、事件注册、异步任务等
和 std::function
的关系
std::function<void()>
的实现原理和这个 function
几乎一样,只是:
std::function
还做了 Small Buffer Optimization(小对象不堆分配)std::function
支持拷贝构造、赋值、空检查等
总结一句话
这段代码就是一个最小实现的
std::function<void()>
,它使用类型擦除技术,让你能把“任意可调用对象”封装为一个可以调用的统一接口。
使用 std::function
或类似类型擦除方案时涉及的性能成本
1. 虽然 std::move
在运行时是零成本(只是一个类型转换),但它在编译时会触发复杂的模板元编程逻辑,比如:
std::remove_reference
std::is_rvalue_reference
std::decay
std::enable_if
这些模板机制对编译器是有一定负担的,尤其在大项目中可能导致编译时间变慢。
2. 类型擦除实现用到了虚函数(如 virtual void perform()
),这会导致:
- 通过 vtable(虚函数表) 进行函数调用
- 这种调用方式相比普通函数指针或内联代码,慢一些
- 特别在频繁调用时,性能差异明显(如高性能场景:图形、音频、计算)
大多数std::function
实现,在类型擦除时需要: new
一个Container<Lambda>
对象- 这会在堆上动态分配内存,速度慢 + 容易内存碎片
但如果你传入的lambda
很小,比如只捕获一个int
引用,那么: - 编译器可能使用 “小对象优化” (SBO, Small Buffer Optimization)
- 即在
std::function
对象内部直接存储 lambda,避免堆分配
这就像std::string
的小字符串优化:短字符串不在堆上分配。
3. 你传给 std::function
的 lambda
:
auto f = [x, &y] { ... };
在封装成 function
或 Container<Lambda>
时,会发生:
- Lambda 的 移动构造
- 所有捕获的变量也会随着一起被移动
如果你捕获的是一个大的对象(如:大数组、大字符串等),移动可能代价高。
但如果你是按引用捕获(如&x
),那么: - 移动构造成本几乎为 0
- 因为只是复制一个指针
总结(:
| ------------------------------- |
| std::move
编译期会触发复杂模板机制,影响编译速度 |
| 虚函数表调度导致运行时性能开销 |
| 堆内存分配开销大,可能影响性能 |
| 如果 lambda 很小,可用小对象优化,避免堆分配 |
| 封装 lambda 时一定会 move 构造,影响捕获变量 |
| 如果捕获的是引用,move 成本很低 |
template <class Lambda>
class AtScopeExit {Lambda& m_lambda;
public:AtScopeExit(Lambda& action) : m_lambda(action) {}~AtScopeExit() { m_lambda(); }
};
#define TOKEN_PASTEx(x, y) x##y
#define TOKEN_PASTE(x, y) TOKEN_PASTEx(x, y)
#define Auto_INTERNAL1(lname, aname, ...) \auto lname = [&]() { __VA_ARGS__; }; \AtScopeExit<decltype(lname)> aname(lname);
#define Auto_INTERNAL2(ctr, ...) \Auto_INTERNAL1(TOKEN_PASTE(Auto_func_, ctr), TOKEN_PASTE(Auto_instance_, ctr), __VA_ARGS__)
#define Auto(...) Auto_INTERNAL2(__COUNTER__, __VA_ARGS__)
C++ 类 MDTable
,它代表了一个表格结构(可能类似数据库的表),其中的“列”和“键”是通过原始指针存储的。
这段代码定义了什么?
class MDTable {MDColumn* m_columns;MDKey* m_keys;int m_columnCount;int m_keyCount;
public:MDColumn* GetColumns() const { return m_columns; }int GetNumColumns() const { return m_columnCount; }MDIndex* GetKeys() const { return m_keys; }int GetNumKeys() const { return m_keyCount; }
};
成员变量说明:
成员变量 | 含义 |
---|---|
m_columns | 指向 MDColumn 数组的指针(即所有列) |
m_keys | 指向 MDKey 数组的指针(即所有键) |
m_columnCount | 列的数量 |
m_keyCount | 键的数量 |
成员函数说明:
函数 | 功能 |
---|---|
GetColumns() | 返回列数组的指针 |
GetNumColumns() | 返回列的数量 |
GetKeys() | 返回键数组的指针 |
GetNumKeys() | 返回键的数量 |
这类设计为何被称为 “Inside-Out Container”?
“Inside-out container” 是一种设计模式,在这种设计中:
- 容器类本身(这里是
MDTable
)并不自己管理元素(比如使用std::vector
),而是暴露出“裸指针 + 数量”来让使用者自行访问。 - 所有数据都暴露为原始数组,你要通过
GetColumns()
和GetNumColumns()
来访问元素。
这种设计常用于: - 高性能场景(避免 STL 带来的拷贝、构造开销)
- 与 C 接口兼容
- 结构紧凑,占用少量内存
但也有缺点: - 不安全(容易越界)
- 不现代(不支持 range-based for)
- 不好维护(不够封装)
可能的改进方向:make_iterable
为了让这类结构能像现代 C++ 一样支持:
for (auto& col : table.GetColumns()) { ... }
我们可以构建一个工具,比如 make_iterable(begin_ptr, count)
,让 MDTable
像 std::vector
一样支持迭代器。
例如:
template<typename T>
struct IterableFromPointer {T* m_ptr;int m_size;T* begin() const { return m_ptr; }T* end() const { return m_ptr + m_size; }
};
template<typename T>
IterableFromPointer<T> make_iterable(T* ptr, int size) {return {ptr, size};
}
用法就变成这样:
for (auto& col : make_iterable(table.GetColumns(), table.GetNumColumns())) {// 使用 col
}
总结:
项 | 内容 |
---|---|
类名 | MDTable |
核心思想 | 裸数组 + 元素数量 的容器结构 |
特点 | 高效但不安全,不符合现代 C++ 审美 |
可改进点 | 使用 make_iterable 创建迭代器支持,更安全、更现代 |
对比传统 C 风格接口(裸指针 + 计数)和现代 C++ 风格(如 std::vector
和 range-based for
循环)的优缺点,并在此基础上提出了一个更现代、优雅的改进方式:通过包装工具函数如 Columns(tab)
和 Keys(tab)
,让代码更简洁、更安全。
为什么不直接用 std::vector
?
当前的设计(裸指针):
class MDTable {MDKey* m_keys; // 同时保存普通键和外键int m_keyCount; // 键总数int m_firstForeignKey; // 外键在数组中的起始索引
public:MDIndex* GetNormalKeys() const { return m_keys; }int GetNumNormalKeys() const { return m_firstForeignKey; }MDIndex* GetForeignKeys() const { return m_keys + ...; }int GetNumForeignKeys() const { return m_keyCount - ...; }
};
为什么不这样写?
std::vector<MDKey> m_keys;
因为这样设计是出于效率和内存布局考虑:
原因总结:
原因 | 说明 |
---|---|
时间效率 | 裸指针访问和按索引访问没有边界检查,比 std::vector::at() 快 |
空间效率 | 避免了 std::vector 的额外容量(capacity)或元数据开销 |
内存控制 | 可以将所有键(普通键 + 外键)存储为一个连续块,节省分配次数 |
C 接口兼容性 | 裸指针在与 C 函数、旧代码、内存映射文件打交道时更方便 |
所以虽然 std::vector 更方便、更安全,但在一些对性能极度敏感或者需要内存映射/兼容旧代码的系统中,裸指针 + 数量仍被广泛使用。 |
这种用法很笨拙
使用方式如下:
for (int i = 0; i < tab->GetNumColumns(); ++i) {MDColumn& col = tab->GetColumns()[i];// ... 处理 col ...
}
对程序员要求较高,容易越界,写法也冗长。
我们理想中想写的方式:
for (MDColumn& col : Columns(tab)) {// ... 处理 col ...
}
这就是现代 C++ 的风格 —— 使用 range-based for
来遍历容器,无需关心长度和索引细节。
解决方案:自定义可迭代包装器
你可以写一个类似 make_iterable()
的函数,让这些裸指针接口也能用 for (auto& x : ...)
方式遍历。
例如:
template<typename T>
struct PointerRange {T* m_ptr;int m_size;T* begin() const { return m_ptr; }T* end() const { return m_ptr + m_size; }
};
PointerRange<MDColumn> Columns(MDTable* tab) {return {tab->GetColumns(), tab->GetNumColumns()};
}
PointerRange<MDKey> Keys(MDTable* tab) {return {tab->GetKeys(), tab->GetNumKeys()};
}
使用效果就很舒服了:
for (auto& col : Columns(tab)) { ... }
for (auto& key : Keys(tab)) { ... }
总结
项目 | 原始方式 | 现代方式 |
---|---|---|
可读性 | 较差(手写索引) | 极好(range-for) |
安全性 | 易越界 | 可封装检查 |
性能 | 极致性能可控 | 稍有代价(可忽略) |
扩展性 | 差 | 高 |
用一个简单的“可迭代包装器” (iterable
) 把原始的裸指针数组包装起来,从而可以使用 C++11 的范围 for
循环(range-based for loop)来访问数组元素。
逐行解释
#include "iterable.h"
引入一个 iterable
类型和 make_iterable()
函数的定义。这个头文件里大概定义了:
template<typename Iterator>
struct iterable {Iterator m_begin, m_end;Iterator begin() const { return m_begin; }Iterator end() const { return m_end; }
};
template<typename Iterator>
iterable<Iterator> make_iterable(Iterator begin, Iterator end) {return {begin, end};
}
第一段:列(Columns)
static inline iterable<MDColumn*> Columns(MDTable* tab)
{MDColumn* cols = tab->GetColumns();return make_iterable(cols, cols + tab->GetNumColumns());
}
tab->GetColumns()
返回MDColumn*
指针(列数组的起始地址)cols + tab->GetNumColumns()
表示末尾地址(即 [begin, end) 区间)- 把这个范围封装成
iterable<MDColumn*>
,使得你可以这样用:
for (MDColumn* col : Columns(tab)) {// 使用 col
}
第二段:键(Keys)
static inline iterable<MDKey*> Keys(MDTable* tab)
{MDKey* keys = tab->GetKeys();return make_iterable(keys, keys + tab->GetNumKeys());
}
与 Columns
完全同理,用于遍历 MDKey*
指针数组。
总结:目的与好处
目标 | 说明 |
---|---|
简化遍历 | 替代手动 for (int i=0; ...) 索引访问 |
更现代 | 支持范围 for:for (auto x : Columns(tab)) |
更安全 | 减少越界风险 |
更抽象 | 将数组细节封装起来,提高可维护性 |
零开销 | 所有逻辑在编译期展开,不引入额外开销 |
示例用法
void TransformTable(MDTable* tab) {for (MDColumn* col : Columns(tab)) {// 使用 col}for (MDKey* key : Keys(tab)) {// 使用 key}
}
Columns()
和 Keys()
函数,利用了一个叫做 iterable
的包装器(定义在 "iterable.h"
里),把原本的裸指针数组和大小包装成一个支持范围 for
循环遍历的对象。
具体来说:
Columns(tab)
返回一个从tab->GetColumns()
指针开始,到tab->GetColumns() + tab->GetNumColumns()
结束的区间包装对象。Keys(tab)
返回一个从tab->GetKeys()
指针开始,到tab->GetKeys() + tab->GetNumKeys()
结束的区间包装对象。
这让你可以写:
for (MDColumn* col : Columns(tab)) { ... }
for (MDKey* key : Keys(tab)) { ... }
代替以前麻烦且易错的索引循环,代码更简洁、易读,也减少越界风险。
简而言之:
Columns()
和Keys()
就是“把裸指针+长度”转换成“可迭代对象”,方便范围for
循环。make_iterable
返回的iterable
类型封装了 begin/end 指针,支持标准的迭代器接口。
这是iterable
和make_iterable
的完整模板实现,功能就是包装一对迭代器(这里用的是普通指针也可以)使它们可以用在范围for
循环中。
逐步解释:
template<class It>
class iterable
{It m_first, m_last; // 保存起始和结束的迭代器(或者指针)
public:iterable() = default; // 默认构造函数iterable(It first, It last) : m_first(first), m_last(last) {} // 构造时传入起始和结束It begin() const { return m_first; } // begin() 返回起始迭代器It end() const { return m_last; } // end() 返回结束迭代器
};
It
可以是任何支持迭代器语义的类型,比如指针、std::vector
迭代器、其他容器的迭代器。begin()
和end()
是范围for
循环所需的接口。
template<class It>
inline iterable<It> make_iterable(It a, It b)
{return iterable<It>(a, b);
}
make_iterable
是辅助函数,方便用两个迭代器快速创建iterable
对象。- 这使得写法更简洁,比如:
auto r = make_iterable(ptr_start, ptr_end);
for (auto it : r) {// 使用 it
}
总结
iterable
是一个轻量级的范围封装器,提供标准 begin/end 接口。make_iterable
用于创建iterable
,让范围循环写起来更方便。- 这就是你们之前
Columns()
和Keys()
函数背后的核心实现。
两个迭代器(begin 和 end)来封装一个“区间视图”或“容器视图”,这其实就是所谓的“range”或“iterator pair”概念。
核心点总结
iterable
类
封装两个迭代器(m_first
,m_last
),实现了begin()
和end()
,使其能在范围for
循环中使用。- “容器视图”(Container View)
它不是一个真正拥有数据的容器,而是“内部反转”(Inside-out)容器。- 可以从已有对象或数组上动态生成。
- 一个对象可以有多个“视图”或子区间。
- 方便你只遍历一部分数据(子范围),而不用暴露数据结构内部实现。
- 类似概念的历史与名称
- Marshall Clow 叫它
iterator_pair
- Google 叫它
std::range
(提案) - Boost 叫它
iterator_range
- Alisdair Meredith 的提案(N2977)也称
std::range
这些都本质上是两个迭代器组成的范围封装,目的是在 C++11 范围 for 还没支持真正的“ranges”概念之前,提供一种简单的范围抽象。
- Marshall Clow 叫它
- C++ 标准与未来
- C++17/20 标准后真正引入了 ranges 库(
std::ranges
),大大丰富了范围操作接口。 - 这里你看到的实现是一个非常轻量级、简单的范围封装,是 ranges 思想的前身。
- C++17/20 标准后真正引入了 ranges 库(
代码示例回顾
template<class It>
class iterable {It m_first, m_last;
public:iterable() = default;iterable(It first, It last) : m_first(first), m_last(last) {}It begin() const { return m_first; }It end() const { return m_last; }
};
template<class It>
inline iterable<It> make_iterable(It a, It b) {return iterable<It>(a, b);
}
作用
- 让你快速把任意
[begin, end)
迭代器对包装成一个“容器”, - 然后用范围
for
来遍历它, - 不需要写额外的容器类,不需要暴露内部数据结构。