【问题标题】:C++ equivalent of StringBuffer/StringBuilder?C++ 等价于 StringBuffer/StringBuilder?
【发布时间】:2023-03-04 23:17:02
【问题描述】:

是否有提供高效字符串连接功能的 C++ 标准模板库类,类似于 C# 的 StringBuilder 或 Java 的 StringBuffer

【问题讨论】:

  • 简短的回答是:是的,STL 有一个类,它是std::ostringstream
  • 嘿@andrew。你能改变接受的答案吗?有一个明确的获胜答案,但不是当前公认的答案。
  • @null 已经完成了!

标签: c++ stl string-concatenation


【解决方案1】:

C++ 方法是使用std::stringstream 或只是简单的字符串连接。 C++ 字符串是可变的,因此连接的性能考虑不太重要。

关于格式化,您可以对流进行所有相同的格式化,但 in a different way, similar to cout。或者你可以使用一个强类型的函子来封装它并提供一个类似 String.Format 的接口,例如boost::format

【讨论】:

  • C++ 字符串是可变的:完全正确。 StringBuilder 存在的全部原因是 cover the inefficiency of Java's immutable basic String type。换句话说,StringBuilder 是拼凑而成,所以我们应该庆幸我们在 C++ 中不需要这样的类。
  • @bobobobo 不可变字符串还有其他好处,它是课程的好马
  • 不要纯字符串连接创建一个新对象,所以和Java中的不变性问题一样吗?考虑以下示例中的所有变量都是字符串:a = b + c + d + e + f;不是要在 b 和 c 上调用 operator+,然后在结果和 d 上调用 operator+ 等等吗?
  • 等一下,标准的字符串类知道如何改变自己,但这并不意味着不存在低效率。据我所知,std::string 不能简单地扩展其内部 char* 的大小。这意味着以需要更多字符的方式对其进行变异需要重新分配和复制。它与字符向量没有什么不同,在这种情况下保留所需的空间当然更好。
  • @TrygveSkogsholm - 它与字符向量没有什么不同,但字符串的“容量”当然可以大于其大小,因此并非所有附加都需要重新分配。一般来说,字符串将使用指数增长策略,因此附加仍然摊销到线性成本操作。这与 Java 的不可变字符串不同,Java 的每个追加操作都需要将两个字符串中的所有字符复制到一个新字符串中,因此一系列追加通常以 O(n) 结束。
【解决方案2】:

std::string.append 函数不是一个好的选择,因为它不接受多种形式的数据。更有用的替代方法是使用std::stringstream;像这样:

#include <sstream>
// ...

std::stringstream ss;

//put arbitrary formatted data into the stream
ss << 4.5 << ", " << 4 << " whatever";

//convert the stream buffer into a string
std::string str = ss.str();

【讨论】:

    【解决方案3】:

    注意这个答案最近受到了一些关注。我不提倡将此作为解决方案(这是我在过去看到的解决方案,在 STL 之前)。这是一种有趣的方法,只有在分析代码后发现这有所改进时,才应将其应用于 std::stringstd::stringstream

    我通常使用std::stringstd::stringstream。我从来没有遇到过这些问题。如果我事先知道字符串的粗略大小,我通常会先预留一些空间。

    我曾看到其他人在遥远的过去制作了自己的优化字符串生成器。

    class StringBuilder {
    private:
        std::string main;
        std::string scratch;
    
        const std::string::size_type ScratchSize = 1024;  // or some other arbitrary number
    
    public:
        StringBuilder & append(const std::string & str) {
            scratch.append(str);
            if (scratch.size() > ScratchSize) {
                main.append(scratch);
                scratch.resize(0);
            }
            return *this;
        }
    
        const std::string & str() {
            if (scratch.size() > 0) {
                main.append(scratch);
                scratch.resize(0);
            }
            return main;
        }
    };
    

    它使用两个字符串,一个用于大部分字符串,另一个用作连接短字符串的暂存区。它通过将短附加操作批处理到一个小字符串中然后将其附加到主字符串来优化附加,从而减少主字符串变大时所需的重新分配次数。

    std::stringstd::stringstream 不需要这个技巧。我认为它是在 std::string 之前与第三方字符串库一起使用的,那是很久以前的事了。如果您采用这种策略,请先配置您的应用程序。

    【讨论】:

    • 重新发明轮子。 std::stringstream 是正确的答案。请参阅下面的好答案。
    • @Kobor42 我同意你的观点,正如我在答案的第一行和最后一行指出的那样。
    • 我认为scratch 字符串在这里并没有真正完成任何事情。主字符串的重新分配数量很大程度上取决于它的最终大小,而不是附加操作的数量,除非string 实现真的很差(即不使用指数增长)。所以“批处理”append 并没有帮助,因为一旦底层的string 很大,它只会偶尔增长。最重要的是,它添加了一堆冗余复制操作,并且可能会更多重新分配(因此调用new/delete),因为您要附加到一个短字符串。
    • @BeeOnRope 我同意你的看法。
    • 我很确定str.reserve(1024); 会比这东西更快
    【解决方案4】:

    std::string C++ 等价物:它是可变的。

    【讨论】:

      【解决方案5】:

      您可以使用 .append() 来简单地连接字符串。

      std::string s = "string1";
      s.append("string2");
      

      我认为你甚至可以做到:

      std::string s = "string1";
      s += "string2";
      

      至于C#的StringBuilder的格式化操作,我相信snprintf(或者sprintf,如果你想冒险写错误代码;-))到字符数组并转换回字符串是唯一的选项。

      【讨论】:

      • 与 printf 或 .NET 的 String.Format 不同,是吗?
      • 说他们是唯一的方法有点不诚实
      • @jk - 在比较 .NET 的 StringBuilder 的格式化能力时,它们是唯一的方法,这是原始问题专门提出的。我确实说过“我相信”,所以我可能是错的,但你能告诉我一种在不使用 printf 的情况下在 C++ 中获得 StringBuilder 功能的方法吗?
      • 更新了我的答案以包含一些替代格式选项
      【解决方案6】:

      由于 C++ 中的 std::string 是可变的,因此您可以使用它。它有一个+= operator 和一个append 函数。

      如果您需要附加数字数据,请使用std::to_string 函数。

      如果您想以能够将任何对象序列化为字符串的形式获得更大的灵活性,请使用std::stringstream 类。但是您需要实现自己的流式操作符函数,以便它与您自己的自定义类一起使用。

      【讨论】:

        【解决方案7】:

        一个方便的 c++ 字符串生成器

        就像许多人之前回答的那样,std::stringstream 是首选方法。 它工作得很好,并且有很多转换和格式化选项。 IMO 虽然它有一个非常不方便的缺陷:你不能将它用作一个衬里或作为一种表达方式。 你总是要写:

        std::stringstream ss;
        ss << "my data " << 42;
        std::string myString( ss.str() );
        

        这很烦人,尤其是当你想在构造函数中初始化字符串时。

        原因是,a) std::stringstream 没有到 std::string 的转换运算符和 b) stringstream 的运算符

        解决方案是重写 std::stringstream 并为其提供更好的匹配运算符:

        namespace NsStringBuilder {
        template<typename T> class basic_stringstream : public std::basic_stringstream<T>
        {
        public:
            basic_stringstream() {}
        
            operator const std::basic_string<T> () const                                { return std::basic_stringstream<T>::str();                     }
            basic_stringstream<T>& operator<<   (bool _val)                             { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (char _val)                             { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (signed char _val)                      { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (unsigned char _val)                    { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (short _val)                            { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (unsigned short _val)                   { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (int _val)                              { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (unsigned int _val)                     { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (long _val)                             { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (unsigned long _val)                    { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (long long _val)                        { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (unsigned long long _val)               { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (float _val)                            { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (double _val)                           { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (long double _val)                      { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (void* _val)                            { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (std::streambuf* _val)                  { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (std::ostream& (*_val)(std::ostream&))  { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (std::ios& (*_val)(std::ios&))          { std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (std::ios_base& (*_val)(std::ios_base&)){ std::basic_stringstream<T>::operator << (_val); return *this; }
            basic_stringstream<T>& operator<<   (const T* _val)                         { return static_cast<basic_stringstream<T>&>(std::operator << (*this,_val)); }
            basic_stringstream<T>& operator<<   (const std::basic_string<T>& _val)      { return static_cast<basic_stringstream<T>&>(std::operator << (*this,_val.c_str())); }
        };
        
        typedef basic_stringstream<char>        stringstream;
        typedef basic_stringstream<wchar_t>     wstringstream;
        }
        

        有了这个,你可以写出类似的东西

        std::string myString( NsStringBuilder::stringstream() << "my data " << 42 )
        

        即使在构造函数中。

        我不得不承认我没有测量性能,因为我还没有在大量使用字符串构建的环境中使用它,但我认为它不会比 std::stringstream 差多少,因为一切都是通过引用完成的(除了转换为字符串,但这也是 std::stringstream 中的复制操作)

        【讨论】:

        • 这很整洁。我不明白为什么std::stringstream 不这样做。
        【解决方案8】:

        std::string 的 += 不适用于 const char* (似乎是“要添加的字符串”之类的东西),所以绝对使用 stringstream 是最接近所需的 - 你只需使用

        【讨论】:

          【解决方案9】:

          Rope 容器可能是值得的,如果必须将字符串插入/删除到目标字符串的随机位置或长字符序列。 以下是 SGI 的实现示例:

          crope r(1000000, 'x');          // crope is rope<char>. wrope is rope<wchar_t>
                                          // Builds a rope containing a million 'x's.
                                          // Takes much less than a MB, since the
                                          // different pieces are shared.
          crope r2 = r + "abc" + r;       // concatenation; takes on the order of 100s
                                          // of machine instructions; fast
          crope r3 = r2.substr(1000000, 3);       // yields "abc"; fast.
          crope r4 = r2.substr(1000000, 1000000); // also fast.
          reverse(r2.mutable_begin(), r2.mutable_end());
                                          // correct, but slow; may take a
                                          // minute or more.
          

          【讨论】:

            【解决方案10】:

            由于以下原因,我想添加一些新内容:

            第一次尝试失败

            std::ostringstreamoperator&lt;&lt;

            效率,但通过更多尝试,我能够制作在某些情况下更快的 StringBuilder。

            每次我附加一个字符串时,我都会在某处存储对它的引用并增加总大小的计数器。

            我最终实现它的真正方法(恐怖!)是使用不透明缓冲区(std::vector ):

            • 1 字节标头(2 位来判断后面的数据是:移动的字符串、字符串还是字节[])
            • 6 位来判断 byte[] 的长度

            对于字节[]

            • 我直接存储短字符串字节(用于顺序内存访问)

            对于移动的字符串(字符串附加std::move

            • 指向std::string 对象的指针(我们拥有所有权)
            • 如果有未使用的保留字节,则在类中设置一个标志

            字符串

            • 指向std::string 对象的指针(无所有权)

            还有一个小的优化,如果最后插入的字符串被移动,它会检查空闲的保留但未使用的字节,并在其中存储更多字节而不是使用不透明缓冲区(这是为了节省一些内存,它实际上使它稍微慢一些,可能还取决于 CPU,而且很少看到带有额外保留空间的字符串)

            这终于比std::ostringstream 稍微快了一点,但也没有什么缺点:

            • 我假设是固定长度的 char 类型(所以 1,2 或 4 个字节,不适合 UTF8),我并不是说它不适用于 UTF8,只是我没有检查它是否懒惰。
            • 我使用了糟糕的编码习惯(不透明的缓冲区,容易让它不便携,顺便说一下我相信我的是便携的)
            • 缺少ostringstream 的所有功能
            • 如果在合并所有字符串之前删除了某个引用的字符串:未定义的行为。

            结论?采用 std::ostringstream

            它已经解决了最大的瓶颈,而在我的实施中获得几个 % 点的速度是不值得的。

            【讨论】:

              猜你喜欢
              • 2013-11-24
              • 1970-01-01
              • 2017-05-27
              • 2011-02-27
              • 2014-10-06
              • 2015-12-13
              • 2011-04-16
              • 1970-01-01
              • 2011-06-06
              相关资源
              最近更新 更多