3

Using gcc 4.8 with C++11 enabled, I have a class like this:

class OutStream {
public:
    OutStream& operator<<(const char* s);
    OutStream& operator<<(int n);
    OutStream& operator<<(unsigned int n);
    // ...
    OutStream& vformat(const char* fmt, __VALIST args);
    OutStream& format(const char* fmt, ...);
};

When I use this class by calling the operators directly, it works as I expected:

OutStream out;
out.operator<<(1).format(" formatted %04X ", 2).operator<<("3\n");

Output:

1 formatted 0002 3

Now, I'd like to got the same output but by using the << streaming notation, maybe like this:

OutStream out;
out << 1 << format(" formatted %04X ", 2) << "3\n";

Of course, this wouldn't compile, because there was no such operator for streaming my OutStream.format() method.

There might be a solution where format() was a free function which returns a string, but this needs to first write all the output of format() into a buffer. I need a solution without std::string or some other heap or buffer usage—at best a solution which creates nearly the same code as when calling the operators directly.

Any suggestions?

Edit, 2014-10-20:

  • For better understanding my requirements: I'm on bare metal embedded development using gcc-arm-embedded gcc cross toolchain.
  • I need to apply the solution for some different embedded target systems (most are Cortex-M0/M3/M4). Some of them have very limited resources (Ram & Flash) and a part of my target systems must run without any heap usage.
  • For some reasons, I'm not using Stl iostream. However, the iostream tag has been set by seh edit; I'd keep it set because of thematic match and a found solution for my problem may also be applicable for Stl iostream.
Joe
  • 3,090
  • 6
  • 37
  • 55
  • Flags? like ``, ex: `out << format << ""` – yizzlez Oct 17 '14 at 15:56
  • 4
    Make `format` a variadic-template function returning a template-class which saves all arguments, and add a templated inserter for that, which does the actual formatting. – Deduplicator Oct 17 '14 at 15:58
  • @awesomeyi: Such flags were constructed as temporary instances and a bit later called by it's streaming operator. Therefore I had to buffer all the constructor arguments over this time, what's a bit difficult because of variable arguments. – Joe Oct 17 '14 at 16:00
  • @Deduplicator: variadic-template sounds like code bloat, isn't it? I forgot to say, that I'm on a embedded target with limited flash resources. But however, might you sketch an answer for that approach (because I'm not a very template expert)? – Joe Oct 17 '14 at 16:04
  • You'd need to design the template in such a way that it does the minimum work to remember its arguments, and then do the bulk of the work in a non-templated function. – StilesCrisis Oct 17 '14 at 16:08
  • @Joe: It should not actually use any more instructions after compilation than calling it directly. – Deduplicator Oct 17 '14 at 16:33
  • @seh: Thanks for your edit. Note that I'd intentionally not set the `ìostream` tag, because I'm not using `stl iostream`. But hoever, I think it's ok to keep the tag set because the thematic matches also. – Joe Oct 20 '14 at 10:58
  • I apologize for misinterpreting that aspect of your question. – seh Oct 20 '14 at 20:44

4 Answers4

5

Using C++14 index_sequence (There are a million different implementations on SO):

template <typename...Ts>
class formatter {
    const char* fmt_;
    std::tuple<Ts...> args_;

    template <std::size_t...Is>
    void expand(OutStream& os, std::index_sequence<Is...>) && {
        os.format(fmt_, std::get<Is>(std::move(args_))...);
    }

public:
    template <typename...Args>
    formatter(const char* fmt, Args&&...args) :
        fmt_{fmt}, args_{std::forward<Args>(args)...} {}

    friend OutStream& operator << (OutStream& os, formatter&& f) {
        std::move(f).expand(os, std::index_sequence_for<Ts...>{});
        return os;
    }
};

template <typename...Args>
formatter<Args&&...> format(const char* fmt, Args&&...args) {
    return {fmt, std::forward<Args>(args)...};
}

DEMO

The compiler should easily be able to inline the operation of the formatter and elide the temporary object. Indeed this function:

void test_foo() {
    OutStream out;
    out << 1 << format(" formatted %04X ", 2) << "3\n";
}

results in the assembly (g++ 4.9.0 -std=c++1y -O3 targeting x64):

.LC0:
    .string " formatted %04X "
.LC1:
    .string "3\n"
test_foo():
    pushq   %rbx
    movl    $1, %esi
    subq    $16, %rsp
    leaq    15(%rsp), %rdi
    call    OutStream::operator<<(int)
    movl    $2, %edx
    movl    $.LC0, %esi
    movq    %rax, %rbx
    movq    %rax, %rdi
    xorl    %eax, %eax
    call    OutStream::format(char const*, ...)
    movq    %rbx, %rdi
    movl    $.LC1, %esi
    call    OutStream::operator<<(char const*)
    addq    $16, %rsp
    popq    %rbx
    ret

so everything is properly inlined; there is no trace of the formatter in the produced code.

Community
  • 1
  • 1
Casey
  • 41,449
  • 7
  • 95
  • 125
  • `formatter` and `formatter(const char* fmt, Ts&&...args)` and `std::move(f).expand` and `void expand(OutStream& os, std::index_sequence) const&&` and optionally `std::tuple` (leaving it as `Ts...` is a touch safer, as it means a `formatter` that is reference lifetime extended is safe, but could cost performance) is how I would do it, but just a difference of taste. Hmm, `expand` as pure `&&` with a `move` around the `args` might add efficiency in some extreme corner cases? – Yakk - Adam Nevraumont Oct 17 '14 at 21:12
  • I'm very interested in this kind of solution, but it unfortionately [won't compile with gcc 4.8](http://goo.gl/OpgIAg) because there is no `std::integer_sequence`. – Joe Oct 20 '14 at 12:11
  • @Casey: I'd tried with [this inplementation of `index_sequence`](http://stackoverflow.com/a/20101039/2880699) but [got an `error: no matching function for call to formatter::expand(OutStream&, make_index_sequence<2ul>)`](http://goo.gl/r90Pq4). Did you have an idea how to fix that (or should I use a different index_sequence implementation)? – Joe Oct 22 '14 at 11:42
  • 1
    @Joe [That implementation of `index_sequence`](http://stackoverflow.com/a/20101039/2880699) had a bit of a problem in that `make_index_sequence` was not derived from `index_sequence`. I've fixed it. [Your test program compiles properly with the corrected version](http://goo.gl/35NJoo), [even with g++ 4.4](http://goo.gl/wzYQqz). – Casey Oct 22 '14 at 14:52
  • @Yakk I was intending to perfectly forward the arguments to the `formatter` constructor to `OutStream::format`, but hadn't properly considered how `std:get` modifies the tuple field types depending on the value category of its argument. So yes, it's necessary to `std::move(args_)` and safer to `&&`-constrain `expand`. Given that perfect forwarding I don't see any value in enabling lifetime extension / capture of a `formatter`; constraining the `operator<<` to require a `formatter` rvalue should make it hard to accidentally do so. – Casey Oct 22 '14 at 15:23
  • @Casey: Yep, after adding the `::type` it works as expected. Great job! – Joe Oct 22 '14 at 21:16
  • @Casey: After playing around with this solution I found out, that `__attribute__((format(printf,x,y)))` unfortunately won't work properly here. `out.format("Wrong %s", 2);` produces a warning as expected, but `out << format("Wrong %s", 2);` compiles without a warning. It affects both the [GCC 4.8](http://goo.gl/TrUCwo) and also the [GCC 4.9](http://goo.gl/BZK6Nr) variant of the solution. – Joe Oct 22 '14 at 21:57
  • @Joe Yes, it seems that the format attribute doesn't work with variadic templates, only with old-fashioned `...`. Shame. – Casey Oct 22 '14 at 23:49
3

There are three extension points for class std::basic_ostream and its operator<< that look relevant here:

  • "Insert" a function that takes and returns a std::ios_base&.
  • "Insert" a function that takes and returns a std::basic_ios<C, T>&.
  • "Insert" a function that takes and returns a std::basic_ostream&.

It's unfortunate that all three operate on function pointers, and not std::function instances, which makes it harder to supply a closure. In your case, you'd like to supply the format string—and maybe the format arguments—a la std::setw().

You can find a discussion of how to implement these manipulators in Cay Horstmann's well-aged essay Extending the iostream Library. In particular, look at section 3, "Manipulators", to see how you can return an object from your format() function that serves as a closure, and write an operator<<() function for that object.

Doing so will involve some extra copying if you want to capture temporary values in your closure, and you may have difficulty capturing a variadic argument list. Start with a simple interface (maybe taking just one argument), make sure it writes to the target stream, and build up from there.

seh
  • 14,999
  • 2
  • 48
  • 58
  • Interesting information, but note that I'm not handling with `Stl` `iostream` but with my own custom stream implementation. – Joe Oct 20 '14 at 11:12
2

Try the following:

OutStream out;
(out << 1).format(" formatted %04X ", 2) << "3\n";
Fabio A. Correa
  • 1,968
  • 1
  • 17
  • 26
  • Nice trick to push through the C++ operator priority (`<<` versus `.`). But this unfortunately impacts the readability of my application code. – Joe Oct 20 '14 at 11:30
1

Consider using GNU's autosprintf. It's very small. No, really. It's essentially a wrapper around vasprintf. All autosprintf needs is an std::string implementation and your usual freestanding C headers. Here is the header file and documentation. Example of how you could use it:

OutStream out;
out << 1 << gnu::autosprintf(" formatted %04X ", 2) << "3\n";

(Actually if you use fixed-sized strings, you could modify this to avoid any usage of std::string at all. Of course, there's still the assumption that you have implemented vasprintf and some form of heap allocation.)

  • It allocates memory. Of course, compare to IO, memory is usually fast. – Yakk - Adam Nevraumont Oct 17 '14 at 21:14
  • In my case, the main reason for avoiding heap allocation isn't execution speed but heap fragmentation. And I want to apply the solution also for very small bare metal embedded systems were I have absolutely no heap. – Joe Oct 20 '14 at 11:36