cpp-ru / ideas

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

Переработать логику исключения std::ios_base::failure #258

Open apolukhin opened 3 years ago

apolukhin commented 3 years ago

Перенос предложения: голоса +4, -1 Автор идеи: pavel.darashkevich

Возвращать исходную ошибку в исключении. Не бросать исключение в коде, который корректен (код ниже), если не включать исключения. Добавить возвращение кода ошибки последним аргументом в функциях std::ifstream/std::ofstream и им подобным.

Пишем простую программу, которая считывает все числа из файла (файл состоит из чисел типа int, разделённых пробелами), после этого выводим сумму.

#include <iostream>
#include <vector>
#include <fstream>
#include <numeric>
#include <iterator>

int main(const int argc, const char* argv[]) {
    if (argc == 1) {
        std::cerr << "Usage: program path_to_file" << std::endl;
        return EXIT_FAILURE;
    }

    const std::string path { argv[1] };

    std::ifstream in(path);
    const auto numbers = std::vector<int>(
        std::istream_iterator<int>(in),
        std::istream_iterator<int>()
    );

    std::cout << std::accumulate(numbers.begin(), numbers.end(), 0LL);

    return EXIT_SUCCESS;
}

Код короткий, достаточно понятный. А теперь подумаем: что будет, если файла нет или во время чтения достанут флешку, на которой этот файл записан? Хотелось бы как-то обрабатывать эти ситуации.

Мы-то знаем, что std::ifstream может бросать исключение при ошибках. Перепишем:

std::ifstream in;
in.exceptions(std::ios_base::failbit);
in.open(path);

И сразу же замечаем 2 плохие вещи:

  1. Исключение std::ios_base::failure содержит мало информации об ошибке:
    terminate called after throwing an instance of 'std::ios_base::failure'
    what():  basic_ios::clear: iostream error

Что можно понять из этого сообщения? Только то, что что-то пошло не так при работе с потоками, а что именно (проблема с файлом или с его содержимым) -- не понятно. Более того, в большой программе это вообще не информативный вывод, хуже не придумаешь.

  1. Даже если файл существует, содержит только числа, всё равно кидается исключение после того, как достигается конец потока (кроме eofbit выставляется failbit, что логично). Как побороть это, не написав кучу некрасивого кода? Отказываться от исключений и писать так?
#include <iostream>
#include <vector>
#include <fstream>
#include <numeric>
#include <iterator>
#include <string>

int main(const int argc, const char* argv[]) {
    if (argc == 1) {
        std::cerr << "Usage: program path_to_file" << std::endl;
        return EXIT_FAILURE;
    }

    const std::string path { argv[1] };

    std::ifstream in;
    in.open(path);

    if (in.fail()) {
        perror("Can't open file");
        return EXIT_FAILURE;
    }

    std::vector<int> numbers;

    while (!in.eof()) {
        int number;
        in >> number;

        if (in.eof()) {
            break;
        }

        if (in.fail()) {
            if (errno) {
                perror("Error while reading from file");
            } else {
                std::cerr << "Invalid file format! Value out of `int` domain or not number." << std::endl;
            }

            return EXIT_FAILURE;
        }

        numbers.push_back(number);
    }

    std::cout << std::accumulate(numbers.begin(), numbers.end(), 0LL);
    return EXIT_SUCCESS;
}

Что-то длинно получилось... При этом я не учёл то, что на Windows ошибки правильнее смотреть через GetLastError() и выводить по-другому. Что хотелось бы написать?

#include <iostream>
#include <vector>
#include <fstream>
#include <numeric>
#include <iterator>
#include <string>

int main(int argc, char** argv) {
    if (argc == 1) {
        std::cerr << "Usage: program path_to_file" << std::endl;
        return EXIT_FAILURE;
    }

    try {
        const std::string path { argv[1] };

        std::ifstream in;
        in.exceptions(std::ios_base::failbit);
        in.open(path);

        const std::vector<int> numbers {
            std::istream_iterator<int>(in),
            std::istream_iterator<int>()
        };

        std::cout << std::accumulate(numbers.begin(), numbers.end(), 0LL);
    } catch (const std::ios_base::failure& e) {
        std::cerr << "Error while processing file: "
                  << "Action: " << e.action().name() // See below
                  << "Reason: " << e.code().message() // Invalid format or system error
                  << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Коротко и очень информативно, если возвращается соответствующая ошибка.

Итого, что предлагается?

  1. Улучшить качество отдаваемых исключений:

1.1. Возвращать ошибку, код которой соответствует исходной (файл не найден и т.п.). При ошибке чтения из-за неверного формата -- тоже соответствующая ошибка (новую категорию parse_error {domain, format}?). И только в том случае, если код ошибки получить невозможно -- текущий вариант.

1.2. Также вместо бессмысленного basic_ios::clear тогда уже писать имя функции, которую пользователь вызвал. А ещё лучше -- завести enum (или что-то более хитрое) и его тоже возвращать:

enum stream_action {
    open,
    close,
    read,
    write,
    seek,
    // etc
};

Зачем? Чтобы можно было не только вывести хорошее сообщение об ошибке после отлавливания в любом месте программы, но и реализовать какую-то логику на основе этого.

1.3. Может подумать над тем, чтобы в исключении ещё было имя файла? Не думал, технически реализуемо ли это без накладных расходов, но всё же.

  1. При использовании конструкции
const std::vector<int> numbers (
    std::istream_iterator<int>(in),
    std::istream_iterator<int>()
);

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

Как это исправить? Сделать функцию (название и вид можно придумать лучше):

template <typename T>
bool try_read(T& var);

template <typename T>
bool try_read(T& var, std::error_code& code); // чтобы узнать, eof или что-то другое, хотя может быть лишнее, тут можно подумать.

И использовать её внутри данного итератора. Да и кроме него она, думаю, будет полезной.

  1. В std::ifstream сделать функции с последним аргументом типа std::error_code для его получения, как сделано в std::filesystem.
void open(const std::string& path, std::error_code& code);
void close(std::error_code& error);
// etc

Что-то очень объёмно получилось... Даже не знаю, как это лучше было разделить, поэтому написал как есть. Надеюсь, среди этого наболевшего есть хоть что-то полезное ;).

Спасибо.