如何将一个字符串向量优雅地合并成一个字符串

127

我正在寻找将字符串向量拼接成一个字符串的最优雅方法。下面是我现在使用的解决方案:

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

还有其他人吗?


2
为什么你把这个函数叫做implode? - Colonel Panic
12
按照PHP中implode()方法的类比,它会将数组元素连接起来并将它们作为单个字符串输出。我想知道你为什么要问这个问题 :) - ezpresso
9
在Python中:'delim.join(elems)'。抱歉,我忍不住了。C++仍然没有附带电池。 :-) 这个问题在2021年已经有10年历史了,但没有一个可行且优雅的答案(尾部分隔符、过高的运行时间、比朴素实现更多的#include行……)。 - Johannes Overmann
23个回答

144

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

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

还可以参考这个问题


138
建议在创建简单字符串时包含并链接大型Boost库是荒谬的。 - Julian
15
大多数项目已经在做这件事了。我同意STL没有包含一种方法来做这件事情是荒谬的,然而。我可能也同意这不应该是最佳答案,但其他答案显然也是可行的。 - River Tam
7
大多数Boost库都是仅包含头文件的,因此没有什么需要链接的。其中一些甚至被纳入标准中。 - jbruni
20
标准库中没有这个基本功能是荒谬的。 - Kiruahxh
@Kiruahxh 在2013年提出,但由于某些原因似乎陷入了停滞状态。链接分别为:提出停滞 - Jason C
显示剩余3条评论

44

14
请记住,使用std::ostream_iterator构造函数的第二个参数在流的末尾会添加额外的分隔符。 - Michael Krelin - hacker
29
“implode”的重点在于不应在最后添加定界符。很遗憾,这个答案在最后添加了该定界符。 - Jonny
幸运的是,我也需要将令牌添加到最后!感谢您提供的解决方案。 - Константин Ван
我需要在implode之后得到实际的字符串表示,所以我使用了imploded.str()来获取一个std::string。此外,对于那些不想要分隔符作为字符串的最后一部分的人,可以在转换为std::string后使用pop_back()方法来删除最后一个字符。感谢您的开端! - rayryeng

29

我喜欢使用这个一行累加器(没有尾随分隔符):

std::accumulate 定义在 <numeric> 中)

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

15
空的时候小心。 - Carlos Pinzón

28

你应该使用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)。

1
或者使用已经编写好的代码:https://dev59.com/BXA75IYBdhLWcg3wDUm2#3497021 - Jerry Coffin

24

因为我喜欢一行代码(它们非常有用,可以用于各种奇怪的东西,正如最后您将看到的),这里是使用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指出的那样,上面的代码在数组以空字符串开头时会出现错误行为,因为缺少对“第一次运行”的检查,这意味着之前的运行没有产生额外字符,并且 - 在所有运行上运行“是第一次运行”的检查很奇怪(即,代码未经过优化)。

如果我们确切知道列表至少有一个元素,则这两个问题的解决方案都很容易。另一方面,如果我们确切知道列表没有至少一个元素,则可以进一步缩短运行时间。

我认为结果代码并不太好看,所以我将其作为正确的解决方案添加在这里,但我认为上面的讨论仍然有价值:

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] 替换向量。
  • 根据评论和聊天中的讨论,lambda 仍然会进行一些复制操作。可以通过使用这个(不太美观的) lambda 函数来将其最小化:[](auto&& a, auto&& b) -> auto& { a += ','; a += b; 返回 a; }),在 GCC 10 上可以将性能提高超过 x10。感谢 @Deduplicator 的建议。我还在试图弄清楚这里发生了什么。

7
不要在字符串中使用accumulate。大多数其他答案的时间复杂度为O(n),但accumulate的时间复杂度为O(n^2),因为它在每个元素附加之前会创建累加器的临时副本。而且,移动语义也无法解决这个问题。 - Oktalist
2
@Oktalist,我不确定你为什么这么说 - http://www.cplusplus.com/reference/numeric/accumulate/ 上写着“复杂度与第一个和最后一个元素之间的距离成线性关系”。 - Guss
1
这是在假设每个单独的加法都需要恒定时间的情况下。如果 T 有一个重载的 operator+(就像 string 一样),或者如果您提供自己的函数对象,那么所有的赌注都会失效。虽然我可能过于草率地说移动语义没有帮助,但它们并不能解决我检查过的两个实现中的问题。请参阅我的答案 类似 问题 - Oktalist
1
每次迭代都会复制?你是说它们都是O(n^2)吗?在我的机器上不是这样的。它们之所以不在每次迭代时复制,与在附加delim时不复制的原因相同,即string append通常是摊销O(1),因为它分配的空间比实际需要的要多得多。这是一个快速的基准测试:http://codepad.org/mfubiiMg - Oktalist
14
我进行了一项基准测试,结果表明累加函数比 O(n) 字符串流更快。 - kirbyfan64sos
显示剩余12条评论

21

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

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为空的检查。 - xtofl
2
@xtofl 你高估了那个检查的成本。 - c z
1
我只是遵循“不为你不使用的东西付费”的规则。在大多数情况下,你是正确的,其中lst不是巨大的。 - xtofl
2
除了 ret.empty() 是一个微不足道的检查之外,这是一个非常适合分支预测器的用例,因为在第一次测试之后它总是会评估为 false。 - Bruce Nielsen

16

使用 fmt,你可以做到。

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

但我不确定join方法是否会包含在std::format中。


至少在C++20中不会出现。 - Franklin Yu
@FranklinYu 他在谈论的是这个fmt库,而不是标准的C++库。 - perrocallcenter
@FranklinYu 他在谈论的是 这个 fmt 库,而不是标准的 C++ 库。 - undefined
@perrocallcenter 我知道。我上面的评论是指“如果加入将成为std::format的一部分”,这是指fmt库的大部分已经添加到C++20中(即std::format),但由于某种原因,fmt::join()被遗漏了。“只需使用fmt库”是一个简单的解决方法,但对于每个团队来说可能并不可行。将类似于fmt::join()的内容添加到<format>仍然很有用。 - Franklin Yu
@perrocallcenter,就像FranklinY所说,std::format(https://en.cppreference.com/w/cpp/header/format)和std::print(https://en.cppreference.com/w/cpp/io/print, C++ 23)都基于fmt库。我原本希望它们也能标准化join,但事实似乎并非如此。看来它们只为views标准化了join(https://en.cppreference.com/w/cpp/23)。 - andreas777

12
string join(const vector<string>& vec, const char* delim)
{
    stringstream res;
    copy(vec.begin(), vec.end(), ostream_iterator<string>(res, delim));
    return res.str();
}

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


你不需要每次都进行检查。只需在循环外添加第一个元素,并从第二个元素开始循环即可... - Jason C
我不明白为什么你要添加那个。这个函数中没有循环,而且 accumulate 已经接收了第一个元素并被告知从第二个元素开始... - xtofl
1
我的意思是:“特别是在处理大型集合时,您希望避免检查是否仍在添加第一个元素,以确保没有尾随分隔符。” 您可以通过将第一个元素从循环中提取出来来避免在您所述的循环方法中进行此检查。抱歉,我有点含糊不清;我是在评论前提,而不是解决方案。您提供的解决方案非常好。 - Jason C
我赞同你的想法。相关链接:https://dev59.com/AHVC5IYBdhLWcg3w4VVz。 - xtofl

7

通常我会建议按照顶部回答的方式使用Boost,但是我认识到在某些项目中这可能不被允许。

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

现代C++现在有一种方法可以使用std::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++需要多现代化?我尝试使用set(CMAKE_CXX_STANDARD 11)#include <experimental/iterator>,但出现了“error: 'std::experimental' has not been declared”的错误。 - sdbbs
C++需要多现代化?我尝试使用set(CMAKE_CXX_STANDARD 11)#include <experimental/iterator>,但出现了“error: 'std::experimental' has not been declared”的错误。 - undefined
你的编译器需要支持库基础 TS v2。在撰写本文时,其中的部分已合并到C++17和C++20中,但ostream_joiner尚未成为包括C++23在内的任何现代标准的一部分。我知道至少最近版本的GCC支持它 - 你可能需要使用-std=gnu++2b进行构建。 - Riot

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