Skip to content

Commit

Permalink
Preliminary implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Bloomfield committed Apr 26, 2020
1 parent 02ea0df commit 199cbe6
Show file tree
Hide file tree
Showing 2 changed files with 335 additions and 1 deletion.
101 changes: 100 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,101 @@
# integral_io
A ridiculously verbose way to make C++ streams handle a 1 byte integer as a number instead of a character

This is a small header-only library which helps ensure C++ streams read/write integers of all sizes
consistently. Commonly, 1-byte integers are handled as characters, while all other integers are
handled as numbers. Using this library, you can make sure they are all handled as numbers.

This library has a permissive licence (MIT). You are free to use it in whatever way you like,
including in closed-source / commerical projects.


## Quick example
To try out this library, simply copy the header file into your project. You can then use it like
this:

```c++
#include "integral_io.hpp"
#include <iostream>

using integral_io::as_integer;

int main()
{
std::int8_t value;
std::cout << "Enter a number: ";
std::cin >> as_integer(value);
std::cout << "The number was: " << as_integer(value) << std::endl;
return 0;
}
```

The main public interface of this library is the `as_integer()` function. It will take any integer
you give it and ensure the stream treats it as a number (not a character), no matter what size it
is. It should work for input and output using any standard streams, including `cin`, `cout`,
`fstream`, and `stringstream`.

The example above is quite simple and contrived. In practice, I expect it will be more useful in
situations where the integer type is a template parameter. In these cases, it prevents the need to
write your own conditional logic for single-byte types.


## C++ version
This library requires C++11 or later.


## Background
### The problem I wanted to solve
Consider the following simple C++ program:

```c++
#include <iostream>

int main()
{
std::int8_t value1 = 42;
std::int16_t value2 = 42;
std::cout << value1 << " " << value2;
return 0;
}
```

At first glance, it looks like it should print "`42 42`" to the console. However, it actually prints
"`* 42`" on many compilers.

The reason is that the 1-byte integer (`value1`) is treated as a character not a number. It renders
an asterisk (`*`) because that is the ASCII/UTF-8 character with value 42. Meanwhile, the 2-byte
integer is rendered as the number we'd expect.

A similar problem exists when reading from an input stream. When reading into a 1-byte type, the
stream will not parse the number. Instead, it will read a single character and store its ASCII/UTF-8
code.

This library aims to provide a readable way to resolve this inconsistency.

### Why does the problem exist?
The C++ standard does not require compilers to distinguish between 1-byte integers and characters.
In many cases (perhaps most), all 1-byte integers are aliases of an equivalent `char` type (i.e. a
1-byte character). This means the stream operators cannot tell them apart so they prefer the
well-known character insertion/extraction procedures.

It's an annoying quirk, but we're probably stuck with it. Changing it now would likely cause a lot
of existing code to break. Additionally, streams are likely to be somewhat superseded by
[format strings in C++20](https://en.cppreference.com/w/cpp/utility/format/format) anyway.

### Aren't there easier ways to do this?
Yes. If you know the limits of your integer when you're writing your code then you can make it much
simpler. A static cast and (in the case of input) a temporary variable are all you need. (Or just
don't use streams in the first place.)

The complexity of this library's implementation therefore *massively* outweighs the scale of the
problem. It's really more of an interesting exercise than something I'd expect anyone to use for
anything serious.


## TODO list

* Implement unit tests.
* Automatically build/test on every push.
* Ensure code only uses C++11 features if possible.
* Ensure wide streams work correctly.
* Ensure code works on a variety of compilers/platforms.
* Avoid doing anything if the compiler treats 1-byte integers as distinct from chars.
235 changes: 235 additions & 0 deletions integral_io.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
#include <iostream>
#include <type_traits>
#include <limits>

#if defined(min) || defined(max)
# error min() and max() macros must not be defined. For Windows, define NOMINMAX before including the Windows headers.
#endif

namespace integral_io
{
// Generic trait which handles any signed or unsigned integer which is bigger than 1 byte.
template <typename Integer, typename = std::enable_if_t<std::is_integral_v<Integer>, Integer>, std::size_t = sizeof(Integer), bool = std::is_signed<Integer>::value>
struct integral_io_trait
{
using type = Integer;
};

// Trait specialised for signed integers which are exactly 1 byte.
template <typename Integer>
struct integral_io_trait<Integer, Integer, 1, true>
{
using type = std::int16_t;
};

// Trait specialised for unsigned integers which are exactly 1 byte.
template <typename Integer>
struct integral_io_trait<Integer, Integer, 1, false>
{
using type = std::uint16_t;
};

// Helper template alias.
template <typename Integer>
using integral_io_t = typename integral_io_trait<Integer>::type;

// Generic output-only wrapper for signed and unsigned integers which are bigger than 1 byte.
template <typename Integer, typename = typename std::enable_if<std::is_integral<Integer>::value, Integer>::type, std::size_t = sizeof(Integer)>
struct integral_output_wrapper final
{
integral_output_wrapper(const Integer value) : m_value{ value } {}
integral_output_wrapper(integral_output_wrapper&) = default;
integral_output_wrapper(integral_output_wrapper&&) = default;
integral_output_wrapper& operator=(const integral_output_wrapper&) = delete;
integral_output_wrapper& operator=(integral_output_wrapper&&) = delete;
~integral_output_wrapper() = default;

template <typename Elem, typename Traits>
void output(std::basic_ostream<Elem, Traits>& os) const
{
os << m_value;
}

const Integer m_value;
};

// Output-only wrapper specialised for signed and unsigned integers which are exactly 1 byte.
template <typename Integer>
struct integral_output_wrapper<Integer, Integer, 1> final
{
integral_output_wrapper(const Integer value) : m_value{ value } {}
integral_output_wrapper(integral_output_wrapper&) = default;
integral_output_wrapper(integral_output_wrapper&&) = default;
integral_output_wrapper& operator=(const integral_output_wrapper&) = delete;
integral_output_wrapper& operator=(integral_output_wrapper&&) = delete;
~integral_output_wrapper() = default;

template <typename Elem, typename Traits>
void output(std::basic_ostream<Elem, Traits>& os) const
{
os << static_cast<integral_io_t<Integer>>(m_value);
}

const Integer m_value;
};

// Generic input/output wrapper for signed and unsigned integers which are bigger than 1 byte.
template <typename Integer, typename = typename std::enable_if<std::is_integral<Integer>::value, Integer>::type, std::size_t = sizeof(Integer), bool = std::is_signed<Integer>::value>
struct integral_io_wrapper
{
integral_io_wrapper(Integer& value) : m_value{ value } {}
integral_io_wrapper(integral_io_wrapper&) = default;
integral_io_wrapper(integral_io_wrapper&&) = default;
integral_io_wrapper& operator=(const integral_io_wrapper&) = delete;
integral_io_wrapper& operator=(integral_io_wrapper&&) = delete;
~integral_io_wrapper() = default;

template <typename Elem, typename Traits>
void output(std::basic_ostream<Elem, Traits>& os) const
{
os << m_value;
}

template <typename Elem, typename Traits>
void input(std::basic_istream<Elem, Traits>& is)
{
is >> m_value;
}

Integer& m_value;
};

// Input/output wrapper specialised for signed integers which are exactly 1 byte.
template <typename Integer>
struct integral_io_wrapper<Integer, Integer, 1, true>
{
integral_io_wrapper(Integer& value) : m_value{ value } {}
integral_io_wrapper(integral_io_wrapper&) = default;
integral_io_wrapper(integral_io_wrapper&&) = default;
integral_io_wrapper& operator=(const integral_io_wrapper&) = delete;
integral_io_wrapper& operator=(integral_io_wrapper&&) = delete;
~integral_io_wrapper() = default;

template <typename Elem, typename Traits>
void output(std::basic_ostream<Elem, Traits>& os) const
{
os << static_cast<std::int16_t>(m_value);
}

template <typename Elem, typename Traits>
void input(std::basic_istream<Elem, Traits>& is)
{
std::int16_t temp;
is >> temp;

// Emulate the stream's usual bounds-checking behaviour for signed types.
if (temp > static_cast<std::int16_t>(std::numeric_limits<Integer>::max()))
{
m_value = std::numeric_limits<Integer>::max();
is.setstate(std::ios_base::failbit);
return;
}
if (temp < static_cast<std::int16_t>(std::numeric_limits<Integer>::min()))
{
m_value = std::numeric_limits<Integer>::min();
is.setstate(std::ios_base::failbit);
return;
}

m_value = static_cast<Integer>(temp);
}

Integer& m_value;
};

// Input/output wrapper specialised for unsigned integers which are exactly 1 byte.
template <typename Integer>
struct integral_io_wrapper<Integer, Integer, 1, false>
{
integral_io_wrapper(Integer& value) : m_value{ value } {}
integral_io_wrapper(integral_io_wrapper&) = default;
integral_io_wrapper(integral_io_wrapper&&) = default;
integral_io_wrapper& operator=(const integral_io_wrapper&) = delete;
integral_io_wrapper& operator=(integral_io_wrapper&&) = delete;
~integral_io_wrapper() = default;

template <typename Elem, typename Traits>
void output(std::basic_ostream<Elem, Traits>& os) const
{
os << static_cast<std::int16_t>(m_value);
}

template <typename Elem, typename Traits>
void input(std::basic_istream<Elem, Traits>& is)
{
// We have to do signed input so that we can correctly handle negatives which wrap around.
// If we use unsigned then we won't be able to tell the difference between a positive value
// which is too big, and a negative value which has wrapped round.
std::int16_t temp;
is >> temp;

// We need to emulate the stream's usual bounds-checking behaviour for signed types.
if (temp > static_cast<std::int16_t>(std::numeric_limits<Integer>::max()))
{
m_value = std::numeric_limits<Integer>::max();
is.setstate(std::ios_base::failbit);
return;
}
if (temp < 0)
{
// A negative number is allowed to wrap around to positive, as long as its magnitude is
// less than the maximum representable value.
if ((temp * -1) <= static_cast<std::int16_t>(std::numeric_limits<Integer>::max()))
{
m_value = static_cast<Integer>(std::numeric_limits<Integer>::max() + temp + 1);
return;
}

// Any negative numbers with a larger magnitude are out of bounds.
m_value = std::numeric_limits<Integer>::max();
is.setstate(std::ios_base::failbit);
return;
}

m_value = static_cast<Integer>(temp);
}

Integer& m_value;
};

// Stream operators:
template <typename Elem, typename Traits, typename Integer>
std::basic_ostream<Elem, Traits>& operator<<(std::basic_ostream<Elem, Traits>& os, const integral_output_wrapper<Integer>&& wrapper)
{
wrapper.output(os);
return os;
}

template <typename Elem, typename Traits, typename Integer>
std::basic_ostream<Elem, Traits>& operator<<(std::basic_ostream<Elem, Traits>& os, const integral_io_wrapper<Integer>&& wrapper)
{
wrapper.output(os);
return os;
}

template <typename Elem, typename Traits, typename Integer>
std::basic_istream<Elem, Traits>& operator>>(std::basic_istream<Elem, Traits>& is, integral_io_wrapper<Integer>&& wrapper)
{
wrapper.input(is);
return is;
}


// Main public interface:
template <typename Integer>
integral_output_wrapper<Integer> as_integer(const Integer& value)
{
return integral_output_wrapper<Integer>(value);
}

template <typename Integer>
integral_io_wrapper<Integer> as_integer(Integer& value)
{
return integral_io_wrapper<Integer>(value);
}
}

0 comments on commit 199cbe6

Please sign in to comment.