CppCon 2014 学习:CHEAP, SIMPLE, AND SAFE LOGGING USING C++ EXPRESSION TEMPLATES
这段代码定义了一个简单的日志宏 LOG
,用来在代码里方便地打印调试信息。
代码细节解析:
#define LOG(msg) \if (s_bLoggingEnabled) \std::cout << __FILE__ << "(" << __LINE__ << "): " << msg << std::endl
s_bLoggingEnabled
是一个全局开关,控制是否启用日志输出。__FILE__
和__LINE__
是预定义宏,分别表示当前文件名和当前代码行号。msg
是传入的日志内容,利用 C++ 的流式输出(<<
)格式,类型安全且灵活。- 宏展开后,相当于:
if (s_bLoggingEnabled)std::cout << "当前文件名(行号): " << msg << std::endl;
使用示例:
void foo() {std::string file = "blah.txt";int error = 123;LOG("Read failed: " << file << " (" << error << ")");
}
打印结果示例:
test.cpp(5): Read failed: blah.txt (123)
总结:
- 方便:只需写
LOG(...)
,无需重复写打印格式代码。 - 类型安全:利用 C++ 流操作符,无需手动格式化字符串。
- 带上下文:自动打印文件名和代码行号,方便定位问题。
- 可控开关:通过
s_bLoggingEnabled
动态开启或关闭日志。
这段是对之前的 LOG
宏经过预处理(pre-processing)后的展开效果:
解释
- 预处理器把
LOG("Read failed: " << file << " (" << error << ")");
展开成了:if (s_bLoggingEnabled)std::cout << "foo.cpp" << "(" << 53 << "): " << "Read failed: " << file << " (" << error << ")"<< std::endl;
- 其中
"foo.cpp"
是当前文件名(__FILE__
),53
是代码行号(__LINE__
)。 file
和error
是变量,仍保持流式输出,类型安全。
作用
- 宏的作用就是把日志输出封装成一条语句,自动附加文件名和行号,方便调试定位。
- 预处理后你看到的就是具体的
if
判断加标准输出语句。
你给的这段汇编代码是编译器对含有日志宏 LOG(...)
的 C++ 代码生成的机器指令示例,展示了日志语句的具体实现细节。
void foo() {string file = "blah.txt";int error = 123;...movb g_bLogging(%rip), %altestb %al, %alje ..B2.14movl $_ZSt4cout, %edimovl $.L_2__STRING.3, %esicall ostream& operator<<(ostream&, char const*)movq %rax, %rdimovl $.L_2__STRING.0, %esicall ostream& operator<<(ostream&, char const*)movq %rax, %rdimovl $19, %esicall ostream::operator<<(int)...movq %rax, %rdimovl $ostream& endl(ostream&), %esicall ostream::operator<<(ostream& (*)(ostream&))
}
解析:
movb g_bLogging(%rip), %al
取日志开关变量的值到寄存器%al
。testb %al, %al
测试%al
(即判断是否启用日志)。je ..B2.14
如果日志未启用,跳过日志输出代码。- 接下来是一系列调用
std::ostream
的输出操作:operator<<
依次输出文件名、行号、消息内容等。- 每次调用后
%rax
保存返回的ostream&
,作为下次调用的第一个参数(链式调用)。 - 最后调用输出流的
endl
,刷新输出缓冲。
- 编译参数包括
-O3
(优化),说明生成的汇编是经过高优化的。 - 总体代码约33条指令,10次函数调用,比较高效。
总结:
- 通过汇编代码你可以看到,日志开关检查是条件跳转,性能代价低。
- 每条日志输出通过调用标准流操作符实现,保持类型安全和灵活性。
- 编译器对日志宏生成的代码做了优化,但调用开销仍在(函数调用多)。
- 汇编代码体现了日志宏背后的底层实现细节。
你指出了日志代码带来的性能和优化问题,总结得很准确:
问题:
- 日志相关的指令多,尤其是流操作符和条件判断,导致代码臃肿。
- 这些指令会阻碍编译器的优化,因为它们可能引入不可预测的控制流和函数调用。
- 影响指令缓存(icache)性能,代码体积变大,缓存命中率降低。
- 但我们又想保持日志的速度、类型安全和使用方便。
目标:
- 减少调用日志时的指令数量,即代码体积更小。
- 仍然保持日志输出速度快,不会显著拖慢程序。
- 保持C++流操作符的类型安全。
- 使用上仍然简洁方便。
这就是日志设计中的经典权衡点。接下来,可以考虑的优化方案包括: - 使用**零开销日志(zero-overhead logging)**技巧,编译期剔除无用日志。
- 设计更轻量的日志API,避免不必要的函数调用。
- 利用宏和模板元编程在编译期计算是否启用日志。
- 结合缓冲区或异步日志减少运行时开销。
是一种通过“表达式模板”(Expression Templates)技术解决日志调用开销的方案,核心思路如下:
核心问题
- 保持 流式(
operator<<
)接口 的优雅和类型安全 - 但避免 每次日志调用都产生多次函数调用(
operator<<
的开销)
解决方案:表达式模板(Expression Templates)
- 利用 C++ 运算符重载,在编译期把日志表达式 封装成一个类型(表达式树)
- 日志表达式如
"Read failed: " << file << " (" << error << ")"
不在运行时一步步调用operator<<
,而是先变成一个编译期的表达式对象 - 这样可以在运行时 一次性处理整个表达式,减少函数调用次数和指令数量
类比示例
- 矩阵计算:
Matrix D = A + B * C;
编译器用表达式模板避免生成临时矩阵,多做合并优化。 - 条件查询:
polygons.Find(VERTICES == 4 && WIDTH >= 20);
编译时构建查询条件表达式。 - 日志表达式:
LOG("Read failed: " << file << " (" << error << ")");
通过表达式模板,编译时构造表达式树,运行时统一输出。
总结
- 通过表达式模板,可以实现 零运行时开销的流式日志
- 保持 类型安全 和 方便易用的接口
- 大幅减少日志调用时的指令数量,提高性能和优化空间
这是用表达式模板实现日志系统的一部分代码,解析如下:
代码结构
#define LOG(msg) \if (s_bLoggingEnabled) \(Log(__FILE__, __LINE__, LogData<None>() << msg))
template<typename List>
struct LogData {typedef List type;List list;
};
struct None { };
说明
LOG(msg)
宏:- 检查日志开关
s_bLoggingEnabled
- 创建一个空的
LogData<None>()
对象(表示空的日志数据列表) - 利用重载的
operator<<
把msg
添加进这个LogData
,构建日志表达式 - 把文件名、行号和构造的日志数据传给
Log()
函数
- 检查日志开关
LogData<List>
模板结构体:- 作为表达式模板的核心,保存“日志消息链”(这里用
List
代表消息列表或表达式树) - 通过模板递归展开,实现链式拼接日志内容
- 作为表达式模板的核心,保存“日志消息链”(这里用
None
:- 表示初始的空日志数据类型
整体作用
- 利用模板和运算符重载,将日志消息“拼接”成一个类型安全的表达式模板结构
- 日志内容在运行时才调用
Log()
输出,之前只构建表达式类型,减少多次函数调用开销 - 保持了流式接口的使用习惯,同时允许编译期优化
这段代码是实现 LogData
表达式模板的关键 operator<<
,它实现了日志数据的链式拼接。具体分析如下:
代码内容
template<typename Begin, typename Value>
LogData<std::pair<Begin&&, Value&&>> operator<<(LogData<Begin>&& begin, Value&& v) noexcept {return {{ std::forward<Begin>(begin.list), std::forward<Value>(v) }};
}
解释
- 模板参数
Begin
:表示已有的日志数据类型(表达式模板中的“前半部分”)Value
:本次要加入的新日志值类型(右操作数)
- 参数
begin
:右值引用,表示已有的LogData<Begin>
对象(即当前已有的日志链)v
:要追加的值(如字符串、变量等)
- 返回类型
- 返回一个新的
LogData
,其模板参数是一个std::pair
,组合了之前的begin.list
和新值v
- 这样就形成了一个链表结构,每个节点都包含前面的表达式和当前新值
- 返回一个新的
- 实现
- 使用
std::forward
完美转发参数,保证传递值的引用性质(左值/右值) - 通过花括号初始化
std::pair
和LogData
对象
- 使用
作用
- 每次执行
operator<<
都是把一个新的值追加到已有的LogData
链表中,形成嵌套的std::pair
类型链 - 这个链条在编译时展开,运行时可以一次性遍历输出全部日志内容
- 保持类型安全和无额外函数调用开销
举例
调用示例:
auto data = LogData<None>() << "Error: " << errorCode;
- 第一次
<< "Error:"
,把"Error:"
包装进LogData<std::pair<None, const char*>>
- 第二次
<< errorCode
,把errorCode
和之前链表继续包成新的LogData<std::pair<std::pair<None, const char*>, int>>
#结构解析:
LOG("Read failed: " << file << " (" << error << ")");
在宏展开、运算符重载作用下,会构建出如下嵌套类型结构:
LogData<pair<pair<pair<pair<pair<None,char const*>, // "Read failed: "string const&>, // filechar const*>, // " ("int const&>, // errorchar const*> // ")"
>
这是怎么产生的?
每次你使用 <<
,都会走这段代码:
template<typename Begin, typename Value>
LogData<std::pair<Begin&&, Value&&>> operator<<(LogData<Begin>&& begin, Value&& v);
这就把“前一个表达式”和“当前要加入的值”合并为一个新的 std::pair
。
所以会形成链式嵌套结构,每个新 LogData
都把前一个表达式 Begin
作为链表的头节点。
为什么这样做?
这是表达式模板的经典技巧,优点如下:
优点 | 描述 |
---|---|
编译期结构构建 | 表达式在编译期以类型嵌套的形式构建完成,无运行时拼接开销 |
类型安全 | 所有内容在编译期就明确了类型(string, int 等),无字符串格式化出错问题 |
无需临时变量 | 不会像 stringstream 那样创建临时对象,减小运行时开销 |
高性能日志输出 | 编译器可以优化掉未启用日志的所有代码路径(零开销) |
总结并详细解释一下你提供的 LOG()
实现和其递归原理:
整体结构
你写的是一个 表达式模板日志系统的运行期输出逻辑,它会:
- 在编译期 使用模板构建日志消息的链式结构(嵌套
std::pair
) - 在运行期 递归展开这个链并输出每一部分
代码详解
Log()
template<typename TLogData>
void Log(const char* file, int line, TLogData&& data) noexcept __attribute__((__noinline__)) {std::cout << file << "(" << line << "): ";Log_Recursive(std::cout, std::forward<typename TLogData::type>(data.list));std::cout << std::endl;
}
TLogData
是一个LogData<...>
类型TLogData::type
是嵌套的std::pair
类型(日志表达式链)data.list
是实际数据链- 使用
Log_Recursive
递归遍历整个链并输出 __noinline__
:避免编译器内联该函数(保持调试时清晰)
Log_Recursive()
:递归版本
template<typename TLogDataPair>
void Log_Recursive(std::ostream& os, TLogDataPair&& data) noexcept {Log_Recursive(os, std::forward<typename TLogDataPair::first_type>(data.first));os << std::forward<typename TLogDataPair::second_type>(data.second);
}
- 遍历
pair<前面内容, 当前值>
- 先递归打印
.first
(链表的前面) - 然后输出
.second
(当前这一层的值)
Log_Recursive()
:终止版本
inline void Log_Recursive(std::ostream& os, None) noexcept
{ }
- 终止条件,当到达最前端
None
类型时不再输出
打印流程图
对于:
LOG("Read failed: " << file << " (" << error << ")");
最终输出类似:
main.cpp(42): Read failed: blah.txt (123)
内部运行时递归调用顺序大致如下:
Log_Recursive(..., pair<..., ")">)└── Log_Recursive(..., pair<..., error>)└── Log_Recursive(..., pair<..., " (">)└── ...└── Log_Recursive(..., None)
每层输出一个值,直到链表遍历完成。
优点总结
优点 | 描述 |
---|---|
零运行时开销(禁用时) | 宏判断 + 模板链式结构避免执行 |
编译时类型检查 | 所有拼接内容都必须合法 << 运算符 |
可扩展性强(可加入日志策略) | 如 mock、profile、日志级别 |
性能优越 | 优化后无多余函数调用 |
详细解释一下这段代码的目的和原理,尤其是 处理 std::endl
这样的流操作符(stream manipulators)。
问题背景:为什么要特别处理 manipulators?
当你这样写:
LOG("Count: " << count << std::endl);
其中 std::endl
不是普通的值,而是一个 函数指针,它的签名是:
std::ostream& endl(std::ostream&);
这叫做 stream manipulator,像 std::endl
、std::flush
、std::hex
都是这样。
如果不专门处理它,表达式模板机制就会在编译时出错(类型不匹配)。
解决方案:函数指针偏特化模板
类型定义:
typedef std::ostream& (*PfnManipulator)(std::ostream&);
这就是 manipulator 的类型,本质上是一个函数指针,指向返回 ostream&
并接受一个 ostream&
参数的函数。
operator<< 重载:
template<typename Begin>
LogData<std::pair<Begin&&, PfnManipulator>> operator<<(LogData<Begin>&& begin, PfnManipulator pfn) noexcept {return {{ std::forward<Begin>(begin.list), pfn }};
}
Begin
是前面链式结构的日志数据类型pfn
是std::endl
这类操作符的函数指针- 继续构建嵌套
std::pair
链
最终效果
这段代码允许如下语法合法且能被正确处理:
LOG("Result is: " << result << std::endl);
在 Log_Recursive()
函数中,这个 pfn
也能被正确递归处理为:
os << pfn; // 等效于 os << std::endl;
总结
项目 | 内容 |
---|---|
解决的问题 | 支持 std::endl 等流操作符 |
处理方式 | 为 ostream& (*)(ostream&) 类型专门提供 operator<< 重载 |
兼容性 | 完整支持任意流拼接表达式 |
类型安全 | 编译期确保所有内容都能插入 ostream |
这一段代码处理的是 字符串字面量优化(String Literal Optimization)。我们来一步一步拆解并说明这段代码的意图和作用。
问题背景
当你写:
LOG("Error: " << error);
其中的 "Error: "
是一个 字符串字面量(string literal),它的类型是:
const char[8] // 对于 "Error: " 来说是8(含 null terminator)
这个类型和 const char*
不一样,所以没有合适的 operator<<
重载时会导致模板匹配失败。
解决方案:模板重载接受 const char (&sz)[n]
template<typename Begin, size_t n>
LogData<std::pair<Begin&&, const char*>> operator<<(LogData<Begin>&& begin, const char (&sz)[n]) noexcept {return {{ std::forward<Begin>(begin.list), sz }};
}
参数解释:
Begin
:前面构建好的链式日志数据结构sz
:引用类型的字符数组,也就是 string literal,如"Error"
是const char (&)[6]
n
:模板参数,用来匹配任意长度的字符串字面量
返回值:
- 返回一个新的
LogData
,在已有链条的基础上添加一个const char*
类型
这就将"Error"
类型从const char[6]
转换成了const char*
存储,更加轻量,也统一了类型。
优势
优点 | 描述 |
---|---|
支持字符串字面量 | 不支持会导致编译错误 |
避免每次都拷贝字符串字面量的内容 | 只存指针,效率高 |
统一类型为 const char* | 简化后续递归处理逻辑 |
零开销类型转换 | 编译期完成,不增加运行时负担 |
示例用法
LOG("Error at line: " << lineNum << " in file: " << filename);
对于上面这段,模板重载会自动识别 "Error at line: "
和 " in file: "
为 string literal,匹配这个特化模板,转成 const char*
。
总结
你看到的这一段代码实现的是:
专门支持
"字符串字面量"
这种特殊的数组类型,并将其优化为const char*
来存储,提高日志系统的灵活性和效率。
你这段汇编和其后的模板展开反映的是 高性能、低指令开销的日志系统实现方式。
场景回顾
你在使用这样的日志语句:
LOG("Read failed: " << file << " (" << error << ")");
其中 LOG
是一个宏,最终展开为对 Log(...)
的一次调用,使用了 表达式模板(expression templates) 技术来延迟表达式求值、减少运行时指令。
汇编代码解释
movb g_bLogging(%rip), %al ; 加载全局布尔变量 s_bLoggingEnabled
testb %al, %al ; 测试它是否为真
je ..B6.7 ; 如果为假,跳转(不执行日志)
movb $0, (%rsp) ; 临时栈处理(非关键)
movl $.L_2__STRING.4, %ecx ; 加载字符串字面量指针(如 " (" )
movl $.L_2__STRING.3, %edi ; 加载另一个字符串(如 "Read failed: ")
movl $40, %esi ; 加载行号
lea 128(%rsp), %r9 ; 设置临时地址作为参数
call Log<...> ; 调用唯一的 Log 模板函数实例
模板展开(推导出的 LogData)
Log<pair<pair<pair<pair<pair<None,char const* // "Read failed: ">,string const& // file>,char const* // " (">,int const& // error>,char const* // ")"
>>
这是你通过 <<
运算符链式拼接出来的日志数据表达式,模板在编译期就构建好了这些类型。
最终传给唯一的 Log(...)
函数,运行时只需一次函数调用(pimp’d function call)即可完成整条日志输出。
优点
优点 | 说明 |
---|---|
编译期表达式组合 | << 构建表达式的结构体,不执行实际操作 |
运行期延迟调用 | 只有一次 Log 函数调用(模板实例化) |
少量汇编指令 | 这里只用了 9 条汇编指令来构造参数和调用 |
保留语义完整性 | LOG(...) 保持流式语法,且类型安全 |
优化友好 | 编译器能轻松做内联或省略,性能极高 |
理解重点
你看到的这一切说明:表达式模板技术允许你写出非常干净的代码,而编译器又能生成非常高效的汇编。 它兼顾了:
- 语法简洁
- 类型安全
- 运行时性能
- 最小的指令和函数调用开销
总结提到的是:使用表达式模板的日志系统,在性能和编译优化方面的巨大优势。下面逐点解释:
SUMMARY 理解
• Expression templates solution
表达式模板方案
使用表达式模板技术(即 <<
拼接被延迟到编译期生成类型结构),实现日志消息的构建。这种方式不在日志调用处做字符串拼接或格式化,而是传递一个类型安全的结构(LogData<...>
)到一个统一的日志处理函数。
• Reduced instructions at call site by 73% (33 → 9)
调用处的汇编指令减少了 73%(从 33 条降到 9 条)
传统的日志实现使用大量的 operator<<
,每次 <<
都是函数调用。表达式模板把这些函数调用“挪”到编译期,只留下最终调用 Log(...)
的一次函数调用。
这极大地提升了性能,特别是在嵌入式或高频调用场景下。
• Mo’ args, mo’ savings
参数越多,节省越大
传统日志系统每多一个参数,就多一到两个函数调用。而表达式模板只构建更复杂的类型结构,运行时开销不变。你写:
LOG("Read failed: " << file << " (" << error << ") at offset " << offset << ", reason: " << reason);
无论参数多少,运行时也就一两次函数调用。结果是:
参数越多,节省越多!
总结一句话
表达式模板让你保留了优雅的写法,却几乎不付出运行时代价,是一种兼顾语义表达和极致性能的高级技巧。
可变参数模板(Variadic Template)日志系统 的完整代码版本,并附带了详细注释,帮助你理解其结构与作用:
完整代码:Variadic Template Logging
#include <iostream>
#include <string>
bool s_bLoggingEnabled = true; // 控制是否启用日志
// 提前声明递归函数模板
template <typename T, typename... Args>
void Log_Recursive(const char* file, int line, std::ostream& os, T first, const Args&... rest);
inline void Log_Recursive(const char* file, int line, std::ostream& os); // 终止版本声明
// 定义 LOG 宏,用于自动捕获 __FILE__ 和 __LINE__,并转发可变参数到 Log_Variadic
#define LOG(...) Log_Variadic(__FILE__, __LINE__, __VA_ARGS__)
// 主日志函数模板,接受任意数量和类型的参数
template <typename... Args>
void Log_Variadic(const char* file, int line, const Args&... args) {if (!s_bLoggingEnabled) return; // 如果日志未启用,直接返回std::cout << file << "(" << line << "): "; // 打印文件名和行号Log_Recursive(file, line, std::cout, args...); // 展开参数并输出std::cout << std::endl; // 换行
}
// 递归模板函数:处理一个参数,然后递归处理剩余的参数
template <typename T, typename... Args>
void Log_Recursive(const char* file, int line, std::ostream& os, T first, const Args&... rest) {os << first; // 打印当前参数Log_Recursive(file, line, os, rest...); // 递归处理剩余参数
}
// 递归终止函数:不做任何事(没有参数时)
inline void Log_Recursive(const char* file, int line, std::ostream& os) {// 什么都不做,递归终止
}
使用示例
// 示例
int main() {std::string file = "config.txt";int errorCode = 404;LOG("Failed to read file: ", file, ", error code: ", errorCode);
}
输出示例:
/home/xiaqiu/test/CppCon/day46/code/main.cpp(40): Failed to read file: config.txt, error code: 404
特点总结
特性 | 说明 |
---|---|
类型安全 | 使用模板展开参数,支持任意类型(只要可输出到 std::ostream ) |
语法简洁 | 与 printf 相比更现代、更安全,且无需格式字符串 |
可扩展 | 可添加如时间戳、线程 ID、颜色等功能 |
条件编译 | 可以结合宏或 s_bLoggingEnabled 控制开关 |
下面这段代码(专门用于优化字符串字面量处理)的完整可运行示例,包含基础 LogData
类型、递归输出逻辑、宏定义等内容。这个版本模拟了使用 表达式模板风格的日志系统,特别是你提到的 operator<<
针对 const char[N]
的特化处理。
完整示例代码:支持字符串字面量优化的日志系统
#include <iostream>
#include <string>
#include <utility>
#include <type_traits>
// 控制是否启用日志
bool s_bLoggingEnabled = true;
// 基础类型 None,用于终止递归链表结构
struct None { };
// 表达式模板用的包装类型 LogData
template<typename List>
struct LogData {using type = List;List list;
};
// 重载 operator<<:通用版本,将任意类型“连接”到 LogData 链中
template<typename Begin, typename Value>
LogData<std::pair<Begin&&, Value&&>>
operator<<(LogData<Begin>&& begin, Value&& v) noexcept {return {{ std::forward<Begin>(begin.list), std::forward<Value>(v) }};
}
// 特化版本:优化 const char[N] 字符串字面量的拼接(避免构造 std::string)
template<typename Begin, size_t n>
LogData<std::pair<Begin&&, const char*>>
operator<<(LogData<Begin>&& begin, const char (&sz)[n]) noexcept {return {{ std::forward<Begin>(begin.list), sz }};
}
// 操作流控制符(如 std::endl)的版本
using PfnManipulator = std::ostream& (*)(std::ostream&);
template<typename Begin>
LogData<std::pair<Begin&&, PfnManipulator>>
operator<<(LogData<Begin>&& begin, PfnManipulator pfn) noexcept {return {{ std::forward<Begin>(begin.list), pfn }};
}
// 宏:包装日志调用,自动注入文件名和行号
#define LOG(msg) \if (s_bLoggingEnabled) \Log(__FILE__, __LINE__, LogData<None>() << msg)
// 主日志函数(展开表达式链)
template<typename TLogData>
void Log(const char* file, int line, TLogData&& data) noexcept {std::cout << file << "(" << line << "): ";Log_Recursive(std::cout, std::forward<typename TLogData::type>(data.list));std::cout << std::endl;
}
// 递归打印表达式链
template<typename TLogDataPair>
void Log_Recursive(std::ostream& os, TLogDataPair&& data) noexcept {Log_Recursive(os, std::forward<typename TLogDataPair::first_type>(data.first));os << std::forward<typename TLogDataPair::second_type>(data.second);
}
// 递归终止函数
inline void Log_Recursive(std::ostream& os, None) noexcept {// Do nothing
}
// 示例
int main() {std::string filename = "data.txt";int errCode = 42;LOG("Error opening " << filename << ": code " << errCode << std::endl);
}
输出示例
如果编译并运行,会输出类似:
main.cpp(87): Error opening data.txt: code 42
特点说明
- 表达式模板
LogData
+operator<<
允许将多个元素拼接成“延迟求值”的链表结构; - 字符串字面量版本的
operator<<
避免将其视为模板推导中需要额外构造的std::string
,提升效率; - 支持任意类型拼接、流操作符(如
std::endl
); - 所有输出逻辑最终只在
Log()
函数中统一处理,便于拦截、替换输出流(比如写入文件)或增加 profiling。