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

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

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

Is there any others out there?

131427 次浏览

您应该使用 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 writes the delimiter after every single element, including the last. So you'd either need to erase the last one at the end, or write your own version of the iterator which doesn't have this annoyance. It'd be worth doing the latter if you have a lot of code that needs things like this; otherwise the whole mess might be best avoided (i.e. use ostringstream but not ostream_iterator).
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));

(包括 <string><vector><sstream><iterator>)

如果你想有一个干净的结束(没有尾随分隔符)看看这里

稍长的解决方案,但不使用 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()));
}

使用 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";
}

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

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

参见 这个问题

因为我喜欢一行程序(它们对于各种奇怪的东西都非常有用,正如你将在最后看到的) ,这里有一个使用 std: :  积累和 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 在注释中指出的那样,当数组以空字符串开始时,上面的代码会出现错误行为,这是因为“首次运行”的检查错过了以前的运行,没有产生额外的字符,而且——在所有运行中运行“首次运行”的检查是很奇怪的(即代码未经优化)。

The solution for both of these problems is easy if we know for a fact that the list has at least one element. OTOH, if we know for a fact that the list 没有 have at least one element, then we can shorten the run even more.

我认为最终得到的代码没有那么漂亮,所以我把它作为 正确的解决方案添加到这里,但是我认为上面的讨论仍然有优点:

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; return a; }),它(在 GCC 10上)提高了超过 x10的性能。感谢@Deduplicator 的建议。我还在想这到底是怎么回事。
string join(const vector<string>& vec, const char* delim)
{
stringstream res;
copy(vec.begin(), vec.end(), ostream_iterator<string>(res, delim));
return res.str();
}

下面是另一个没有在最后一个元素后面添加分隔符的例子:

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 "";

Here's what I use, simple and flexible

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

特别是对于更大的集合,您希望避免检查是否仍然添加第一个元素或不确保没有尾随分隔符..。

因此,对于空元素或单元素列表,根本不存在迭代。

空范围是微不足道的: 返回“”。

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

那简单愚蠢的解决办法呢?

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

使用这个 回答的一部分来回答另一个问题,会得到一个连接的 this,基于一个没有后面逗号的分隔符,

用法:

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

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

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

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

三元算子 ?:的一种可能的解决方案。

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

用 fmt 你可以做到。

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

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

这个问题可以用助推器解决

#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"

Another simple and good solution is using ranges v3. The current version is C++14 or greater, but there are older versions that are C++11 or greater. Unfortunately, C++20 ranges don't have the intersperse function.

这种方法的好处是:

  • 很优雅
  • 容易处理空字符串
  • 处理列表的最后一个元素
  • Efficiency. Because ranges are lazily evaluated.
  • 小而实用的图书馆

功能分解(参考文献) :

  • accumulate = 类似于 std::accumulate,但参数是一个范围和初始值。有一个可选的第三个参数是运算符函数。
  • std::filter一样,过滤不符合谓词的元素。
  • 键函数! 在范围输入元素之间插入一个分隔符。
#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
}

虽然我通常会建议使用 Boost 作为每个顶级答案,我认识到,在一些项目中,这是不希望的。

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

现在有一种使用 std::experimental::ostream_joiner的现代 C + + 实现这一点的方法:

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

如果您已经在使用 C + + 基本库(用于常用的工具) ,通常会包含字符串处理特性。除了上面提到的 Boost,Abseil provides:

std::vector<std::string> names {"Linus", "Dennis", "Ken"};
std::cout << absl::StrJoin(names, ", ") << std::endl;

Folly 规定:

std::vector<std::string> names {"Linus", "Dennis", "Ken"};
std::cout << folly::join(", ", names) << std::endl;

两者都给出字符串 "Linus, Dennis, Ken"