четверг, 15 октября 2015 г.

[prog.c++] Превращаем простой рефакторинг в непростой: добавляем жестяной жести! :)

Продолжение предыдущего поста. Тогда была цель сделать минимальный, простейший рефакторинг. После ее достижения можно пойти дальше и сделать вариант в духе Modern C++ Overdesign, с преферансом и куртизанками. Т.е. с исключениями и RAII в полный рост :)

Итак, первое, что хочется сделать -- это уйти от API в стиле plain old C. Поэтому определяется обычный C++ный класс file_mapped_memory с методами. Поскольку экземпляры этого класса будут хранить дескрипторы ресурсов, то это будет Moveable класс, но не Copyable. Так же, экземпляр данного класса прячет от пользователя системно-зависимые детали реализации. Для чего используется идиома PImpl.

Однако, использование идиомы PImpl не будет приводить к увеличению косвенности по сравнению с первоначальным вариантом. Произойдет это потому, что по-прежнему динамически будет создаваться всего один объект, хранящий в себе системно-зависимые дескрипторы. Только раньше указатель на этот объект доставался пользователю либо в голом виде (в первоначальном варианте ув.тов.asfikon-а), либо обернутый в unique_ptr. Сейчас этот указатель будет обернут в file_mapped_memory. Однако сам file_mapped_memory не создается динамически. Он возвращается пользователю по значению. Т.е., по сути, file_mapped_memory является несколько более толстой оберткой над динамически созданным объектом типа file_mapped_memory::io_data, чем unique_ptr.

Кроме того, при доступе к адресу отображенного в память содержимого файла file_mapped_memory как раз устраняет лишнюю косвенность. Если в первоначальном варианте нужно было вызывать fileMappingGetPointer, который извлекал адрес из динамически созданного объекта, то в file_mapped_memory адрес содержимого файла вынесен в отдельное поле m_begin. Соответственно, обращение к нему будет более "прямым", чем обращение через динамически созданный объект с системно-зависимыми дескрипторами. Правда, при этом расходуется чуть больше памяти, но ведь память уже не ресурс? ;)

Итак, заменяем внешний интерфейс fileMapping-а на вот такой:

#ifndef AFISKON_FILEMAPPING_H
#define AFISKON_FILEMAPPING_H

#include <memory>
#include <iterator>

class file_mapped_memory {
public :
   using byte_type = unsigned char;

   file_mapped_memory( const file_mapped_memory & ) = delete;
   file_mapped_memory( file_mapped_memory && r );
   ~file_mapped_memory();

   file_mapped_memory & operator=( file_mapped_memory && r );
   void swap( file_mapped_memory & r );

   const byte_type * begin() const { return m_begin; }
   const byte_type * end() const { return m_end; }
   size_t size() const { return std::distance(begin(), end()); }

   static file_mapped_memory map_file( const char * file_name );

private :
   struct io_data;
   using io_data_unique_ptr = std::unique_ptr< io_data >;

   io_data_unique_ptr m_io_data;
   const byte_type * m_begin;
   const byte_type * m_end;

   file_mapped_memory( io_data_unique_ptr io_data, const byte_type * b, const byte_type * e );
};

#endif //AFISKON_FILEMAPPING_H

Использоваться он может вот таким образом:

#include <iostream>
#include <algorithm>
#include <iterator>

#include "fileMapping.h"

int main(int argc, char **argv)
{
   for(int i = 1; i < argc; ++i )
   {
      std::cout << "=== " << argv[i] << " ===" << std::endl;

      try
      {
         auto mapping = file_mapped_memory::map_file(argv[i]);
         std::copy(std::begin(mapping), std::end(mapping),
               std::ostream_iterator<unsigned char>(std::cout));
         std::cout << std::endl;
      }
      catchconst std::exception & x )
      {
         std::cerr << "Exception: " << x.what() << std::endl;
      }
   }
}

Можно заметить, что открытие файла и отображение его в ОП осуществляется не в конструкторе file_mapped_memory, а с помощью порождающего статического метода map_file. Тут нет никакой глубокой мысли. Просто на очередной итерации был получен вариант с map_file, но без использования исключений в реализации file_mapped_memory. А когда на следующих итерациях исключения были добавлены, то показалось, что смысла отказываться от map_file нет. Да и работы при этом меньше :)

Теперь можно взглянуть на саму реализацию file_mapped_memory. Можно обратить внимание, что структура file_mapped_memory::io_data определена не в заголовочном, а в cpp-файле. Т.е. системно-зависимые детали реализации скрыты от пользователя и при их изменении не придется перестраивать все, что зависит от fileMapping-а.

Так же нужно отметить пустой деструктор у file_mapped_memory. Он нужен потому, что в объявлении класса используется неполный тип file_mapped_memory::io_data. Без полного его определения компилятор не сможет сгенерировать деструктор по-умолчанию. Если же разметить пустой деструктор в cpp-файле, то проблем с разрушением атрибута file_mapped_memory::m_io_data нет.

struct file_mapped_memory::io_data {
   file_handle m_file;
   memory_handle m_mapping;
   memory_view_handle m_mapped_ptr;
};

file_mapped_memory::file_mapped_memory(
   io_data_unique_ptr io_data, const byte_type * b, const byte_type * e )
   :  m_io_data{ std::move( io_data ) }, m_begin{ b }, m_end{ e }
{}

file_mapped_memory::file_mapped_memory( file_mapped_memory && r )
   :  m_io_data{ std::move( r.m_io_data ) }, m_begin{ r.m_begin }, m_end{ r.m_end }
{
   r.m_begin = r.m_end = nullptr;
}

file_mapped_memory::~file_mapped_memory()
{}

file_mapped_memory & file_mapped_memory::operator=( file_mapped_memory && r ) {
   file_mapped_memory tmp{ std::move(r) };
   tmp.swap( *this );
   return *this;
}

void file_mapped_memory::swap( file_mapped_memory & r ) {
   std::swap( m_io_data, r.m_io_data );
   std::swap( m_begin, r.m_begin );
   std::swap( m_end, r.m_end );
}

file_mapped_memory file_mapped_memory::map_file( const char * file_name ) {
   auto handle_error = [file_name](const char * what) {
      std::ostringstream s;
      s << "map_file - " << what << " failed, fname = " << file_name
         << ", last_error: " << GetLastError();
      throw std::runtime_error{ s.str() };
   };

   auto h = io_data_unique_ptr{ new io_data{} };

   h->m_file = file_handle{ CreateFile(file_name, GENERIC_READ, 0nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
   if( !h->m_file )
      handle_error( "CreateFile" );

   auto file_size = GetFileSize(h->m_file, nullptr);
   if(file_size == INVALID_FILE_SIZE)
      handle_error( "GetFileSize" );

   h->m_mapping = memory_handle{ CreateFileMapping(h->m_file, nullptr, PAGE_READONLY, 00nullptr) };
   if( !h->m_mapping ) 
      handle_error( "CreateFileMapping" );

   h->m_mapped_ptr = memory_view_handle{ static_castconst byte_type * >(MapViewOfFile(h->m_mapping, FILE_MAP_READ, 00, file_size)) };
   if( !h->m_mapped_ptr )
      handle_error( "MapViewOfFile" );

   auto b = h->m_mapped_ptr.get();
   auto e = b + file_size;

   return file_mapped_memory{ std::move(h), b, e };
}

Если посмотреть в код повнимательнее, то можно обнаружить, что в нем не так уж и много системно-зависимых вещей используется. Нет HANDLE, нет INVALID_HANDLE_VALUE и тому подобного. Да и сама структура io_data содержит какие-то file_handle, memory_handle и memory_view_handle, но не имеет деструктора, в котором бы проверялось, что и как закрывается. Где все это хозяйство?

А вот тут-то и начинается самая жестяная жесть ;)

Системно-зависимые дескрипторы являются достаточно тривиальной абстракцией: это какой-то тип (HANDLE или char *), какое-то начальное значение (INVALID_HANDLE_VALUE или NULL), какая-то функция для освобождения дескриптора. Посему эту абстракцию можно представить простым шаблоном (хотя его простота чуть-чуть спрятана под объемом кода):

templatetypename TRAITS >
class handle_holder {
   using type = typename TRAITS::type;
public :
   handle_holder() : m_handle{ TRAITS::default_value() } {}
   handle_holder( type value ) : m_handle{ value } {}
   handle_holder( const handle_holder & ) = delete;
   handle_holder( handle_holder && r ) : m_handle{ r.m_handle } {
      r.m_handle = TRAITS::default_value();
   }
   ~handle_holder() {
      if( TRAITS::default_value() != m_handle ) TRAITS::destroy( m_handle );
   }
   void swap( handle_holder & r ) { std::swap( m_handle, r.m_handle ); }
   handle_holder & operator=( handle_holder && r ) {
      handle_holder tmp{ std::move( r ) };
      tmp.swap( *this );
      return *this;
   }

   operator bool() const { return TRAITS::default_value() != m_handle; }
   type get() const { return m_handle; }
   operator type() const { return get(); }

private :
   type m_handle;
};

Шаблон handle_holder определяет Moveable тип, который настраивается на конкретный вид дескрипторов ресурсов через параметр TRAITS. Пока в C++ нет концептов, нельзя для параметра TRAITS формально специфицировать его интерфейс. Поэтому приходится делать это "на пальцах": в типе TRAITS должен быть тип type, который является псевдонимом для типа дескриптора (например HANDLE или char*). Должен быть статический метод default_value(), возвращающий нулевое значение или его аналог (так, в одном случае для HANDLE нулевым значением должен быть NULL, в другом -- INVALID_HANDLE_VALUE). И еще должен быть статический метод destroy(), отвечающий за очистку ресурсов. Посредством такого несложного интерфейса handle_holder может держать как HANDLE, так и char*, так и int-ы.

Для платформы Windows определяются следующие TRAITS для handle_holder-а:

struct win32_handle_traits {
   using type = HANDLE;
   static void destroy( type v ) { CloseHandle(v); }
};

struct file_handle_traits : public win32_handle_traits {
   static type default_value() { return INVALID_HANDLE_VALUE; }
};

struct memory_handle_traits : public win32_handle_traits {
   // yes, NULL, not INVALID_HANDLE_VALUE, see MSDN
   static constexpr type default_value() { return NULL; }
};

struct memory_view_handle_traits {
   using type = const unsigned char *;
   static constexpr type default_value() { return nullptr; }
   static void destroy( type v ) { UnmapViewOfFile(v); }
};

Тут, кстати, можно заметить, как наследование в C++ используется для уменьшения дублирования кода. Хотя это наверняка противоречит чистоте чьих-нибудь взглядов на ООП :)

Кстати говоря, изначально хотелось все методы default_value() объявить как constexpr, но оказалось, что INVALID_HANDLE_VALUE -- это коряво определенный define, который компилятор Visual C++ в компайл-тайм ну никак не хотел преобразовывать в значение типа HANDLE, даже через разные формы кастов. Тогда как GCC такое преобразование в компайл-тайм проглатывал.

Ну и осталось показать еще одну штуку: собственно определение file_handle, memory_handle и memory_view_handle. Тут вообще все элементарно:

using file_handle = handle_holder< file_handle_traits >;
using memory_handle = handle_holder< memory_handle_traits >;
using memory_view_handle = handle_holder< memory_view_handle_traits >;

Ну вот, собственно, и все. Теперь можно переходить к disclaimer-ам :)

Disclaimer 1. В коде могут быть ошибки. Какого-то серьезного тестирования, включая нагрузочное, естественно не было.

Disclaimer 2. Все это делалось just for fun. Никаких попыток убедить кого-то в том, что на C++ нужно писать именно так. Лично я бы использовал такие навороты только в случае, если у меня в команде работают вменяемые C++ники (ну или если бы работал в одиночку). Кроме того, вряд ли я бы стал писать столько кода для проекта размером в несколько сотен строк. А вот если бы проект был побольше или если бы мне приходилось много работать с системно-зависимыми дескрипторами, что что-то вроде handle_holder-а, наверняка бы соорудил.

Disclaimer 3. Не часто приходилось писать Moveable классы, поэтому, возможно, что-то у меня сделано не по фен-шую. Однако, насколько я могу судить, именно такая реализация оператора перемещения (т.е. через временный объект и операцию swap) защищает и от попытки перемещения себя в себя (т.е. a=std::move(a)).

Disclaimer 4. А вот как лучше определять swap в нонешнем C++ толком и не знаю. Лично я делаю по старинке: метод-член swap в самом классе. Плюс, если вспоминаю, делаю специализацию std::swap для своего типа. Хотя вроде как лучшие собаководы сейчас высказывают другие мнения, мол, негоже лезть своими грязными руками в std и что лучше полагаться на argument-dependent lookup... Но с этим пока не разобрался, все-таки мои знания C++ очень часто оставляют желать много лучшего :(

Комментариев нет: