Problem, z którym chciałem się zmierzyć:
- Niektóre typy, takie jak aliasy z
cstdint
czy, choćby czysto teoretycznie, nawetsize_t
, mogą (ale nie muszą) być zdefiniowane jakochar
,signed char
lubunsigned char
. W praktyceint_least8_t
często bywa definiowany jakosigned char
, auint_least8_t
– jakounsigned char
. Jest to o tyle dziwne, że semantycznie rzecz ujmując, wszystkie te typy mają przedstawiać liczby całkowite, a nie znaki. - Jeśli mamy do czynienia ze zmienną typu znakowego, operator
>>
wczytuje do zmiennej kod ASCII pierwszego napotkanego na wejściu niebiałego znaku, zaś<<
wypisuje na wyjście znak o kodzie ASCII równym wartości zmiennej. - Tym samym może się zdarzyć, że jeżeli np. zadeklarujemy zmienną jako
int_least8_t
i będziemy próbowali coś do niej wczytać / coś z niej wypisać, to rezultaty będą dalekie od zamierzonych. Jest to tym bardziej uciążliwe, że to, czy takie aliasy będą zdefiniowane jako typ znakowy, czy nie, zależy tylko od konkretnej implementacji i nie wiadomo tego z góry.
Ilustracja problemu na przykładzie: http://ideone.com/jkeNE4 Dowód, że czysto teoretycznie może to spotkać nawet size_t
: http://stackoverflow.com/questions/32915434/is-it-guaranteed-that-size-t-vectorsize-type-etc-typedefs-wont-bind-to-a-ch
Rozwiązanie problemu wypisywania jest proste: trzeba promować wypisywaną zmienną, o której się nie wie, czy jest char
em czy nie. Przykład: http://ideone.com/3Qi1Y3
Z wczytywaniem jest już gorzej. Nie znalazłem lepszego rozwiązania niż stworzenie zmiennej pomocniczej typu promowanej zmiennej, do której chcemy wczytać, wczytanie do zmiennej pomocniczej, a potem przypisanie naszej zmiennej wartości zmiennej pomocniczej. Trzeba ręcznie sprawdzać zakres: przy „normalnym” wczytywaniu strumienie robią to same http://www.cplusplus.com/reference/locale/num_get/get/ , a przecież zakres zmiennej pomocniczej może być większy, niż zakres zmiennej, do której chcemy wczytać. Ilustracja: http://ideone.com/OL2UO0
Poszukiwałem zatem jakiegoś generycznego, możliwie najwygodniejszego rozwiązania, które pozwoliłoby zmusić strumienie do wczytywania każdej liczby całkowitej jako liczby całkowitej, a nie jako znaku. Oto, co zrobiłem:
#ifndef INTIO_H
#define INTIO_H
#include <istream>
#include <ostream>
#include <type_traits>
#include <limits>
template
<class T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
class integer_IO
{
T &val;
public:
integer_IO(T &arg) : val(arg) {}
integer_IO(T &&arg) : val(arg) {}
friend std::istream &operator>> (std::istream &is, integer_IO<T> &&i)
{
using TT = decltype(+i.val);
TT hlp;
is >> hlp;
TT constexpr minval = static_cast<TT>(std::numeric_limits<T>::min());
TT constexpr maxval = static_cast<TT>(std::numeric_limits<T>::max());
i.val = static_cast<T>(hlp > maxval ? maxval : hlp < minval ? minval : hlp);
if(hlp > maxval || hlp < minval)
is.setstate(std::ios::failbit);
return is;
}
friend std::ostream &operator<< (std::ostream &os, integer_IO<T> const &&i)
{
os << +i.val;
return os;
}
};
template
<class T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
integer_IO<T> intIO(T &arg)
{
return integer_IO<T>(arg);
}
template
<class T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
integer_IO<T> intIO(T &&arg)
{
return integer_IO<T>(arg);
}
#endif
Użycie jest proste: jeśli chcemy mieć pewność, że zmienna zostanie wczytana / wypisana jako typ całkowity, po prostu zamiast niej przekazujemy strumieniowi tyczmasowy obiekt klasy integer_IO
, który zadba o poprawne wczytanie/wypisanie. Czyli:
#include <iostream>
#include <cstdint>
#include "intIO.hpp"
using namespace std;
int dwaplusdwa()
{
return 2+2;
}
int main()
{
int_least8_t i;
cin >> intIO(i);
cout << intIO(i) << '\n';
cout << intIO(dwaplusdwa()) << '\n';
}
To działa, liczby są wczytywane / wypisywane poprawnie: http://ideone.com/fLfp6r
Ponieważ jedyną intencją stworzenia tej klasy jest zapewnienie poprawnego wczytywania / wypisywania, to nie widziałem sensu jakoś specjalnie tworzyć explicite odpowiednich instancji tej klasy, tak jak integer_IO<cośtam> = intIO(zmienna)
. Wydaje mi się, że do tego zastosowania byłoby to niewygodne i „przegadane”. Wydaje mi się, że mój koncept jest nieco podobny do std::move
, więc chyba jest to uprawnione działanie z mojej strony? Właściwie zastanawiam się nawet, czy nie można by uprywatnić konstruktora i zrobić z intIO
funkcji zaprzyjaźnionej.
Czy to jest rozwiązanie całkowicie poprawne? Czy nie ma jakiś „kruczków”, które przegapiłem?
Na ile to rozwiązanie jest „ładne”, a na ile „mega nieczytelne”, „brzydkie” i „horrendalne”?
EDIT: Jednak coś przeoczyłem. Konieczne jest przeładowanie dla rlvalue references, bo inaczej nie można wypisywać z użyciem tej klasy obiektów tymczasowych (zwracanych przez funkcje). Kod poprawiony.