Подтвердить что ты не робот

Почему стандартные потоки файлов С++ более не соответствуют соглашениям RAII?

Почему в потоках стандартной библиотеки С++ используется семантика open()/close(), отделяемая от времени жизни объекта? Закрытие при уничтожении может по-прежнему технически создавать классы RAII, но независимость приобретения/освобождения оставляет дыры в областях, где дескрипторы могут указывать на ничего, но все же требуют проверки времени выполнения, чтобы поймать.

Почему дизайнеры библиотеки выбрали свой подход по поводу открытия только в конструкторах, которые бросают неудачу?

void foo() {
  std::ofstream ofs;
  ofs << "Can't do this!\n"; // XXX
  ofs.open("foo.txt");

  // Safe access requires explicit checking after open().
  if (ofs) {
    // Other calls still need checks but must be shielded by an initial one.
  }

  ofs.close();
  ofs << "Whoops!\n"; // XXX
}

// This approach would seem better IMO:
void bar() {
  std_raii::ofstream ofs("foo.txt"); // throw on failure and catch wherever
  // do whatever, then close ofs on destruction ...
}

Лучшая формулировка вопроса может заключаться в том, что доступ к незакрытому fstream когда-либо стоит иметь. Контроль продолжительности открытого файла с использованием продолжительности работы с рукояткой, по-видимому, не является бременем, но на самом деле является преимуществом безопасности.

4b9b3361

Ответ 1

Хотя другие ответы действительны и полезны, я думаю, что настоящая причина проще.

Дизайн iostreams намного старше, чем большая часть стандартной библиотеки, и предшествует широкому использованию исключений. Я подозреваю, что для совместимости с существующим кодом использование исключений было сделано необязательным, а не по умолчанию для отказа открыть файл.

Кроме того, ваш вопрос действительно имеет отношение к потокам файлов, другие типы стандартного потока не имеют функций-членов open() или close(), поэтому их конструкторы не бросают, если файл не открывается: -)

Для файлов вам может потребоваться проверить, что вызов close() преуспел, поэтому вы знаете, что данные были записаны на диск, так что хорошая причина не сделать это в деструкторе, потому что к моменту уничтожения объекта слишком поздно делать что-нибудь полезное с ним, и вы почти наверняка не хотите бросать исключение из деструктора. Таким образом, fstreambuf вызовет close в своем деструкторе, но вы также можете сделать это вручную до уничтожения, если хотите.

В любом случае я не согласен с тем, что он не соответствует соглашениям RAII...

Почему дизайнеры библиотеки выбрали свой подход по поводу открытия только в конструкторах, которые бросают неудачу?

N.B. RAII не означает, что вы не можете иметь отдельного члена open() в дополнение к конструктору-приобретению ресурса, или вы не можете очистить ресурс до уничтожения, например. unique_ptr имеет член reset().

Кроме того, RAII не означает, что вы должны отказаться от отказа или объект не может находиться в пустом состоянии, например. unique_ptr может быть сконструирован с нулевым указателем или построенным по умолчанию, и поэтому также может указывать на ничего, и поэтому в некоторых случаях вам нужно проверить его перед разыменованием.

Файловые потоки приобретают ресурс по строительству и освобождают его при уничтожении - это RAII, насколько мне известно. То, против чего вы возражаете, требует проверки, которая пахнет двухступенчатой ​​инициализацией, и я согласен, что это немного вонючий. Однако это не делает RAII.

В прошлом я решил запах с классом CheckedFstream, который представляет собой простую оболочку, которая добавляет одну особенность: бросание в cosntructor, если поток не может быть открыт. В С++ 11 это просто:

struct CheckedFstream : std::fstream
{
  CheckedFstream() = default;

  CheckedFstream(std::string const& path, std::ios::openmode m = std::ios::in|std::ios::out)
  : fstream(path, m)
  { if (!is_open()) throw std::ios::failure("Could not open " + path); }
};

Ответ 2

Таким образом вы получаете больше и ничего меньше.

  • Вы получаете тот же: вы все равно можете открыть файл через конструктор. Вы все равно получите RAII: он автоматически закроет файл при уничтожении объекта.

  • Вы получаете больше: вы можете использовать тот же поток для повторного открытия другого файла; вы можете закрыть файл, когда хотите, а не ограничиваться ожиданием выхода объекта из области действия или его разрушения (это очень важно).

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

Как о проверке ошибок и исключениях исключения, см. @PiotrSs ответ. Понятно, что я не вижу разницы между проверкой статуса возврата и ошибкой. Ошибка все еще существует; разница заключается в том, как вы его обнаруживаете. Но, как указано @PiotrS, вы можете выбрать оба.

Ответ 3

Дизайнеры библиотеки предоставили вам альтернативу:

std::ifstream file{};
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);

try
{
    file.open(path); // now it will throw on failure
}
catch (const std::ifstream::failure& e)
{
}

Ответ 4

Стандартные потоки файлов библиотеки предоставляют RAII в смысл, что вызов деструктора на один закроет любой файл который бывает открытым. По крайней мере, в случае выхода, однако это чрезвычайная мера, которая должна использоваться только если вы столкнулись с другой ошибкой и не собираетесь использовать файл, который записывался в любом случае. (Хорошее программирование практика должна была бы удалить его.) Как правило, вам нужно проверить статус потока после его закрытия, и это операция, которая может потерпеть неудачу, поэтому не следует делать в деструктор.

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

Что касается open: вы можете так же легко открыть конструктор, и для изолированных применений, как вы показываете, это вероятно, предпочтительным решением. Но есть случаи, когда вы может захотеть повторно использовать std::filebuf, открыть его и закрыть явно и, конечно, почти во всех случаях, вы захотите обрабатывать отказ открыть файл немедленно, а не через какое-то исключение.

Ответ 5

Это зависит от того, что вы делаете, чтения или записи. Вы можете инкапсулировать входной поток по пути RAII, но это не относится к выходным потокам. Если конечным пунктом является файл диска или сетевой сокет, НИКОГДА, НИКОГДА не добавляйте fclose/close в деструктор. Поскольку вам нужно проверить возвращаемое значение fclose, и нет способа сообщить об ошибке, произошедшей в деструкторе. см. Как я могу обработать деструктор, который не работает