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.
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\$std::ostream
with a null buffer, or redirecting to/dev/null
. \$\endgroup\$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\$