将字符串从std::ostringstream中提取出来

34

如果我使用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将不再使用。

是否有任何方法可以在不复制整个内容的情况下构造字符串?


1
你确定那是在复制吗?我认为这是一个完全合理的应用RVO的情况。检查一下汇编代码,看看你的编译器在做什么。 - Manu343726
2
@Manu343726 RVO适用于“返回”值。这里没有“返回”。 - Walter
6
标准说明了str()函数的作用:“返回一个字符串对象,其内容为当前流的副本。”因此,它确实会复制。 - galinette
1
作为QoI,实现可以在move(ss).str()中做一些好的事情,但我不知道现在是否有任何实现。 - Marc Glisse
1
@galinette 是的,你可以这样做,不过我认为大多数编译器支持得不是很好。 - Walter
显示剩余15条评论
6个回答

16

14

std::ostringstream提供了没有公共接口可以访问其内存缓冲区,除非它不可移植地支持pubsetbuf(即使这样,您的缓冲区也是固定大小,参见cppreference示例

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

#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
}

1
+1 对 Karma 的方法当然是好的。不过,如果 Boost 出现在画面中,为什么不直接使用 Boost Iostreams 并让 ostream 透明地写入容器或数组 呢 :) - sehe
@sehe 谢谢,并且是的,boost::iostreams::array_sink 值得一提(毕竟,cppreference 上 std::ostrstream 的页面就提到了它) - Cubbi
1
C++20现在提供了一种简单得多的方法,详见兄弟回答 - NicholasM

5
+1给@Cubbi的Boost Karma和建议,在此链接中移动字符串“create your own streambuf-dervied type that does not make a copy, and give that to the constructor of a basic_istream<>。”。
更通用的答案位于这两个答案之间,它使用了Boost Iostreams。
using string_buf = bio::stream_buffer<bio::back_insert_device<std::string> >;

这里是一个演示程序: 在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::wstringstd::vector<char>等替换std :: string

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

在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 的内容一起使用。 - sehe
字符串 output 可以随意清除吗?还是这会破坏流程? - Lightness Races in Orbit
@BoundaryImposition 有趣的问题。如果 back_insert_device 确实做了它名字所示的事情,那应该没问题。但我不认为我想依赖它,因为实例化一个新的 stream_buffer 不应该很昂贵。 - sehe
"reserve(100)" 是重要的,还是只是在输出大小可以确定时进行速度优化? - AkariAkaori
@AkariAkaori 这只是分配优化,正如reserve的文档所确认的那样。 - sehe

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>;

不需要使用xsputn()sync(),但是在init()中使用&m_str.front()&m_str.back()是错误的;当字符串为空时,这会导致未定义行为。在GCC 4.8.5中,&m_str.front()在这种情况下是在&m_str.back()之后的一个位置!然后,在xsputn()streamsize是-1(而不是0),所有事情都会失控。&m_str[0]&m_str[m_str.size()]应该可以工作(即使后者超出了末尾;在C++11中,这样的实现方式必须能够正常工作)。 - Lightness Races in Orbit
坦白地说,使用vector<char>会更加安全(特别是当你冒着COW的风险时,咳嗽 GCC),但对于调用take_str()的人来说,它并不是那么有用。 - Lightness Races in Orbit
我认为在str()中调用setp(在字符串复制和返回之间)应该能完成工作。 如果您有兴趣和/或希望合并我的更改,则以下是我的当前实现:https://pastebin.com/jLZ3TF3b - Lightness Races in Orbit
哎呀,把我不小心留在那里的愚蠢(而且有问题的)xsputn()删掉吧 ;) - Lightness Races in Orbit
1
@ceztko 即使在C++11模式下,libstdc++也有几年使用COW字符串的情况(是的,这是不合规的)。不过自GCC 5以来就没问题了。实际上,我会倾向于使用编译时检查来为符合规范的工具链构建出那个额外的hack。 - Lightness Races in Orbit
显示剩余4条评论

1
更新:面对人们持续的不喜欢这个答案,我想做出修改并解释一下。
  1. 不,没有办法避免字符串复制(stringbuf具有相同的接口)。

  2. 这永远不会有影响。实际上这样更有效率。(我会尝试解释一下)

想象一下编写一个版本的 stringbuf ,它始终保持完美、可移动的 std::string 可用(我确实尝试过这样做)。

添加字符很容易——我们只需在基础字符串上使用 push_back 即可。

好的,但是删除字符(从缓冲区读取)怎么办?我们将不得不移动一些指针来解决我们已经删除的字符,这很好。然而,我们有一个问题——我们正在保持的合同说我们将始终有一个可用的 std::string

每当我们从流中删除字符时,都需要从基础字符串中erase它们。这意味着将所有剩余的字符向下移动(memmove/memcpy)。因为每次控制流离开我们的私有实现时必须保持此约定,所以在实践中,这意味着每次我们在字符串缓冲区上调用getcgets时都必须从字符串中删除字符。这意味着每个<<操作都要调用erase
然后当然还有实现推回缓冲区的问题。如果您将字符推回到基础字符串中,则必须将它们插入到位置0 - 将整个缓冲区向上移动。
长话短说,您可以编写仅用于构建std::string的ostream-only流缓冲区。当基础缓冲区增长时,仍然需要处理所有重新分配,因此最终您只能保存一个字符串副本。因此,也许我们从4个字符串副本(和malloc/free的调用)变成了3个或3个变成了2个。
您还需要处理的问题是,streambuf接口没有分成istreambuf和ostreambuf。这意味着您仍然需要提供输入接口,并且如果有人使用它,则必须抛出异常或断言。这相当于欺骗用户-我们未能实现预期的接口。
为了获得这种微小的性能改进,我们必须付出以下代价:
1.开发一个(考虑到语言环境管理而相当复杂的)软件组件。 2.失去仅支持输出操作的streambuf的灵活性。 3.为未来的开发人员铺设地雷。

6
"String copies on a modern CPU are extremely cheap" 的意思是:现代CPU上的字符串复制非常便宜。但如果我的程序需要解析几十GB文本数据呢?(有时会这样做) - Neil Kirk
2
解析所需时间将比复制长得多,差距非常大。 - Richard Hodges
1
如果速度非常重要,不幸的是,您必须使用C解析。这并不是一个普遍的事实,但它确实更快。 - Neil Kirk
2
@NeilKirk 如果你的程序确实在复制几千兆字节的字符串数据,那么有许多技巧可以在不将所有数据读入内存的情况下迭代处理数据。内存映射文件、直接从输入流转换、批量处理、不进行转换(以二进制格式存储)等等。效率问题几乎总是选择正确算法而不是优化现有算法的问题。 - Richard Hodges
@RichardHodges,我喜欢你提到的谬论。然而,将输出流仅限于预分配的缓冲区显然非常有用。你觉得我在我的回答中使用的 ~4行方法¹(使用Boost)怎么样? - sehe
显示剩余13条评论

0

我对@Kuba的很好的答案进行了一些修改以解决一些问题(不幸的是他目前没有回应)。特别是:

  • 添加了一个safe_pbump来处理64位偏移量;
  • 返回一个string_view而不是string(内部字符串没有正确的缓冲区大小);
  • 在move语义take_str方法中调整string的大小以适应当前缓冲区大小;
  • 修复了take_str方法在返回之前使用init进行move语义操作;
  • 删除了init方法上无用的memcpy
  • 将模板参数char_type重命名为CharT,以避免与basic_streambuf::char_type造成歧义;
  • 使用string::data()和指针算术代替可能导致未定义行为的string::front()string::back(),正如@LightnessRacesinOrbit所指出的;
  • 使用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>;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接