An Open-Source Big Decimal in C++

Bill Seymour
2022-11-13


Contents


Introduction

This paper describes the public API of an open-source decimal number that can work as either a fixed point type or a floating point type.  It can provide precision up to some user-selectable number of decimal digits, with even more digits possible for intermediate values until you explicitly round the value, expose the value to the outside world, or do any division (and so maybe have to round repeating decimals).

The code is distributed under the Boost Software License which isn’t viral as some others are said to be.  (This is not part of Boost.  The author just likes their open-source license.)

This paper contains links, usually to cppreference.com, to explain some terms to readers who aren’t familiar with features that were introduced in C++11 or later.
Argument types and names in italics are descriptive terms intended for documentation only.  They’re not necessarily the names in the actual code.
Text with a gray background provides additional information or rationale for the design.
Text with a yellow background points out issues with the author’s implementation that you might want to watch out for.

The bigdec class is intended to provide a C++ equivalent to SQL’s NUMERIC and DECIMAL types for use in a database access library that the author is developing; so it’s in that library’s dbacc namespace; and like that library, it deliberately doesn’t require any language or library features past C++11.

The class is written with only C++11 in mind because the principal target audience is engaged in good old “business data processing”, and large companies are often slow to approve new development tools (for good reason).

Also supplied are equivalents of a few <cmath> functions that are expected to return exact values; but the class is not really intended for serious numerical work; and the performance of the author’s implementation might be sub-optimal if you do lots of arithmetic.

The class has no NaN or infinity values (because SQL’s NUMERIC and DECIMAL types don’t); and attempting to construct a bigdec from a floating point NaN or infinity will throw an invalid_argument exception.  Division by zero will throw a domain_error exception.

Also supplied is a rounding mode taken from WG21’s P1889R1 §3.3.

This class uses the author’s bigint class, principally to provide a type in which it can return very large integers.  You can also explicitly construct a bigdec from a bigint and assign a bigint to *this.


The source files

And for bigint


Synopsis

A Rounding Mode

namespace dbacc
{
    enum class rounding
    {
        all_to_neg_inf, all_to_pos_inf,
        all_to_zero, all_away_zero,
        all_to_even, all_to_odd,
        all_fastest, all_smallest,
        all_unspecified,
        tie_to_neg_inf, tie_to_pos_inf,
        tie_to_zero, tie_away_zero,
        tie_to_even, tie_to_odd,
        tie_fastest, tie_smallest,
        tie_unspecified
    };
}


The bigdec Class

#include "bigint.hpp"

#ifndef BIGDEC_MAX_DIGS
#define BIGDEC_MAX_DIGS 38
#endif

namespace dbacc {

class bigdec final
{
public:
  //
  // A member type and three constants:
  //
    using scale_type = int;
    static constexpr scale_type floating_scale = INT_MIN;
    static constexpr rounding default_rounding = rounding::tie_to_even;
    static constexpr rounding bankers_rounding = rounding::tie_away_zero;
    static constexpr int max_digs = BIGDEC_MAX_DIGS;

  //
  // Special member functions and swap:
  //
    bigdec(rounding = default_rounding, scale_type = 0);

    bigdec(const bigdec&);
    bigdec(bigdec&&);

    bigdec& operator=(const bigdec&);
    bigdec& operator=(bigdec&&);

    ~bigdec() noexcept;

    void swap(bigdec&);

  //
  // Construction from other types:
  //
    template<typename FundamentalArithmeticTypeNotBool>
      bigdec(FundamentalArithmeticTypeNotBool, scale_type = see below, rounding = default_rounding);
    explicit bigdec(bool, scale_type = 0, rounding = rounding::all_to_zero);
    explicit bigdec(const bigint&, scale_type = 0, rounding = default_rounding);
    explicit bigdec(const char*, rounding = default_rounding, const char** = nullptr);
    explicit bigdec(const std::string&, rounding = default_rounding, std::size_t* = nullptr);

  //
  // Assignment from other types:
  //
    template<typename FundamentalArithmeticTypeNotBool>
      bigdec& operator=(FundamentalArithmeticTypeNotBool);
    template<typename FundamentalArithmeticTypeNotBool>
      bigdec& assign(FundamentalArithmeticTypeNotBool, scale_type = see below, rounding = default_rounding);
    bigdec& assign(bool, scale_type = 0, rounding = rounding::all_to_zero);
    bigdec& assign(const bigint&, scale_type = 0, rounding = default_rounding);
    bigdec& assign(const char*, rounding = default_rounding, const char** = nullptr);
    bigdec& assign(const std::string&, rounding = default_rounding, std::size_t* = nullptr);

  //
  // Conversion to other types:
  //
    explicit operator bool() const noexcept;
    explicit operator long double() const noexcept;

    bigint to_bigint(rounding = rounding::all_to_zero) const;

    std::string to_string(bool scientific = false) const;
    std::string to_string(int precision, bool scientific = false) const;

  //
  // Rounding and scale:
  //
    rounding get_rounding() const noexcept;
    void set_rounding(rounding) noexcept;

    scale_type scale() const noexcept;
    scale_type current_scale() const noexcept;
    bool isfloat() const noexcept;

    void set_user_scale(scale_type) noexcept;
    void adjust_scale_by(scale_type) noexcept;
    void change_scale_to(scale_type) noexcept;

  //
  // Observers:
  //
    bool iszero() const noexcept;
    bool isneg()  const noexcept;
    bool ispos()  const noexcept;
    bool isone()  const noexcept;
    bool isone(bool negative) const noexcept;

    bigdec rawval() const;

    int signum() const noexcept;

  //
  // Mutators:
  //
    void clear() noexcept;
    void all_clear() noexcept;

    bigdec& set_to_zero() noexcept;
    bigdec& set_to_one(bool negative = false);

    bigdec& negate() noexcept;
    bigdec& abs() noexcept;

    bigdec& round();
    bigdec& round(int number_of_digits);
    bigdec& round(int number_of_digits, rounding);

  //
  // Comparisons:
  //
    bool operator==(const bigdec&) const;
    bool operator!=(const bigdec&) const;
    bool operator< (const bigdec&) const;
    bool operator> (const bigdec&) const;
    bool operator<=(const bigdec&) const;
    bool operator>=(const bigdec&) const;

  //
  // Arithmetic:
  //
    bigdec& operator++();
    bigdec& operator--();

    bigdec  operator++(int);
    bigdec  operator--(int);

    bigdec& operator+=(const bigdec&);
    bigdec& operator-=(const bigdec&);
    bigdec& operator*=(const bigdec&);
    bigdec& operator/=(const bigdec&);

  //
  // Size, etc.:
  //
    std::size_t digits();
    std::size_t unnormalized_digits() const noexcept;

    bool empty() const noexcept;
    std::size_t size() const noexcept;
    void shrink_to_fit();
};


Non-member Functions

//
// Miscellaneous functions:
//
using std::swap;
void swap(bigdec&, bigdec&);

bigint to_bigint(const bigdec&, rounding = rounding::all_to_zero) const;

std::string to_string(const bigdec&, bool scientific = false);
std::string to_string(const bigdec&, int precision, bool scientific = false);

//
// Non-member operators:
//
bigdec operator+(const bigdec&);
bigdec operator-(const bigdec&);

bigdec operator+(const bigdec&, const bigdec&);
bigdec operator-(const bigdec&, const bigdec&);
bigdec operator*(const bigdec&, const bigdec&);
bigdec operator/(const bigdec&, const bigdec&);

//
// <cmath>-like functions:
//
bigdec abs(const bigdec&) noexcept;
bigdec ceil(const bigdec&);
bigdec floor(const bigdec&);
bigdec trunc(const bigdec&);
bigdec round(const bigdec&, rounding = rounding::tie_away_zero);
bigdec rint(const bigdec&);
bigdec modf(const bigdec&, bigdec*);
bigdec fmod(const bigdec&, const bigdec&);
bigdec remainder(const bigdec&, const bigdec&, rounding = rounding::tie_to_even);
bigdec sqr(const bigdec&);
bigdec copysign(const bigdec&, const bigdec&);
bigdec fma(const bigdec&, const bigdec&, const bigdec&);

//
// Formatted I/O:
//
template<typename CharType, typename CharTraits>
  std::basic_ostream<CharType,CharTraits>&
    operator<<(std::basic_ostream<CharType,CharTraits>&, const bigdec&);

template<typename CharType, typename CharTraits>
  std::basic_istream<CharType,CharTraits>&
    operator>>(std::basic_istream<CharType,CharTraits>&, bigdec&);

} // namespace dbacc


Detailed descriptions

The class is implemented internally as a sign-magnitude value and a scale, the number of decimal digits to the right of the decimal point.  No operation will ever yield a negative zero that’s visible outside the class; and all operations will eagerly discard leading and trailing zeros, decrementing the scale as appropriate when trailing zeros are discarded.

Note that the scale can be less than zero if the represented value is divisible by an integral power of ten greater than zero because there are unstored zeros to the left of the decimal point (and so fewer than zero digits to the right of the decimal point).

The class normally behaves internally as a floating point type, and rounding to the desired scale occurs when the value becomes externally visible (to_string() for example), when division would otherwise result in a quotient with an extremely large number of decimal digits, or when the user explicitly calls one of the round() overloads.

Note that the functions that expose the value to the outside world are all const and so they can’t round *this.  Instead, they create temporary objects and round those.  The only way to round *this is to explicitly call one of the round() overloads (or the digits() function which, in turn, calls this->round()).

It’s primarily intended to be used as a fixed point type, but you can make it work like a floating point type by explicitly setting the desired scale to floating_scale (which basically just turns off rounding except for division as stated above).

Addition, subtraction and multiplication are exact until rounding occurs.  Division that results in very large quotients will generate a guard digit and then round if necessary.

Since the class is explicitly advertised as a decimal type, formatted I/O happens in decimal only.  The oct and hex I/O manipulators and their associated flags have no effect.


A Rounding Mode

enum class rounding
{
    all_to_neg_inf, all_to_pos_inf,
    all_to_zero, all_away_zero,
    all_to_even, all_to_odd,
    all_fastest, all_smallest,
    all_unspecified,
    tie_to_neg_inf, tie_to_pos_inf,
    tie_to_zero, tie_away_zero,
    tie_to_even, tie_to_odd,
    tie_fastest, tie_smallest,
    tie_unspecified
};
This scoped enumeration is taken from a Technical Specification (a kind of warning about future standardization) that’s currently being developed by WG21’s Study Group 6 – Numerics. See P1889R1 §3.3.

The bigdec class supports all the rounding modes.  All of the fastest, smallest and unspecified modes act like their corresponding to_zero modes.


The bigdec Class


A Member Type and Three Constants

using scale_type = int;
static constexpr scale_type floating_scale = INT_MIN;
The class normally acts like a fixed point type (at least when the value becomes externally visible), but you can make it act like a floating point type by explicitly setting the desired scale to floating_scale.  This just turns off rounding except for division.
static constexpr rounding default_rounding = rounding::tie_to_even;
If you don’t specify a rounding mode when calling a function that takes a rounding argument, that argument will default to default_rounding except as noted.
static constexpr rounding bankers_rounding = rounding::tie_away_zero;
There’s also a symbol for a rounding mode that might be more appropriate for financial applications, but this library makes no use of it.
#ifndef BIGDEC_MAX_DIGS
#define BIGDEC_MAX_DIGS 38
#endif
static constexpr int max_digs = BIGDEC_MAX_DIGS;
As a practical matter, we can’t have an infinite number of digits; so when using this class as a fixed point type, exposing the value to the outside world will round the value to at most max_digs digits.  This can also happen if the class is being used as a floating point type when division would generate more than max_digs digits, in which case division will generate a guard digit and then round.

Before you #include bigdec.hpp, you can define the macro, BIGDEC_MAX_DIGS, to select the value for max_digs; but if you don’t, the author’s implementation defaults to 38 digits.  If you do define the macro, it must be the same in all of a complete program’s translation units that use the bigdec class to avoid violations of the One Definition Rule.

This class is intended to support a database access library, and the author notes that both Oracle and SQL Server have exact numeric types that can hold up to 38 decimal digits.  That’s where the 38 comes from.  (ISO/IEC 9075, the SQL standard, leaves the maximum precision for exact types implementation-defined, and Postgresql allows up to 131072 digits; but that seems over the top, and division could grind to a halt if we’re generating a repeating decimal.)


Special Member Functions and swap

explicit bigdec(rounding = default_rounding, scale_type = 0);
The default constructor constructs a bigdec equal to zero; and it can optionally be used to initialize the rounding mode and the user’s requested scale.
~bigdec() noexcept;
The destructor is non-trivial.
bigdec(const bigdec&);
bigdec& operator=(const bigdec&);

bigdec(bigdec&&);
bigdec& operator=(bigdec&&);

void swap(bigdec&);
bigdecs are freely copyable, moveable, and swappable.


Construction from Other Types

template<typename FundamentalArithmeticTypeNotBool>
bigdec(FundamentalArithmeticTypeNotBool, scale_type = ?, rounding = default_rounding);
There are actually three constructor templates that provide implicit conversion from fundamental arithmetic types, one for signed integers, one for unsigned integers, and one for floating point values.  They all use some template tricks to generate the correct overload and to make sure that there’s no implicit conversion from bool.  If the only argument is an integer, the scale defaults to zero, otherwise it defaults to floating_scale.  Attempting to construct a bigdec from a floating point NaN or infinity will throw an invalid_argument exception.

The details aren’t documented here because they would just obscure the high-level purpose of these constructors.  C++ coders who are familiar with type traits templates will likely find the details unsurprising (and probably tedious as well).

explicit bigdec(bool, scale_type = 0, rounding = rounding::all_to_zero);
If you really need to construct a bigdec from a bool, you can do it explicitly.  Note that the rounding mode defaults to all_to_zero, not default_rounding.
explicit bigdec(const bigint&, scale_type = 0, rounding = default_rounding);
There’s no implicit conversion from bigints because both bigdec and bigint have implicit conversions from floating point types which could lead to ambiguity.
explicit bigdec(const char*        value, rounding = default_rounding, const char** termptr = nullptr);
explicit bigdec(const std::string& value, rounding = default_rounding, std::size_t* termpos = nullptr);
Instances can also be explicitly constructed from C-style strings and std::strings.  When constructing from strings, the scale is inferred from the position of the decimal point (or its absence).  The string may begin with a '+' or a '-', and it may be in either fixed-point or scientific notation.  The 'e' in scientific notation may be upper or lower case.

An optional third argument can be used to discover the character that terminated the parse:


Assignment from Other Types

template<typename FundamentalArithmeticTypeNotBool>
  bigdec& operator=(FundamentalArithmeticTypeNotBool);
Instances can be assigned values of any fundamental arithmetic type, but as with the implicit constructors, there are template tricks that fix it so that you can’t assign a bool; and attempting to assign a floating point NaN or infinity will throw an invalid_argument exception.
bigdec& assign(bool, scale_type = 0, rounding = rounding::all_to_zero);
But there’s an assign member function that you can use to explicitly assign from bool if you really need to.  Note that the rounding mode defaults to all_to_zero, not default_rounding.
template<typename FundamentalArithmeticTypeNotBool>
  bigdec& assign(FundamentalArithmeticTypeNotBool, scale_type = ?, rounding = default_rounding);
There’s also an assign member template that allows you to specify a scale and a rounding mode.  As with the implicit constructors, if the only argument is an integer, the scale will default to zero; but if it’s a floating point value, the scale will default to floating_scale and a NaN or infinity will cause the function to throw an invalid_argument exception.
bigdec& assign(const bigint&, scale_type = 0, rounding = default_rounding);
You can also explicitly assign bigints and optionally specify a scale and rounding mode.
There’s no operator=(bigint) because both bigdec and bigint have implicit conversions from floating point types which could lead to ambiguity.
bigdec& assign(const char*, rounding = default_rounding, const char** = nullptr);
bigdec& assign(const std::string&, rounding = default_rounding, std::size_t* = nullptr);
Assignment from strings behaves like construction from strings.


Conversion to Other Types

explicit operator bool() const noexcept;
The conversion to bool returns whether *this is non-zero.  It’s intended to support the if(my_bigint) idiom.
explicit operator long double() const noexcept;
Conversion to long double is also provided.  If *this is outside the range, [LDBL_MIN,LDBL_MAX], the function will return HUGE_VALL with errno set to ERANGE.  Conversion to long double can quietly result in loss of precision.
bigint to_bigint(rounding = rounding::all_to_zero) const;
The to_bigint function makes a copy of *this and then rounds the copy.  By default, the value will be truncated toward zero; but you can specify a different rounding mode if you need to.
std::string to_string(bool sci = false) const;
std::string to_string(int prec, bool sci = false) const;
The to_string() function makes a copy of *this, rounds the copy, and then returns that value in either fixed-point or scientific notation.  A decimal point will be a period.  The string can begin with '-', but it will never begin with '+'.

In general, the function rounds to prec (precision) decimal digits, or to max_digs digits if prec is greater than max_digs; but if prec is not passed, it will default to a number of digits appropriate given the user’s requested scale; and if prec is exactly INT_MIN, no rounding will occur.

In fixed-point notation (sci == false):

In scientific notation (sci == true):

The sci and prec arguments are intended mainly to generate strings that are suitable for the output stream << operator, but there’s no compelling reason to keep them secret and disallow their use for other purposes.


Rounding and Scale

rounding get_rounding() const noexcept;
void set_rounding(rounding = default_rounding) noexcept;
The user’s requested rounding mode can be examined and changed after construction.
int scale() const noexcept;
int current_scale() const noexcept;
bool isfloat() const noexcept { /*as if*/ return scale() == floating_scale; }
scale() returns the user’s desired scale; current_scale() returns the scale of the current representation before rounding.
void set_user_scale(scale_type) noexcept;
void adjust_scale_by(scale_type) noexcept;
void change_scale_to(scale_type) noexcept;

set_user_scale(scale_type) allows the user to set the desired scale after construction without changing the represented value.

adjust_scale_by(scale_type adj) adds adj to the current internal scale without changing the raw (unscaled) value.  This has the effect of multiplying the represented value by 10adj.

change_scale_to(scale_type new_scale) just sets the current internal scale.  This has the effect of arbitrarily setting the represented value to rawval() × 10new_scale.


Observers

bool iszero() const noexcept;
bool isneg()  const noexcept;
bool ispos()  const noexcept;
bool isone()  const noexcept;
bool isone(bool negative) const noexcept;
iszero() returns whether *this is zero.
isneg() returns whether *this is less than zero.
ispos() returns whether *this is greater than zero.
isone() with no argument returns whether *this is ±1.
isone(false) returns whether *this is +1; isone(true) returns whether *this is −1.
bigdec rawval() const;
rawval() just returns a copy of *this with the current scale set to zero, the effect being to return an integer value made of the currently represented digits.  It doesn’t change the user’s requested scale.
int signum() const noexcept;
signum() returns +1 if *this is greater than zero, 0 if *this is equal to zero, or −1 if *this is less than zero.


Mutators

void clear() noexcept;
void all_clear() noexcept;
bigdec& set_to_zero() noexcept;
clear() and set_to_zero() both assign zero to *this without changing the user’s requested scale or rounding mode.  all_clear() has the additional effect of setting the requested scale to zero and the rounding mode to default_rounding.
bigdec& set_to_one(bool negative = false);

set_to_one(false) assigns +1 to *this; set_to_one(true) assigns −1 to *this.

bigdec& negate() noexcept;
negate() changes the sign of *this if *this is non-zero.  It will never create a negative zero.
bigdec& abs() noexcept;
abs() unconditionally makes *this non-negative.
bigdec& round();
bigdec& round(int number_of_digits);
bigdec& round(int number_of_digits, rounding);
The round() function with no argument does nothing if the user requested floating_scale, otherwise it rounds to the user’s requested scale using the user’s requested rounding mode.

The other overloads round to a particular number of decimal digits without regard to the user’s requested scale (and so round even if the user requested floating_scale).  If no rounding mode is specified, it defaults to the user’s requested mode.  If number_of_digits is less than one, *this is set to zero.

Users may call these functions explicitly if they think that memory usage is getting out of hand; and as stated above, several functions that expose the value to the outside world will first make a copy of *this and then call the no-argument round().

All but clear() and all_clear() return *this.


Comparisons

bool operator==(const bigdec&) const;
bool operator!=(const bigdec&) const;
bool operator< (const bigdec&) const;
bool operator> (const bigdec&) const;
bool operator<=(const bigdec&) const;
bool operator>=(const bigdec&) const;

There’s no operator<=> (a.k.a., “spaceship operator”) because the database access library that the bigdec class is intended to support tries very hard not to require any language or library features past C++11; and in the absence of NaNs, we always expect strong ordering anyway.


Arithmetic

bigdec& operator++();
bigdec& operator--();

bigdec  operator++(int);
bigdec  operator--(int);
The increment and decrement operators add or subtract 1.0 as expected; they don’t just add ±1 to the currently represented LSD.
bigdec& operator+=(const bigdec&);
bigdec& operator-=(const bigdec&);
bigdec& operator*=(const bigdec&);
bigdec& operator/=(const bigdec&);
Addition, subtraction and multiplication are exact and so can generate more than max_digs digits.  Division, if it would generate a quotient of more than max_digs digits, generates a guard digit and then rounds using the user’s requested rounding mode.

Division by zero throws a domain_error exception.


Size, etc.

std::size_t digits();
std::size_t unnormalized_digits() const noexcept;
unnormalized_digts() returns the number of decimal digits currently represented (before rounding).

digits() first calls this->round() and then returns the number of digits represented.  This could be fewer than the number of significant digits since it excludes trailing zeros to the right of the decimal point.

bool empty() const noexcept;
std::size_t size() const noexcept;
void shrink_to_fit();
The internal representation of the value is stored in a standard container of some sort; and empty(), size() and shrink_to_fit() all just call that container’s functions of the same name.

In the author’s implementation, the raw value is just a deque<signed char> with each element containing a single BCD digit, so unnormalized_digits() and size() will always return the same value.


Non-member Functions


Miscellaneous Functions

using std::swap;
void swap(bigdec&, bigdec&);

bigint to_bigint(const bigdec&, rounding = rounding::all_to_zero) const;

std::string to_string(const bigdec&, bool sci = false);
std::string to_string(const bigdec&, int prec, bool sci = false);
All have the same semantics as do their corresponding member functions.


Operators

bigdec operator+(const bigdec&);
bigdec operator-(const bigdec&);
The unary + operator returns a copy of its argument; the unary - operator returns a negated copy of its argument.
bigdec operator+(const bigdec& lhs, const bigdec& rhs);
bigdec operator-(const bigdec& lhs, const bigdec& rhs);
bigdec operator*(const bigdec& lhs, const bigdec& rhs);
bigdec operator/(const bigdec& lhs, const bigdec& rhs);
Addition, subtraction and multiplication are exact.  Division, if it would generate more than max_digs digits, will generate a guard digit and round using lhs’ rounding mode.  Division by zero will throw a domain_error exception.


<cmath>-like Functions

bigdec abs(const bigdec&) noexcept;
bigdec ceil(const bigdec&);
bigdec floor(const bigdec&);
bigdec trunc(const bigdec&);
bigdec round(const bigdec&, rounding = rounding::tie_away_zero);
bigdec rint(const bigdec&);
bigdec modf(const bigdec&, bigdec*);
bigdec fmod(const bigdec&, const bigdec&);
bigdec remainder(const bigdec&, const bigdec&, rounding = rounding::tie_to_even);
bigdec sqr(const bigdec&);
bigdec copysign(const bigdec&, const bigdec&);
bigdec fma(const bigdec&, const bigdec&, const bigdec&);
Although the bigdec class is not really intended for serious numerical work, a few <cmath>-like functions that are expected to return exact values are supplied.  All have behavior that mimics the standard functions of the same name.

Note that the nonmember round function allows you to specify a rounding mode.  This defaults to tie_away_zero which mimics std::round().

Since calling any of the ceil, floor, trunc, round or rint functions explicitly requests an integer value, the returned value’s scale() will always return 0, and current_scale() could return a value less than zero if trailing zeros have been rounded away (because there are fewer than zero digts to the right of the decimal point).

The remainder function also allows specifying a rounding mode for the quotient of the first two arguments.  This defaults to tie_to_even which yields the IEEE 754 remainder (like std::remainder() does); but users can play with computing other kinds of remainders if they want to.  (This is actually how we implement fmod():  it just calls remainder() using all_to_zero as the rounding mode.)

The fma function doesn’t do anything special since no rounding would occur between the multiplication and the addition in any event; but the function is included because it’s “canonical”, and because it might simplify updating legacy code to use bigdecs.  The FP_FAST_FMA macro tells you nothing about this function.

For all of these functions, the result’s requested scale and rounding mode will be the same as those of the first, or only, argument.


Formatted I/O

#ifdef BIGNUM_NO_IO_OPERATORS
  #define BIGDEC_NO_IO_OPERATORS
  #define BIGINT_NO_IO_OPERATORS
#endif

#ifndef BIGDEC_NO_IO_OPERATORS
  template<typename Ch, typename Tr>
  std::basic_ostream<Ch,Tr>& operator<<(std::basic_ostream<Ch,Tr>&, const bigdec&);

  template<typename Ch, typename Tr>
  std::basic_istream<Ch,Tr>& operator>>(std::basic_istream<Ch,Tr>&, bigdec&);
#endif
The canonical iostream insertion and extraction operators are provided.  They’re localized (ctype and numpunct facets) and recognize most of the ios_base and basic_ios<> formatting facilities that make sense for floating-point numbers. I/O happens in decimal only, so hex and oct are not supported; and uppercase affects only the 'e' when writing in scientific notation.

The << operator makes a copy of its second argument and rounds the copy to the stream’s precision() before doing the output.

The >> operator will happily accept input in either fixed-point or scientific notation, and with the 'e' in scientific notation either upper or lower case, regardless of the flags which might otherwise affect that.  Any invalid character or EOF encountered after a valid value is parsed will just quietly terminate the parse; but if we don’t have a valid value yet, invalid characters and EOF will set the stream’s failbit, and the bigdec argument won’t be touched.

C++20’s formatting library is not supported because, as with the spaceship operator, we deliberately refrain from requiring any language or library features past C++11.

Issue:  the author’s implementation of the >> operator recognizes localized thousands separators but just ignores them.  It doesn’t require that they be in the right places.

Although not shown in the synopsis at the beginning of this paper, if you won’t be doing any I/O of bigdecs in a particular translation unit (TU), you can #define BIGDEC_NO_IO_OPERATORS before you #include "bigdec.hpp", which might save compilation time in complicated builds (although it won’t reduce code size or running time).  You can define that macro in some TUs and not in others without fear of ODR violations.  (Note that the bigint class does the same thing, so you might want to #define BIGINT_NO_IO_OPERATORS as well.  You can #define BIGNUM_NO_IO_OPERATORS and get both.)

The actual code in bigdec.hpp is:
  } // end of namespace dbacc
  #ifndef BIGDEC_NO_IO_OPERATORS
    #include "bigdec_io.hpp"
  #endif
and bigdec_io.hpp contains the actual template definitions in the dbacc namespace.  The hope is that the compiler’s lexer won’t even have to scan that code if it doesn’t need to.

bigdec_io.hpp is a proper header file beginning and ending in the global namespace, and with the usual include guard.  If you include bigdec.hpp with BIGINT_NO_IO_OPERATORS defined, you can still get the I/O operators later in the same TU by explicitly including bigdec_io.hpp yourself.  It’s not clear why you’d want to do that, but there it is.
And note that any explicit #include of bigdec_io.hpp may appear only in the global namespace if you want ADL to work right.


All suggestions and corrections will be welcome; all flames will be amusing.
Mail to was@pobox.com.