Что такое traits? - Определение размера файла с помощью класса ifstream

ОГЛАВЛЕНИЕ


Определение размера файла с помощью класса ifstream
Теперь мы знаем основные принципы для работы с traits. Так что давайте рассмотрим пример, который помогает расширить стандартную библиотеку C++.
Давайте рассмотрим такую задачу(перефразировано из вопроса с форума):
Цитата
Я хочу определить размер файла с помощь класса ifstream. Сам файл весит больше 5 Гб. Функция tellg() возвращает какое-то нереальное значение. Как можно правильно определить размер файла?

На данный момент все известные мне версии стандартной библиотеки C++ представляют позицию в файле 32-разрядным целым. Но дело в том, что обычные 32-разрядные целые числа не могут представлять размер файла, большего 4 ГБ(происходит переполнение). То есть нам надо каким-либо образом заставить стандартную библиотеку использовать не 32-разрядные числа, а, например 64-разрядные(или вообще наш собственный тип(класс), который мы опишем). Как это сделать? Как вы уже догадались, помогут нам traits.

Как известно, ifstream - это только typedef от класса basic_ifstream. Сам же класс basic_ifstream принимает 2 параметра шаблона: первый из них определяет тип символа, а второй определяет свойста(traits). Так вот эти свойста и должны определять, каким типом представлять позицию в файле, как сравнивать символы и тд. Второй параметр шаблона класса basic_ifstream по умолчанию будет классом char_traits. Это стандартный класс, который описывает основные свойста: нужные типы, как сравнивать символы, присваивать и тд.. Так как мы не собираемся переопределять это все(нам надо заменить только 2 типа), тогда хорошей идеей будет унаследоваться от класса char_traits.

У класса char_traits есть 2 интересующих нас типа(полный список типов можно найти в документации):

  1. pos_type - тип, используемый для представления позиции в потоке
  2. off_type - тип, используемый для представления смещений между позициями в потоке

Вот их-то как раз нам и надо переопределить. Давайте сделаем первую попытку:

template <typename char_t>
struct long_pointer_traits : public std::char_traits<char_t> {
 typedef __int64 pos_type;
 typedef __int64 off_type;
};

typedef std::basic_ifstream<char, long_pointer_traits<char> > long_ifstream;

// используем long_ifstream

Но вот незадача: этот код не компилируется. Дело в том, что pos_type должен уметь конструироваться из нескольких заранее определенных типов(как показало исследование, 2). Базовые типы этого делать не умеют, так что придется написать свой собственный класс. Я не буду заострять внимание на этом классе, так как статья немного не на эту тему. Я просто приведу реализацию этого класса, а если у вас будут какие-то вопросы, то писать либо здесь, либо в PM. Итак, вот код:

// пространство имен, в которое заносятся детали реализации
namespace detail {
 template <typename num_type, typename state_type = std::mbstate_t>
 class pos_type_t {
     typedef pos_type_t<num_type, state_type> my_type;

     num_type    m_pos;
     state_type  m_state;

     static state_type initial_state;

    public:
    // конструкторы
     pos_type_t(std::streampos off) : m_pos(off), m_state(initial_state) {}
        pos_type_t(num_type off = 0) : m_pos(off), m_state(initial_state) {}
        pos_type_t(state_type state, num_type pos) : m_pos(pos), m_state(state) {}

        // получение состояния потока
        state_type state() const {
            return(m_state);
        }

        // установка состояния потока
        void state(state_type st) {
            m_state = st;
        }

        // получение позиции
        num_type seekpos() const    {
        return(m_pos);
        }

        // оператор преобразования
        operator num_type() const {
        return(m_pos);
        }

        // далее идут операторы, которые осуществляют арифметические операции

        num_type operator- (const my_type& rhs) const {
    return(static_cast<num_type>(*this) - static_cast<num_type>(rhs));
        }

        my_type& operator+= (num_type pos) {
            m_pos += pos;
            return(*this);
        }

        my_type& operator-= (num_type pos) {
            m_pos -= pos;
            return(*this);
        }

        my_type operator+ (num_type pos) const {
            my_type tmp(*this);
            return(tmp += pos);
        }

        my_type operator- (num_type pos) const {
            my_type tmp(*this);
            return(tmp -= pos);
        }

        // операторы сравнения

        bool operator== (const my_type& rhs) const {
    return(static_cast<num_type>(*this) == static_cast<num_type>(rhs));
        }

        bool operator!= (const my_type& rhs) const {
    return(!(*this == rhs));
        }
 };
//---------------------------------------------------
 // статическая константа, которая обозначает начальное состояние
 template <typename num_type, typename state_type>
 state_type pos_type_t<num_type, state_type>::initial_state;
}
//---------------------------------------------------
// наконец-то наш класс свойств:
template <typename char_t, typename long_pos_t>
struct long_pointer_traits : public std::char_traits<char_t> {
 // определение pos_type через наш только что написанный класс
 typedef detail::pos_type_t<long_pos_t> pos_type;

 // определение off_type через тип, переданный во 2 аргументе шаблона
 typedef long_pos_t off_type;
};
//---------------------------------------------------
// вводим тип "длинного" файла
typedef std::basic_ifstream<char, long_pointer_traits<char, __int64> > long_ifstream;

// используем long_ifstream для получения размера файла
ОК, теперь все компилируется и работает. Но кроме получения позиции в файле, нам обычно надо работать еще с этими файлами(читать, писать). И, конечно, нам приходится работать со строками. Тогда если мы попытаемся считать строку из файла таким образом:
long_ifstream infile(strFileName, std::ios::binary);
std::string res;
std::getline(infile, res);

То мы получим ошибку компиляции. Проблема в том, что std::string - это "всего лишь" typedef от std::basic_string. Этот класс принимает 2 параметра шаблона: первый - тип для представления символа, а второй(как вы уже, наверное, догадались) - traits. Так вот, для корректной работы нам надо определить и свой тип строки:

// "длинные" типы:
typedef std::basic_ifstream<char, long_pointer_traits<char, __int64> >  long_ifstream;
typedef std::basic_string<char, long_pointer_traits<char, __int64> >    long_string;

long_ifstream infile(strFileName, std::ios::binary);
long_string res;
std::getline(infile, res);

Теперь все работает прекрасно. Таким образом, для правильного взаимодействия компонентов стандартной библиотеки нам придется определять нужные типы и работать с ними. К сожалению, на данный момент я не знаю способа, как можно было бы создать нужный тип для стандартных потоков ввода/вывода(cin, cout, cerr, clog). Так что чтобы вывести такую "длинную" строку на экран, надо будет написать свой оператор вывода такой строки. Другого решения мне неизвестно(если кто-то знает - поделитесь, буду признателен).

Также хочу сказать несколько слов о совместимости и переносимости: приведенный мной код по определению размера большого файла был проверен на компиляторах VC7.1 и Intel C++ 8.0. Использовалась стандартная библиотека, которая идет по умолчанию с VC. При работе с ней замечено никаких ошибок не было. Проверялся код и с использованием STLPort версий 4.6.2 и 5.0. Компилировался он без проблем, но работал неправильно. Надеюсь, в дальнейших версиях STLPort'а это будет исправлено и работать будет корректно, так как данный код соответствует стандарту.