std::vector to string with custom delimiter
Asked Answered
W

14

53

I would like to copy the contents of a vector to one long string with a custom delimiter. So far, I've tried:

// .h
string getLabeledPointsString(const string delimiter=",");
// .cpp
string Gesture::getLabeledPointsString(const string delimiter) {
    vector<int> x = getLabeledPoints();
    stringstream  s;
    copy(x.begin(),x.end(), ostream_iterator<int>(s,delimiter));
    return s.str();
}

but I get

no matching function for call to ‘std::ostream_iterator<int, char, std::char_traits<char> >::ostream_iterator(std::stringstream&, const std::string&)’

I've tried with charT* but I get

error iso c++ forbids declaration of charT with no type

Then I tried using char and ostream_iterator<int>(s,&delimiter) but I get strange characters in the string.

Can anyone help me make sense of what the compiler is expecting here?

Whencesoever answered 14/2, 2012 at 13:42 Comment(2)
yes wouldn't it be nice if the compiler kindly told you what type it was expecting. Incidentally you will get a comma after your last element too.Kokand
The most elegant way is to use boost::algorithm::join() for this as described in https://mcmap.net/q/172484/-how-to-implode-a-vector-of-strings-into-a-string-the-elegant-wayAnus
S
36

Use delimiter.c_str() as the delimiter:

copy(x.begin(),x.end(), ostream_iterator<int>(s,delimiter.c_str()));

That way, you get a const char* pointing to the string, which is what ostream_operator expects from your std::string.

Susi answered 14/2, 2012 at 13:47 Comment(3)
+1, but also note that this will write a trailing delimiter in the output.Beth
I'm not sure about the performance here. A stringstream manages its own buffer, so it must grow dynamically. Here you know before generating the string what length it's going to be, so you should reserve the buffer before concatenating.Crucifix
"you know before generating the string what length it's going to be" - or at any rate you know an upper bound. If stringstream grows its buffer exponentially then I wouldn't worry about performance, but I don't know whether that's the case.Bair
O
25

C++11:

vector<string> x = {"1", "2", "3"};
string s = std::accumulate(std::begin(x), std::end(x), string(),
                                [](string &ss, string &s)
                                {
                                    return ss.empty() ? s : ss + "," + s;
                                });
Ovate answered 17/2, 2016 at 9:2 Comment(9)
Looks neat, but wouldn't this create many strings in the process? Any way to improve this using string stream?Doldrums
no, sttringsream is slower even more. in fact if you have only container of strings it is better to write usual for-loop like code string result; result.reserve(128); for (auto &it: x) { if (!result.emty()) { result.append(","); } result.append(it); } codeOvate
Sounds reasonable, but that's not what the code above does. It's not appending - it's generating new strings.Doldrums
You can avoid the conditional if you start from begin(x)+1, with *begin(x) as initial element.Ellord
@Ellord that will fail for an empty vector where begin == endEscadrille
any reason you are passing non-const references?Escadrille
Great solution to join operation. But producing initial delimiter. If you don't want to delimit empty strings use rather return B.empty() ? A : (A.empty() ? B : A + " " + B); And the signature of lambda could be [](const std::string& A, const std::string& B).Neutralize
great answer - especially if you make it a constexpr function; then all the performance issues everyone raises go awayGardiner
See this post, clean and beautiful https://mcmap.net/q/172484/-how-to-implode-a-vector-of-strings-into-a-string-the-elegant-wayCambodia
F
13

Another way to do it:

#include <iostream>
#include <string>
#include <vector>
#include <sstream>
using namespace std;

template <typename T>
string join(const T& v, const string& delim) {
    ostringstream s;
    for (const auto& i : v) {
        if (&i != &v[0]) {
            s << delim;
        }
        s << i;
    }
    return s.str();
}

int main() {
    cout << join(vector<int>({1, 2, 3, 4, 5}), ",") << endl;
}

(c++11 range-based for loop and 'auto' though)

Fredericton answered 14/2, 2012 at 15:31 Comment(0)
L
13

This is an extension to the two answers already provided above as run-time performance seemed to be a theme in the comments. I would have added it as comments, but I do not have that privilege yet.

I tested 2 implementations for run-time performance using Visual Studio 2015:

Using stringstream:

std::stringstream result;
auto it = vec.begin();
result << (unsigned short)*it++;
for (; it != vec.end(); it++) {
    result << delimiter;
    result << (unsigned short)*it;
}
return result.str();

Using accumulate:

std::string result = std::accumulate(std::next(vec.begin()), vec.end(),
    std::to_string(vec[0]),
    [&delimiter](std::string& a, uint8_t b) {
    return a + delimiter+ std::to_string(b);
});
return result;

Release build run-time performance was close with a couple subtleties.

The accumulate implementation was slightly faster (20-50ms, ~10-30% of the overall run-time (~180ms) on 1000 iterations over a 256 element vector). However, the accumulate implementation was only faster when the a parameter to the lambda function was passed by reference. Passing the a parameter by value resulted in a similar run-time difference favoring the stringstream implementation. The accumulate implementation also improved some when the result string was returned directly rather than assigned to a local variable that was immediately returned. YMMV with other C++ compilers.

The Debug build was 5-10 times slower using accumulate so I think the extra string creation noted in several comments above is resolved by the optimizer.

I was looking at a specific implementation using a vector of uint8_t values. The full test code follows:

#include <vector>
#include <iostream>
#include <sstream>
#include <numeric>
#include <chrono>

using namespace std;
typedef vector<uint8_t> uint8_vec_t;

string concat_stream(const uint8_vec_t& vec, string& delim = string(" "));
string concat_accumulate(const uint8_vec_t& vec, string& delim = string(" "));

string concat_stream(const uint8_vec_t& vec, string& delimiter)
{
    stringstream result;

    auto it = vec.begin();
    result << (unsigned short)*it++;
    for (; it != vec.end(); it++) {
        result << delimiter;
        result << (unsigned short)*it;
    }
    return result.str();
}

string concat_accumulate(const uint8_vec_t& vec, string& delimiter)
{
    return accumulate(next(vec.begin()), vec.end(),
        to_string(vec[0]),
        [&delimiter](string& a, uint8_t b) {
        return a + delimiter + to_string(b);
    });
}

int main()
{
    const int elements(256);
    const int iterations(1000);

    uint8_vec_t test(elements);
    iota(test.begin(), test.end(), 0);

    int i;
    auto stream_start = chrono::steady_clock::now();
    string join_with_stream;
    for (i = 0; i < iterations; ++i) {
        join_with_stream = concat_stream(test);
    }
    auto stream_end = chrono::steady_clock::now();

    auto acc_start = chrono::steady_clock::now();
    string join_with_acc;
    for (i = 0; i < iterations; ++i) {
        join_with_acc = concat_accumulate(test);
    }
    auto acc_end = chrono::steady_clock::now();

    cout << "Stream Results:" << endl;
    cout << "    elements: " << elements << endl;
    cout << "    iterations: " << iterations << endl;
    cout << "    runtime: " << chrono::duration<double, milli>(stream_end - stream_start).count() << " ms" << endl;
    cout << "    result: " << join_with_stream << endl;

    cout << "Accumulate Results:" << endl;
    cout << "    elements: " << elements << endl;
    cout << "    iterations: " << iterations << endl;
    cout << "    runtime: " << chrono::duration<double, milli>(acc_end - acc_start).count() << " ms" << endl;
    cout << "    result:" << join_with_acc << endl;

    return 0;
}
Liston answered 14/10, 2016 at 22:24 Comment(2)
I love that you provided performance numbers -- very useful for making a decision one way or the other, thanks!Ditty
Just to state the obvious: concat_stream() and concat_accumulate() only work for non empty vectors.Sectionalism
M
9
std::string Gesture::getLabeledPointsString(const std::string delimiter) {
  return boost::join(getLabeledPoints(), delimiter);
}

I am not that convinced about introducting getLabeledPointsString at this point ;)

Mccoy answered 14/2, 2012 at 13:54 Comment(2)
ehehehheheh is there a cpp without boost? +1 for boost, thanks!Whencesoever
@nkint: Oh you can certainly program without boost. But that's about as difficult as Python without its libraries: you just need to create all the tools by yourself ;)Mccoy
F
5
string join(const vector<string> & v, const string & delimiter = ",") {
    string out;
    if (auto i = v.begin(), e = v.end(); i != e) {
        out += *i++;
        for (; i != e; ++i) out.append(delimiter).append(*i);
    }
    return out;
}

A few points:

  • you don't need an extra conditional to avoid an extra trailing delimiter
  • make sure you don't crash when the vector is empty
  • don't make a bunch of temporaries (e.g. don't do this: x = x + d + y)
Files answered 8/11, 2016 at 23:7 Comment(0)
D
1

I know this is an old question, but I have a similar problem and none of the above answers suits all my needs, so I'll post here my solution.

My requirements are:

  • I need a generic solution able to work with any iterable container and with any data type, of course for custom data types you'll have to provide a suitable operator<<()
  • I need an easy way to apply transforms to the data (for example, by default int8_t and uint8_t are handled as chars by std::stringstream: maybe this is what you want or maybe not, so I want to be able to make this choice)
  • I want to be able to specify the delimiter as a string literal, but also accept chars and std::strings
  • I like to have the ability to add enclosing characters, but this is probably very personal taste

This assumes C++11. I choose to use std::stringstream because it implements a standard but still customizable way to convert something to a string. Any comments are very welcome.

#include <iterator>
#include <sstream>
#include <string>
#include <iostream> // used only in main
#include <vector> // used only in main

template< typename T >
typename std::iterator_traits< T >::value_type
identity(typename std::iterator_traits< T >::value_type v) {
  return v;
}

template< typename T > using IdentityType = decltype(identity< T >);

template< class InItr,
          typename StrType1 = const char *,
          typename StrType2 = const char *,
          typename StrType3 = const char *,
          typename Transform = IdentityType< InItr > >
std::string join(InItr first,
                 InItr last,
                 StrType1 &&sep = ",",
                 StrType2 &&open = "[",
                 StrType3 &&close = "]",
                 Transform tr = identity< InItr >) {

  std::stringstream ss;

  ss << std::forward< StrType2 >(open);

  if (first != last) {

    ss << tr(*first);

    ++first;
  }

  for (; first != last; ++first)
    ss << std::forward< StrType1 >(sep) << tr(*first);

  ss << std::forward< StrType3 >(close);

  return ss.str();
}


int main(int argc, char** argv) {

  const std::vector< int > vec{2, 4, 6, 8, 10};

  std::cout << join(vec.begin(), vec.end()) << std::endl;
  std::cout << join(vec.begin(), vec.end(), "|", "(", ")",
                    [](int v){ return v + v; }) << std::endl;

  const std::vector< char > vec2{2, 4, 6, 8, 10};
  std::cout << join(vec2.begin(), vec2.end()) << std::endl;
  std::cout << join(vec2.begin(), vec2.end(), "|", "(", ")",
          [](char v){ return static_cast<int>(v); }) << std::endl;
}

outputs something like:

[2,4,6,8,10]
(4|8|12|16|20)
[<unprintable-char>,<unprintable-char>,<unprintable-char>,
]
(2|4|6|8|10)
Doralia answered 3/6, 2020 at 11:50 Comment(0)
R
1

There was absolultey no need for super fancy stuff. Simple stuff like this would have achieved the same results.

int vectorCounter=0;

//this is where you loop over the contents of your vector
for (auto it = StrVec.begin(); it != StrVec.end(); ++it){
                
    vectorCounter++;
    
    //this print contents of the vector
    cout << *it;

    //this will put a custom delimiter
    if (vectorCounter < StrVec.size())
    {
        //This is where you define your delimiter
        cout << ",";
    }
}//end-for-loop


OUTPUT:
1,2,3,4
Roadway answered 29/1, 2022 at 21:41 Comment(0)
O
1

If the usage of ABSL is ok, you could do it like this:

std::vector<std::string> strings{"h", "e", "ll", "o"};
auto joined(absl::StrJoin(strings, "-"));

See godbolt

Boost has a similar thing like @StefanQ suggested

Overrate answered 26/1, 2023 at 12:30 Comment(0)
S
1

Similar to another answer, but I just wanted to point out that there is an example on cppreference for std::accumulate

std::vector<int> v {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
auto dash_fold = [](std::string a, int b)
{
  return std::move(a) + '-' + std::to_string(b);
};
 
std::string s = std::accumulate(std::next(v.begin()), v.end(),
                                std::to_string(v[0]),
                                dash_fold);
// s: 1-2-3-4-5-6-7-8-9-10
Synclastic answered 8/11, 2023 at 12:53 Comment(0)
M
0

Another potential option is std::experimental::ostream_joiner:

#include <algorithm>
#include <experimental/iterator>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> i{1, 2, 3, 4, 5};
    std::copy(i.begin(), i.end(), 
              std::experimental::make_ostream_joiner(std::cout, ", "));

    return 0;
}

Output:

1, 2, 3, 4, 5

NOTE: As implied by the experimental namespace, this is not yet standardized and is currently not available on some compilers. e.g. I was able to use it in GCC and Clang, but not MSVC.

Multifid answered 23/3, 2023 at 14:23 Comment(0)
L
0

A very simple yet not terribly slow answer that I'm surprised I haven't seen yet:

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

std::string join(std::vector<std::string> &strings, std::string &delimiter) {
    std::string joined = "";
    for (std::string i : strings) joined += delimiter + i;
    return joined.erase(0, delimiter.size());
}

int main() {
    std::vector<std::string> strings = {"first", "second", "third"};
    std::string delimiter = " + ";
    std::cout << join(strings, delimiter) << "\n";
    
    return 0;
}

Output:

first + second + third
Liz answered 22/5 at 15:33 Comment(0)
S
-1
int array[ 6 ] = { 1, 2, 3, 4, 5, 6 };
std::vector< int > a( array, array + 6 );
stringstream dataString; 
ostream_iterator<int> output_iterator(dataString, ";"); // here ";" is delimiter 
std::copy(a.begin(), a.end(), output_iterator);
cout<<dataString.str()<<endl;

output= 1;2;3;4;5;6;

Stockbroker answered 23/4, 2016 at 20:21 Comment(2)
Generally, answers are much more helpful if they include an explanation of what the code is intended to do, and why that solves the problem without introducing others.Enterectomy
This includes a trailing delimiter. Granted, the OP didn't specify but usually one places delimiters between fields and not at the end of the final field.Ellipsoid
O
-2

faster variant:

vector<string> x = {"1", "2", "3"};
string res;
res.reserve(16);

std::accumulate(std::begin(x), std::end(x), 0,
                [&res](int &, string &s)
                {
                    if (!res.empty())
                    {
                        res.append(",");
                    }
                    res.append(s);
                    return 0;
               });

it doesn't create interim strings, but just allocate memory once for the whole string result and appends each elem to the end of &res

Ovate answered 1/4, 2016 at 21:42 Comment(3)
You're using an accumulate() but totally ignore that result. You should be using std::for_each() instead.Bataan
actually return value of this function call is 0 (see lambda -> return 0;). but 'res' contains result stringOvate
Did you know accumulate expects its function to not have side-effects? This code would better be expressed as a for_each.Ellord

© 2022 - 2024 — McMap. All rights reserved.