I've always wanted to be able to steer stream indentation, so that I could write code like this:
/// This probably has to be called once for every program:
// http://stackoverflow.com/questions/26387054/how-can-i-use-stdimbue-to-set-the-locale-for-stdwcout
std::ios_base::sync_with_stdio(false);
std::cout << "I want to push indentation levels:\n" << indent_manip::push
<< "To arbitrary depths\n" << indent_manip::push
<< "and pop them\n" << indent_manip::pop
<< "back down\n" << indent_manip::pop
<< "like this.\n"
Finally, I've done it, but I had to do a kind of nasty hack.
The problem is:
- I need to call a facet that is called on every string, which appears to mean that I have to use codecvt.
- I need the indentation level to persist, which means using
iosbase::iword
. codecvt
doesn't have a reference toiosbase
anywhere.
Combine all that with the fact that facets have to be constant objects, and you get a kind of tricky problem. I solved it with a dirty trick:
- I use
iword
to keep track of the indentation level, since I get stream tracking for free that way. If I was only dealing with a single stream it would useless information. - Whenever
indent_manip::push
is called, theiword
is incremented, a new fact is created, a locale is created with that facet, and the current stream is imbued with that locale.
I put the implementation, along with a more involved demo/test on GitHub.
Tested on gcc 5.1 with:
g++ -o indenter --std=c++14 indent_test.cpp
What I'm most interested in
Obviously it's not going to win performance prizes, and there's some easy places to optimize. I'm curious about the safety of what I'm trying to do. Is there a cleaner alternative?
Obviously I can create a custom ostream
, and that'll be phase two, but I have a use case where I want to imbue this behavior into an existing std::ostream
, and this was the only way I could figure out how to do it.
How's the usability?
#pragma once
#include <locale>
#include <algorithm>
#include <iostream>
#include <iomanip>
#include <cassert>
class indent_facet : public std::codecvt<char, char, std::mbstate_t> {
public:
explicit indent_facet( int indent_level, size_t ref = 0)
: std::codecvt<char, char, std::mbstate_t>(ref), m_indentation_level(indent_level) {}
typedef std::codecvt_base::result result;
typedef std::codecvt<char, char, std::mbstate_t> parent;
typedef parent::intern_type intern_type;
typedef parent::extern_type extern_type;
typedef parent::state_type state_type;
int &state(state_type &s) const { return *reinterpret_cast<int *>(&s); }
protected:
virtual result do_out(state_type &need_indentation,
const intern_type *from, const intern_type *from_end, const intern_type *&from_next,
extern_type *to, extern_type *to_end, extern_type *&to_next
) const override;
// Override so the do_out() virtual function is called.
virtual bool do_always_noconv() const throw() override {
return m_indentation_level==0;
}
unsigned int m_indentation_level = 0;
};
indent_facet::result indent_facet::do_out(state_type &need_indentation,
const intern_type *from, const intern_type *from_end, const intern_type *&from_next,
extern_type *to, extern_type *to_end, extern_type *&to_next
) const
{
result res = std::codecvt_base::noconv;
for (; (from < from_end) && (to < to_end); ++from, ++to) {
// 0 indicates that the last character seen was a newline.
// thus we will print a tab before it. Ignore it the next
// character is also a newline
if ((state(need_indentation) == 0) && (*from != '\n')) {
res = std::codecvt_base::ok;
state(need_indentation) = 1;
for(int i=0; i<m_indentation_level; ++i){
*to = '\t'; ++to;
}
if (to == to_end) {
res = std::codecvt_base::partial;
break;
}
}
*to = *from; // Copy the next character.
// If the character copied was a '\n' mark that state
if (*from == '\n') {
state(need_indentation) = 0;
}
}
if (from != from_end) {
res = std::codecvt_base::partial;
}
from_next = from;
to_next = to;
return res;
};
/// I hate the way I solved this, but I can't think of a better way
/// around the problem. I even asked stackoverflow for help:
///
/// http://stackoverflow.com/questions/32480237/apply-a-facet-to-all-stream-output-use-custom-string-manipulators
///
///
namespace indent_manip{
static const int index = std::ios_base::xalloc();
static std::ostream & push(std::ostream& os)
{
auto ilevel = ++os.iword(index);
os.imbue(std::locale(os.getloc(), new indent_facet(ilevel)));
return os;
}
std::ostream& pop(std::ostream& os)
{
auto ilevel = (os.iword(index)>0) ? --os.iword(index) : 0;
os.imbue(std::locale(os.getloc(), new indent_facet(ilevel)));
return os;
}
/// Clears the ostream indentation set, but NOT the raii_guard.
std::ostream& clear(std::ostream& os)
{
os.iword(index) = 0;
os.imbue(std::locale(os.getloc(), new indent_facet(0)));
return os;
}
/// Provides a RAII guard around your manipulation.
class raii_guard
{
public:
raii_guard(std::ostream& os):
start_level(os.iword(index)),
oref(os)
{}
~raii_guard()
{
reset();
}
/// Resets the streams indentation level to the point itw as at
/// when the guard was created.
void reset()
{
oref.iword(index) = start_level;
oref.imbue(std::locale(oref.getloc(), new indent_facet(start_level)));
}
private:
std::ostream& oref;
int start_level;
};
}
std::ostream
. \$\endgroup\$