UPD: добавил в конец поста бонус для читателей Хабра.
Привет! Я время от времени рассказываю на Хабре о решениях распространённых задач на C++ и вообще люблю делиться опытом. Поэтому даже написал целую книгу, которая называется «Разработка приложений на С++ с использованием Boost». Она может быть интересна разработчикам, которые уже немного знакомы со стандартной библиотекой языка, хотят глубже изучить Boost, упростить и повысить качество разработки приложений. Уверен, что информация, которую я собрал в книге, будет полезна — всё больше библиотек Boost становятся частью стандарта. Сегодня предлагаю прочитать главу, посвящённую работе с файлами. В ней я рассказываю о перечислении файлов в каталоге, стирании и создании файлов и каталогов, а также о самом быстром способе чтения. Надеюсь, будет интересно. И, пожалуйста, не забывайте делиться впечатлениями в комментариях.
Перечисление файлов в каталоге
Существуют функции и классы стандартной библиотеки для чтения и записи данных в файлы. Но до появления C++17 в ней не было функций для вывода списка файлов в каталоге, получения типа файла или получения прав доступа к файлу.
Давайте посмотрим, как можно исправить эту несправедливость с помощью Boost. Мы будем создавать программу, которая перечисляет имена файлов, права на запись и типы файлов в текущем каталоге.
Знание основ C++ более чем достаточно для использования этого рецепта. Этот рецепт требует линковки с библиотеками boost_system и boost_filesystem.
Как это делается...
Этот и последующий рецепты посвящены переносимым оберткам для работы с файловой системой.
- Нам нужно подключить следующие два заголовочных файла:
#include <boost/filesystem/operations.hpp> #include <iostream>
- Теперь нужно указать каталог:
int main() { boost::filesystem::directory_iterator begin("./");
- После указания каталога переберите его содержимое:
boost::filesystem::directory_iterator end; for (; begin != end; ++ begin) {
- Следующий шаг – получение информации о файле:
boost::filesystem::file_status fs = boost::filesystem::status(*begin);{
- Теперь выведите информацию о файле:
switch (fs.type()) { case boost::filesystem::regular_file: std::cout << "FILE "; break; case boost::filesystem::symlink_file std::cout << "SYMLINK "; break; case boost::filesystem::directory_file: std::cout << "DIRECTORY "; break; default: std::cout << "OTHER "; break; } if (fs.permissions() & boost::filesystem::owner_write) { std::cout << "W "; } else { std::cout << " "; }
- Последний шаг – вывод имени файла:
std::cout << *begin << 'n'; } /*for*/ } /*main*/
Готово. Теперь, если мы запустим программу, она выведет что-то вроде этого:
FILE W "./main.o"
FILE W "./listing_files"
DIRECTORY W "./some_directory"
FILE W "./Makefile"
Как это работает...
Функции и классы Boost.Filesystem просто оборачивают системные вызовы для работы с файлами.
Обратите внимание на использование знака "./" на этапе 2. Системы POSIX используют косую черту для указания путей; Windows по умолчанию использует обратную косую черту. Тем не менее Windows также понимает косую черту, а даже если бы не понимала, то библиотека Boost позаботилась бы о неявном преобразовании формата пути.
Посмотрите на этап 3, где мы вызываем конструктор по умолчанию для класса boost::filesystem::directory_iterator. Этот конструктор работает по аналогии с конструктором по умолчанию класса std::istream_iterator, – создает итератор конца диапазона.
Этап 4 сложен не потому, что эту функцию трудно понять, а из-за того, что происходит много преобразований. Разыменование итератора begin возвращает boost::filesystem::directory_entry, который неявно преобразуется в boost::filesystem::path, использующийся в качестве параметра для функции boost::filesystem::status. На самом деле можно написать намного лучше:
boost::filesystem::file_status fs = begin->status();
Совет:
Внимательно прочитайте справочную документацию, чтобы избежать ненужных неявных преобразований.
Этап 5 очевиден, поэтому мы переходим к этапу 6, где неявное преобразование в boost::filesystem::path происходит снова. Более явное решение выглядит так:
std::cout << begin->path() << 'n';
Здесь begin->path() возвращает константную ссылку на переменную
boost::filesystem::path, которая содержится в boost::filesystem::directory_entry.
Дополнительно...
Boost.Filesystem является частью C++17. Все содержимое в C++17 находится в одном заголовочном файле в пространстве имен std::filesystem::. Версия стандартной библиотеки несколько отличается от Boost-версии, в основном за счет использования перечислений с областью видимости (enum class) там, где Boost.Filesystem использовала просто перечисление без области видимости.
Совет:
Есть класс directory_entry, который обеспечивает кеширование информации о файловой системе. Так что если вы много работаете с файловой системой и запрашиваете различную информацию, попробуйте использовать directory_entry для лучшей производительности.
Как и в случае с другими библиотеками Boost, Boost.Filesystem работает с компиляторами для стандарта, предшествующего C++17, и даже с компиляторами для стандарта, предшествующего C++11.
См. также
- Рецепт «Стирание и создание файлов и каталогов» покажет еще один пример использования Boost.Filesystem;
- прочтите официальную документацию по Boost.Filesystem, чтобы получить больше информации о ее возможностях на странице http://boost.org/libs/filesystem;
- рабочую версию проекта C++17 можно найти по адресу http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf.
Стирание и создание файлов и каталогов
Давайте рассмотрим следующие строки кода:
std::ofstream ofs("dir/subdir/file.txt");
ofs << "Boost.Filesystem is fun!";
В этих строках мы пытаемся записать что-то в файл file.txt в каталоге dir/ subdir. Если такой директории нет, эта попытка будет неудачной.
В этом рецепте мы создадим каталог и подкаталог, запишем некие данные в файл и попробуем создать символическую ссылку. Если создание символической ссылки не удается, то мы удалим созданные сущности. В примере мы также будем избегать использования исключений в качестве механизма сообщения об ошибках, отдавая предпочтение чему-то вроде кодов возврата.
Давайте посмотрим, как можно сделать это элегантно, используя Boost.
Подготовка
Для этого рецепта требуются базовые знания C++ и класса std::ofstream.
Boost.Filesystem не является библиотекой header-only, поэтому код в этом ре-
цепте требуется линковать с библиотеками boost_system и boost_filesystem.
Как это делается...
Мы продолжаем работать с переносимыми обертками для файловой системы и в этом рецепте посмотрим, как изменить содержимое каталога.
- Как всегда, нам нужно подключить несколько заголовочных файлов:
#include <boost/filesystem/operations.hpp> #include <cassert> #include <fstream>
- Теперь нам нужна переменная для хранения ошибок (если таковые появятся):
int main() { boost::system::error_code error;
- Мы также создадим каталоги:
boost::filesystem::create_directories("dir/subdir", error); assert(!error);
- Затем запишем данные в файл:
std::ofstream ofs("dir/subdir/file.txt"); ofs << "Boost.Filesystem is fun!"; assert(ofs); ofs.close();
- Попытаемся создать символическую ссылку:
boost::filesystem::create_symlink( "dir/subdir/file.txt", "symlink", error);
- Потом нам нужно проверить, что файл доступен через эту ссылку:
if (!error) { std::cerr << "Symlink createdn"; assert(boost::filesystem::exists("symlink"));
- Удалим файл, если создание символической ссылки завершится неудачно:
} else { std::cerr << "Failed to create a symlinkn"; boost::filesystem::remove_all("dir", error); assert(!error); boost::filesystem::remove("symlink", error); assert(!error); } /*if (!error)*/ } /*main*/
Как это работает...
boost::system::error_code может хранить информацию об ошибках и широко используется во всех библиотеках Boost.
Если вы не предоставите экземпляр boost::system::error_code для функций Boost.Filesystem, код будет компилироваться. В этом случае при возникновении ошибки будет выброшено исключение boost::filesystem::filesystem_error.
Внимательно посмотрите на этап 3. Мы использовали функцию boost::filesystem::create_directories вместо boost::filesystem::create_directory, потому что последняя не может создавать вложенные подкаталоги. Та же самая история с boost::filesystem::remove_all и boost::filesystem::remove. Первая удаляет каталоги, которые могут содержать файлы и подкаталоги. Вторая удаляет один файл.
Остальные шаги просты для понимания и не должны вызывать проблем.
Дополнительно...
Класс boost::system::error_code является частью C++11. Его можно найти в заголовочном файле <system_error> в пространстве имен std::. Классы Boost.Filesystem являются частью C++17.
Наконец, небольшая рекомендация для тех, кто собирается использовать Boost.Filesystem. Когда ошибки при работе с файловой системой являются частым явлением, или приложение требует высокой отзывчивости/производительности, используйте класс boost::system::error_codes. В противном случае для обработки ошибок перехват исключений будет предпочтительнее и надежнее.
См. также
Рецепт «Перечисление файлов в каталоге» также содержит информацию о Boost. Filesystem. Прочтите официальную документацию по адресу http://boost.org/libs/filesystem, где приводится больше информации и примеров.
Самый быстрый способ чтения файлов
В интернете люди спрашивают: «Какой самый быстрый способ чтения файлов?» Давайте усложним задачу для этого рецепта: какой самый быстрый и переносимый способ чтения двоичных файлов?
Подготовка
Для этого рецепта требуются базовые знания C++ и std::fstream.
Подготовка
Техника из этого рецепта широко используется приложениями, чувствительными к производительности ввода-вывода. Это самый быстрый способ чтения файлов.
Как это делается...
Техника из этого рецепта широко используется приложениями, чувствительными к производительности ввода-вывода. Это самый быстрый способ чтения файлов.
- Нам нужно подключить два заголовка из библиотеки Boost.Interprocess:
#include <boost/interprocess/file_mapping.hpp> #include <boost/interprocess/mapped_region.hpp>
- Теперь нужно открыть файл:
const boost::interprocess::mode_t mode = boost::interprocess::read_only; boost::interprocess::file_mapping fm(filename, mode);
- Основная часть этого рецепта отображает все файлы в память:
boost::interprocess::mapped_region region(fm, mode, 0, 0);
- Получение указателя на данные в файле:
const char* begin = static_cast<const char*>( region.get_address() );
Готово! Теперь мы можем работать с файлом, как с обычной памятью:
const char* pos = std::find(
begin, begin + region.get_size(), '1'
);
Как это работает...
Все популярные операционные системы имеют возможность отображать файл в адресное пространство процессов. После того как такое отображение было выполнено, процесс может работать с этими адресами так же, как с обычной памятью. Операционная система сама заботится обо всех файловых операциях, таких как кеширование и упреждающее чтение.
Почему это быстрее, чем традиционные операции чтения и записи? Это связано с тем, что в большинстве случаев чтение и запись реализуются как отображение в память и копирование данных в указанный пользователем буфер. Таким образом, чтение обычно делает немного больше, чем отображение.
Как и в случае с std::fstream из стандартной библиотеки, мы должны передать режим открытия файла. См. этап 2, где мы предоставили режим boost::interprocess::read_only.
См. этап 3, где мы отобразили весь файл сразу. Эта операция действительно очень быстрая, потому что ОС не читает все данные с диска, а сразу возвращает нам управление и ожидает запросов к части отображаемой области. После запроса ОС загружает запрошенную часть файла с диска в память. Как мы видим, операции отображения в память являются ленивыми, а размер отображаемой области не влияет на производительность.
Однако 32-разрядная ОС не может отображать в память большие файлы, поэтому вам придется отображать их по частям. Операционные системы POSIX (Linux) требуют определения макроса _FILE_OFFSET_ BITS=64 для всего проекта, чтобы работать с большими файлами на 32-битной платформе. В противном случае ОС не сможет отобразить части файла, размер находится за границей первых 4 ГБ.
Теперь пришло время измерить производительность:
$ TIME="%E" time ./reading_files m
mapped_region: 0:00.08
$ TIME="%E" time ./reading_files r
ifstream: 0:00.09
$ TIME="%E" time ./reading_files a
C: 0:00.09
Как и ожидалось, отображенные в память файлы немного быстрее по сравнению с традиционными операциями чтения. Также видно, что чистые методы C имеют такую же производительность, что и класс C++ std::ifstream, поэтому по возможности не используйте FILE* функции в C++. Они предназначены только для C, а не для C++!
Для обеспечения оптимальной производительности std::ifstream не забудьте открыть файлы в двоичном режиме и читать данные блоками:
std::ifstream f(filename, std::ifstream::binary);
// ...
char c[kilobyte];
f.read(c, kilobyte);
Дополнительно...
К сожалению, классы для отображения файлов в память не являются частью C++20, и, похоже, их не будет и в C++23.
Запись в области с отображением в память также является очень быстрой операцией. Операционная система кеширует записи и не сбрасывает изменения на диск немедленно. Существует разница между кешированием данных в ОС и std::ofstream. В случае std::ofstream данные кешируются приложением, и если работа приложения аварийно завершается, закешированные данные могут быть потеряны. Когда данные кешируются ОС, завершение работы приложения не приводит к их потере. Сбои питания и сбои ОС приводят к потере данных в обоих случаях.
Если несколько процессов отображают один файл и один из процессов изменяет отображаемую область, то изменения сразу видны другим процессам (даже без фактической записи данных на диск! Современные ОС очень умные! Только не забывайте про синхронизацию доступа).
См. также
Библиотека Boost.Interprocess содержит множество полезных функций для работы с системой; не все из них описаны в этой книге. Вы можете прочитать больше об этой грандиозной библиотеке на официальном сайте: http://boost.org/libs/interprocess.
Читатели Хабра могут приобрести книгу целиком на сайте издательства со скидкой 25%. Достаточно в интерфейсе корзины ввести промокод: Boost25
Автор: Antony Polukhin