3
\$\begingroup\$

I stumbled into an idea for creating a simple class that would entirely avoid streaming of any data unless some condition is met. The idea here is that a ConditionalOStream object can be constructed around an instance of a std::ostream * and, optionally, a bool indicating whether streaming should occur. If the bool is true and the stream is non-null, then the real stream insertion operators are called. In either case, the ConditionalOStream is returned so that the operators can be daisy chained the same way a typical stream insertion would. In my somewhat limited usage of this code at work, it is working as a drop-in replacement for std::cout and std::ofstream instances.

The only "bad" part I can see so far is that the if is done once for each << in the stream expression. I took a look at the x86-64 assembly, and the function is very short, only containing one conditional jump. I think that's reasonable considering how expensive actually attempting to stream any of the operands would be.

Is this a reasonable solution to conditional streaming?

Are there any corner-cases or exceptions that would cause this to fail to compile, or behave unpredictably?

I am not concerned about problems inherent to using streams in C++, only potential problems with using this class to avoid attempts to stream things.

#pragma once

#include <ostream>


//
// Conditional Output Stream
//

struct ConditionalOStream {
  std::ostream *const mOStream { nullptr };

  // Explicit constructors so that implicit
  // conversion from `std::ostream &`s can't
  // happen and lead to ambiguous overloads.

  explicit ConditionalOStream() {}

  explicit ConditionalOStream(
    std::ostream *inOStream
  ):
    mOStream(inOStream)
  {}

  explicit ConditionalOStream(
    std::ostream &inOStream
  ):
    mOStream(&inOStream)
  {}

  explicit ConditionalOStream(
    std::ostream *inOStream,
    bool const &inEnabled
  ):
    mOStream(inEnabled ? inOStream : nullptr)
  {}

  explicit ConditionalOStream(
    std::ostream &inOStream,
    bool const &inEnabled
  ):
    mOStream(inEnabled ? &inOStream : nullptr)
  {}

};


//
// Templated daisy-chaining stream insertion operator
// for ConditionalOStream and a type T
//

// For a const T
template <typename T>
inline ConditionalOStream const &operator<<(
  ConditionalOStream const &inConditionalOStream,
  T const &inT
) {
  if (inConditionalOStream.mOStream) {
    (*inConditionalOStream.mOStream) << inT;
  }
  return inConditionalOStream;
}

// For a non-const T
template <typename T>
inline ConditionalOStream const &operator<<(
  ConditionalOStream const &inConditionalOStream,
  T &&inT
) {
  if (inConditionalOStream.mOStream) {
    (*inConditionalOStream.mOStream) << inT;
  }
  return inConditionalOStream;
}


//
// Special stream insertion operator for std::endl
// and other similar "special" stream functions.
//

inline ConditionalOStream const &operator<<(
  ConditionalOStream const &inConditionalOStream,
  std::ostream&(* const &inStreamFunction)(std::ostream&)
) {
  if (inConditionalOStream.mOStream) {
    (*inConditionalOStream.mOStream) << inStreamFunction;
  }
  return inConditionalOStream;
}

Edit: Usage is intended to be as flexible as possible, with long lived instances being just as acceptable as one-time-use instances. In fact, all 5 constructors are implemented without delegation to minimize the cost of construction. I wrote up some concise, but contrived, examples of usage. In doing so I realized that the addition of copy and move constructors would make this easier to use in the long-lived case. To accomplish that, I had to remove the const from the member stream, which does potentially expose it to some mutability issues while streaming. I think that's ok, since it would require that a user write their own set of stream insertion operators which accepted a non-const reference. I have not modified the code above to add those constructors, so that it remains relevant. The updated code can be found at the project link at the bottom.

Example 1: Temporary Instances

The first four expressions result in output, the final two do not.

#include <iostream>

#include "conditional_ostream.h"


int main(int argc, char *argv[]) {
  // Used as a temporary for one streaming expression
  //   This works because the operator<< accepts a const-reference
  //   as the left-hand argument, and the compiler will provide
  //   lifetime extension for the ConditionalOStream temporary.
  ConditionalOStream(&std::cout       ) << "Stream to std::cout pointer\n";
  ConditionalOStream( std::cout       ) << "Stream to std::cout reference\n";
  ConditionalOStream(&std::cout, true ) << "Stream to std::cout pointer, true\n";
  ConditionalOStream( std::cout, true ) << "Stream to std::cout reference, true\n";
  ConditionalOStream(&std::cout, false) << "Stream to std::cout pointer, false\n";
  ConditionalOStream( std::cout, false) << "Stream to std::cout reference, false\n";
  return 0;
}

Example 2: Long Lived Instances

This contrived application uses local ConditionalOStream instances to control verbosity. It's very poorly written, but should get the point across without cluttering the code with a ton of input handling stuff.

#include <iostream>
#include <string>

#include "conditional_ostream.h"


int main(int argc, char *argv[]) {
  // A simple example of long-lived ConditionalOStreams
  // This application has four output mode flags:
  //   -s  silent
  //   -q  quiet
  //   -v  verbose
  // If none are provided then it is "normal"
  // Because it's an example, I'm *not* implementing full argument handling

  // quiet is on as long as we're not silent
  ConditionalOStream quiet   { std::cout, (argc == 2 && argv[1] != std::string("-s")) || argc == 1 };
  // normal is on if we're verbose or there are no args
  ConditionalOStream normal  { std::cout, (argc == 2 && argv[1] == std::string("-v")) || argc == 1 };
  // verbose is on if we're verbose
  ConditionalOStream verbose { std::cout, (argc == 2 && argv[1] == std::string("-v")) };

  quiet << "Welcome to example.out!\n";
  if (argc == 2) {
    normal << "  One argument was provided: '" << argv[1] << "'\n";
  }
  else {
    normal << "  No arguments were provided\n";
  }
  verbose << "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n";

  return 0;
}

Invoked as a.out [-s|-q|-v] it will print different amounts of output, based on a single command line argument. The lack of copy/move constructors makes this somewhat less practical, if the initialization conditions are complex.

Assembly Stuff

I wanted to make sure the Example 1: Temporary Instances would perform ok, so I tried to make the constructors as efficient as I could. The x86-64 assembly for each constructor is below, it was compiled with clang-11 and disassembled using objdump --demangle -f a.out.

0000000000406260 <ConditionalOStream::ConditionalOStream(std::ostream*)>:
  406260:   55                      push   %rbp
  406261:   48 89 e5                mov    %rsp,%rbp
  406264:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
  406268:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  40626c:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  406270:   48 8b 4d f0             mov    -0x10(%rbp),%rcx
  406274:   48 89 08                mov    %rcx,(%rax)
  406277:   5d                      pop    %rbp
  406278:   c3                      retq   
  406279:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

00000000004062c0 <ConditionalOStream::ConditionalOStream(std::ostream&)>:
  4062c0:   55                      push   %rbp
  4062c1:   48 89 e5                mov    %rsp,%rbp
  4062c4:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
  4062c8:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  4062cc:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  4062d0:   48 8b 4d f0             mov    -0x10(%rbp),%rcx
  4062d4:   48 89 08                mov    %rcx,(%rax)
  4062d7:   5d                      pop    %rbp
  4062d8:   c3                      retq   
  4062d9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

0000000000406320 <ConditionalOStream::ConditionalOStream(std::ostream*, bool const&)>:
  406320:   55                      push   %rbp
  406321:   48 89 e5                mov    %rsp,%rbp
  406324:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
  406328:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  40632c:   48 89 55 e8             mov    %rdx,-0x18(%rbp)
  406330:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  406334:   48 8b 4d e8             mov    -0x18(%rbp),%rcx
  406338:   f6 01 01                testb  $0x1,(%rcx)
  40633b:   48 89 45 e0             mov    %rax,-0x20(%rbp)
  40633f:   0f 84 0d 00 00 00       je     406352 <ConditionalOStream::ConditionalOStream(std::ostream*, bool const&)+0x32>
  406345:   48 8b 45 f0             mov    -0x10(%rbp),%rax
  406349:   48 89 45 d8             mov    %rax,-0x28(%rbp)
  40634d:   e9 0b 00 00 00          jmpq   40635d <ConditionalOStream::ConditionalOStream(std::ostream*, bool const&)+0x3d>
  406352:   31 c0                   xor    %eax,%eax
  406354:   48 89 45 d8             mov    %rax,-0x28(%rbp)
  406358:   e9 00 00 00 00          jmpq   40635d <ConditionalOStream::ConditionalOStream(std::ostream*, bool const&)+0x3d>
  40635d:   48 8b 45 d8             mov    -0x28(%rbp),%rax
  406361:   48 8b 4d e0             mov    -0x20(%rbp),%rcx
  406365:   48 89 01                mov    %rax,(%rcx)
  406368:   5d                      pop    %rbp
  406369:   c3                      retq   
  40636a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

00000000004063b0 <ConditionalOStream::ConditionalOStream(std::ostream&, bool const&)>:
  4063b0:   55                      push   %rbp
  4063b1:   48 89 e5                mov    %rsp,%rbp
  4063b4:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
  4063b8:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  4063bc:   48 89 55 e8             mov    %rdx,-0x18(%rbp)
  4063c0:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  4063c4:   48 8b 4d e8             mov    -0x18(%rbp),%rcx
  4063c8:   f6 01 01                testb  $0x1,(%rcx)
  4063cb:   48 89 45 e0             mov    %rax,-0x20(%rbp)
  4063cf:   0f 84 0d 00 00 00       je     4063e2 <ConditionalOStream::ConditionalOStream(std::ostream&, bool const&)+0x32>
  4063d5:   48 8b 45 f0             mov    -0x10(%rbp),%rax
  4063d9:   48 89 45 d8             mov    %rax,-0x28(%rbp)
  4063dd:   e9 0b 00 00 00          jmpq   4063ed <ConditionalOStream::ConditionalOStream(std::ostream&, bool const&)+0x3d>
  4063e2:   31 c0                   xor    %eax,%eax
  4063e4:   48 89 45 d8             mov    %rax,-0x28(%rbp)
  4063e8:   e9 00 00 00 00          jmpq   4063ed <ConditionalOStream::ConditionalOStream(std::ostream&, bool const&)+0x3d>
  4063ed:   48 8b 45 d8             mov    -0x28(%rbp),%rax
  4063f1:   48 8b 4d e0             mov    -0x20(%rbp),%rcx
  4063f5:   48 89 01                mov    %rax,(%rcx)
  4063f8:   5d                      pop    %rbp
  4063f9:   c3                      retq   
  4063fa:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

For completeness, the entire project can be found here, including a very simple test suite.

\$\endgroup\$
9
  • 2
    \$\begingroup\$ A bigger issue is computing the stuff that would be streamed out. logfile << format_fancy_report(x) can be expensive just to throw away. So, in general, such things are best done as conditional blocks around the output statements. \$\endgroup\$
    – JDługosz
    May 18, 2021 at 16:29
  • \$\begingroup\$ @JDługosz That's fair, I think any expensive function call should be avoided by the programmer. The problem I'm trying to solve is avoiding streaming, not avoiding expensive arbitrary function calls. The pattern I'm trying to replace is constructing an std::ostream with a null buffer, or redirecting to /dev/null. \$\endgroup\$
    – Tom Manner
    May 18, 2021 at 17:40
  • \$\begingroup\$ Have to be careful that some "relied" upon state update doesn't happen in your stream methods (or in your methods if you the the conditional blocks alternative) - otherwise you might be in for a surprise when deployed with the stream off, or visa versa. Alternatively could have a /dev/null like stream instance - i.e. it implements everything it needs to but just never does anything with it - eliminates the if ... although I imagine the method calls might be more of a cost than the branch predicition ... \$\endgroup\$
    – Mr R
    May 18, 2021 at 22:30
  • 2
    \$\begingroup\$ The functions like std::endl are not "special" in any way, they are overloaded! Usually, the stream type (left of <<) provides the correct hint which overload to select, but for your class that's not possible here, hence the need for a specialized inserter. \$\endgroup\$
    – uli
    May 19, 2021 at 6:52
  • 1
    \$\begingroup\$ @uli Gotcha, that makes sense! I updated the comment in the code on GitHub so it's not misleading anymore. \$\endgroup\$
    – Tom Manner
    May 19, 2021 at 15:07

1 Answer 1

3
\$\begingroup\$

Your ConditionalOStream is not a std::ostream:

The advantage is that formatting will be completely skipped if it doesn't have a target.
The disadvantage is that all functions expecting a std::ostream must be modified to accept this new type.

Just writing a blackhole-streambuffer reverses the picture:

You have the standard std::ostream-interface. But even if the output is blackholed, it will be formatted. Which is a waste.

Whether skipping the formatting is worth the cost is your decision. But without measuring, I dare say probably not.

#include <iterator>
#include <streambuf>

class blackhole_buf final : std::streambuf {
    char_type target[64];
    std::streamsize xsputn(const char_type*, std::streamsize n) { return n; }
    int_type overflow(int_type = traits_type::eof()) {
        setp(target, std::end(target));
        return 0;
    }
};
\$\endgroup\$
11
  • \$\begingroup\$ I just added some usage examples to help show the front-end of this. I've used it a bit at my day job, and messed around at home with it. I have not had to modify any streaming functions so far, because the template deduction for the operator<<s will match, and then call the proper std::ostream operator. \$\endgroup\$
    – Tom Manner
    May 19, 2021 at 14:52
  • \$\begingroup\$ I never said you cannot use it. You just cannot pass it as a std::ostream. \$\endgroup\$ May 19, 2021 at 15:09
  • 1
    \$\begingroup\$ Fixed the verbiage, added streambuf. \$\endgroup\$ May 19, 2021 at 15:18
  • 1
    \$\begingroup\$ @Tom, even if you don't write new code yourself, you could be increasing program size by instantiating additional templates, so it's still not zero-cost. \$\endgroup\$ May 19, 2021 at 15:31
  • 1
    \$\begingroup\$ @TobySpeight Yes. Or what one should actually do is derive their own ostream which fully manages the streambuf. \$\endgroup\$ May 19, 2021 at 15:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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