2

An ugly block of code that we would like to get rid of

I have this block of code in one of my source files - yuck!

std::string error;
std::vector<string> errors;
// ... populate errors with some strings
for(const auto &item : errors)
{
    if(!error.empty())
    {
        error += std::string("\n");
    }
    error += item;
}

I think there should be a better way to do it, possibly taking inspiration from ostream_iterator. See here

An initial attempt, which on reflection isn't an improvement

My first thought was that perhaps std::for_each would work with a lambda.

std::for_each(errors.cbegin(), errors.cend(),
    [&error] (const auto &item)
    {
        if(!error.empty())
        {
            error += std::string("\n");
        }
        error += item;
    }
);

But then I realized that this is really no improvement what so ever, and in some ways obfuscates the code further through the unnecessary use of a lamba where the range based for loop worked perfectly fine, and was clear to understand.

A similar working example with ostream_iterator

I think that there should be a way to take inspiration from std::copy or std::for_each to do this.

The example given using cout and ostream_iterator is the following:

std::ostream_iterator<int> out_it(std::cout, ", ");
std::copy(myvector.begin(), myvector.end(), out_it);

It is pretty close to what I want to achieve. Instead of std::cout it should use an instance of a std::string object. Instead of ", " the delimiter should be "\n". It already iterates over a vector, and calls out_it, the argument of which should be a string instead of int.

But I don't think this will quite work, since a string does not inherit from ostream. It is not an ostream type.

A final failed attempt

I also attempted this, but didn't really expect it to work. (It doesn't compile.)

std::for_each(errors.cbegin(), errors.cend(),
    std::back_inserter(error)
);

What to do?

FreelanceConsultant
  • 13,167
  • 27
  • 115
  • 225
  • Or the C++20-era equivalent [Joining a range of strings with a delimiter using standard ranges](https://stackoverflow.com/q/63637187/364696). – ShadowRanger Mar 30 '22 at 11:15
  • 3
    Remark about the title: some things in the C++ standard library are based on (taken from?) the STL library, but from a standard C++ point of view, STL isn't really a thing anymore, so std::string might be clearer and 'container' in the context of C++ should be clear enough. – stefaanv Mar 30 '22 at 11:31
  • 1
    Also, you're actually asking for an elegant join function with separators only in between items. This isn't obvious from the title. – stefaanv Mar 30 '22 at 11:32
  • @ShadowRanger Ah, I see it can be done with a `stringstream`. Not quite what I had in mind, but if it works I guess it looks like it could be a good option – FreelanceConsultant Mar 30 '22 at 11:34
  • 3
    `std::string` has never been a part of the STL. – Evg Mar 30 '22 at 11:37
  • @Evg Not sure about that - string is in std, templated containers which I assume is what you mean by STL are in std. Should be fairly clear what is meant – FreelanceConsultant Mar 30 '22 at 11:40
  • 2
    No, they're just being pedantic. The STL *was* a thing. It no longer is. From it, we now have the C++ Standard Library. However, enough people use both terms interchangeably that it *should* be understood what's meant unless someone is working with ancient code/tooling. – sweenish Mar 30 '22 at 12:50
  • @FreelanceConsultant Your idea with `std::ostream_iterator` can be easily improved to make working [code like this](https://godbolt.org/z/6efajzGen), is it beautiful enough, the way you wanted? It has no loops. – Arty Mar 30 '22 at 17:55
  • @Arty It looks decent, however my query would be is there any overhead with using stringstream? For my purposes, it doesn't actually matter but I would be interested to know the performance numbers – FreelanceConsultant Mar 30 '22 at 18:39
  • @FreelanceConsultant My solution above was only meant to make it look more different then a loop solution. If most important for you is speed (and memory consumption), not beuty, then you need other strategy and make [this kind of code](https://godbolt.org/z/PKcrdEx7h). This code has two advantages, first that it at start pre-allocates resulting `error` string of exactly size that is needed, this allows to consume less memory and do fewer heap allocations (`error += errors[i];` may re-allocate Heap many times, and final size of string might be bigger than necessary). – Arty Mar 31 '22 at 07:04
  • @FreelanceConsultant Continuing previous comment. Second advantage of my mentioned code is that it uses `std::memcpy()`, that is fastest possible way of copying memory, because regular operation like `error += errors[i];` besides doing heap re-allocation also may copy in a loop char by char, with constructing char object (although I expect modern C++ stdlib does optimization of using `std::memcpy()` too). Anyway my code that I mentioned does everything by hand in low-level fasion, so should have highest speed and smallest memory consumption. Although my low-level code looks longer then yours. – Arty Mar 31 '22 at 07:04
  • @FreelanceConsultant One more very considerable speedup can be made if size of delimiter (character `'\n'` in your case) is known to be exactly 1 character. Then in my last mentioned code above it is advantageous to replace adding delimiter to `error` not by `std::memcpy()` as I did (inside `if (i > 0) { ... }` block), but through much faster and simpler code `if (i > 0) { error[off] = delim[0]; ++off; }`. And also better to make type of `delim` not `std::string` but `char` type in this case. This only works if delimiter is known to be just 1 character. – Arty Mar 31 '22 at 07:18

3 Answers3

3

Benefiting from the adoption of views::join_with and range::to, in C++23 you can simply do

std::vector<string> errors;
auto error = errors | std::views::join_with('\n') 
                    | std::ranges::to<std::string>();

Demo (with range-v3 equivalent since no compiler implements them yet)

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
0

I don't think there is too much you can do with this except wrap it in a function so the rest of your code is a little tidier. I went for this version:

std::string smush(std::vector<std::string> const& errors)
{
    std::string error;

    for(auto const& e: errors)
            error += e + '\n';
    error.pop_back();

    return error;
}
Galik
  • 47,303
  • 4
  • 80
  • 117
  • He only wants to append (and prepend a `\n` on entries that are non-empty. This is not the same output, is it? – Kevin Anderson Mar 30 '22 at 11:57
  • 2
    @KevinAnderson Nope, it seems you overlooked the question ^^ The emptiness check is only for the first iteration where `error` is empty (and thus useless to perform every time, since it is not empty after the first insertion). The goal was to prevent to start with a delimiter. The output of the proposed solution here is the same, but without the unnecessary branching. – Fareanor Mar 30 '22 at 12:04
  • @KevinAnderson I think you'll find his code appends all entries regardless if they are empty or not. His test for emptiness is made to ensure his resultant string doesn't have a `"\n"` at the beginning. – Galik Mar 30 '22 at 12:07
  • I reversed which variable was doing what in my head. Indeed what you both say is true. – Kevin Anderson Mar 30 '22 at 12:09
0

Here's a semi-ugly piece of templated code that I use whenever I need to join strings together (I have it in a utilities header), along with an optional end piece too. Will work with any collection type that supports std::begin and std::end (fixed-size arrays as well, though if you have a pointer and length, you'll have to wrap it in a std::span first), and as long as the element inside supports the << operator to make a string with, should work with custom types too.

// Joins collections of strings together.
template<typename InputCollection>
std::string joiner(const InputCollection& collection, const std::string& separator = ", ", const std::string& ending = "")
{
    std::ostringstream os;
    auto begin = std::begin(collection);
    auto end = std::end(collection);

    if (begin != end)
    {
        os << *begin++;
    }

    while (begin != end)
    {
        os << separator;
        os << *begin++;
    }

    os << ending;
    return os.str();
}

So it's wordy, but it's easy-to-understand, which I consider more important. Use like this:

vector<string> collection = { "hey", "there", "all" };
auto output = joiner(collection); // output: "hey, there, all"
std::string stringArray[3] = { "now", "an", "array" };
output = joiner(stringArray, "-"); // Different separator - output: "now-an-array"

So use that if you think it'll be beneficial long-term, but that's obviously longer than what you had originally.

Kevin Anderson
  • 6,850
  • 4
  • 32
  • 54