【问题标题】:How to implode a vector of strings into a string (the elegant way)如何将字符串向量内爆成字符串(优雅的方式)
【发布时间】:2011-08-07 00:52:03
【问题描述】:

我正在寻找将字符串向量内爆为字符串的最优雅方法。以下是我现在使用的解决方案:

static std::string& implode(const std::vector<std::string>& elems, char delim, std::string& s)
{
    for (std::vector<std::string>::const_iterator ii = elems.begin(); ii != elems.end(); ++ii)
    {
        s += (*ii);
        if ( ii + 1 != elems.end() ) {
            s += delim;
        }
    }

    return s;
}

static std::string implode(const std::vector<std::string>& elems, char delim)
{
    std::string s;
    return implode(elems, delim, s);
}

还有其他人吗?

【问题讨论】:

  • 为什么称这个函数为内爆?
  • @ColonelPanic,类似于 PHP 的 implode() 方法,它连接数组元素并将它们作为单个字符串输出。我想知道你为什么要问这个问题:)
  • 在 Python 中:'delim.join(elems)'。对不起,无法抗拒。 C++ 仍然没有包含电池。 :-) 问题在 2021 年已经 10 岁了,而且没有一个有效的 优雅的答案(尾随分隔符、过多的运行时间、更多 #include 行,而幼稚的实现......)

标签: c++ string stl stdstring implode


【解决方案1】:

你应该使用std::ostringstream而不是std::string来构建输出(然后你可以在最后调用它的str()方法来得到一个字符串,所以你的接口不需要改变,只有临时的s) .

从那里,您可以改为使用std::ostream_iterator,如下所示:

copy(elems.begin(), elems.end(), ostream_iterator<string>(s, delim)); 

但这有两个问题:

  1. delim 现在需要是 const char*,而不是单个 char。没什么大不了的。
  2. std::ostream_iterator 在每个元素之后写入分隔符,包括最后一个元素。所以你要么需要在最后删除最后一个,要么编写你自己的迭代器版本,它没有这种烦恼。如果你有很多代码需要这样的东西,那么后者是值得的;否则最好避免整个混乱(即使用ostringstream而不是ostream_iterator)。

【讨论】:

【解决方案2】:
std::vector<std::string> strings;

const char* const delim = ", ";

std::ostringstream imploded;
std::copy(strings.begin(), strings.end(),
           std::ostream_iterator<std::string>(imploded, delim));

(包括&lt;string&gt;&lt;vector&gt;&lt;sstream&gt;&lt;iterator&gt;

If you want to have a clean end (no trailing delimiter) have a look here

【讨论】:

  • 请记住,它会在流的末尾添加额外的分隔符(std::ostream_iterator 构造函数的第二个参数。
  • “内爆”的重点是最后不要添加分隔符。不幸的是,这个答案最后添加了分隔符。
  • 幸运的是,我还需要最后添加令牌!感谢您的解决方案。
【解决方案3】:

略长的解决方案,但不使用std::ostringstream,并且不需要破解来删除最后一个分隔符。

http://www.ideone.com/hW1M9

还有代码:

struct appender
{
  appender(char d, std::string& sd, int ic) : delim(d), dest(sd), count(ic)
  {
    dest.reserve(2048);
  }

  void operator()(std::string const& copy)
  {
    dest.append(copy);
    if (--count)
      dest.append(1, delim);
  }

  char delim;
  mutable std::string& dest;
  mutable int count;
};

void implode(const std::vector<std::string>& elems, char delim, std::string& s)
{
  std::for_each(elems.begin(), elems.end(), appender(delim, s, elems.size()));
}

【讨论】:

    【解决方案4】:

    使用std::accumulate的版本:

    #include <numeric>
    #include <iostream>
    #include <string>
    
    struct infix {
      std::string sep;
      infix(const std::string& sep) : sep(sep) {}
      std::string operator()(const std::string& lhs, const std::string& rhs) {
        std::string rz(lhs);
        if(!lhs.empty() && !rhs.empty())
          rz += sep;
        rz += rhs;
        return rz;
      }
    };
    
    int main() {
      std::string a[] = { "Hello", "World", "is", "a", "program" };
      std::string sum = std::accumulate(a, a+5, std::string(), infix(", "));
      std::cout << sum << "\n";
    }
    

    【讨论】:

      【解决方案5】:

      使用boost::algorithm::join(..):

      #include <boost/algorithm/string/join.hpp>
      ...
      std::string joinedString = boost::algorithm::join(elems, delim);
      

      另见this question

      【讨论】:

      • 建议包含和链接庞大的 boost 库以创建一个简单的字符串是荒谬的。
      • @Julian 大多数项目已经这样做了。但是,我同意 STL 不包含执行此操作的方法是荒谬的。我可能也同意这不应该是 top 答案,但其他答案显然是可用的。
      • 我同意@Julian。 Boost 使用起来可能很优雅,但就开销而言,它绝不是“最优雅的方式”。在这种情况下,这是 OP 算法的解决方法,而不是问题本身的解决方案。
      • 大多数 Boost 库都是只有头文件的,所以没有什么要链接的。有些甚至进入了标准。
      • 在 stdlib 中没有这个基本功能是荒谬的。
      【解决方案6】:

      因为我喜欢单线(它们对于各种奇怪的东西都非常有用,正如您将在最后看到的那样),这里有一个使用 std::accumulate 和 C++11 lambda 的解决方案:

      std::accumulate(alist.begin(), alist.end(), std::string(), 
          [](const std::string& a, const std::string& b) -> std::string { 
              return a + (a.length() > 0 ? "," : "") + b; 
          } )
      

      我发现这种语法对流操作符很有用,我不想让各种奇怪的逻辑超出流操作的范围,只是为了做一个简单的字符串连接。例如,考虑使用流运算符(使用 std;)格式化字符串的方法的返回语句:

      return (dynamic_cast<ostringstream&>(ostringstream()
          << "List content: " << endl
          << std::accumulate(alist.begin(), alist.end(), std::string(), 
              [](const std::string& a, const std::string& b) -> std::string { 
                  return a + (a.length() > 0 ? "," : "") + b; 
              } ) << endl
          << "Maybe some more stuff" << endl
          )).str();
      

      更新:

      正如@plexando 在 cmets 中指出的那样,当数组以空字符串开头时,上述代码会出现异常行为,因为“第一次运行”的检查缺少以前没有额外字符的运行,而且 - 在所有运行时检查“首次运行”是很奇怪的(即代码未优化)。

      如果我们知道列表至少有一个元素,那么这两个问题的解决方案就很容易了。 OTOH,如果我们知道列表没有至少有一个元素,那么我们可以进一步缩短运行时间。

      我认为生成的代码不是那么漂亮,所以我在这里将其添加为正确的解决方案,但我认为上面的讨论仍然有优点:

      alist.empty() ? "" : /* leave early if there are no items in the list */
        std::accumulate( /* otherwise, accumulate */
          ++alist.begin(), alist.end(), /* the range 2nd to after-last */
          *alist.begin(), /* and start accumulating with the first item */
          [](auto& a, auto& b) { return a + "," + b; });
      

      注意事项:

      • 对于支持直接访问第一个元素的容器,最好将它用于第三个参数,因此alist[0] 用于向量。
      • 根据 cmets 和聊天中的讨论,lambda 仍然会进行一些复制。这可以通过使用这个(不太漂亮的)lambda 来最小化:[](auto&amp;&amp; a, auto&amp;&amp; b) -&gt; auto&amp; { a += ','; a += b; return a; })(在 GCC 10 上)将性能提高了 10 倍以上。感谢@Deduplicator 的建议。我仍在试图弄清楚这里发生了什么。

      【讨论】:

      • 不要将accumulate 用于字符串。大多数其他答案是 O(n) 但accumulate 是 O(n^2) 因为它在附加每个元素之前制作了累加器的临时副本。不,移动语义没有帮助。
      • @Oktalist,我不知道你为什么这么说 - cplusplus.com/reference/numeric/accumulate 说“复杂性在第一个和最后一个之间的距离是线性的”。
      • 这是假设每个单独的添加都需要恒定的时间。如果T 有一个重载的operator+(就像string 一样),或者如果你提供了你自己的函子,那么所有的赌注都没有了。尽管我可能仓促地说移动语义没有帮助,但它们并没有解决我检查过的两个实现中的问题。查看我对similarquestions 的回答。
      • skwllsp 的评论与此无关。就像我说的那样,大多数其他答案(以及 OP 的implode 示例)都在做正确的事情。它们是 O(n),即使它们没有在字符串上调用 reserve。只有使用累积的解决方案是 O(n^2)。不需要 C 风格的代码。
      • 我做了一个benchmark,accumulate 实际上比 O(n) 字符串流快。
      【解决方案7】:
      string join(const vector<string>& vec, const char* delim)
      {
          stringstream res;
          copy(vec.begin(), vec.end(), ostream_iterator<string>(res, delim));
          return res.str();
      }
      

      【讨论】:

        【解决方案8】:

        这是另一个没有在最后一个元素后添加分隔符的:

        std::string concat_strings(const std::vector<std::string> &elements,
                                   const std::string &separator)
        {       
            if (!elements.empty())
            {
                std::stringstream ss;
                auto it = elements.cbegin();
                while (true)
                {
                    ss << *it++;
                    if (it != elements.cend())
                        ss << separator;
                    else
                        return ss.str();
                }       
            }
            return "";
        

        【讨论】:

          【解决方案9】:

          这是我用的,简单灵活

          string joinList(vector<string> arr, string delimiter)
          {
              if (arr.empty()) return "";
          
              string str;
              for (auto i : arr)
                  str += i + delimiter;
              str = str.substr(0, str.size() - delimiter.size());
              return str;
          }
          

          使用:

          string a = joinList({ "a", "bbb", "c" }, "!@#");
          

          输出:

          a!@#bbb!@#c
          

          【讨论】:

            【解决方案10】:

            尤其是对于较大的集合,您希望避免检查是否仍在添加第一个元素以确保没有尾随分隔符...

            所以对于空的或者单元素的列表,根本就没有迭代。

            空范围很简单:返回 ""。

            单元素或多元素都可以完美处理accumulate

            auto join = [](const auto &&range, const auto separator) {
                if (range.empty()) return std::string();
            
                return std::accumulate(
                     next(begin(range)), // there is at least 1 element, so OK.
                     end(range),
            
                     range[0], // the initial value
            
                     [&separator](auto result, const auto &value) {
                         return result + separator + value;
                     });
            };
            

            运行示例(需要 C++14):http://cpp.sh/8uspd

            【讨论】:

            • 你永远不需要每次都检查。只需在循环外添加第一个元素,然后在第二个开始循环...
            • 我不明白你为什么要添加它。此函数中没有循环,accumulate 确实接收第一个元素并被告知从第二个元素开始...
            • 我的意思是:“特别是对于较大的集合,您希望避免检查是否仍在添加第一个元素以确保没有尾随分隔符。” - - 通过将第一个元素拉出循环,您可以避免在语句引用的循环方法中检查这一点。对不起,我有点含糊;我评论的是前提,而不是解决方案。您提供的解决方案非常好。
            • 我同意你的想法。相关:stackoverflow.com/questions/156650/….
            【解决方案11】:

            那么简单愚蠢的解决方案呢?

            std::string String::join(const std::vector<std::string> &lst, const std::string &delim)
            {
                std::string ret;
                for(const auto &s : lst) {
                    if(!ret.empty())
                        ret += delim;
                    ret += s;
                }
                return ret;
            }
            

            【讨论】:

            • 我希望编译器足够聪明,可以在每次迭代中取消对ret为空的检查。
            【解决方案12】:

            将这个answer 的一部分用于另一个问题会给你一个加入这个,基于没有尾随逗号的分隔符,

            用法:

            std::vector<std::string> input_str = std::vector<std::string>({"a", "b", "c"});
            std::string result = string_join(input_str, ",");
            printf("%s", result.c_str());
            /// a,b,c
            

            代码:

            std::string string_join(const std::vector<std::string>& elements, const char* const separator)
            {
                switch (elements.size())
                {
                    case 0:
                        return "";
                    case 1:
                        return elements[0];
                    default:
                        std::ostringstream os;
                        std::copy(elements.begin(), elements.end() - 1, std::ostream_iterator<std::string>(os, separator));
                        os << *elements.rbegin();
                        return os.str();
                }
            }
            

            【讨论】:

              【解决方案13】:

              我喜欢使用这种单行累加(没有尾随分隔符):

              std::accumulate(
                  std::next(elems.begin()), 
                  elems.end(), 
                  elems[0], 
                  [](std::string a, std::string b) {
                      return a + delimiter + b;
                  }
              );
              

              【讨论】:

              • 空的时候要小心。
              【解决方案14】:

              使用三元运算符?: 的可能解决方案。

              std::string join(const std::vector<std::string> & v, const std::string & delimiter = ", ") {
                  std::string result;
              
                  for (size_t i = 0; i < v.size(); ++i) {
                      result += (i ? delimiter : "") + v[i]; 
                  }
              
                  return result;
              }
              

              join({"2", "4", "5"}) 会给你2, 4, 5

              【讨论】:

                【解决方案15】:

                使用 fmt 你可以做到。

                #include <fmt/format.h>
                auto s = fmt::format("{}",fmt::join(elems,delim)); 
                

                但我不知道 join 是否会变成 std​​::format。

                【讨论】:

                  【解决方案16】:

                  这可以使用 boost 解决

                  #include <boost/range/adaptor/filtered.hpp>
                  #include <boost/algorithm/string/join.hpp>
                  #include <boost/algorithm/algorithm.hpp>
                  
                  std::vector<std::string> win {"Stack", "", "Overflow"};
                  const std::string Delimitor{","};
                  
                  const std::string combined_string = 
                                    boost::algorithm::join(win |
                                           boost::adaptors::filtered([](const auto &x) {
                                                                        return x.size() != 0;
                                                                        }), Delimitor);
                  
                  Output:
                  
                  combined_string: "Stack,Overflow"
                  

                  【讨论】:

                    【解决方案17】:

                    另一个简单而好的解决方案是使用ranges v3。当前版本是 C++14 或更高版本,但也有 C++11 或更高版本的旧版本。不幸的是,C++20 范围没有intersperse 函数。

                    这种方法的好处是:

                    • 优雅
                    • 轻松处理空字符串
                    • 处理列表的最后一个元素
                    • 效率。因为范围是惰性求值的。
                    • 小而实用的库

                    功能分解(Reference):

                    • accumulate = 类似于std::accumulate,但参数是范围和初始值。还有一个可选的第三个参数是操作符函数。
                    • filter = 与std::filter 一样,过滤不符合谓词的元素。
                    • intersperse = 关键功能!在范围输入元素之间散布分隔符。
                    #include <iostream>
                    #include <string>
                    #include <vector>
                    #include <range/v3/numeric/accumulate.hpp>
                    #include <range/v3/view/filter.hpp>
                    #include <range/v3/view/intersperse.hpp>
                    
                    int main()
                    {
                        using namespace ranges;
                        // Can be any std container
                        std::vector<std::string> a{ "Hello", "", "World", "is", "", "a", "program" };
                        
                        std::string delimiter{", "};
                        std::string finalString = 
                            accumulate(a | views::filter([](std::string s){return !s.empty();})
                                         | views::intersperse(delimiter)
                                      , std::string());
                        std::cout << finalString << std::endl; // Hello, World, is, a, program
                    }
                    

                    【讨论】:

                      【解决方案18】:

                      虽然我通常会根据最佳答案推荐使用 Boost,但我承认在某些项目中这是不希望的。

                      使用 std::ostream_iterator 建议的 STL 解决方案将无法按预期工作 - 它会在末尾附加一个分隔符。

                      现在有一种方法可以用现代 C++ 做到这一点,但是,使用 https://en.cppreference.com/w/cpp/experimental/ostream_joiner

                      std::ostringstream outstream;
                      std::copy(strings.begin(),
                                strings.end(),
                                std::experimental::make_ostream_joiner(outstream, delimiter.c_str()));
                      return outstream.str();
                      

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2022-01-19
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2011-04-25
                        • 2013-08-13
                        相关资源
                        最近更新 更多