Обновить

std::expected в C++23: гайд по миграции с исключений на функциональный error handling

Уровень сложностиСредний
Время на прочтение12 мин
Охват и читатели17K
Всего голосов 20: ↑20 и ↓0+30
Комментарии13

Комментарии 13

Нет оператора ? , поэтому вложенные цепочки быстро превращаются в лапшу из лямбд.

Но вообще хорошо язык пошёл развиваться

Отсутствие ? можно компенсировать наличием корутин в языке. Один из примеров того, как это можно реализовать: https://gist.github.com/jemand2001/adb0cd09460bdcc44ca50e346eb7599c

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

Цепочку лямбд неудобно отлаживать, и выглядит такой код не очевидно, и места они занимают много.

Ну это несколько странная телега. То что вы можете писать лямбды не требует от вас это делать - пишите обычные функции. Ну и отлаживать код с исключениями не сказать чтобы сильно проще, особенно в многопотоке.

В этом смысле сишники давно изобрели errno

оно на маленьком коде может и хорошо, но иметь некоторое глобальное состояние да ещё и в многопотоке, довольно заметный минус для оптимизатора - появляются неоптимизируемые индирекции. Ну и как в си гадать от кого из стека прилетело -1 вместо того чтобы явно видеть DbError::AccessDenied имеет свои следствия для безопасности финального кода.

Исключения дороги по производительности на throw‑path, требуют exception‑safe‑кода во всей цепочке вызовов, плохо взаимодействуют с многопоточностью

Не устану повторять, что в С++ принципы обеспечения exception safety прекрасно работают и для обычных return-ов. Поэтому если разработчик озадачился вопросами exception safety, то нет разницы, сообщается ли об ошибке через exception или через early return.

Спасибо за честную оценку ситуации, когда ошибка - исключительное явление, а не регулярное событие: ¨Для функций, которые вызываются миллион раз в секунду и почти всегда успешны, исключения дешевле (бесплатны на happy path).¨ Это надо учесть всем, кто задумывается о переходе на std::expected.

А будет режим, когда код не скомпилируется, пока не учтёшь возможные ошибки? Я так понимаю, в Rust это главная фишка. А то, вот это:

value() бросает исключение std::bad_expected_access если в expected лежит ошибка

Всю идею обесценивает. Такого быть просто не должно, компилятор должен за этим следить. Или, хотя бы, #pragma с форсом такого поведения добавить.

По идее, если всем этим функциям добавлять аттрибут [[nodiscard]] , то проигнорировать ошибку будет сложнее. С другой стороны, там же value() берётся, а на ошибку проверить могут и забыть...

В Rust .unwrap() на ошибке тоже паникует. Думаю, статические анализаторы смогут отловить отсутствие проверки перед вызовом. @Andrey2008?

Я извиняюсь, но разве в первом примере не надо добавить noexcept?

std::expected<int, ParseError> parse_positive_int(const std::string& input) noexcept {
    //...

noexcept по-большому счёту несёт справочных характер для компилятора. Если бы там гнались за перформансом, то по-хорошему стоило бы указать, но статья не про это и по большому счёту принципиальной разницы это не добавляет.

Хорошая попытка, но нет.

Почти идеальная реализация ошибок могла бы выглядеть примерно так, через structured binding (или destructuring, кому как нравится):

do { 
    auto [result, error] = TheService::callService(params);
    switch (error) { 
        case TheService::ErrorCodes::TIMEOUT_REACHED: 
            System::fatal("TheService Not Available"); 
        case TheService::ErrorCodes::RETRY: 
            System::sleep(TheService::DEFAULT_EXT_CALL_TIMEOUT); 
            break; 
        case TheService::ErrorCodes::OK: 
            return result;
    }    
} while (true);        

Код условный, реальный чуть посложнее будет. Но ключевой момент тут показан: TheService::ErrorCodes это enum c конечным числом значений. И если функция TheService::callService начинает еще какой-то четвертый тип ошибок возвращать (добавили еще одно значение в enum), то компилятор нам сразу высветит все switch места, где нет case обработки этого нового типа ошибки, и их нужно срочно пойти дописать.

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

И что писать в result , если произошла ошибка? Опять какое-нибудь optional. И чем, тогда, это лучше простого expected? С ним ты точно так же можешь сделать, если ошибка или её часть - enum.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS