Мы все ценим 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;
}
}
Поскольку первый путь не существует, а второй является директорией, мы получим исключения. Вот только в тексте ошибки нет ни пути к файлу, ни точной причины. Если вы запишете такую ошибку в лог, чем это вам поможет?
Типичный код, использующий 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 напрямую
- подробнее об этом — в докладе Михаила Матросова С++ without new and delete
- не используйте низкоуровневые примитивы синхронизации, такие как mutex и thread
Я бы добавил ещё одно, четвёртное правило повседневного C++ кода. Не пишите на языке Си-Си-Плюс-Плюс. Не смешивайте бизнес-логику и язык C.
- Заворачивайте язык C как минимум в один слой изоляции.
- Если речь об асинхронном коде, заворачивайте в два слоя: первый изолирует C, второй — прячем примитивы синхронизации и шедулинг задач на потоках
Причины прекрасно показаны в этой статье. Сформулируем их так:
Только настоящий герой может написать абсолютно надёжный код на C/C++. Если на работе вам каждый день нужен герой — у вас проблема.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (63)
mickvav
18.06.2017 10:01+4А ничего, что вы выбросили смысл вашего кода и забыли про это? Если после того, как вы в else-блоке закрыли файл и вернули оси блокировку, произойдёт переключение контекста и ваш файл успешно потрёт другой поток, приложение свалится нафиг.
sergey_shambir
19.06.2017 15:10Я был невнимателен, когда готовил первый кусок кода к публикации. Спасибо.
madcomaker
18.06.2017 10:17+2// Держи это, если ты вендовоз #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; errno_t err; #if defined(_MSC_VER) err = ::fopen_s(&file, filepath, mode); #else file = ::fopen(filepath, mode); if (!file) err = errno; #endif if(err) { const char* reason = strerror(err); throw std::runtime_error("opening '"s + filepath + "' failed: "s + reason); } return FileUniquePtr(file); }
sergey_shambir
19.06.2017 15:59+2Теперь в ветке
#else
переменная err не инициализирована и при корректном file может содержать мусор, что превратится в фальшивую ошибку. Я не люблю функции с суффиксом "_s" как раз по этой причине: они создают видимость более безопасного решения, при этом зачастую даже провоцируют ошибки, как scanf_s. Ну и кроме того, это специфичный для компилятора код, введённый без особых причин. На использование wchar_t версии хотя бы есть причина — поддержка Unicode-путей.madcomaker
19.06.2017 16:33+1Да, заметил после того, как запостил, все же окошко ответа не лучший редактор кода. Поскольку надо было только идею показать, то и париться не стал.
Специфичный для компилятора код все равно присутствовал в виде отключения ворнингов CRT. Дико злюсь, когда вижу это в заголовках какой-нибудь библиотеки, как и #pragma warning(disable) без возврата «как было». В своем .cpp можно творить что угодно, но зачем чужому коду указывать, каким ему быть.
orcy
18.06.2017 15:26Считается ли модным сейчас в C++ использовать исключения или коды возвраты?
madcomaker
18.06.2017 17:53Мода тут ни при чем. Код возврата это одно, исключение это другое. Если исключение никто не ловит, программа упадет, и бросать исключение надо в ожидании, что его никто не ловит, то есть пусть падает, все равно уже «дальше жить незачем». В остальных случаях нужно возвращать код ошибки.
nolane
19.06.2017 00:22Если "пусть падает", то юзайте сразу terminate. Зачем в таком случае исключение бросать?
madcomaker
19.06.2017 00:58+2Во-первых, в случае исключения отработают деструкторы, и не говорите, что мол все равно падать, поэтому давайте оставим открытые сокеты, висящие коннекты, залоченные файлы, кучу мусора в папке temp и все такое прочее. Во-вторых, то, что некий модуль считает фатальной ошибкой, на высшем уровне иерархии может таковой не являться (например, есть другой модуль, решающий ту же задачу иначе). С этой точки зрения опять-таки важно, чтобы отработали деструкторы и программа осталась в состоянии, из которого возможен «второй круг». В-третьих, даже если программа не знает, что делать дальше, она все-таки поймает исключение на самом верхнем уровне, отобразит соответствующее сообщение, запишет причину в лог, отправит отчет об ошибке и т.п. terminate это «школьный крэш», сразу показывающий уровень команды, такое чудо разработавшей (либо уровень пофигизма в этой команде, что в принципе одно и то же).
nolane
19.06.2017 10:59Да, буду говорить если падать, то давайте оставим открытые сокеты, висячие коннекты, залоченные файлы, кучу мусора в папке temp. Что в этом плохого? Система всё почистит. Это в любом случае возможный исход работы программы. Представьте что будет если внезапно отключить питание.
Раскрутка стека нужна, только если программа будет продолжать работать. Если падаем, то раскручивать смысла нет.
Что такое "второй круг"?
Вы сказали исключение никто не ловит, а теперь у вас уже какие то логи.
madcomaker
19.06.2017 13:44Что в этом плохого?
Представьте что будет если внезапно отключить питание.
Вот в том и дело, что обычный рабочий момент приравнивается к внезапному отключению питания. Давайте будем взрывать системный блок, если не нашли своего раздела в реестре?
Что такое «второй круг»?
то, что некий модуль считает фатальной ошибкой, на высшем уровне иерархии может таковой не являться (например, есть другой модуль, решающий ту же задачу иначе)
Вы сказали исключение никто не ловит
Я не говорил такого. Я сказал, что бросать исключение нужно в расчете, что его никто не ловит. А ловит ли его кто-то, из места, где оно бросается, не видно. От того, что некая клетка организма умерла, вовсе не следует, что и весь организм должен за ней последовать. Но клетка для себя — умерла.nolane
19.06.2017 17:04Вот в том и дело, что обычный рабочий момент приравнивается к внезапному отключению питания. Давайте будем взрывать системный блок, если не нашли своего раздела в реестре?
то, что некий модуль считает фатальной ошибкой, на высшем уровне иерархии может таковой не являться (например, есть другой модуль, решающий ту же задачу иначе)Мы говорим про "обычный рабочий момент" или про "дальше жить незачем"?
От того, что некая клетка организма умерла, вовсе не следует, что и весь организм должен за ней последовать. Но клетка для себя — умерла.
Неуместное сравнение.
madcomaker
19.06.2017 18:09Не понимаю, чего вы от меня добиться хотите. Завершение любой программы — это обычный рабочий момент независимо от причины. Неспособность одного из модулей программы выполнить свой контракт — это обычный рабочий момент. Сам модуль умер, да здравствует модуль. Но вы предлагаете еще и всю программу свалить, причем самым дурацким из возможных способом, не дав ей за собой почистить.
nolane
19.06.2017 18:48+1Не понимаю, чего вы от меня добиться хотите.
Хочу, что б вы признали, что исключения, которые никто не ловит, бессмысленны.
Но вы предлагаете еще и всю программу свалить, причем самым дурацким из возможных способом, не дав ей за собой почистить.
Может вы перестанете сыпать эпитетами и скажете, наконец, что конкретно не так с отсутсвием освобождения ресурсов при завершении программы?
madcomaker
19.06.2017 20:22Исключения, которые никто не ловит, это единственный способ аварийно завершить программу, написанную на C++ (а не на C). Но я еще раз повторю, речь шла не об этом.
Конкретно. Как будет закрыто TCP-соединение, если оно существовало в момент заверешения? Так оно должно закрываться? Как будет завершена сессия TLS поверх этого соединения? Так она должна завершаться? Что станет со встроенной базой данных на sqlite? Что сделается с криптоключами, созданными программой? Чем закончится асинхронная запись в файл? Даже попросту что произойдет с иконкой программы в трее?nolane
19.06.2017 22:59Исключения, которые никто не ловит, это единственный способ аварийно завершить программу, написанную на C++ (а не на C). Но я еще раз повторю, речь шла не об этом.
Ага, как же. terminate, exit, return -1 из main в конце концов. А о чём тогда?
Конкретно. Как будет закрыто TCP-соединение, если оно существовало в момент заверешения? Так оно должно закрываться? Как будет завершена сессия TLS поверх этого соединения? Так она должна завершаться? Что станет со встроенной базой данных на sqlite? Что сделается с криптоключами, созданными программой? Чем закончится асинхронная запись в файл? Даже попросту что произойдет с иконкой программы в трее?
Почаще задавайте себе такие вопросы. TCP-соединение отвалится по таймауту, соответственно, и tls сессия. С sqlite все будет отлично. Там транзакционноссть гарантирует целостность данных. Про ключи не понял. С иконкой, действительно, беда, но это в первую очередь демонстрирует кривость windows.
Стоит уточнить, что я не утверждаю, что исключения не нужны, что terminate можно использовать где угодно, и, конечно, что ресурсы не нужно освобождать перед завершением работы программы. Я хочу скзать, что в случаях, когда ошибку обработать невозможно, к примеру при инициализации глобальных объектов, нужно вызывать terminate, а не бросать какие-то исключения.
madcomaker
20.06.2017 00:12Пусть вы автор класса
class A { A() { // 1 } };
Вот так я его буду использовать
static A a_static; int main(int, char **) { A a; // do something with a and a_static return 0; }
Впишите, пожалуйста, в конструктор класса A (где комментарий с цифрой 1), как вы будете разбираться, где вас инстанцировали глобальным объектом, а где автоматическим, и, в предположении, что свершилось страшное, завершите мне программу двумя разными способами, через исключение и через terminate.nolane
20.06.2017 00:25Я не понял, что вы хотели сказать фразой
бросать исключение надо в ожидании, что его никто не ловит, то есть пусть падает, все равно уже «дальше жить незачем».
По-вашему, если в этом конструкторе произдёт ошибка, то исключение бросать не нужно что ли? Ерунда какая-то.
madcomaker
20.06.2017 00:51Конструируемому объекту и правда жить дальше незачем, раз он даже родиться толком не сумел. Конструктор смело бросает исключение. Речь-то шла о другом, о том, возвращать ли код ошибки или бросать исключение из какого-то метода класса. Я написал, что бросать исключение нужно, когда «дальше жить незачем», т.е. ситуация такая же, как в конструкторе, безвыходная. В ожидании, что никто не ловит, — потому что есть идиотская практика бросать исключение там, где ничего исключительного не произошло, заставляя вызывающий код почем зря оборачивать вызов в try-catch, чтобы выполнить одну из ОЖИДАЕМЫХ веток кода. Объяснил?
0xd34df00d
23.06.2017 21:45Во-первых, в случае исключения отработают деструкторы
Только если оно на самом деле где-то ловится. Убегающее за пределы 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
вы не увидите.khim
23.06.2017 22:28Убегающее за пределы main исключение — это UB
С какого перепугу? Вы про std::terminate вообще что-нибудь слышали?
Есть все шансы, что
Это почему ещё? Да, компилятор может «просечь», что в этой программе обязательно будет вызван деструктор, а за ним —yay dtor
вы не увидите.std::terminate
и вместо компиляции сложных таблиц и всего прочего просто напрямую вызватьFoo::~Foo()
(хотя я таких компиляторов не знаю), но просто выкинуть его — он права не имеет: кодFoo::~Foo()
обязан быть вызван доstd::terminate
, а использование std::endl — обязано вызвать flush(), так чтоyay dtor
вы увидите точно. Вот что вы увидите потом — это уже зависит от компилятора, а также от того, что программа установила (если установила) в std::terminate_handlerе…0xd34df00d
23.06.2017 22:37+1Позор на мои патлы.
Да, std::terminate обязан вызваться, но вот произведётся ли в этом случае раскрутка стека (и вызовы деструкторов, да) — implementation-defined.
Так что истина где-то посередине.
И запустите код-то.
khim
24.06.2017 00:02Implementation-defined и undefined — это две большие разницы. На тех платформах, где я работаю стек раскручивается и деструкторы вызываются. В том числе в вашем примере. Интересно какие платформы имели в виду разработчики стандарта, где этого не происходит.
0xd34df00d
24.06.2017 01:17+1Ну да, потому и позор на мои патлы. У меня почему-то в голове отложилось, что это аж целое UB, а не что лишь вопрос вызова деструкторов и раскрутки стека — implementation-defined.
На линуксах под gcc и clang с libstdc++ деструкторы не вызываются: пруф.
madcomaker
23.06.2017 22:44Как «вендовоз» могу сказать, что конкретно в MSVC исключения подхватываются в виде uncaught в любом случае, даже будучи брошенными до main(), просто CRT так устроен. А с точки зрения стандарта насчет UB в этом случае не знаю. Если не затруднит, можно ссылочку, где это сказано, гугль что-то не захотел меня просветить.
mayorovp
19.06.2017 09:59Вы невнимательно читаете между слов:
и бросать исключение надо в ожидании, что если его никто не ловит, то есть пусть падает
terminate плох тем, что его нельзя нормально поймать, никак. Если он произошел — то уже все. Смысл исключений — в том, что их можно поймать, но если исключение не поймали — то лучше упасть чем продолжить работу.
nolane
19.06.2017 11:04+1Там нет "если". Да и странное предложение у вас получается. Что тут ожидать? Ясно что упадёт, если не поймают. Вот исходная фраза понятна, но я ней не согласен. Бросать надо только в надежде, что кто то поймает и обработает. Если ситуацию обработать невозможно, нет смысла и бросать исключение.
mayorovp
19.06.2017 12:07Ясно что упадёт, если не поймают.
В случае кодов возврата это не так. И это зачастую сильно запутывает отладку.
0xd34df00d
23.06.2017 21:48+2А это не библиотечному коду решать, можно обработать ситуацию или нет.
nolane
24.06.2017 16:06Кто сказал, что речь о библиотеке?
0xd34df00d
25.06.2017 04:14+3Заведомо завязываться на то, что делают вышестоящие слои, особенно там, где можно на это не завязываться — это, ну, не стоит так делать, уровни абстракций нарушаются.
orcy
19.06.2017 07:03+4> Мода тут ни при чем.
В модных языках типа go/rust свой появился свой взгляд на то как обрабатывать ошибки, потому и спрашиваю. В статье например исключение бросается когда файл не найден внутри fopenX, и мне такое подход для общего случая не очень нравится, ведь отсутствия файла это обычно ожидаемая ситуация а не исключительная.madcomaker
19.06.2017 13:48Да, и я о том же. Есть ошибки, есть исключения. Ошибка это часть обычного выполнения, одно из предусмотренных состояний. Исключение — непредусмотренное состояние.
mayorovp
19.06.2017 13:54Только строго наоборот. Исключение (exception) — это особая (исключительная) ситуация, которая требует особой же обработки. А ошибка (fault) — это ситуация, когда программа уже работает неправильно, и исправить тут ничего нельзя.
madcomaker
19.06.2017 14:44+1Я бы разделил на ошибки (errors), ловушки (traps) и неудачи (faults). Первое — нормальное выполнение (например, отсутствует файл, сервер ответил 5xx и т.п., ничего именно исключительного нет), второе — непредусмотренная ситуация, неразрешимая на данном уровне (например, отсутствует ветка программы в реестре, out of memory и т.п.), третье — ошибка в коде (выход за границу массива и т.п.). Первое — один из нормальных путей в вызывающем коде (if/else), второе — вызывающий код на такое не рассчитан, но может передать выше для принятия решения (например, ветку в реестре можно создать и начать заново «с самого верха»), третье — однозначно крэш как меньшее из зол.
sergey_shambir
19.06.2017 15:19+1В прикладном коде удобно использовать исключения. Идеологически можно обосновать так:
- при написании системного сервиса или веб-сервера отказ — это типичная ситуация, и коды возврата будут хороши, а ещё лучше, на мой взгляд, std::expected
- при написании прикладной бизнес-логики отказ означает провал всей операции (либо целого варианта её выполнения), и исключение — это лучший способ раскрутить стек до того момента, где мы можем сообщить пользователю об ошибке либо обнаружить известную проблему и пойти по запасному плану
Опять же, если есть удобный шаблон вида std::expected, то можно написать вариант fopenX без выброса исключений. Будет примерно как в Rust/Go.
fareloz
18.06.2017 16:21+2Кст в первых двух блоках кода показана очень распространенная проблема форматирования кода — избыточные уровни вложения. Всегда пытаюсь бороться с лишними уровнями вложения. Каждое условие выбрасывает исключение, то есть прерывает выполнение. Это делает ненужным последующие else.
mbait
18.06.2017 17:52+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)) { ... }
sergey_shambir
19.06.2017 16:10+2Я не могу назвать такую специализацию unique_ptr правильным вариантом, потому что всегда придётся в конструктор передавать второй аргумент, т.к. подходящего дефолтного конструктора у указателя на функцию нет. Поэтому и заводят структуру с одним оператором.
Readme
21.06.2017 13:19Нет, вы тоже не знаете. Правильный вариант:
Всё-таки автор более корректен в объявленииdeleter
'а: во-первых, как было отмечено, второй аргумент теперь будет являться обязательным в конструкторе, а во-вторых, размер такогоunique_ptr
'а раздувается на размер хранимого указателя на функцию-удалитель (несмотря на то, чтоfclose
по сути является глобальной свободной функцией). Сравнение размеров.
Если такойunique_ptr
является членом RAII-класса, то размер может иметь значение. Однако если обработка выполняется только в фиксированном {}-скопе (т.е. C-указатель не покидает блока), действительно можно обойтись указанным объявлением, хотя выигрыш в количестве кода при хорошем форматировании сомнителен.
FancyRansomware
19.06.2017 16:01-1Код первого примера
.... HANDLE fileLock = CreateFileW( .... if (!fileLock) { CloseHandle(fileLock); ....
Баг №1:
CreateFileW не умеет возвращать нулевое значение совсем (см MSDN). В слечае ошибки возвращается INVALID_HANDLE_VALUE которое суть (HANDLE)(-1). Условие не сработает никогда.
Баг №2:
Если вдруг Windows совсем заболеет и CreateFileW всё-же вернёт 0 то в условии вы закрываете нулевой хэндл.
На этот случай CloseHandle может кинуть exception invalid handle.
Читать дальше перехотелось.
kovserg
19.06.2017 16:02-1Вот почему в ядре linux пишут на C, а не на C++.
Если уж с открытием фала такой геморой, представлю что там со всем остальным.
Если вы не доверяете программисту пишите на Go.
dadwin
19.06.2017 16:02есть три недостатка: она принимает параметры по указателям
ОК, но ваша fopen5 тоже принимает параметр по указателю — const char* mode. или есть на то причина? как с ним быть?
с модулями компиляция (без компоновки) станет намного быстрее
а что именно станет быстрее?
sergey_shambir
19.06.2017 16:03Я думаю мне стоило заменить
const char* mode
наconst char mode[]
. Для анализатора это может дать верную подсказку.
P.S. с модулями все заголовки STL будут разбираться только один раз, а сейчас без хороших precompiled headers они разбираются парсером каждый раз при компиляции очередного файла. Частично precompiled headers решают эту задачу, но хранение на диске в виде интерфейса модуля будет компактнее, чем хранение в виде сериализованного AST. Т.е. модули грозят быть быстрее precompiled headers в плане нагрузки на I/O между запусками компилятора системой сборки, кроме того, суженному интерфейсу легче поместиться в уровни кеша процессора.
Nick_Shl
19.06.2017 16:06Типичный код, использующий API в стиле C, ведёт себя хуже: он даже не даёт гарантии безопасности исключений. В примере ниже при выбросе исключения из вставки //… остальной код файл никогда не будет закрыт.
Я не понял: неужели автор думает, что если fopen() вернула ему NULL, он сможет по этому NULL закрыть файл??? В этом случае и закрывать нечего — файл никогда не был открыт.sergey_shambir
19.06.2017 16:07Если вместо
//..остальной код
вставить реальный код, бросающий исключение, то уже открытый ранее файл никем не будет закрыт. Я даже встречал связанный с этим баг в одной из библиотек: при ошибке чтения PNG из файла файл становился заблокированным на чтение самим же процессом, потому что где-то в библиотеке утёкFILE*
.Nick_Shl
23.06.2017 02:45Дошло. Сбило с толку это:
if (!in) { throw std::runtime_error("open failed"); }
Luke0208
19.06.2017 16:08-3> не пишите низкоуровневые циклы for и while
> используйте алгоритмы и другие средства из STL/Boost
Зачем вы так с новичками? А если они поверят что STL/Boost это хорошо?
Вырезка из википедии(цитата Торвальдса)
С++ приводит к очень, очень плохим проектным решениям. Неизбежно начинают применяться «замечательные» библиотечные возможности вроде STL, и Boost, и прочего мусора, которые могут «помочь» программированию, но порождают:
— невыносимую боль, когда они не работают (и всякий, кто утверждает, что STL и особенно Boost стабильны и портируемы, настолько погряз во лжи, что это даже не смешно)
— неэффективно абстрагированные программные модели, когда спустя два года обнаруживается, что какая-то абстракция была недостаточно эффективна, но теперь весь код зависит ото всех окружающих её замечательных объектных моделей, и её нельзя исправить, не переписав всё приложение.
ivan_petroff
19.06.2017 16:08-2Страсть, как ненавижу иностранный словечки. Джуниор: Junior — имеет 5 разных значений в зависимости от страны(Британия или США).
Можно просто новичок или начинающий?
Mehazawa
19.06.2017 16:08>auto f = std::unique_ptr<FILE, decltype(&std::fclose)>(std::fopen(path, mode), &std::fclose);
Выглядит четко. Только вот состояние unique_ptr придётся еще дополнительно проверить, легче обернуть fopen в функцию и выкинуть exception на конструкции указателя, чем проверять потом его состояние.
>Разве что, «джуниоры». Остальные разработчики проверяют булево состояние потока вначале (необязательно) и при каждой операции чтения:
>необязательно
А как же выбросить exception на свалившееся открытие? У автора написано грамотнее.
shevmax
У вас ошибка в первом коде. CreateFileW возвращает дескриптор файла или INVALID_HADLE_VALUE = (HANDLE)-1. т.е. проверка на !fileLock не имеет смысла и тем более дальнейшее закрытие тоже.
Door
+ логика странная (зачем открывать файл и сразу же его закрывать) + ф-я возвращала bool, судя по всему, но потом была переделка на исключения. В общем, пример неудачен да ещё и с ошибками
khim
Пример как раз удачен. Типичный результат tester-driven development — я уверен, что это всё закрывало какие-то баги в багтерекре.
А вся статья, как бы, описывает: как стоило бы решить ту же проблему нормально…
madcomaker
Открыть и закрыть файл это самый быстрый и удобный способ проверить его существование. Все обертки внутри это самое и делают.
Door
я имел в виду то, что написал mickvav ниже
ColdPhoenix
не все, большинство делают запрос к ФС.
да и опять же, тут как с проверкой интернета, надо сразу делать то, что надо и корректно обрабатывать ошибки.
Quei
Открыть файл как раз самый медленный способ проверить его существование. Он отрабатывает быстро только после попадания файла в дисковый кеш.
Dmitri-D
нет. Нормальные оберкти внутри ничего такого не делают- файлы не открываете.
Под windows — используют FindFirstFileW() и stat() под unix
иначе вы расширяете проверку до «есть ли файл и можно ли его открыть текущему польвавателю»
sergey_shambir
Спасибо! Я был уж очень невнимателен, когда выделял код для публикации (позор, позор мне). В оригинале всё-таки был кастомный RAII FileHandle, который считал INVALID_HANDLE_VALUE за нулевое значение и корректно вызывал CloseHandle в остальных случаях. Я хотел убрать этот RAII чтобы не запутывать.
Исправил в статье.