能否零拷贝获取std::stringstream累积数据的原始指针?
我想要实现的操作如下:
std::stringstream s; s << "Some " << std::hex << 123 << " hex data" << ...;
拿到上述s对象后我可以很轻松地传递它,但后续需要将它传入一个仅接受const void */size_t参数对(用于描述内存区域)的接口。
就我目前了解的情况而言(如有误欢迎指正):C++17及更早版本中没有零拷贝实现该需求的方式,必须调用.str()方法生成一份字符串副本才能拿到底层数据。
我自己实现了如下hack方案:
struct stringstream_extractor_t { // 该结构用于获取protected权限的pbase成员函数 // 实现方式为继承目标类 template <typename T> struct accessor_t : public T { static const char* data(const accessor_t* pT) { return pT->pbase(); } }; // 获取std::stringstream::rdbuf的返回类型 using bufferType = std::remove_pointer<decltype(((std::stringstream*)nullptr)->rdbuf())>::type; // 拿到std::stringstream::rdbuf的结果类型后,即可创建对应的访问器类型 // 从而调用内部的pbase方法 using accessorType = accessor_t<bufferType>; // 以只读方式存储数据 const std::string_view _data; // 语法糖:从流引用初始化_data stringstream_extractor_t(std::stringstream& stream) : _data{getBuffer(stream), static_cast<size_t>(stream.tellp())} {} // 语法糖:从流指针初始化_data stringstream_extractor_t(std::stringstream* pStream) : _data{pStream ? getBuffer(*pStream) : nullptr, pStream ? static_cast<size_t>(pStream->tellp()) : 0} {} // 用访问器类型获取数据 static const char* getBuffer(const std::stringstream& stream) { // 拿到buffer并转换为访问器类型,此处安全的原因是访问器没有额外成员,仅包含静态函数 const accessorType* pBuffer = reinterpret_cast<accessorType*>(stream.rdbuf()); return accessorType::data(pBuffer); } // 便捷方法 inline const char* data() const { return _data.data(); } inline size_t size() const { return _data.size(); } };
配合C风格接口的使用示例如下:
std::stringstream s; s << "Hi there! " << std::hex << 0xdeadbeef; const stringstream_extractor_t e(s); write(2, e.data(), e.size());
我清楚该方案需要保证std::stringstream实例的生命周期,避免悬垂指针等生命周期相关问题。
请问是否有更简洁、不这么迂回的方式来实现这个基础需求:通过移动语义从输出stringstream中获取底层缓冲区?
我总觉得我遗漏了什么可行方案,这件事不应该这么难实现。
你对C17及更早版本的判断是正确的:标准库确实没有提供从std::stringstream零拷贝获取底层缓冲区的公开接口,str()方法一定会生成完整的字符串副本,不存在其他合法的公开调用方式。
你写的hack方案本质是利用继承绕过protected成员的访问限制,该方案在主流标准库实现(libstdc、libc++、MSVC STL)上都可以正常运行,但属于依赖实现细节的未定义行为,不保证跨编译器、跨标准库版本兼容。
如果想要更稳妥的实现,可以按场景选择以下方案:
1. 升级到C++20及以上版本
C++20为std::ostringstream新增了str()的右值重载,支持直接移动底层字符串,完全没有拷贝开销,是标准规定的合法行为:
std::ostringstream s; s << "Hi there! " << std::hex << 0xdeadbeef; // 移动语义,零拷贝获取底层字符串 std::string buf = std::move(s).str(); // 直接传buf.data()和buf.size()给C接口即可
只要可以升级编译器版本,这是最优解。
2. 自定义streambuf实现(兼容C++17及更早版本)
如果必须兼容旧标准,又不想依赖未定义行为,可以自己实现支持提取缓冲区的streambuf,套在std::ostream上使用,全程符合标准规则:
// 固定大小缓冲区的streambuf示例 class membuf : public std::streambuf { public: membuf(char* base, size_t size) { setp(base, base + size); } size_t written() const { return pptr() - pbase(); } }; // 使用示例 char buf[4096]; // 也可以用动态分配的缓冲区 membuf mb(buf, sizeof(buf)); std::ostream os(&mb); os << "Hi there! " << std::hex << 0xdeadbeef; // 直接拿buf和mb.written()传入C接口即可
如果需要动态扩容的缓冲区,只需要在membuf中重载overflow方法实现扩容逻辑即可,整体代码量虽稍多于你的hack方案,但完全合法,没有兼容性风险。
现有hack方案的注意事项
如果坚持使用现有实现,需要额外注意:如果后续对原流还有写入操作,可能触发缓冲区扩容,你之前拿到的string_view会直接变成悬垂引用,必须保证提取缓冲区后不再对原流做任何写入操作。
内容的提问来源于stack exchange,提问作者shiretu

