You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

编译期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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 00:12:23