如何从std::vector<string>构建一个std::string?

65
我想用一个 std::vector<std::string> 构建一个 std::string。我可以使用 std::stringstream,但是想象一下还有更简短的方法:
std::string string_from_vector(const std::vector<std::string> &pieces) {
  std::stringstream ss;

  for(std::vector<std::string>::const_iterator itr = pieces.begin();
      itr != pieces.end();
      ++itr) {
    ss << *itr;
  }

  return ss.str();
}

还有什么其他方法可以做到这一点?


1
也许是 std::string res; for (...) { res += *it; } - user529758
8个回答

123

C++03

std::string s;
for (std::vector<std::string>::const_iterator i = v.begin(); i != v.end(); ++i)
    s += *i;
return s;

C++11 (MSVC 2010子集)

std::string s;
std::for_each(v.begin(), v.end(), [&](const std::string &piece){ s += piece; });
return s;

C++11

std::string s;
for (const auto &piece : v) s += piece;
return s;

请勿使用std::accumulate进行字符串连接,这是一个经典的 Schlemiel the Painter's algorithm ,甚至比在C中使用strcat更糟糕。没有C++11移动语义,每个向量元素都会产生两个不必要的累加器副本。即使具有移动语义,每个元素仍然会产生一个不必要的累加器副本。
上面三个示例为O(n)
对于字符串,std::accumulateO(n²)

You could make std::accumulate O(n) for strings by supplying a custom functor:

std::string s = std::accumulate(v.begin(), v.end(), std::string{},
    [](std::string &s, const std::string &piece) -> decltype(auto) { return s += piece; });

Note that s must be a reference to non-const, the lambda return type must be a reference (hence decltype(auto)), and the body must use += not +.

C++20

在预计成为C++20的当前草案中,std::accumulate的定义已被修改,在向累加器添加内容时使用了std::move,因此从C++20开始,对于字符串,accumulate将成为O(n),并且可以用作一行代码:

std::string s = std::accumulate(v.begin(), v.end(), std::string{});

3
我喜欢FP的方式,但目前看起来有些奇怪。真的很期待C++20能够澄清这些问题! - Mathemagician
1
你是怎么想到调用 operator+= 会导致线性增长的呢?除非你预留了 string 的容量,否则每次追加操作都可能需要重新定位内容。这对于 +=appendaccumulate 都是一样的——它们都可能是 O(n²) 的。 - ABaumstumpf
1
@ABaumstumpf 在所有主要实现中,追加单个字符的摊销复杂度为O(1)。它们通过指数增长因子增加容量,使得随着字符串增长,重新分配的频率缩小。因此,在追加许多字符后,由于重新分配而复制的总字符数与追加字符数成比例。accumulate的复杂度是 O(n²),这将是一种非常病态的最坏情况,O(n)是平均情况。将大小相加以调用reserve可能是一种优化或一种悲观情况,具体取决于情况。 - Oktalist
@Oktalist "在所有主要的实现中。" 但是,标准并不保证它,因此您声称的O(n)很可能是O(n²)。另一方面,使用reserve可以保证它为O(n)。 - ABaumstumpf
C++20的std::accumulate是否确认使用std::move? - kwaalaateimaa
显示剩余2条评论

40
你可以使用来自头文件<numeric>std::accumulate()标准函数(它能够正常工作是因为对于string类型定义了一个重载的operator +,它返回其两个参数的连接结果):
#include <vector>
#include <string>
#include <numeric>
#include <iostream>

int main()
{
    std::vector<std::string> v{"Hello, ", " Cruel ", "World!"};
    std::string s;
    s = accumulate(begin(v), end(v), s);
    std::cout << s; // Will print "Hello, Cruel World!"
}

或者,您可以使用更高效、更小的 for 循环:

#include <vector>
#include <string>
#include <iostream>

int main()
{
    std::vector<std::string> v{"Hello, ", "Cruel ", "World!"};
    std::string result;
    for (auto const& s : v) { result += s; }
    std::cout << result; // Will print "Hello, Cruel World!"
}

11
可爱但不够细心。它将为每个操作分配一个新字符串,因为它使用 operator+ 生成新字符串,而不是使用 operator+= 修改现有字符串。 - Benjamin Lindley
1
我已经编写了一个库,使用它只需 s = v | sum();,它在内部使用 += 而不是 + ;-) - Nawaz
@PSIAlt:啥?这句话少了些词吧?因为我不理解它。 - Benjamin Lindley
1
@PSIAlt:请再读一遍,每个操作都会为其分配一个新的字符串,因为每个操作都会_生成一个新的字符串_。加倍大小优化不会影响这一点。BenjaminLindley:他谈论的是当你往字符串中放入太多内容时,它会“加倍”容量的方式。 - Mooing Duck
使用GNU C++ 2011 v5.3.1,我在使用该示例时遇到了错误:' 在函数'int main()'中 错误:在此作用域中未声明's' std::cout << s; // 将打印“Hello, Cruel World!”' - John Greene
显示剩余6条评论

13

我个人的选择会是基于范围的for循环,就像Oktalist的回答中所示。

Boost也提供了一个不错的解决方案:

#include <boost/algorithm/string/join.hpp>
#include <iostream>
#include <vector>

int main() {

    std::vector<std::string> v{"first", "second"};

    std::string joined = boost::algorithm::join(v, ", ");

    std::cout << joined << std::endl;
}

这会打印出:

first, second


8

为什么不直接使用operator +将它们加在一起呢?

std::string string_from_vector(const std::vector<std::string> &pieces) {
   return std::accumulate(pieces.begin(), pieces.end(), std::string(""));
}

默认情况下,std::accumulate使用std::plus,在C++中将两个字符串相加是连接操作,因为std::string重载了运算符+。


我会将该函数命名为 to_string 而不是 string_from_vector - Nawaz
2
我也可能会这么做,但那是原始问题中使用的名称。 - bstamour

5
谷歌Abseil有一个函数absl::StrJoin可以做你需要的事情。
他们header文件的例子。 请注意,分隔符也可以是""
//   std::vector<std::string> v = {"foo", "bar", "baz"};
//   std::string s = absl::StrJoin(v, "-");
//   EXPECT_EQ("foo-bar-baz", s);

3
如果不需要末尾空格,可以使用<numeric>中定义的带有自定义连接lambda的accumulate函数。
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;


int main() {
    vector<string> v;
    string s;

    v.push_back(string("fee"));
    v.push_back(string("fi"));
    v.push_back(string("foe"));
    v.push_back(string("fum"));

    s = accumulate(begin(v), end(v), string(),
                   [](string lhs, const string &rhs) { return lhs.empty() ? rhs : lhs + ' ' + rhs; }
    );
    cout << s << endl;
    return 0;
}

输出:

fee fi foe fum

3

我有点晚来参加聚会,但我喜欢我们可以使用初始化列表的事实:

std::string join(std::initializer_list<std::string> i)
{
  std::vector<std::string> v(i);
  std::string res;
  for (const auto &s: v) res += s;
  return res;   
}

那么您可以简单地调用(Python风格):
join({"Hello", "World", "1"})

1
你为什么要使用 std::initializer_list 而不是 std::vector?此外,我认为你不需要复制向量,可以通过常量引用传递。 - j b
@jb,无论是初始化列表还是向量都是错误的。处理集合的函数应该接受范围。通过应用鸭子类型原则、最小要求原则和解耦原则。 - v.oddou
@v.oddou,看起来非常酷,但在这个答案的背景下,我认为说一个const std:vector<std::string>&是“错误”的是不公平的,因为使用range引入了一个实质性的第三方依赖来解决问题。如果它被接受到标准库中,那就是另外一回事了。 - j b
@v.oddou 这是一个我之前没有考虑过的有趣方法。我总是希望C++有Python的可迭代性,采用“两个迭代器”的方法可以达到类似模式的效果。 - j b
1
@jb 是的,这是库标准委员会做出的选择。你会发现这里的所有函数 https://en.cppreference.com/w/cpp/algorithm 都符合“两个迭代器”模式。而且可能会在 C++20 中转换为范围。 - v.oddou
显示剩余3条评论

1
使用c++11,stringstream的方法并不太可怕:

#include <vector>
#include <string>
#include <algorithm>
#include <sstream>
#include <iostream>

int main()
{
    std::vector<std::string> v{"Hello, ", " Cruel ", "World!"};
   std::stringstream s;
   std::for_each(begin(v), end(v), [&s](const std::string &elem) { s << elem; } );
   std::cout << s.str();
}

1
为什么不使用 for (auto &i: v) { s << i; } 代替 for_each 行? - user283145
3
为什么要使用stringstream?直接使用string类型的变量s,并将lambda函数改为{ s += elem } - Oktalist
@Oktalist 不是的!这是Schlemiel画家低效化的教科书例子。https://www.joelonsoftware.com/2001/12/11/back-to-basics/ - v.oddou
2
@v.oddou 这不是一个 Schlemiel 画家问题,因为 std::string 知道它自己的长度。它不必迭代 s 来查找空终止符。但是,通过使用 std::string::reserve 来避免对 s 的重复分配,可以改进它。另请参见我上面得票最高的答案(但未被接受)。 - Oktalist
@Oktalist 我也给你的答案点了赞,很棒。但是,“知道自己的大小”似乎并不能保证。https://dev59.com/0HVC5IYBdhLWcg3wjx1d#256309 - v.oddou
1
@Oktalist 好的,显然在考虑end()-begin()时是有保证的。请忽略我的胡言乱语。 - v.oddou

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