Повседневный C++: изолируем API в стиле C

    Мы все ценим C++ за лёгкую интеграцию с кодом на C. И всё же, это два разных языка.


    Наследие C — одна из самых тяжких нош для современного C++. От такой ноши нельзя избавиться, но можно научиться с ней жить. Однако, многие программисты предпочитают не жить, а страдать. Об этом мы и поговорим.


    Не смешивайте C и бизнес-логику на C++


    Не так давно я случайно заметил в своём любимом компоненте новую вставку. Мой код стал жертвой Tester-Driven Development.


    Согласно википедии, Tester-driven development — это антиметодология разработки, при которой требования определяются багрепортами или отзывами тестировщиков, а программисты лишь лечат симптомы, но не решают настоящие проблемы

    Я сократил код и перевёл его на С++17. Внимательно посмотрите и подумайте, не осталось ли чего лишнего в рамках бизнес-логики:


    bool DocumentLoader::MakeDocumentWorkdirCopy()
    {
        std::error_code errorCode;
        if (!std::filesystem::exists(m_filepath, errorCode) || errorCode)
        {
            throw DocumentLoadError(DocumentLoadError::NotFound(), m_filepath, errorCode.message());
        }
        else
        {
            // Lock document
            HANDLE fileLock = CreateFileW(m_filepath.c_str(),
                    GENERIC_READ,
                    0, // Exclusive access
                    nullptr, // security attributes
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    nullptr //template file
                );
            if (fileLock == INVALID_HANDLE_VALUE)
            {
                throw DocumentLoadError(DocumentLoadError::IsLocked(), m_filepath, "cannot lock file");
            }
            CloseHandle(fileLock);
        }
    
        std::filesystem::copy_file(m_filepath, m_documentCopyPath);
    }

    Давайте опишем словесно, что делает функция:


    • если файл не существует, выбрасывается исключение с кодом NotFound и путём к файлу
    • иначе открыть файл с заданным путём на чтение, с эксклюзивными правами доступа, без аттрибутов безопасности, по возможности открыть существующий, при создании нового файла поставить ему обычные атрибуты файла, не использовать файл-шаблон
    • и если предыдущая операция не удалась, закрываем файл и бросаем исключение с кодом IsLocked
    • иначе закрываем файл и копируем его

    Вам не кажется, что кое-что тут выпадает из уровня абстракции функции?


    Ненужная иллюстрация


    Не смешивайте слои абстракции, код с разным уровнем детализации логики должен быть разделён границами функции, класса или библиотеки. Не смешивайте C и C++, это разные языки.


    На мой взгляд, функция должна выглядеть так:


    bool DocumentLoader::MakeDocumentWorkdirCopy()
    {
        boost::system::error_code errorCode;
        if (!boost::filesystem::exists(m_filepath, errorCode) || errorCode)
        {
            throw DocumentLoadError(DocumentLoadError::NotFound(), m_filepath,      errorCode.message());
        }
        else if (!utils::ipc::MakeFileLock(m_filepath))
        {
            throw DocumentLoadError(DocumentLoadError::IsLocked(), m_filepath, "cannot lock file");
        }
    
        fs::copy_file(m_filepath, m_documentCopyPath);
    }

    Почему C и C++ разные?


    Начнём с того, что они родились в разное время и у них разные ключевые идеи:


    • лозунг C — "Доверяй программисту", хотя многим современным программистам уже нельзя доверять
    • лозунг C++ — "Не плати за то, что не используешь", хотя вообще-то дорого заплатить можно и просто за неоптимальное использование

    В C++ ошибки обрабатываются с помощью исключений. Как они обрабатываются в C? Кто вспомнил про коды возврата, тот неправ: стандартная для языка C функция fopen не возвращает информации об ошибке в кодах возврата. Далее, out-параметры в C передаются по указателю, а в C++ программиста за такое могут и отругать. Далее, в C++ есть идиома RAII для управления ресурсами.


    Мы не будем перечислять остальные отличия. Просто примем как факт, что мы, C++ программисты, пишем на C++ и вынуждены использовать API в стиле C ради:


    • OpenGL, Vulkan, cairo и других графических API
    • CURL и других сетевых библиотек
    • winapi, freetype и других библиотек системного уровня

    Но использовать не значит "пихать во все места"!


    Как открыть файл


    Если вы используете ifstream, то с обработкой ошибок попытка открыть файл выглядит так:


    int main()
    {
        try
        {
            std::ifstream in;
            in.exceptions(std::ios::failbit);
            in.open("C:/path-that-definitely-not-exist");
        }
        catch (const std::exception& ex)
        {
            std::cout << ex.what() << std::endl;
        }
        try
        {
            std::ifstream in;
            in.exceptions(std::ios::failbit);
            in.open("C:/");
        }
        catch (const std::exception& ex)
        {
            std::cout << ex.what() << std::endl;
        }
    }

    Поскольку первый путь не существует, а второй является директорией, мы получим исключения. Вот только в тексте ошибки нет ни пути к файлу, ни точной причины. Если вы запишете такую ошибку в лог, чем это вам поможет?


    Скриншот ошибки fstream


    Типичный код, использующий API в стиле C, ведёт себя хуже: он даже не даёт гарантии безопасности исключений. В примере ниже при выбросе исключения из вставки // .. остальной код файл никогда не будет закрыт.


    // Держи это, если ты вендовоз
    #if defined(_MSC_VER)
    #define _CRT_SECURE_NO_WARNINGS
    #endif
    
    int main()
    {
        try
        {
            FILE *in = ::fopen("C:/path-that-definitely-not-exist", "r");
            if (!in)
            {
                throw std::runtime_error("open failed");
            }
            // ..остальной код..
            fclose(in);
        }
        catch (const std::exception& ex)
        {
            std::cout << ex.what() << std::endl;
        }
    }

    А теперь мы возьмём этот код и покажем, на что способен C++17, даже если перед нами — API в стиле C.


    А почему бы не сделать как советует ООП?


    Валяйте, попробуйте. У вас получится ещё один iostream, в котором нельзя просто взять и узнать, сколько байт вам удалось прочитать из файла, потому что сигнатура read выглядит примерно так:


    basic_istream& read(char_type* s, std::streamsize count);

    А если вы всё же хотите воспользоваться iostream, будьте добры вызвать ещё и tellg:


    // Функция читает не более чем count байт из файла, путь к которому задан в filepath
    std::string GetFirstFileBytes(const std::filesystem::path& filepath, size_t count)
    {
        assert(count != 0);
    
        // Бросаем исключение, если открыть файл нельзя
        std::ifstream stream;
        stream.exceptions(std::ifstream::failbit);
    
        // Маленький фокус: C++17 позволяет конструировать ifstream
        //  не только из string, но и из wstring
        stream.open(filepath.native(), std::ios::binary);
    
        std::string result(count, '\0');
        // читаем не более count байт из файла
        stream.read(&result[0], count);
        // обрезаем строку, если считано меньше, чем ожидалось.
        result = result.substr(0, static_cast<size_t>(stream.tellg()));
    
        return result;
    }

    Одна и та же задача в C++ решается двумя вызовами, а в C — одним вызовом fread! Среди множества библиотек, предлагающих C++ wrapper for X, большинство создаёт подобные ограничения или заставляет вас писать неоптимальный код. Я покажу иной подход: процедурный стиль в C++17.


    Шаг первый: RAII


    Джуниоры не всегда знают, как создавать свои RAII для управления ресурсами. Но мы-то знаем:


    namespace detail
    {
    // Функтор, удаляющий ресурс файла
    struct FileDeleter
    {
        void operator()(FILE* ptr)
        {
            fclose(ptr);
        }
    };
    }
    
    // Создаём FileUniquePtr - синоним специализации unique_ptr, вызывающей fclose
    using FileUniquePtr = std::unique_ptr<FILE, detail::FileDeleter>;

    Такая возможность позволяет завернуть функцию ::fopen в функцию fopen2:


    // Держи это, если ты вендовоз
    #if defined(_MSC_VER)
    #define _CRT_SECURE_NO_WARNINGS
    #endif
    
    // Функция открывает файл, пути в Unicode открываются только в UNIX-системах.
    FileUniquePtr fopen2(const char* filepath, const char* mode)
    {
        assert(filepath);
        assert(mode);
        FILE *file = ::fopen(filepath, mode);
        if (!file)
        {
            throw std::runtime_error("file opening failed");
        }
        return FileUniquePtr(file);
    }

    У такой функции ещё есть три недостатка:


    • она принимает параметры по указателям
    • исключение не содержит никаких подробностей
    • не обрабатываются Unicode-пути на Windows

    Если вызвать функцию для несуществующего пути и для пути к каталогу, получим следующие тексты исключений:


    Скриншот ошибки


    Шаг второй: собираем информацию об ошибке


    Во-первых мы должны узнать у ОС причину ошибки, во-вторых мы должны указать, по какому пути она возникла, чтобы не потерять контекст ошибки в процессе полёта по стеку вызовов.


    И тут надо признать: не только джуниоры, но и многие мидлы и синьоры не в курсе, как правильно работать с errno и насколько это потокобезопасно. Мы напишем так:


    // Держи это, если ты вендовоз
    #if defined(_MSC_VER)
    #define _CRT_SECURE_NO_WARNINGS
    #endif
    
    // Функция открывает файл, пути в Unicode открываются только в UNIX-системах.
    FileUniquePtr fopen3(const char* filepath, const char mode)
    {
        using namespace std::literals; // для литералов ""s.
    
        assert(filepath);
        assert(mode);
        FILE *file = ::fopen(filepath, mode);
        if (!file)
        {
            const char* reason = strerror(errno);
            throw std::runtime_error("opening '"s + filepath + "' failed: "s + reason);
        }
        return FileUniquePtr(file);
    }

    Если вызвать функцию для несуществующего пути и для пути к каталогу, получим более точные тексты исключений:


    Скриншот подробной ошибки


    Шаг третий: экспериментируем с filesystem


    C++17 принёс множество маленьких улучшений, и одно из них — модуль std::filesystem. Он лучше, чем boost::filesystem:


    • в нём решена проблема 2038 года, а в boost::filesystem не решена
    • в нём есть однозначный способ получить UTF-8 путь, а ведь ряд библиотек (например, SDL2) требуют именно UTF-8 пути
    • реализация boost::filesystem содержит опасные игры с разыменованием указателей, в ней много Undefined Behavior

    Для нашего случая filesystem принёс универсальный, не чувствительный к кодировкам класс path. Это позволяет прозрачно обработать Unicode пути на Windows:


    // В VS2017 модуль filesystem пока ещё в experimental
    #include <cerrno>
    #include <cstring>
    #include <experimental/filesystem>
    #include <fstream>
    #include <memory>
    #include <string>
    
    namespace fs = std::experimental::filesystem;
    
    FileUniquePtr fopen4(const fs::path& filepath, const char* mode)
    {
        using namespace std::literals;
    
        assert(mode);
    #if defined(_WIN32)
        fs::path convertedMode = mode;
        FILE *file = ::_wfopen(filepath.c_str(), convertedMode.c_str());
    #else
        FILE *file = ::fopen(filepath.c_str(), mode);
    #endif
        if (!file)
        {
            const char* reason = strerror(errno);
            throw std::runtime_error("opening '"s + filepath.u8string() + "' failed: "s + reason);
        }
        return FileUniquePtr(file);
    }

    Мне кажется очевидным, что такой код трудно написать и что писать его должен один раз кто-то из опытных инженеров в общей библиотеке. Джуниорам в такие дебри лезть не стоит.


    Заглядывая в будущее: мир без препроцессора


    Сейчас я покажу вам код, который в июне 2017 года, скорее всего, не скомпилирует ни один компилятор. Во всяком случае, в VS2017 constexpr if ещё не реализован, а GCC 8 почему-то компилирует ветку if и выдаёт следующую ошибку:


    Скриншот ошибки компиляции


    Да-да, речь пойдёт о constexpr if из C++17, который предлагает новый способ условной компиляции исходников.


    FileUniquePtr fopen5(const fs::path& filepath, const char* mode)
    {
        using namespace std::literals;
    
        assert(mode);
        FILE *file = nullptr;
        // Если тип path::value_type - это тип wchar_t, используем wide-функции
        // На Windows система хочет видеть пути в UTF-16, и условие истинно.
        //  примечание: wchar_t пригоден для UTF-16 только на Windows.
        if constexpr (std::is_same_v<fs::path::value_type, wchar_t>)
        {
            fs::path convertedMode = mode;
            file = _wfopen(filepath.c_str(), convertedMode.c_str());
        }
        // Иначе у нас система, где пути в UTF-8 или вообще нет Unicode
        else
        {
            file = fopen(filepath.c_str(), mode);
        }
        if (!file)
        {
            const char* reason = strerror(errno);
            throw std::runtime_error("opening '"s + filepath.u8string() + "' failed: "s + reason);
        }
        return FileUniquePtr(file);
    }

    Это потрясающая возможность! Если в язык C++ добавят модули и ещё несколько возможностей, то мы сможем забыть препроцессор из языка C как страшный сон и писать новый код без него. Кроме того, с модулями компиляция (без компоновки) станет намного быстрее, а ведущие IDE будут с меньшей задержкой реагировать на автодополнение.


    Плюсы процедурного стиля


    Хотя в индустрии правит ООП, а в академическом коде — функциональный подход, фанатам процедурного стиля пока ещё есть чему радоваться.


    • процедурный стиль легче понять, он проще для джуниоров и на нём написано большинство коротких примеров в сети
    • вы можете завернуть функции C, практически не меняя семантику: наша функция fopen4 по-прежнему использует флаги, mode и другие фокусы в стиле C, но надёжно управляет ресурсами, собирает всю информацию об ошибке и аккуратно принимает параметры
    • документация функции fopen всё-ещё актуальна для нашей обёртки, это сильно облегчает поиск, понимание и переиспользование другими программистами

    Я рекомендую все функции стандартной библиотеки C, WinAPI, CURL или OpenGL завернуть в подобном процедурном стиле.


    Подведём итоги


    На C++ Russia 2016 и C++ Russia 2017 замечательный докладчик Михаил Матросов показывал всем желающим, почему не нужно использовать циклы и как жить без них:



    Насколько известно, вдохновением для Михаила служил доклад 2013 года "C++ Seasoning" за авторством Sean Parent. В докладе было выделено три правила:


    • не пишите низкоуровневые циклы for и while
      • используйте алгоритмы и другие средства из STL/Boost
      • если готовые средства не подходят, заверните цикл в отдельную функцию
    • не работайте с new/delete напрямую
    • не используйте низкоуровневые примитивы синхронизации, такие как mutex и thread

    Я бы добавил ещё одно, четвёртное правило повседневного C++ кода. Не пишите на языке Си-Си-Плюс-Плюс. Не смешивайте бизнес-логику и язык C.


    • Заворачивайте язык C как минимум в один слой изоляции.
    • Если речь об асинхронном коде, заворачивайте в два слоя: первый изолирует C, второй — прячем примитивы синхронизации и шедулинг задач на потоках

    Причины прекрасно показаны в этой статье. Сформулируем их так:


    Только настоящий герой может написать абсолютно надёжный код на C/C++. Если на работе вам каждый день нужен герой — у вас проблема.
    Доступен ли вам великолепный C++17?

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 63
    • +9
      У вас ошибка в первом коде. CreateFileW возвращает дескриптор файла или INVALID_HADLE_VALUE = (HANDLE)-1. т.е. проверка на !fileLock не имеет смысла и тем более дальнейшее закрытие тоже.
      • +1
        + логика странная (зачем открывать файл и сразу же его закрывать) + ф-я возвращала bool, судя по всему, но потом была переделка на исключения. В общем, пример неудачен да ещё и с ошибками
        • 0
          Пример как раз удачен. Типичный результат tester-driven development — я уверен, что это всё закрывало какие-то баги в багтерекре.

          А вся статья, как бы, описывает: как стоило бы решить ту же проблему нормально…
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              я имел в виду то, что написал mickvav ниже
              • 0

                не все, большинство делают запрос к ФС.


                да и опять же, тут как с проверкой интернета, надо сразу делать то, что надо и корректно обрабатывать ошибки.

                • 0
                  Открыть файл как раз самый медленный способ проверить его существование. Он отрабатывает быстро только после попадания файла в дисковый кеш.
                  • 0
                    нет. Нормальные оберкти внутри ничего такого не делают- файлы не открываете.
                    Под windows — используют FindFirstFileW() и stat() под unix
                    иначе вы расширяете проверку до «есть ли файл и можно ли его открыть текущему польвавателю»
                • 0
                  Спасибо! Я был уж очень невнимателен, когда выделял код для публикации (позор, позор мне). В оригинале всё-таки был кастомный RAII FileHandle, который считал INVALID_HANDLE_VALUE за нулевое значение и корректно вызывал CloseHandle в остальных случаях. Я хотел убрать этот RAII чтобы не запутывать.
                  Исправил в статье.
                • +4
                  А ничего, что вы выбросили смысл вашего кода и забыли про это? Если после того, как вы в else-блоке закрыли файл и вернули оси блокировку, произойдёт переключение контекста и ваш файл успешно потрёт другой поток, приложение свалится нафиг.
                  • 0
                    Я был невнимателен, когда готовил первый кусок кода к публикации. Спасибо.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • +2

                      Теперь в ветке #else переменная err не инициализирована и при корректном file может содержать мусор, что превратится в фальшивую ошибку. Я не люблю функции с суффиксом "_s" как раз по этой причине: они создают видимость более безопасного решения, при этом зачастую даже провоцируют ошибки, как scanf_s. Ну и кроме того, это специфичный для компилятора код, введённый без особых причин. На использование wchar_t версии хотя бы есть причина — поддержка Unicode-путей.

                      • НЛО прилетело и опубликовало эту надпись здесь
                      • 0

                        На самом деле, надо проверять не только _MSC_VER, но и __STDC_VERSION__ ...

                      • 0
                        Считается ли модным сейчас в C++ использовать исключения или коды возвраты?
                        • НЛО прилетело и опубликовало эту надпись здесь
                          • 0

                            Если "пусть падает", то юзайте сразу terminate. Зачем в таком случае исключение бросать?

                            • НЛО прилетело и опубликовало эту надпись здесь
                              • 0

                                Да, буду говорить если падать, то давайте оставим открытые сокеты, висячие коннекты, залоченные файлы, кучу мусора в папке temp. Что в этом плохого? Система всё почистит. Это в любом случае возможный исход работы программы. Представьте что будет если внезапно отключить питание.


                                Раскрутка стека нужна, только если программа будет продолжать работать. Если падаем, то раскручивать смысла нет.


                                Что такое "второй круг"?


                                Вы сказали исключение никто не ловит, а теперь у вас уже какие то логи.

                                • НЛО прилетело и опубликовало эту надпись здесь
                                  • 0
                                    Вот в том и дело, что обычный рабочий момент приравнивается к внезапному отключению питания. Давайте будем взрывать системный блок, если не нашли своего раздела в реестре?

                                    то, что некий модуль считает фатальной ошибкой, на высшем уровне иерархии может таковой не являться (например, есть другой модуль, решающий ту же задачу иначе)

                                    Мы говорим про "обычный рабочий момент" или про "дальше жить незачем"?


                                    От того, что некая клетка организма умерла, вовсе не следует, что и весь организм должен за ней последовать. Но клетка для себя — умерла.

                                    Неуместное сравнение.

                                    • НЛО прилетело и опубликовало эту надпись здесь
                                      • +1
                                        Не понимаю, чего вы от меня добиться хотите.

                                        Хочу, что б вы признали, что исключения, которые никто не ловит, бессмысленны.


                                        Но вы предлагаете еще и всю программу свалить, причем самым дурацким из возможных способом, не дав ей за собой почистить.

                                        Может вы перестанете сыпать эпитетами и скажете, наконец, что конкретно не так с отсутсвием освобождения ресурсов при завершении программы?

                                        • НЛО прилетело и опубликовало эту надпись здесь
                                          • 0
                                            Исключения, которые никто не ловит, это единственный способ аварийно завершить программу, написанную на C++ (а не на C). Но я еще раз повторю, речь шла не об этом.

                                            Ага, как же. terminate, exit, return -1 из main в конце концов. А о чём тогда?


                                            Конкретно. Как будет закрыто TCP-соединение, если оно существовало в момент заверешения? Так оно должно закрываться? Как будет завершена сессия TLS поверх этого соединения? Так она должна завершаться? Что станет со встроенной базой данных на sqlite? Что сделается с криптоключами, созданными программой? Чем закончится асинхронная запись в файл? Даже попросту что произойдет с иконкой программы в трее?

                                            Почаще задавайте себе такие вопросы. TCP-соединение отвалится по таймауту, соответственно, и tls сессия. С sqlite все будет отлично. Там транзакционноссть гарантирует целостность данных. Про ключи не понял. С иконкой, действительно, беда, но это в первую очередь демонстрирует кривость windows.


                                            Стоит уточнить, что я не утверждаю, что исключения не нужны, что terminate можно использовать где угодно, и, конечно, что ресурсы не нужно освобождать перед завершением работы программы. Я хочу скзать, что в случаях, когда ошибку обработать невозможно, к примеру при инициализации глобальных объектов, нужно вызывать terminate, а не бросать какие-то исключения.

                                            • НЛО прилетело и опубликовало эту надпись здесь
                                              • 0

                                                Я не понял, что вы хотели сказать фразой


                                                бросать исключение надо в ожидании, что его никто не ловит, то есть пусть падает, все равно уже «дальше жить незачем».

                                                По-вашему, если в этом конструкторе произдёт ошибка, то исключение бросать не нужно что ли? Ерунда какая-то.

                                                • НЛО прилетело и опубликовало эту надпись здесь
                              • 0
                                Во-первых, в случае исключения отработают деструкторы

                                Только если оно на самом деле где-то ловится. Убегающее за пределы main исключение — это UB, и если компилятор может это доказать, то будет больно:


                                #include <iostream>
                                
                                struct Foo
                                {
                                    Foo () { std::cout << "yay ctor" << std::endl; }
                                    ~Foo () { std::cout << "yay dtor" << std::endl; }
                                };
                                
                                int main ()
                                {
                                    Foo foo;
                                    throw 0;
                                }

                                Есть все шансы, что yay dtor вы не увидите.

                                • 0
                                  Убегающее за пределы main исключение — это UB
                                  С какого перепугу? Вы про std::terminate вообще что-нибудь слышали?

                                  Есть все шансы, что yay dtor вы не увидите.
                                  Это почему ещё? Да, компилятор может «просечь», что в этой программе обязательно будет вызван деструктор, а за ним — std::terminate и вместо компиляции сложных таблиц и всего прочего просто напрямую вызвать Foo::~Foo() (хотя я таких компиляторов не знаю), но просто выкинуть его — он права не имеет: код Foo::~Foo() обязан быть вызван до std::terminate, а использование std::endlобязано вызвать flush(), так что yay dtor вы увидите точно. Вот что вы увидите потом — это уже зависит от компилятора, а также от того, что программа установила (если установила) в std::terminate_handlerе…
                                  • +1

                                    Позор на мои патлы.


                                    Да, std::terminate обязан вызваться, но вот произведётся ли в этом случае раскрутка стека (и вызовы деструкторов, да) — implementation-defined.


                                    Так что истина где-то посередине.


                                    И запустите код-то.

                                    • 0
                                      Implementation-defined и undefined — это две большие разницы. На тех платформах, где я работаю стек раскручивается и деструкторы вызываются. В том числе в вашем примере. Интересно какие платформы имели в виду разработчики стандарта, где этого не происходит.
                                      • +1

                                        Ну да, потому и позор на мои патлы. У меня почему-то в голове отложилось, что это аж целое UB, а не что лишь вопрос вызова деструкторов и раскрутки стека — implementation-defined.


                                        На линуксах под gcc и clang с libstdc++ деструкторы не вызываются: пруф.

                                  • НЛО прилетело и опубликовало эту надпись здесь
                                • 0

                                  Вы невнимательно читаете между слов:


                                  и бросать исключение надо в ожидании, что если его никто не ловит, то есть пусть падает

                                  terminate плох тем, что его нельзя нормально поймать, никак. Если он произошел — то уже все. Смысл исключений — в том, что их можно поймать, но если исключение не поймали — то лучше упасть чем продолжить работу.

                                  • +1

                                    Там нет "если". Да и странное предложение у вас получается. Что тут ожидать? Ясно что упадёт, если не поймают. Вот исходная фраза понятна, но я ней не согласен. Бросать надо только в надежде, что кто то поймает и обработает. Если ситуацию обработать невозможно, нет смысла и бросать исключение.

                                    • 0
                                      Ясно что упадёт, если не поймают.

                                      В случае кодов возврата это не так. И это зачастую сильно запутывает отладку.

                                      • +2

                                        А это не библиотечному коду решать, можно обработать ситуацию или нет.

                                        • 0

                                          Кто сказал, что речь о библиотеке?

                                          • +3

                                            Заведомо завязываться на то, что делают вышестоящие слои, особенно там, где можно на это не завязываться — это, ну, не стоит так делать, уровни абстракций нарушаются.

                                  • +4
                                    > Мода тут ни при чем.

                                    В модных языках типа go/rust свой появился свой взгляд на то как обрабатывать ошибки, потому и спрашиваю. В статье например исключение бросается когда файл не найден внутри fopenX, и мне такое подход для общего случая не очень нравится, ведь отсутствия файла это обычно ожидаемая ситуация а не исключительная.
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                      • 0

                                        Только строго наоборот. Исключение (exception) — это особая (исключительная) ситуация, которая требует особой же обработки. А ошибка (fault) — это ситуация, когда программа уже работает неправильно, и исправить тут ничего нельзя.

                                        • НЛО прилетело и опубликовало эту надпись здесь
                                      • +1

                                        В прикладном коде удобно использовать исключения. Идеологически можно обосновать так:


                                        • при написании системного сервиса или веб-сервера отказ — это типичная ситуация, и коды возврата будут хороши, а ещё лучше, на мой взгляд, std::expected
                                        • при написании прикладной бизнес-логики отказ означает провал всей операции (либо целого варианта её выполнения), и исключение — это лучший способ раскрутить стек до того момента, где мы можем сообщить пользователю об ошибке либо обнаружить известную проблему и пойти по запасному плану

                                        Опять же, если есть удобный шаблон вида std::expected, то можно написать вариант fopenX без выброса исключений. Будет примерно как в Rust/Go.

                                  • +2
                                    Кст в первых двух блоках кода показана очень распространенная проблема форматирования кода — избыточные уровни вложения. Всегда пытаюсь бороться с лишними уровнями вложения. Каждое условие выбрасывает исключение, то есть прерывает выполнение. Это делает ненужным последующие else.
                                    • +2
                                      Джуниоры не всегда знают, как создавать свои RAII для управления ресурсами. Но мы-то знаем:

                                      Нет, вы тоже не знаете. Правильный вариант:


                                       auto f = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen(path, mode), &std::fclose);

                                      Если вы используете ifstream, то с обработкой ошибок попытка открыть файл выглядит так:

                                      Разве что, "джуниоры". Остальные разработчики проверяют булево состояние потока вначале (необязательно) и при каждой операции чтения:


                                      std::ifstream fs ( "foo.txt" );
                                      if (!fs) std::cerr << std::strerror(errno); // можно, если очень хочется, но не обязательно
                                      std::string s;
                                      while (getline(fs, s)) { ... }
                                      • +2

                                        Я не могу назвать такую специализацию unique_ptr правильным вариантом, потому что всегда придётся в конструктор передавать второй аргумент, т.к. подходящего дефолтного конструктора у указателя на функцию нет. Поэтому и заводят структуру с одним оператором.

                                        • 0
                                          Нет, вы тоже не знаете. Правильный вариант:
                                          Всё-таки автор более корректен в объявлении deleter'а: во-первых, как было отмечено, второй аргумент теперь будет являться обязательным в конструкторе, а во-вторых, размер такого unique_ptr'а раздувается на размер хранимого указателя на функцию-удалитель (несмотря на то, что fclose по сути является глобальной свободной функцией). Сравнение размеров.
                                          Если такой unique_ptr является членом RAII-класса, то размер может иметь значение. Однако если обработка выполняется только в фиксированном {}-скопе (т.е. C-указатель не покидает блока), действительно можно обойтись указанным объявлением, хотя выигрыш в количестве кода при хорошем форматировании сомнителен.
                                        • –1
                                          Код первого примера
                                          ....
                                          HANDLE fileLock = CreateFileW(
                                          ....
                                           if (!fileLock)
                                           {
                                             CloseHandle(fileLock);
                                          ....
                                          

                                          Баг №1:
                                          CreateFileW не умеет возвращать нулевое значение совсем (см MSDN). В слечае ошибки возвращается INVALID_HANDLE_VALUE которое суть (HANDLE)(-1). Условие не сработает никогда.

                                          Баг №2:
                                          Если вдруг Windows совсем заболеет и CreateFileW всё-же вернёт 0 то в условии вы закрываете нулевой хэндл.
                                          На этот случай CloseHandle может кинуть exception invalid handle.

                                          Читать дальше перехотелось.
                                        • 0
                                          Даа модули это хорошо…
                                          • –1
                                            Вот почему в ядре linux пишут на C, а не на C++.
                                            Если уж с открытием фала такой геморой, представлю что там со всем остальным.
                                            Если вы не доверяете программисту пишите на Go.
                                            • 0
                                              есть три недостатка: она принимает параметры по указателям

                                              ОК, но ваша fopen5 тоже принимает параметр по указателю — const char* mode. или есть на то причина? как с ним быть?

                                              с модулями компиляция (без компоновки) станет намного быстрее

                                              а что именно станет быстрее?

                                              • 0

                                                Я думаю мне стоило заменить const char* mode на const char mode[]. Для анализатора это может дать верную подсказку.
                                                P.S. с модулями все заголовки STL будут разбираться только один раз, а сейчас без хороших precompiled headers они разбираются парсером каждый раз при компиляции очередного файла. Частично precompiled headers решают эту задачу, но хранение на диске в виде интерфейса модуля будет компактнее, чем хранение в виде сериализованного AST. Т.е. модули грозят быть быстрее precompiled headers в плане нагрузки на I/O между запусками компилятора системой сборки, кроме того, суженному интерфейсу легче поместиться в уровни кеша процессора.

                                              • 0
                                                Типичный код, использующий API в стиле C, ведёт себя хуже: он даже не даёт гарантии безопасности исключений. В примере ниже при выбросе исключения из вставки //… остальной код файл никогда не будет закрыт.
                                                Я не понял: неужели автор думает, что если fopen() вернула ему NULL, он сможет по этому NULL закрыть файл??? В этом случае и закрывать нечего — файл никогда не был открыт.
                                                • 0

                                                  Если вместо //..остальной код вставить реальный код, бросающий исключение, то уже открытый ранее файл никем не будет закрыт. Я даже встречал связанный с этим баг в одной из библиотек: при ошибке чтения PNG из файла файл становился заблокированным на чтение самим же процессом, потому что где-то в библиотеке утёк FILE*.

                                                  • 0
                                                    Дошло. Сбило с толку это:
                                                            if (!in)
                                                            {
                                                                throw std::runtime_error("open failed");
                                                            }
                                                    
                                                • –3
                                                  > не пишите низкоуровневые циклы for и while
                                                  > используйте алгоритмы и другие средства из STL/Boost
                                                  Зачем вы так с новичками? А если они поверят что STL/Boost это хорошо?
                                                  Вырезка из википедии(цитата Торвальдса)
                                                  С++ приводит к очень, очень плохим проектным решениям. Неизбежно начинают применяться «замечательные» библиотечные возможности вроде STL, и Boost, и прочего мусора, которые могут «помочь» программированию, но порождают:
                                                  — невыносимую боль, когда они не работают (и всякий, кто утверждает, что STL и особенно Boost стабильны и портируемы, настолько погряз во лжи, что это даже не смешно)
                                                  — неэффективно абстрагированные программные модели, когда спустя два года обнаруживается, что какая-то абстракция была недостаточно эффективна, но теперь весь код зависит ото всех окружающих её замечательных объектных моделей, и её нельзя исправить, не переписав всё приложение.
                                                  • –2
                                                    Страсть, как ненавижу иностранный словечки. Джуниор: Junior — имеет 5 разных значений в зависимости от страны(Британия или США).

                                                    Можно просто новичок или начинающий?
                                                    • 0
                                                      >auto f = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen(path, mode), &std::fclose);

                                                      Выглядит четко. Только вот состояние unique_ptr придётся еще дополнительно проверить, легче обернуть fopen в функцию и выкинуть exception на конструкции указателя, чем проверять потом его состояние.

                                                      >Разве что, «джуниоры». Остальные разработчики проверяют булево состояние потока вначале (необязательно) и при каждой операции чтения:
                                                      >необязательно
                                                      А как же выбросить exception на свалившееся открытие? У автора написано грамотнее.

                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.