踩坑記錄:C++17 的 string_view 導(dǎo)致異步日志亂碼的深度剖析
最近在我的付費(fèi)教學(xué)項(xiàng)目 MiniSpdlog 高性能日志庫(kù)實(shí)戰(zhàn) 中,一位細(xì)心的學(xué)員發(fā)現(xiàn)了一個(gè)非常有意思的 bug:
"小康哥,我把 common.h 中的 string_view_t 從 std::string 改成 std::string_view 后,同步日志一切正常,但異步日志全都變成亂碼了!這是為什么?"
看到這個(gè)問(wèn)題,我第一反應(yīng)是:這是一個(gè)非常經(jīng)典的 C++ 內(nèi)存陷阱! 這個(gè)問(wèn)題涉及到:
- string_view 的本質(zhì)
- 移動(dòng)語(yǔ)義的深層理解
- 異步編程中的內(nèi)存生命周期管理
今天,我就用這個(gè)真實(shí)案例,帶大家深入理解這個(gè)問(wèn)題。相信看完這篇文章,你對(duì) string_view 和移動(dòng)語(yǔ)義的理解會(huì)更上一層樓!

問(wèn)題復(fù)現(xiàn)
(1) 觸發(fā)條件
修改 common.h:
// 原來(lái)的定義(沒(méi)問(wèn)題)
using string_view_t = std::string;
// 改成(異步模式亂碼)
using string_view_t = std::string_view;(2) 異常表現(xiàn)
同步日志(正常):
[2025-10-20 21:16:09] [I] Thread 0 - Message #0
[2025-10-20 21:16:09] [I] Thread 1 - Message #1
[2025-10-20 21:16:09] [I] Thread 2 - Message #2異步日志(亂碼):
[2025-10-20 21:16:09] [I] ??ge #1
[2025-10-20 21:16:09] [I] ??ge #1
[2025-10-20 21:16:09] [I] ??ge #1為什么同步?jīng)]問(wèn)題,異步就亂碼了?讓我們開(kāi)始調(diào)查!
第一步:初步診斷
我讓學(xué)員加了一些調(diào)試代碼,在 log_msg_buffer 的構(gòu)造函數(shù)中打印內(nèi)存地址:
explicit log_msg_buffer(const log_msg& msg)
: log_msg(msg)
, buffer(msg.payload.data(), msg.payload.size())
{
std::cout << "Original payload: " << (void*)msg.payload.data()
<< " size: " << msg.payload.size() << std::endl;
std::cout << "Buffer address: " << (void*)buffer.data()
<< " size: " << buffer.size() << std::endl;
payload = string_view_t(buffer.data(), buffer.size());
std::cout << "New payload: " << (void*)payload.data()
<< " size: " << payload.size() << std::endl;
std::cout << "Content: " << std::string(payload) << std::endl;
}輸出結(jié)果:
Original payload: 0x7f580f62db60 size: 21
Buffer address: 0x7f5808000b60 size: 21
New payload: 0x7f5808000b60 size: 21
Content: Thread 0 - Message #0看起來(lái)一切正常?數(shù)據(jù)被正確深拷貝了,payload 也指向了新的 buffer。那問(wèn)題出在哪里呢?
這就是這個(gè) bug 的狡猾之處——問(wèn)題不在構(gòu)造,而在移動(dòng)!
第二步:抓住真兇——移動(dòng)語(yǔ)義的陷阱
讓我們跟蹤一下異步日志的完整流程:
// 1. 用戶(hù)線程調(diào)用
logger->info("Thread {} - Message #{}", t, i);
// 2. 在 logger::log() 中格式化
fmt::memory_buffer buf; // 棧上的臨時(shí)變量
fmt::format_to(std::back_inserter(buf), fmt, args...);
// 3. 創(chuàng)建 log_msg
log_msg msg(name_, lvl, string_view_t(buf.data(), buf.size()));
// ^^^^^^^^^^^^^^ 指向棧上的 buf!
// 4.調(diào)用異步 logger 的 sink_it_()
async_logger::sink_it_(msg);在 async_logger::sink_it_() 中:
void async_logger::sink_it_(const log_msg& msg) {
// 5. 創(chuàng)建 async_msg,深拷貝 payload
async_msg async_m(async_msg_type::log, shared_from_this(), msg);
// 此時(shí):async_m.buffer 存儲(chǔ)了 "Thread 0 - Message #0"
// async_m.payload 指向 async_m.buffer
// 6. 將消息移動(dòng)到隊(duì)列(關(guān)鍵!)
pool_ptr->post_log(shared_from_this(), msg);
└─> q_.enqueue(std::move(async_m));
└─> v_[tail_] = std::move(item); // ? 問(wèn)題就在這里!
}
// 7. 函數(shù)返回,async_m 被析構(gòu)關(guān)鍵問(wèn)題: 在步驟 6 中,async_m 被移動(dòng)到隊(duì)列的 v_[tail_] 中。如果沒(méi)有正確的移動(dòng)賦值運(yùn)算符,會(huì)發(fā)生什么?
第三步:深入理解——用內(nèi)存圖說(shuō)話
(1) 數(shù)據(jù)結(jié)構(gòu)回顧
struct log_msg_buffer : log_msg {
std::string buffer; // 實(shí)際存儲(chǔ)數(shù)據(jù)
string_view_t payload; // 指向 buffer(繼承自 log_msg)
};關(guān)鍵理解:
- std::string buffer 是擁有者,管理堆上的內(nèi)存
- string_view payload 只是觀察者,存儲(chǔ)指針 + 長(zhǎng)度
用圖示表示:
┌─────────────────────────────┐
│ log_msg_buffer 對(duì)象 │
│ │
│ payload (string_view) │
│ ┌─────────────────┐ │
│ │ ptr = 0x1000 │────┐ │
│ │ len = 21 │ │ │
│ └─────────────────┘ │ │
│ │ │
│ buffer (std::string) │ │
│ ┌─────────────────┐ │ │
│ │ data = 0x1000 │────┘ │
│ │ size = 21 │ │
│ └─────────────────┘ │
└─────────────────────────────┘
│
▼
堆內(nèi)存 (0x1000):
"Thread 0 - Message #0"(2) 移動(dòng)過(guò)程的問(wèn)題
① 沒(méi)有自定義移動(dòng)賦值(出問(wèn)題的情況)
編譯器生成的默認(rèn)移動(dòng)賦值:
log_msg_buffer& operator=(log_msg_buffer&& other) {
// 1. 移動(dòng) buffer(正確)
this->buffer = std::move(other.buffer);
// 2. 拷貝 payload(錯(cuò)誤!)
this->payload = other.payload; // 只拷貝了指針值!
return *this;
}移動(dòng)前的內(nèi)存狀態(tài):
棧上的 async_m: 隊(duì)列中的 v_[tail_]:
┌──────────────────┐ ┌──────────────────┐
│ payload.ptr │ │ payload.ptr │
│ = 0x1000 ───┐ │ │ = ?????? │
│ │ │ │ │
│ buffer │ │ │ buffer (空) │
│ data = 0x1000 │ │ │
└──────────────┼───┘ └──────────────────┘
│
▼
堆內(nèi)存 0x1000:
"Thread 0 - Message #0"執(zhí)行 v_[tail_] = std::move(async_m) 后:
棧上的 async_m: 隊(duì)列中的 v_[tail_]:
┌──────────────────┐ ┌──────────────────┐
│ payload.ptr │ │ payload.ptr │
│ = 0x1000 ───┐ │ │ = 0x1000 ───┐ │ ? 危險(xiǎn)!
│ │ │ │ │ │
│ buffer (空) │ │ │ buffer │ │
│ data = null │ │ │ data = 0x2000 │
└──────────────┼───┘ └──────────────┼───┘
│ │
▼ ▼
這塊內(nèi)存不再被管理! 堆內(nèi)存 0x2000:
但 payload 還指向它 "Thread 0 - Message #0"問(wèn)題分析:
- buffer 被正確移動(dòng),字符串內(nèi)容到了新位置 0x2000
- payload.ptr 還是 0x1000(只拷貝了指針值)
- 當(dāng) async_m 析構(gòu)后,0x1000 的內(nèi)存可能被釋放或覆蓋
- 后臺(tái)線程讀取時(shí),payload.ptr 指向無(wú)效內(nèi)存 → 亂碼!
② 有自定義移動(dòng)賦值(正確的情況)
log_msg_buffer& operator=(log_msg_buffer&& other) noexcept {
if (this != &other) {
buffer = std::move(other.buffer);
// ? 關(guān)鍵!手動(dòng)更新 payload 指向新的 buffer
payload = string_view_t(buffer.data(), buffer.size());
}
return *this;
}移動(dòng)后的狀態(tài):
棧上的 async_m: 隊(duì)列中的 v_[tail_]:
┌──────────────────┐ ┌──────────────────┐
│ payload.ptr │ │ payload.ptr │
│ = 0x1000 │ │ = 0x2000 ───┐ │ ? 正確!
│ │ │ │ │
│ buffer (空) │ │ buffer │ │
│ data = null │ │ data = 0x2000 │
└──────────────────┘ └──────────────┼───┘
│
▼
堆內(nèi)存 0x2000:
"Thread 0 - Message #0"現(xiàn)在 payload.ptr 正確指向 buffer.data(),無(wú)論 async_m 何時(shí)析構(gòu)都不影響!
第四步:為什么同步模式?jīng)]問(wèn)題?
對(duì)比同步和異步的執(zhí)行流程:
(1) 同步模式
void logger::sink_it_(const log_msg& msg) {
for (auto& sink : sinks_) {
sink->log(msg); // 直接調(diào)用,msg 還在棧上
}
}
// msg 使用完才銷(xiāo)毀,payload 指向的內(nèi)存一直有效關(guān)鍵: 整個(gè)過(guò)程中,fmt::memory_buffer buf 一直在棧上,msg.payload 指向的內(nèi)存始終有效。
(2) 異步模式
void async_logger::sink_it_(const log_msg& msg) {
async_msg async_m(msg); // 深拷貝到 async_m
q_.enqueue(std::move(async_m)); // 移動(dòng)到隊(duì)列
}
// ? 返回后,async_m 被析構(gòu)
// 如果移動(dòng)不正確,隊(duì)列中的 payload 就懸空了!關(guān)鍵: 消息要跨線程傳遞,必須保證數(shù)據(jù)的獨(dú)立性。如果移動(dòng)語(yǔ)義不正確,就會(huì)出現(xiàn)懸空指針。
第五步:完整的解決方案
(1) 修復(fù)代碼
在 async_msg.h 中添加正確的移動(dòng)語(yǔ)義:
struct log_msg_buffer : log_msg {
std::string buffer;
log_msg_buffer() = default;
// 構(gòu)造時(shí)深拷貝
explicit log_msg_buffer(const log_msg& msg)
: log_msg(msg)
, buffer(msg.payload.data(), msg.payload.size())
{
payload = string_view_t(buffer.data(), buffer.size());
}
// ? 移動(dòng)構(gòu)造函數(shù)
log_msg_buffer(log_msg_buffer&& other) noexcept
: log_msg(other)
, buffer(std::move(other.buffer))
{
// 關(guān)鍵:更新 payload 指向新的 buffer
payload = string_view_t(buffer.data(), buffer.size());
}
// ? 移動(dòng)賦值運(yùn)算符
log_msg_buffer& operator=(log_msg_buffer&& other) noexcept {
if (this != &other) {
log_msg::operator=(other);
buffer = std::move(other.buffer);
// 關(guān)鍵:更新 payload 指向新的 buffer
payload = string_view_t(buffer.data(), buffer.size());
}
return *this;
}
};
struct async_msg : log_msg_buffer {
async_msg_type msg_type{async_msg_type::log};
async_logger_ptr worker_ptr;
async_msg() = default;
~async_msg() = default;
async_msg(const async_msg&) = delete;
async_msg& operator=(const async_msg&) = delete;
// ? 移動(dòng)構(gòu)造
async_msg(async_msg&& other) noexcept
: log_msg_buffer(std::move(other))
, msg_type(other.msg_type)
, worker_ptr(std::move(other.worker_ptr))
{}
// ? 移動(dòng)賦值
async_msg& operator=(async_msg&& other) noexcept {
if (this != &other) {
log_msg_buffer::operator=(std::move(other));
msg_type = other.msg_type;
worker_ptr = std::move(other.worker_ptr);
}
return *this;
}
// 其他構(gòu)造函數(shù)...
};(2) 驗(yàn)證修復(fù)
添加調(diào)試代碼驗(yàn)證:
log_msg_buffer& operator=(log_msg_buffer&& other) noexcept {
if (this != &other) {
std::cout << "=== 移動(dòng)賦值 ===" << std::endl;
std::cout << "移動(dòng)前 payload: " << (void*)payload.data() << std::endl;
std::cout << "other.buffer: " << (void*)other.buffer.data() << std::endl;
buffer = std::move(other.buffer);
std::cout << "移動(dòng)后 buffer: " << (void*)buffer.data() << std::endl;
payload = string_view_t(buffer.data(), buffer.size());
std::cout << "更新后 payload: " << (void*)payload.data() << std::endl;
std::cout << "內(nèi)容: " << std::string(payload) << std::endl;
}
return *this;
}修復(fù)后的輸出:
=== 移動(dòng)賦值 ===
移動(dòng)前 payload: 0x0
other.buffer: 0x7f5808000b60
移動(dòng)后 buffer: 0x7f5808000b60
更新后 payload: 0x7f5808000b60
內(nèi)容: Thread 0 - Message #0完美!payload 正確指向了新的 buffer。
核心知識(shí)點(diǎn)總結(jié)
(1) string_view 的本質(zhì)
class string_view {
const char* data_; // 只是指針
size_t size_; // 和長(zhǎng)度
// 不擁有內(nèi)存!
};記住:string_view 是觀察者,不是擁有者。
(2) 移動(dòng)語(yǔ)義的陷阱
當(dāng)類(lèi)中同時(shí)包含擁有型(如 std::string)和觀察型(如 string_view)成員時(shí):
struct Bad {
std::string data;
string_view view; // 指向 data
// ? 默認(rèn)移動(dòng)不會(huì)更新 view!
};必須手動(dòng)實(shí)現(xiàn)移動(dòng)語(yǔ)義,確保觀察者指向正確的擁有者。
(3) 生活化類(lèi)比
std::string = 房子(你擁有)
string_view = 房子地址(別人用來(lái)找你)
搬家(移動(dòng))后:
- 房子到了新位置
- 但地址沒(méi)更新
- 別人按舊地址找你 → 找錯(cuò)地方!
正確做法:
- 搬家后,更新所有名片上的地址(4) 調(diào)試技巧
遇到類(lèi)似問(wèn)題,可以:
- 打印指針地址:看 string_view 和 std::string 是否對(duì)應(yīng)
- 檢查移動(dòng)時(shí)機(jī):在移動(dòng)構(gòu)造/賦值中加 log
經(jīng)驗(yàn)教訓(xùn)
- 使用 string_view 要謹(jǐn)慎:確保它指向的內(nèi)存生命周期夠長(zhǎng)
- 異步場(chǎng)景更需謹(jǐn)慎:數(shù)據(jù)跨線程傳遞,必須獨(dú)立管理內(nèi)存
- 自定義移動(dòng)語(yǔ)義時(shí):要更新所有"觀察者"成員
- 測(cè)試要全面:不能只測(cè)同步,也要測(cè)異步





























