如何将字符串向量内爆成字符串(优雅的方式)

Posted

技术标签:

【中文标题】如何将字符串向量内爆成字符串(优雅的方式)【英文标题】: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 行,而幼稚的实现......) 【参考方案1】:

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

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

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

但这有两个问题:

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

【讨论】:

或者使用已经写好的:***.com/questions/3496982/…【参考方案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 确实接收第一个元素并被告知从第二个元素开始... 我的意思是:“特别是对于较大的集合,您希望避免检查是否仍在添加第一个元素以确保没有尾随分隔符。” - - 通过将第一个元素拉出循环,您可以避免在语句引用的循环方法中检查这一点。对不起,我有点含糊;我评论的是前提,而不是解决方案。您提供的解决方案非常好。 我同意你的想法。相关:***.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();

【讨论】:

以上是关于如何将字符串向量内爆成字符串(优雅的方式)的主要内容,如果未能解决你的问题,请参考以下文章

按特定顺序选择特定行[重复]

在社交网络的数据库中存储朋友

如何将内爆函数合并到此列表中

C ++:终止遍历字符串向量的循环的最优雅方法?

将数组内爆为来自 mysql 查询的逗号分隔字符串

如何以优雅有效的方式将无符号/有符号整数/长整数转换为 C 字符串?