7
\$\begingroup\$

Today I found myself reinventing output separators for the zillionth time.

I have converged on an approach like the following that seem pretty clean:

Live On Coliru

#include <iostream>

void foo(int a, int b) {
    auto sep = [first=true]() mutable { return std::exchange(first, false); };

    std::cout << "foo(";
    if (a) std::cout << sep() << "a:" << a;
    if (b) std::cout << sep() << "b:" << b;
    std::cout << ")\n";
}

int main() {
    foo(0,0);
    foo(0,1);
    foo(2,3);
    foo(4,0);
}

Extracting Building Blocks?

This could be made a bit more general by extracting the mutating state:

template <typename T = bool, T initial = true, T fallback = false>
struct once {
    mutable T flag = initial;

    operator T() const { return std::exchange(flag, fallback); }
};

So that we can now write the sep() lambda as:

auto sep = [first=once{}]{ return first? "" : ", "; };

Abstracting The Whole One-Off Value

We can go one better using

    auto sep = one_off("", ", ");

With:

template <typename T = char const*>
struct one_off {
    mutable once<> flag;

    T first, other;
    one_off(T first, T other) : first(std::move(first)), other(std::move(other)) {}

    operator T() const { return flag? first : other; }
};

Used as: Live On Coliru

void foo(int a, int b) {
    auto sep = one_off("", ", ");

    std::cout << "foo(";
    if (a) std::cout << sep << "a:" << a;
    if (b) std::cout << sep << "b:" << b;
    std::cout << ")\n";
}

Reflection And Merit?

As you can see these are all potentially useful building blocks. I'm wondering whether anyone else uses similar building blocks and what they learned from actual usage.

Ideas:

  • what about a dynamic separator? It's much easier to capture, say, columnar formatting in a stateful lambda eg: Live On Coliru

    void foo(std::vector<int> const& v) {
        auto sep = [n=0u]() mutable {
            if (n++)
                return n%5==1?"\n" : ", ";
            return "";
        };
    
        for (auto& el : v)
            std::cout << sep() << std::setw(5) << el;
        std::cout << "\n";
    }
    
  • Doesn't this conflict with standard library concepts like std::once_flag from C++11 too much?

  • Intuitively I feel these don't mix too well. Say, we want a every_nth style value wrapper instead of one_off (to replace every nth, instead of just the first)? Suppose we want to return from an input sequence, transforming every nth? Aren't we at risk of reinventing a Range Library?

  • Conversely, is there a straight-forward way to express these building blocks using an existing (range) library such as Boost Range or RangeV3?

Notes:

  • I used C++17 for brevity of exposition; it's not about using C++17 (I'm aware of ostream_joiner).

  • It's not about string joining per se, which is why I chose a contrived example. In this example, obviously you could just print the delimiter if (a && b) but that's not the point of the exercise.

  • Thanks to @LucDanton for suggesting std::exchange

\$\endgroup\$
  • \$\begingroup\$ @TobySpeight good catch. I had played with the idea to make it variadic (mixing in fold-expressions; actually that leads to interesting related discussion ideas) but opted to keep it "simple" \$\endgroup\$ – sehe Jan 17 '18 at 13:08
  • 2
    \$\begingroup\$ ostream_joiner Nice. \$\endgroup\$ – Martin York Jan 17 '18 at 17:00
  • \$\begingroup\$ Your current code doesn't do the same as your Coliru variant (you've deleted the ?" ":"" in sep). Was that on purpose? \$\endgroup\$ – Zeta Mar 19 '18 at 7:03
1
\$\begingroup\$

That's fine as far as it goes.
Though I am not sure why you want all the added complexity for a problem that seems to be already solved.

What I think we all want is to stream containers in a logical way, that is consistent across containers.

Your basic example:

for(auto const& el: c) {
    std::cout << sep() << el;
std::cout << "\n";

This is a specialization of how I would want containers to be stremed:

auto sep = one_off("", sepObject);
stream << beginMarker;
for(auto const& el: c) {
    stream << sep() << el;
stream << endMarker;

So for example:

// An array looks like this:
[ 1, 2, 3, 4, 5, 6 ]        // Note terminator is "]\n"

// A map look like this:
{ a => b, c => d, e => f }

So would it be not more logical to define a container streamer object.

std::Container  c = {1,2,3,4,5};
ContainerStream(std::cout) << c;

Then you can use traits to define the beginMarker, endMarker and sepObject.

Or alternatively rather than wrapping the stream we could potentially wrap the container in something that knows how to print it (and generalize that).

std::cout << jsonExport(c);
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ You focused on only one example that was not the introducing sample (picked from the discussion ideas at the back). In your particular example, I agree (and that's what I do). However, I'm specifically looking for any "proven set", "established convention", real life experience with devising the primitive building blocks with which to elegantly define such manipulators. \$\endgroup\$ – sehe Jan 17 '18 at 20:58
  • \$\begingroup\$ Off-topic: it is convention to put the magic in the streamable, rather than in the stream, since the stream type can't always be controlled. In the case where aspects like beginMarker, endMarker etc. are desired to stick with the stream, they can be stored there using ios::xalloc and friends \$\endgroup\$ – sehe Jan 17 '18 at 20:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.