如何将字符串向量内爆成字符串(优雅的方式)
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));
(包括<string>
、<vector>
、<sstream>
和<iterator>
)
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&& a, auto&& b) -> auto& 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();
【讨论】:
以上是关于如何将字符串向量内爆成字符串(优雅的方式)的主要内容,如果未能解决你的问题,请参考以下文章