Продолжение предыдущего поста. Тогда была цель сделать минимальный, простейший рефакторинг. После ее достижения можно пойти дальше и сделать вариант в духе 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; } catch( const 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, 0, nullptr, 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, 0, 0, nullptr) }; if( !h->m_mapping ) handle_error( "CreateFileMapping" ); h->m_mapped_ptr = memory_view_handle{ static_cast< const byte_type * >(MapViewOfFile(h->m_mapping, FILE_MAP_READ, 0, 0, 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), какая-то функция для освобождения дескриптора. Посему эту абстракцию можно представить простым шаблоном (хотя его простота чуть-чуть спрятана под объемом кода):
template< typename 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++ очень часто оставляют желать много лучшего :(
Комментариев нет:
Отправить комментарий