【问题标题】:Move the string out of a std::ostringstream将字符串移出 std::ostringstream
【发布时间】:2014-12-03 16:21:25
【问题描述】:

如果我使用std::ostringstream 构造一个由空格分隔的浮点值列表组成的字符串:

std::ostringstream ss;
unsigned int s = floatData.size();
for(unsigned int i=0;i<s;i++)
{
    ss << floatData[i] << " ";
}

然后我在std::string 中得到结果:

std::string textValues(ss.str());

但是,这将导致不必要的字符串内容的深层复制,因为ss 将不再被使用。

有什么方法可以在不复制整个内容的情况下构造字符串?

【问题讨论】:

  • 你确定这是复制吗?我认为这是应用 RVO 的一个完全合理的案例。检查你的程序集,看看你的编译器在做什么。
  • @Manu343726 RVO 适用于return 值。这里没有return
  • 标准关于 str() 的说法是:“返回一个带有流当前内容副本的字符串对象。”所以是的,它复制了
  • 作为 QoI,一个实现可以用 move(ss).str() 做一些好事,但我不知道现在有没有。
  • @galinette 是的,你可以,虽然我认为大多数编译器都不能很好地支持。

标签: c++ c++11 move-semantics ostringstream


【解决方案1】:

std::ostringstream 不提供访问其内存缓冲区的公共接口,除非它不可移植地支持pubsetbuf(但即便如此,您的缓冲区也是固定大小的,请参阅cppreference example

如果你想折磨一些字符串流,你可以使用受保护的接口访问缓冲区:

#include <iostream>
#include <sstream>
#include <vector>

struct my_stringbuf : std::stringbuf {
    const char* my_str() const { return pbase(); } // pptr might be useful too
};

int main()
{
    std::vector<float> v = {1.1, -3.4, 1/7.0};
    my_stringbuf buf;
    std::ostream ss(&buf);
    for(unsigned int i=0; i < v.size(); ++i)
        ss << v[i] << ' ';
    ss << std::ends;
    std::cout << buf.my_str() << '\n';
}

直接访问自动调整大小的输出流缓冲区的标准 C++ 方法由 std::ostrstream 提供,在 C++98 中已弃用,但仍是标准 C++14 和计数。

#include <iostream>
#include <strstream>
#include <vector>

int main()
{
    std::vector<float> v = {1.1, -3.4, 1/7.0};
    std::ostrstream ss;
    for(unsigned int i=0; i < v.size(); ++i)
        ss << v[i] << ' ';
    ss << std::ends;
    const char* buffer = ss.str(); // direct access!
    std::cout << buffer << '\n';
    ss.freeze(false); // abomination
}

但是,我认为最干净(也是最快)的解决方案是boost.karma

#include <iostream>
#include <string>
#include <vector>
#include <boost/spirit/include/karma.hpp>
namespace karma = boost::spirit::karma;
int main()
{
    std::vector<float> v = {1.1, -3.4, 1/7.0};
    std::string s;
    karma::generate(back_inserter(s), karma::double_ % ' ', v);
    std::cout << s << '\n'; // here's your string
}

【讨论】:

【解决方案2】:

现在可以使用 C++20,语法如下:

const std::string s = std::move(ss).str();

这是可能的,因为 std::ostringstream 类现在 has a str() overload 是 rvalue-ref 限定的:

basic_string<charT, traits, Allocator> str() &&;  // since C++20

这是在 P0408 修订版 7 中添加的,即 adopted into C++20

【讨论】:

    【解决方案3】:

    @Cubbi 为 Boost Karma 加 1 以及对 "create your own streambuf-dervied type that does not make a copy, and give that to the constructor of a basic_istream&lt;&gt;." 的建议。

    但是,缺少一个更通用的答案,它位于这两者之间。 它使用 Boost Iostream:

    using string_buf = bio::stream_buffer<bio::back_insert_device<std::string> >;
    

    这是一个演示程序:

    Live On Coliru

    #include <boost/iostreams/device/back_inserter.hpp>
    #include <boost/iostreams/stream_buffer.hpp>
    
    namespace bio = boost::iostreams;
    
    using string_buf = bio::stream_buffer<bio::back_insert_device<std::string> >;
    
    // any code that uses ostream
    void foo(std::ostream& os) {
        os << "Hello world " 
           << std::hex << std::showbase << 42
           << " " << std::boolalpha << (1==1) << "\n";
    }
    
    #include <iostream>
    
    int main() {
        std::string output;
        output.reserve(100); // optionally optimize if you know roughly how large output is gonna, or know what minimal size it will require
    
        {
            string_buf buf(output);
            std::ostream os(&buf);
            foo(os);
        }
    
        std::cout << "Output contains: " << output;
    }
    

    请注意,您可以简单地将std::string 替换为std::wstringstd::vector&lt;char&gt; 等。

    更好的是,您可以将它与array_sink 设备一起使用并拥有一个固定大小 缓冲区。这样您就可以避免使用 Iostreams 代码进行任何缓冲区分配!

    Live On Coliru

    #include <boost/iostreams/device/array.hpp>
    
    using array_buf = bio::stream_buffer<bio::basic_array<char>>;
    
    // ...
    
    int main() {
        char output[100] = {0};
    
        {
            array_buf buf(output);
            std::ostream os(&buf);
            foo(os);
        }
    
        std::cout << "Output contains: " << output;
    }
    

    两个程序都打印:

    Output contains: Hello world 0x2a true
    

    【讨论】:

    • 添加了一个固定数组缓冲区示例,适用于接受 std::istreamstd::ostream 的任何内容
    • 字符串output可以随意清除吗?或者这会破坏流吗?
    • @BoundaryImposition 有趣的问题。如果back_insert_device 确实做到了顾名思义,那应该没问题。我认为我不想依赖它,因为实例化一个新的 stream_buffer 应该不会很昂贵。
    • reserve(100) 重要,还是在可以确定输出大小的情况下进行速度优化?
    • @AkariAkaori 这只是分配优化,reserve 的文档将确认
    【解决方案4】:

    我实现了“outstringstream”类,我相信它完全符合您的需要(参见 take_str() 方法)。我部分使用了来自:What is wrong with my implementation of overflow()?

    的代码
    #include <ostream>
    
    template <typename char_type>
    class basic_outstringstream : private std::basic_streambuf<char_type, std::char_traits<char_type>>,
                                  public std::basic_ostream<char_type, std::char_traits<char_type>>
    {
        using traits_type = std::char_traits<char_type>;
        using base_buf_type = std::basic_streambuf<char_type, traits_type>;
        using base_stream_type = std::basic_ostream<char_type, traits_type>;
        using int_type = typename base_buf_type::int_type;
    
        std::basic_string<char_type> m_str;
    
        int_type overflow(int_type ch) override
        {
            if (traits_type::eq_int_type(ch, traits_type::eof()))
                return traits_type::not_eof(ch);
    
            if (m_str.empty())
                m_str.resize(1);
            else
                m_str.resize(m_str.size() * 2);
    
            const std::ptrdiff_t diff = this->pptr() - this->pbase();
            this->setp(&m_str.front(), &m_str.back());
    
            this->pbump(diff);
            *this->pptr() = traits_type::to_char_type(ch);
            this->pbump(1);
    
            return traits_type::not_eof(traits_type::to_int_type(*this->pptr()));
        }
    
        void init()
        {
            this->setp(&m_str.front(), &m_str.back());
    
            const std::size_t size = m_str.size();
            if (size)
            {
                memcpy(this->pptr(), &m_str.front(), size);
                this->pbump(size);
            }
        }
    
    public:
    
        explicit basic_outstringstream(std::size_t reserveSize = 8)
            : base_stream_type(this)
        {
            m_str.reserve(reserveSize);
            init();
        }
    
        explicit basic_outstringstream(std::basic_string<char_type>&& str)
            : base_stream_type(this), m_str(std::move(str))
        {
            init();
        }
    
        explicit basic_outstringstream(const std::basic_string<char_type>& str)
            : base_stream_type(this), m_str(str)
        {
            init();
        }
    
        const std::basic_string<char_type>& str() const
        {
            return m_str;
        }
    
        std::basic_string<char_type>&& take_str()
        {
            return std::move(m_str);
        }
    
        void clear()
        {
            m_str.clear();
            init();
        }
    };
    
    using outstringstream = basic_outstringstream<char>;
    using woutstringstream = basic_outstringstream<wchar_t>;
    

    【讨论】:

    • 这是一个好的开始,但不应返回来自str() 的引用,并且可能需要xsputn() 和/或sync() 覆盖。我还在努力。
    • 好的 - 不需要xsputn()sync(),但你在init() 中使用&amp;m_str.front()&amp;m_str.back() 被破坏了;当字符串为空时,这有 UB。对于 GCC 4.8.5,&amp;m_str.front() 在这种情况下是一个 after &amp;m_str.back()!然后streamsize in xsputn() 是-1(而不是0),所有的地狱都崩溃了。 &amp;m_str[0]&amp;m_str[m_str.size()] 应该可以工作(即使后者是过去式的;一个 impl 有点必​​须在 C++11 中以这种方式工作)。
    • 坦率地说,vector&lt;char&gt; 会更安全(尤其是当你冒着 COW 参与的风险时,咳嗽 GCC),但对于打电话给 @987654341 的人来说,它并没有那么有用@.
    • 我认为在str() 中调用setp(在字符串复制和返回之间)应该可以完成这项工作。这是我当前的实现,如果您有兴趣和/或想要合并我的更改:pastebin.com/jLZ3TF3b
    • @ceztko libstdc++ even in C++11 mode had COW strings for several years(是的,这是不合规的)。不过,从 GCC 5 开始就很好了。实际上,我很想使用编译时检查来为兼容的工具链构建添加的 hack。
    【解决方案5】:

    更新:面对人们持续不喜欢这个答案,我想我会进行编辑和解释。

    1. 不行,没有办法避免字符串复制(stringbuf的接口一样)

    2. 没关系。这样实际上效率更高。 (我会尽量解释)

    想象一下,编写一个stringbuf 的版本,它可以随时保持完美、可移动的std::string。 (我实际上已经尝试过)。

    添加字符很容易——我们只需在底层字符串上使用push_back

    好的,但是删除字符(从缓冲区读取)呢?我们必须移动一些指针来说明我们已删除的字符,一切都很好。

    但是,我们有一个问题 - 我们遵守的合同规定我们将始终有一个 std::string 可用。

    因此,每当我们从流中删除字符时,我们都需要将它们从底层字符串中erase。这意味着将所有剩余的字符向下移动 (memmove/memcpy)。因为每次控制流离开我们的私有实现时都必须保留这个契约,这实际上意味着每次我们在字符串缓冲区上调用getcgets 时都必须从字符串中删除字符。这转化为对流上的每个&lt;&lt; 操作的擦除调用。

    那么当然还有实现推回缓冲区的问题。如果将字符推回底层字符串,则必须在位置 0 处 insert 它们 - 将整个缓冲区向上移动。

    总而言之,您可以编写一个仅限 ostream 的流缓冲区,纯粹用于构建 std::string。随着底层缓冲区的增长,您仍然需要处理所有重新分配,因此最终您可以只保存一个字符串副本。所以也许我们从 4 个字符串副本(以及对 malloc/free 的调用)到 3 个,或者从 3 个到 2 个。

    您还需要处理streambuf 接口未拆分为istreambufostreambuf 的问题。这意味着您仍然必须提供输入接口,并且如果有人使用它,则抛出异常或断言。这相当于对用户撒谎 - 我们未能实现预期的界面。

    为了这个微小的性能提升,我们必须付出以下代价:

    1. 开发(相当复杂,当您考虑区域设置管理时)软件组件。

    2. 失去了只支持输出操作的 streambuf 的灵活性。

    3. 为未来的开发者埋下地雷。

    【讨论】:

    • "现代 cpu 上的字符串副本非常便宜" 是吗?如果我的程序需要解析几 GB 的文本数据怎么办? (有时确实如此)
    • 解析将比复制花费更长的时间。
    • 考虑以下测试代码:pastebin.com/YYtT6VwH 在发布时它们的速度实际上是相同的,但在调试模式下(我也需要使用)f1 几乎快两倍。
    • @galinette 如果速度很重要,不幸的是,你必须使用 C 解析。这不是一个流行的事实,但它更快。
    • @NeilKirk 如果您的程序确实要复制千兆字节的字符串数据,那么有许多技术可以在不将其全部读入内存的情况下对数据进行迭代。内存映射文件,直接从输入流转换,批处理,不转换(以二进制格式存储)等。效率几乎总是选择正确算法的问题,而不是优化现有算法。
    【解决方案6】:

    我改编了非常好的@Kuba answer 来解决一些问题(不幸的是他目前没有反应)。特别是:

    • 添加了 safe_pbump 来处理 64 位偏移;
    • 返回string_view 而不是string(内部字符串的缓冲区大小不正确);
    • string 的大小调整为移动语义take_str 方法上的当前缓冲区大小;
    • 修复了take_str 方法在返回前使用init 移动语义;
    • init 方法上删除了一个无用的memcpy
    • 将模板参数char_type重命名为CharT以避免与basic_streambuf::char_type产生歧义;
    • 使用string::data() 和指针算法,而不是使用@LightnessRacesinOrbit 指出的string::front()string::back() 可能的未定义行为;
    • 使用streambuf 组合实现。
    #pragma once
    
    #include <cstdlib>
    #include <limits>
    #include <ostream>
    #include <string>
    #if __cplusplus >= 201703L
    #include <string_view>
    #endif
    
    namespace usr
    {
        template <typename CharT>
        class basic_outstringstream : public std::basic_ostream<CharT, std::char_traits<CharT>>
        {
            using traits_type = std::char_traits<CharT>;
            using base_stream_type = std::basic_ostream<CharT, traits_type>;
    
            class buffer : public std::basic_streambuf<CharT, std::char_traits<CharT>>
            {
                using base_buf_type = std::basic_streambuf<CharT, traits_type>;
                using int_type = typename base_buf_type::int_type;
    
            private:
                void safe_pbump(std::streamsize off)
                {
                    // pbump doesn't support 64 bit offsets
                    // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47921
                    int maxbump;
                    if (off > 0)
                        maxbump = std::numeric_limits<int>::max();
                    else if (off < 0)
                        maxbump = std::numeric_limits<int>::min();
                    else // == 0
                        return;
    
                    while (std::abs(off) > std::numeric_limits<int>::max())
                    {
                        this->pbump(maxbump);
                        off -= maxbump;
                    }
    
                    this->pbump((int)off);
                }
    
                void init()
                {
                    this->setp(const_cast<CharT *>(m_str.data()),
                        const_cast<CharT *>(m_str.data()) + m_str.size());
                    this->safe_pbump((std::streamsize)m_str.size());
                }
    
            protected:
                int_type overflow(int_type ch) override
                {
                    if (traits_type::eq_int_type(ch, traits_type::eof()))
                        return traits_type::not_eof(ch);
    
                    if (m_str.empty())
                        m_str.resize(1);
                    else
                        m_str.resize(m_str.size() * 2);
    
                    size_t size = this->size();
                    this->setp(const_cast<CharT *>(m_str.data()),
                        const_cast<CharT *>(m_str.data()) + m_str.size());
                    this->safe_pbump((std::streamsize)size);
                    *this->pptr() = traits_type::to_char_type(ch);
                    this->pbump(1);
    
                    return ch;
                }
    
            public:
                buffer(std::size_t reserveSize)
                {
                    m_str.reserve(reserveSize);
                    init();
                }
    
                buffer(std::basic_string<CharT>&& str)
                    : m_str(std::move(str))
                {
                    init();
                }
    
                buffer(const std::basic_string<CharT>& str)
                    : m_str(str)
                {
                    init();
                }
    
            public:
                size_t size() const
                {
                    return (size_t)(this->pptr() - this->pbase());
                }
    
    #if __cplusplus >= 201703L
                std::basic_string_view<CharT> str() const
                {
                    return std::basic_string_view<CharT>(m_str.data(), size());
                }
    #endif
                std::basic_string<CharT> take_str()
                {
                    // Resize the string to actual used buffer size
                    m_str.resize(size());
                    std::string ret = std::move(m_str);
                    init();
                    return ret;
                }
    
                void clear()
                {
                    m_str.clear();
                    init();
                }
    
                const CharT * data() const
                {
                    return m_str.data();
                }
    
            private:
                std::basic_string<CharT> m_str;
            };
    
        public:
            explicit basic_outstringstream(std::size_t reserveSize = 8)
                : base_stream_type(nullptr), m_buffer(reserveSize)
            {
                this->rdbuf(&m_buffer);
            }
    
            explicit basic_outstringstream(std::basic_string<CharT>&& str)
                : base_stream_type(nullptr), m_buffer(str)
            {
                this->rdbuf(&m_buffer);
            }
    
            explicit basic_outstringstream(const std::basic_string<CharT>& str)
                : base_stream_type(nullptr), m_buffer(str)
            {
                this->rdbuf(&m_buffer);
            }
    
    #if __cplusplus >= 201703L
            std::basic_string_view<CharT> str() const
            {
                return m_buffer.str();
            }
    #endif
            std::basic_string<CharT> take_str()
            {
                return m_buffer.take_str();
            }
    
            const CharT * data() const
            {
                return m_buffer.data();
            }
    
            size_t size() const
            {
                return m_buffer.size();
            }
    
            void clear()
            {
                m_buffer.clear();
            }
    
        private:
            buffer m_buffer;
        };
    
        using outstringstream = basic_outstringstream<char>;
        using woutstringstream = basic_outstringstream<wchar_t>;
    }
    

    【讨论】:

      猜你喜欢
      • 2017-04-23
      • 2020-09-26
      • 1970-01-01
      • 2014-08-30
      • 1970-01-01
      • 2015-03-30
      • 1970-01-01
      • 2019-01-21
      相关资源
      最近更新 更多