-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Peter Bloomfield
committed
Apr 26, 2020
1 parent
02ea0df
commit 199cbe6
Showing
2 changed files
with
335 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |