cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
89 stars 0 forks source link

std::expected должен позволять выкидывать неизменённое пользовательское исключение #509

Open apolukhin opened 2 years ago

apolukhin commented 2 years ago

Нужна возможность кастомизации выкидывания исключения. Обсуждение доступно по ссылке https://habr.com/ru/company/yandex/blog/649497/#comment_24051655

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

struct throw_bad_expected_acess {
    template <class E>
    [[noreturn]] void operator()(E&& e) const {
        throw bad_expected_access(std::forward<E>(e))
    }
};

template<class T, class E, class C = throw_bad_expected_acess> class expected;
AndreyG commented 2 years ago

На всякий случай продублирую тут: если вставать на путь кастомизации выкидывания исключения, то логично обработать стандартные варианты E -- std::error_code и std::exception_ptr.

template<class E>
struct on_error_policy {
    [[noreturn]] void operator()(E&& e) const {
        throw bad_expected_access(std::forward<E>(e));
    }
};

template<>
struct on_error_policy<std::error_code> {
    [[noreturn]] void operator()(std::error_code ec) const {
        throw std::system_error(ec);
    }
};

template<>
struct on_error_policy<std::exception_ptr> {
    [[noreturn]] void operator()(std::exception_ptr e) const {
        std::rethrow_exception(std::move(e));
    }
};

template<class T, class E, class C = on_error_policy<E>>
class expected;
alexeysidorov92 commented 2 years ago

С одной стороны, поддерживаю. Хотелось бы иметь возможность кастомизации.

Попробую ещё один простой пример вкинуть, хотя это частный случай unify local and centralized error handling, на который ссылались на Хабре.

Нередко исключения ловятся в одном единственном блоке catch как const std::exception&, в котором просто логируется сообщение об ошибке. Рассмотрим на примере std::filesystem::file_size:

try
{
    const auto size = fs::file_size("C:\\file.txt");

    if (size > 100)
    {
       // code ... 
    }
    else
    {
       // code ... 
    }
}
catch (const std::exception& ex)
{
    std::cerr << ex.what();
}

И если fs::file_size выбросит исключение, то в логах будет актуальная информация (например: "file does not exist: C:\file.txt").

Допустим, что fs::file_size теперь возвращает std::expected<std::size_t, std::error_code>. Если мы просто исправим обращение к size на size.value(), то в случае ошибки потеряем в логах информацию о её причинах. Там будет записано что-то вроде "bad expected access".

Тогда нам нужно будет либо добавить ещё один блок catch:

catch (const std::bad_expected_access<std::error_code>& ex)
{
    std::cerr << ex.error().message();
}

Либо обрабатывать ошибку по месту:

const auto size = fs::file_size("file.txt");

if (!size)
{
    std::cerr << size.error().message();
    return;
}

А если в функции несколько разных std::expected встретятся, то это вообще жуть будет :)

Поэтому хочется, чтобы при обращении к value() вылетало "нормальное" исключение.

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

Может, с целью минимизации энтропии попробовать вместо одного std::expected рассмотреть три? :D

Названия условные:

// Версия expected, что мы здесь обсуждаем
template<class T, class E, class C> 
class customized_expected;

// Версия expected, что предлагается в стандарт - продвинутый optional
template<class T, class E>
using value_or_error = customized_expected<T, E, throw_bad_value_access>

// Версия, что изначально предлагал Александреску. Можно даже добавить дополнительное ограничение в виде `std::derived_from<E, std::exception>`
template<class T, class E>
using expected = customized_expected<T, E, throw_exception>

Те кто хочет легковесный продвинутый optional, будут использовать value_or_error. Тот кто хочет исключения - expected.

Да и пользователям, думаю, будет чуть понятнее, какие исключения в каком случае ловить...

kelbon commented 2 years ago

Поэтому хочется, чтобы при обращении к value() вылетало "нормальное" исключение.

Мне кажется идея std::expected была как раз в том, чтобы не бросать исключение. Ну то есть если у тебя есть некая операция, возвращающая либо значение, либо исключение, то ты уже и так можешь сделать блок catch и забирать просто по значению внутри. Зачем делать промежуточное звено в виде expected в такой ситуации? Это имеет смысл разве что в многопоточной системе и какой то фьюче. А как некая вещь объединяющая error code и значение вроде бы неплохо получилось, использование предполагается типа auto v = Foo(); if (v) do(*v); Как собственно это с еррор кодом происходит, но его неудобно передавать, создавать и можно забыть обработать, в отличие от std::expected