开发者

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

I'm looking for the most elegant way to implode a vector of strings into a string. Below is the solution I'm using now:

static std::string& implode(const std::vector<std::string>& elems, 开发者_如何学Cchar 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?


Use boost::algorithm::join(..):

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

See also this question.


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

(include <string>, <vector>, <sstream> and <iterator>)

If you want to have a clean end (no trailing delimiter) have a look here


You should use std::ostringstream rather than std::string to build the output (then you can call its str() method at the end to get a string, so your interface need not change, only the temporary s).

From there, you could change to using std::ostream_iterator, like so:

copy(elems.begin(), elems.end(), ostream_iterator<string>(s, delim)); 

But this has two problems:

  1. delim now needs to be a const char*, rather than a single char. No big deal.
  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).


Because I love one-liners (they are very useful for all kinds of weird stuff, as you'll see at the end), here's a solution using std::accumulate and 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; 
    } )

I find this syntax useful with stream operator, where I don't want to have all kinds of weird logic out of scope from the stream operation, just to do a simple string join. Consider for example this return statement from method that formats a string using stream operators (using 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();

Update:

As pointed out by @plexando in the comments, the above code suffers from misbehavior when the array starts with empty strings due to the fact that the check for "first run" is missing previous runs that have resulted in no additional characters, and also - it is weird to run a check for "is first run" on all runs (i.e. the code is under-optimized).

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 does not have at least one element, then we can shorten the run even more.

I think the resulting code isn't as pretty, so I'm adding it here as The Correct Solution, but I think the discussion above still has merrit:

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

Notes:

  • For containers that support direct access to the first element, its probably better to use that for the third argument instead, so alist[0] for vectors.
  • As per the discussion in the comments and chat, the lambda still does some copying. This can be minimized by using this (less pretty) lambda instead: [](auto&& a, auto&& b) -> auto& { a += ','; a += b; return a; }) which (on GCC 10) improves performance by more than x10. Thanks to @Deduplicator for the suggestion. I'm still trying to figure out what is going on here.


I like to use this one-liner accumulate (no trailing delimiter):

(std::accumulate defined in <numeric>)

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


what about simple stupid solution?

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


With fmt you can do.

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

But I don't know if join will make it to std::format.


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


Especially with bigger collections, you want to avoid having to check if youre still adding the first element or not to ensure no trailing separator...

So for the empty or single-element list, there is no iteration at all.

Empty ranges are trivial: return "".

Single element or multi-element can be handled perfectly by 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;
         });
};

Running sample (require C++14): http://cpp.sh/8uspd


A version that uses 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";
}


While I would normally recommend using Boost as per the top answer, I recognise that in some projects that's not desired.

The STL solutions suggested using std::ostream_iterator will not work as intended - it'll append a delimiter at the end.

There is now a way to do this with modern C++ using 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();


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

using:

string a = joinList({ "a", "bbb", "c" }, "!@#");

output:

a!@#bbb!@#c


Here is another one that doesn't add the delimiter after the last element:

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


Using part of this answer to another question gives you a joined this, based on a separator without a trailing comma,

Usage:

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

Code:

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


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.

The benefits of this approach are:

  • Elegant
  • Easily handle empty strings
  • Handles the last element of the list
  • Efficiency. Because ranges are lazily evaluated.
  • Small and useful library

Functions breakdown(Reference):

  • accumulate = Similar to std::accumulate but arguments are a range and the initial value. There is an optional third argument that is the operator function.
  • filter = Like std::filter, filter the elements that don't fit the predicate.
  • intersperse = The key function! Intersperses a delimiter between range input elements.
#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
}


A possible solution with ternary operator ?:.

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"}) will give you 2, 4, 5.


If you are already using a C++ base library (for commonly used tools), string-processing features are typically included. Besides Boost mentioned above, Abseil provides:

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

Folly provides:

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

Both give the string "Linus, Dennis, Ken".


Slightly long solution, but doesn't use std::ostringstream, and doesn't require a hack to remove the last delimiter.

http://www.ideone.com/hW1M9

And the code:

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


This can be solved using 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"


I'm using the following approach that works fine in C++17. The function starts checking if the given vector is empty, in which case returns an empty string. If that's not the case, it takes the first element from the vector, then iterates from the second one until the end and appends the separator followed by the vector element.

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

Usage example:

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;

Output:

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜