Problem, z którym chciałem się zmierzyć:

  1. Niektóre typy, takie jak aliasy z cstdint czy, choćby czysto teoretycznie, nawet size_t, mogą (ale nie muszą) być zdefiniowane jako char, signed char lub unsigned char. W praktyce int_least8_t często bywa definiowany jako signed char, a uint_least8_t – jako unsigned char. Jest to o tyle dziwne, że semantycznie rzecz ujmując, wszystkie te typy mają przedstawiać liczby całkowite, a nie znaki.
  2. 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.
  3. 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 charem 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.