编译期string_view格式化拼接:日志宏结构成员与消息合并优化
高效日志消息合并:编译期实现或std::string_view优化方案
问题背景
使用P7 Baical设计日志器,通过宏封装实现日志记录,现有宏定义及Mystruct结构如下:
#define LOG_ARGS __FILE__, __func__, __LINE__ #define LOG_INFO(component, message, ...) \ do { \ if (1) { \ component.format_struct(message); \ log_info(LOG_ARGS, message, ##__VA_ARGS__);} \ } while(0) struct Mystruct { Mystruct(int a, int b) : _a(a), _b(b) {} int _a; int _b; // 需要实现format_struct或类似方法合并消息 }; // 调用示例 int main() { LOG_INFO(Mystruct(1, 2), "test message %d", 100); }
需求是将message处理为"a val: 1, b val: 2 test message %d"(合并Mystruct成员格式化内容与原消息),且需避免std::string的堆分配开销,追求最高性能。
方案一:编译期实现(性能最优,仅支持常量成员)
若Mystruct的_a、_b为编译期常量,可利用C++20的constexpr特性在编译期完成字符串拼接,完全消除运行期开销。
1. 编译期字符串工具实现
#include <array> #include <cstddef> #include <cstring> // 编译期计算整数的位数 constexpr size_t digit_count(int n) { return n == 0 ? 1 : (n < 0 ? 1 : 0) + (n / 10 != 0 ? digit_count(n / 10) : 0) + 1; } // 编译期拼接两个字符串字面量 template<size_t N1, size_t N2> constexpr auto concat(const char (&s1)[N1], const char (&s2)[N2]) { std::array<char, N1 + N2 - 1> result{}; size_t i = 0; for (; i < N1 - 1; ++i) result[i] = s1[i]; for (size_t j = 0; j < N2; ++j) result[i + j] = s2[j]; return result; } // 编译期格式化_a成员 template<size_t SuffixLen> constexpr auto format_a(int a, const char (&suffix)[SuffixLen]) { constexpr size_t digits = digit_count(a); std::array<char, 7 + digits + SuffixLen - 1> result{}; // "a val: " + 位数 + 后缀 size_t pos = 0; // 写入前缀 const char prefix[] = "a val: "; memcpy(result.data(), prefix, 7); pos += 7; // 编译期转整数为字符串(简化版,支持非负整数,负数可扩展) int tmp = a; size_t digit_pos = pos + digits - 1; if (tmp == 0) { result[digit_pos] = '0'; } else { while (tmp > 0) { result[digit_pos--] = '0' + (tmp % 10); tmp /= 10; } } pos += digits; // 写入后缀 memcpy(result.data() + pos, suffix, SuffixLen); return result; } // 编译期格式化_b成员 template<size_t SuffixLen> constexpr auto format_b(int b, const char (&suffix)[SuffixLen]) { constexpr size_t digits = digit_count(b); std::array<char, 7 + digits + SuffixLen - 1> result{}; // "b val: " + 位数 + 后缀 size_t pos = 0; const char prefix[] = "b val: "; memcpy(result.data(), prefix, 7); pos += 7; int tmp = b; size_t digit_pos = pos + digits - 1; if (tmp == 0) { result[digit_pos] = '0'; } else { while (tmp > 0) { result[digit_pos--] = '0' + (tmp % 10); tmp /= 10; } } pos += digits; memcpy(result.data() + pos, suffix, SuffixLen); return result; }
2. 编译期日志宏定义
#define LOG_INFO_COMPILE(a, b, message, ...) \ do { \ constexpr auto merged = concat(concat(format_a(a, ", "), format_b(b, " ")), message); \ log_info(LOG_ARGS, merged.data(), ##__VA_ARGS__); \ } while(0) // 使用示例(a、b必须是编译期常量) int main() { LOG_INFO_COMPILE(1, 2, "test message %d", 100); }
该方案直接在编译期生成合并后的字符串常量,运行期无任何额外开销,但仅适用于Mystruct成员为编译期常量的场景。
方案二:运行期std::string_view优化(支持动态成员)
若Mystruct成员为运行期动态值,可通过栈缓冲区存储合并后的字符串,配合std::string_view避免堆分配,性能接近编译期方案。
1. 改造Mystruct实现
#include <string_view> #include <cstdio> #include <cstring> struct Mystruct { int _a; int _b; char _stack_buf[64]; // 栈缓冲区,大小按需调整(需覆盖前缀+最长消息) size_t _prefix_len; Mystruct(int a, int b) : _a(a), _b(b) { // 预先格式化成员到栈缓冲区 _prefix_len = static_cast<size_t>(snprintf(_stack_buf, sizeof(_stack_buf), "a val: %d, b val: %d ", _a, _b)); } // 合并原消息并返回string_view std::string_view merge(std::string_view msg) { if (_prefix_len + msg.size() < sizeof(_stack_buf)) { // 拷贝原消息到栈缓冲区后缀 memcpy(_stack_buf + _prefix_len, msg.data(), msg.size()); size_t total_len = _prefix_len + msg.size(); _stack_buf[total_len] = '\0'; return std::string_view(_stack_buf, total_len); } else { // 缓冲区不足时的降级处理(用静态缓冲区,避免堆分配) static char fallback_buf[256]; int total_len = snprintf(fallback_buf, sizeof(fallback_buf), "a val: %d, b val: %d %.*s", _a, _b, static_cast<int>(msg.size()), msg.data()); return std::string_view(fallback_buf, static_cast<size_t>(total_len)); } } };
2. 调整日志宏
#define LOG_INFO(component, message, ...) \ do { \ auto merged_view = component.merge(message); \ log_info(LOG_ARGS, merged_view.data(), ##__VA_ARGS__); \ } while(0) // 使用示例(支持动态成员) int main() { int x = 5, y = 7; LOG_INFO(Mystruct(x, y), "test message %d", 100); }
该方案全程使用栈/静态缓冲区,无std::string的堆分配开销,同时支持运行期动态值,适合绝大多数日志场景。需根据实际日志长度调整栈缓冲区大小,避免溢出。
宏优化补充
- 原宏中的
if(1)可移除,do-while(0)已能保证宏的语法正确性; - 若
log_info直接支持std::string_view参数,可直接传入merged_view,无需调用data()。
内容的提问来源于stack exchange,提问作者Enes Aygün
相关产品推荐
相关产品推荐

