Pages

Monday, May 1, 2017

Formatting issues

Stream formatting is very cool in C++ but with the power and flexibility comes the complexity. It's true that many limitations of the traditional C-style I/O were addressed by the streams framework and new I/O operators (put-to and get-from), manipulators and much more. But that didn't happen on a blink-of-an-eye. It's been evolving since many years, sometimes being completely revamped. A sad legacy is that in some (or many?) circumstances, backward compatibility takes precedence over some "fixes" or adjustments. Then it's not rare to have to carry on with some old "misbehavior" at some corners.

For example, consider the following output:
(I could argue that it has "unexpected" behavior; disregard the "[" and "]")

Formatting flags ...       // OK

*******Hello, there!       // OK; better if left aligned

Number 1: .........e       // OK
Number 2: c^^^             // "Should be" [12^^]
Number 3: 14               // "Should be" [20]

Floating 1: 8.33^^^^^^     // "Should be" [      8.33]
Floating 2: 1.83           // "Should be" [  1.83]
Floating 3: 3.33^^^^^^     // "Should be" [     3.333]

Going home...^^            // "Should be" [**Going home...]


I mean "should be" assuming that:
  • member function calls would permanently change the formatting state
  • manipulators calls would temporarily override the formatting state

But as you can see that's not the case and there's no even a "congruent misbehavior". Some member functions and manipulators (fill(), hex, left) permanently change behavior, others (width(), setw()) temporarily override, yet others misbehave strangely as precision (which should affect only how many decimal places to use and not the integral part as well!). In essence each situation is very simple, but when mixed on code, things can become quickly strange, perhaps a nightmare. A programmer can become quickly stressed and choose a less than optimal quick way out! But that's bad in the long run; that's not quality programming. Even the most simple programs such as rc-03 or the one listed below (which generated the above output) can face such annoyances when trying to print formatted output.

The lines of code that generated the above output are:

#include <cstdlib>
#include <iostream>
#include <iomanip>

void inner()
{
  std::cout
     << "Number 1: "
    
<< std::setw( 10 )
    
<< std::setfill( '.' )
    
<< std::hex
    
<< 14
    
<< std::endl;

  std::cout.fill( '^' );

  std::cout
    
<< "Number 2: "
    
<< std::setw( 4 )
    
<< std::left
    
<< 12
    
<< std::endl;

  std::cout
    
<< "Number 3: "
    
<< 20
    
<< std::endl;

  std::cout
    
<< std::endl;

  std::cout
    
<< "Floating 1: "
    
<< std::setw( 10 )
    
<< std::setprecision( 3 )
    
<< ( 25.0 / 3 )
    
<< std::endl;

  std::cout.precision( 4 );
  std::cout.width( 6 );

  std::cout
    
<< "Floating 2: "
    
<< std::setprecision( 3 )
    
<< ( 11.0 / 6 )
    
<< std::endl;

  std::cout
    
<< "Floating 3: "
    
<< std::setw( 10 )
    
<< ( 10.0 / 3 )
    
<< std::endl;
}

void outer()
{
  std::cout.fill( '*' );

  std::cout
    
<< std::setw( 20 )
    
<< "Hello, there!"
    
<< std::endl
    
<< std::endl;

  inner();

  std::cout
    
<< std::endl
    
<< std::setw( 15 )
    
<< "Going home..."
    
<< std::endl;
}

int main()
{
  std::cout
    
<< std::endl
    
<< "Formatting issues ..."
    
<< std::endl
    
<< std::endl;

  outer();

  return EXIT_SUCCESS;
}


This is the problem!
The definite solution would be to fix the framework.
But that I know is not gonna happen soon.
Perhaps on the next decade :-D

So I (we) have to live with that and cope with the problem. But instead of "hard-coding" a lot of "sets" and "unsets" at each I/O flow statement it would be nice to have an easy, light and quick amendment. My personal solution was to devise a lightweight class to help manage the formatting state of a given stream derived from std::basic_ios<>. The class wraps a std::stack<> which elements are a structure of the most common (as above) formatting options. Upon construction and destruction the class will set the given stream to its default formatting state. Furthermore, push() and pop() operations are published to help saving and reseting the more recent default or custom state. Here's the code (header "toolbox.h"):

#ifndef TOOLBOX_H
#define TOOLBOX_H

#include <iostream>
#include <vector>
#include <stack>

namespace TB
{

template< typename T >
struct fmt
{
  explicit fmt(
std::basic_ios< T > & ios ) : ios( ios )
  {
    init();
  }

  ~fmt()
  {
    init();
  }

  void push()
  {
    context.push

    ( 
        { 
            ios.flags(), 
            ios.precision(), 
            ios.width(), 
            ios.fill() 
        } 
    );
  }

  void top()
  {
    if ( ! context.empty() )
    {
      const state_type & state = context.top();

      ios.flags( state.flags );
      ios.precision( state.precision );
      ios.width( state.width );
      ios.fill( state.fill );
    }
  }

  void pop()
  {
    top();
    context.pop();
  }

public:

  fmt( fmt && f ) : context { f.context }, ios ( f.ios )
  {
    f.context = context_type();
  }

  fmt & operator = ( fmt && f )
  {
    context = f.context;
    ios = f.ios;

    f.context = context_type();

    return *this;
  }

private:

  fmt();
  fmt( const fmt & );

  void init()
  {
    ios.flags( std::ios_base::skipws | std::ios_base::dec );
    ios.precision( 6 );
    ios.width( 0 );
    ios.fill( ios.widen( ' ' ) );
  }

private:

  struct
state_type  
  {
    const std::ios_base::fmtflags flags;
    const std::streamsize precision;
    const std::streamsize width;
    const T fill;
  };

  typedef std::stack

          <  
            const state_type
            std::vector< state_type
          > 
          context_type;

  context_type context;

  std::basic_ios< T > & ios;
};

template< typename T >
inline fmt< T > make_fmt( std::basic_ios< T > & ios )
{
  return fmt< T >( ios );
}

}

#endif /* TOOLBOX_H */



Just include this header file and guard the I/O stream code at the most convenient points as needed by the particular program logic. For instance, "correcting" the previous code could be done as:

...
  
#include "toolbox.h"

void inner()
{

  // Reset std::cout formatting to defaults
  // and save this initial state
  auto fmt = TB::make_fmt( std::cout );
  fmt.push();

  std::cout
     << "Number 1: "
     ...
     << std::endl;

  fmt.top();    // Restore any changed formatting option

  
  std::cout.fill( '^' );

  std::cout
     << "Number 2: "
     ...
     << std::endl;

  fmt.top();

  std::cout
     << "Number 3: "
     ...
     << std::endl;

  std::cout
     << std::endl;

  fmt.top();

  std::cout
     << "Floating 1: "
     ...
     << std::endl;

  fmt.top();

  std::cout.precision( 4 );
  std::cout.width( 6 );

  std::cout
     << "Floating 2: "
     ...
     << std::endl;

  fmt.top();

  std::cout
     << "Floating 3: "
     ...
     << std::endl;

  fmt.pop();
}

void outer()
{

  // Reset std::cout formatting to defaults
  // and save this initial state
  auto fmt = TB::make_fmt( std::cout );
  fmt.push();

  std::cout.fill( '*' );

  std::cout
     << std::setw( 20 )
     << "Hello, there!"
     << std::endl
     << std::endl;

  fmt.push();   // Not required but, one never knows...
  inner();
  fmt.pop();   
// Restore fmt state as before inner().
 
  std::cout
     << std::endl
     << std::setw( 15 )
     << "going home..."
     << std::endl;
}

 
...


The output will now be much closer to what would be expected:
(the additions are portable, efficient and clean)

Formatting flags ...

*******Hello, there!

Number 1: .........e
Number 2: 12^^
Number 3: 20

Floating 1:       8.33
Floating 2: 1.83
Floating 3:    3.33333            // Not perfect, but better!

**Going home...


A slight variation of the program may be more suitable for use on a more daily basis... (I may also try some renaming for attempting better clarity):


  1 #ifndef TOOLBOX_H
  2 #define TOOLBOX_H
  3 
  4 #include <iostream>
  5 #include <vector>
  6 #include <stack>
  7 
  8 namespace TB
  9 {
 10 namespace formatting
 11 {
 12 
 13 template< typename T >
 14 struct context
 15 {
 16   explicit context( std::basic_ios< T > & ios ) : ios ( ios )
 17   {
 18     push();
 19   }
 20 
 21   ~context()
 22   {
 23     pop();
 24   }
 25 
 26   void push()
 27   {
 28     history.push
 29     (
 30         {
 31             ios.flags(),
 32             ios.precision(),
 33             ios.width(),
 34             ios.fill()
 35         }
 36     );
 37   }
 38 
 39   void top()
 40   {
 41     if ( ! history.empty() )
 42     {
 43       const status_type & status = history.top();
 44 
 45       ios.flags( status.flags );
 46       ios.precision( status.precision );
 47       ios.width( status.width );
 48       ios.fill( status.fill );
 49     }
 50   }
 51 
 52   void pop()
 53   {
 54     top();
 55     history.pop();
 56   }
 57 
 58 public:
 59 
 60   context( context && c ) : history { c.history }, ios ( c.ios )
 61   {
 62     c.history = history_type();
 63   }
 64 
 65   context & operator = ( context && c )
 66   {
 67     history = c.history;
 68     ios = c.ios;
 69 
 70     c.history = history_type();
 71 
 72     return *this;
 73   }
 74 
 75 private:
 76 
 77   context();
 78   context( const context & );
 79 
 80   void reset()
 81   {
 82     using fmt = std::ios_base;
 83 
 84     ios.flags( fmt::skipws | fmt::dec );
 85     ios.precision( 6 );
 86     ios.width( 0 );
 87     ios.fill( ios.widen( ' ' ) );
 88   }
 89 
 90 private:
 91 
 92   struct status_type
 93   {
 94     const std::ios_base::fmtflags flags;
 95     const std::streamsize precision;
 96     const std::streamsize width;
 97     const T fill;
 98   };
 99 
100   typedef std::stack
101           <
102             const status_type,
103             std::vector< status_type >
104           >
105           history_type;
106 
107   history_type history;
108 
109   std::basic_ios< T > & ios;
110 };
111 
112 template< typename T >
113 inline context< T > make_context( std::basic_ios< T > & ios )
114 {
115   return context< T >( ios );
116 }
117 
118 }
119 }
120 
121 #endif /* TOOLBOX_H */
122