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

How to implode a vector of strings into a string (the elegant way)

本文关键字:字符串 方式 向量      更新时间:2023-10-16

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

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

还有其他人吗?

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

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

参见这个问题。

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而不是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)。

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

(std::accumulate define in )

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

因为我喜欢一行程序(它们对于各种奇怪的东西非常有用,正如您将在最后看到的那样),这里有一个使用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]用于vector。
  • 根据评论和聊天中的讨论,lambda仍然会进行一些复制。这可以通过使用这个(不太漂亮的)lambda来最小化:[](auto&& a, auto&& b) -> auto& { a += ','; a += b; return a; }),它(在GCC 10上)将性能提高了10倍以上。感谢@Deduplicator的建议。我还在试图弄清楚这里发生了什么。

简单愚蠢的解决方案怎么样?

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

使用fmt可以做到。

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

但是我不知道join是否会使它成为std::格式。

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

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

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

空范围不重要:返回"。

单元素或多元素可以通过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

虽然我通常建议根据顶部答案使用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();

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

下面是另一个不在最后一个元素后面添加分隔符的示例:

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

使用三元运算符?:的可能解。

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 .

另一个简单而好的解决方案是使用range v3。当前版本是c++ 14或更高版本,但也有更老的版本是c++ 11或更高版本。遗憾的是,c++ 20的range没有intersperse函数。

这种方法的好处是:

    优雅
  • 轻松处理空字符串
  • 处理列表的最后一个元素
  • 效率。因为范围是惰性求值的。
  • 小而有用的库

功能故障(参考):

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

编辑:正如@Franklin Yu建议的那样,只能使用std库中的std::ranges::views::join_with。但不幸的是,它只适用于c++23。由于我们使用的是c++23,我们也可以使用std::ranges::fold_left来代替std::accumulate来创建一行表达式。std::ranges::fold_left是rage v3的rages::accumulate的std版本。

#include <iostream>
#include <string>
#include <vector>
#include <ranges>
#include <algorithm>
int main()
{
    // Can be any std container
    std::vector<std::string> a{ "Hello", "", "World", "is", "", "a", "program" };
    
    std::string delimiter{", "};
    
    std::string finalString = 
        std::ranges::fold_left(a | std::views::filter([](std::string s){return !s.empty();})
                                 | std::views::join_with(delimiter)
                              , std::string()
                              , std::plus());
 
    std::cout << finalString << std::endl; // Hello, World, is, a, program
}

我是这么用的,简单灵活

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

使用这个答案的一部分来回答另一个问题,基于分隔符,不带逗号,

用法:

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

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

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

愚蠢提供:

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

都给出了字符串"Linus, Dennis, Ken"

这在c++ 23中得到了一个方便的一行代码:

auto str = std::ranges::fold_left(elems | std::views::join_with(delim), std::string{}, std::plus<>{});

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

这可以使用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"

我使用下面的方法,在c++ 17中工作得很好。函数开始检查给定的向量是否为空,在这种情况下返回一个空字符串。如果不是这种情况,则从vector中获取第一个元素,然后从第二个元素开始迭代直到末尾,并在vector元素后面加上分隔符。

template <typename T>
std::basic_string<T> Join(std::vector<std::basic_string<T>> vValues,
   std::basic_string<T> strDelim)
{
   std::basic_string<T> strRet;
   typename std::vector<std::basic_string<T>>::iterator it(vValues.begin());
   if (it != vValues.end())  // The vector is not empty
   {
      strRet = *it;
      while (++it != vValues.end()) strRet += strDelim + *it;
   }
   return strRet;
}

使用例子:

std::vector<std::string> v1;
std::vector<std::string> v2 { "Hello" };
std::vector<std::string> v3 { "Str1", "Str2" };
std::cout << "(1): " << Join<char>(v1, ",") << std::endl;
std::cout << "(2): " << Join<char>(v2, "; ") << std::endl;
std::cout << "(3): [" << Join<char>(v3, "] [") << "]" << std::endl;
输出:

(1): 
(2): Hello
(3): [Str1] [Str2]

另一个std::accumulate解决方案,作为一个函数,模板化,以及错误捕获。

缺点,不适合大型字符串集(可能是O(n^2)),并且使用不当:

join<std::vector<std::string>>(myVec.begin(), myVec.end(), ' ')

在这里:

template<class T>
std::string join(char delimiter, 
                 typename T::iterator begin,
                 typename T::iterator end) {
    if(begin == end) {
        return std::string();
    }
    if(std::next(begin) == end) {
        return std::string(*begin);
    }
    return std::accumulate(std::next(begin),
                           end, 
                           *begin,
                           [delimiter](const std::string a, 
                                       const std::string b) -> std::string 
                                       {return a + delimiter + b;});
}

的例子:

#include <iostream>
#include <numeric>
#include <string>
#include <vector>
// paste join template code here....
int main() {
    std::vector<std::string> vec { "One", "Two", "Three" };
    
    std::cout << "0: " << join<std::vector<std::string>>(' ',vec.begin()+0, vec.end());
    std::cout << std::endl;
    std::cout << "1: " << join<std::vector<std::string>>(' ',vec.begin()+1, vec.end());
    std::cout << std::endl;
    std::cout << "2: " << join<std::vector<std::string>>(' ',vec.begin()+2, vec.end());
    std::cout << std::endl;
    std::cout << "3: " << join<std::vector<std::string>>(' ',vec.begin()+3, vec.end());
    std::cout << std::endl;
    return 0;
}

结果:

0: One Two Three
1: Two Three
2: Three
3: