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

Почему потоки стандартной библиотеки C++ используют семантику 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. Мне кажется, что контроль продолжительности открытого файла с помощью времени жизни дескриптора вовсе не бремя, а преимущество в плане безопасности.


person Jeff    schedule 02.09.2014    source источник
comment
Да, определенно отсутствует значение режима throw_exception. Можно установить исключения для более поздних операций, но лучше использовать конструктор бросков.   -  person BЈовић    schedule 03.09.2014


Ответы (5)


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

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

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

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

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

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

Н.Б. RAII не означает, что вы не можете иметь отдельный элемент open() в дополнение к конструктору, получающему ресурсы, или вы не можете очистить ресурс перед уничтожением, например. unique_ptr есть reset() член.

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

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

В прошлом я решал эту проблему с помощью класса CheckedFstream, который представляет собой простую оболочку, добавляющую единственную функцию: добавление коснтруктора, если поток не может быть открыт. В С++ 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); }
};
person Jonathan Wakely    schedule 02.09.2014
comment
интересный факт о том, почему исключения не являются дефолтными. - person bolov; 02.09.2014
comment
Да, "не" было опечаткой, теперь исправлено. Кроме того, до сих пор я предполагал, что одноэтапная инициализация является основной частью философии RAII, а не просто дополнительным подходом к проектированию. - person Jeff; 02.09.2014
comment
Двухэтапная инициализация не обязательна для fstreams, вы можете вызвать конструктор, и вам не нужно проверять is_open(), вы можете просто начать писать в него. Это потерпит неудачу (и установит failbit, а может быть и выкинет, в зависимости от маски исключения), если файл не был открыт. Таким образом, вы можете использовать его в обычном одноэтапном режиме инициализации, и при необходимости он будет очищаться при уничтожении. Это действительная форма RAII ИМХО. - person Jonathan Wakely; 02.09.2014
comment
@JonathanWakely На самом деле я уже использую пару оберток, практически неотличимых от этого. Однако у меня есть вопрос по поводу вашего проверенного комментария close(): действительно ли возможно выполнить что-либо значимое для ofstream в этот момент или что-то, что не лучше обслужить, предварительно проверив flush? На самом деле я никогда не видел, чтобы кто-то проверял close(), и задавался вопросом, является ли это просто распространенной апатией или более глубокой сложностью. - person Jeff; 02.09.2014
comment
Кроме того, что касается двухэтапной инициализации: объект двухэтапной инициализации заключается в том, что он может оставить объект в непригодном для использования состоянии, что необходимо проверить. Это возражение не относится к IO (и к интеллектуальным указателям, которые поддерживают нулевые значения указателя), потому что они могут стать непригодными для использования в любой момент времени, даже после успешного создания. Так что в любом случае нужно пробовать везде. - person James Kanze; 02.09.2014
comment
Я не уверен, что существующий код имел к этому какое-то отношение. Стандарт сломал очень много существующего кода потоков; Я сомневаюсь, что добавление исключения кого-то обеспокоило. - person James Kanze; 02.09.2014

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

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

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

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

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

person bolov    schedule 02.09.2014
comment
Маскировка std::ios::exceptions() допускает неконтролируемое распространение ошибок, как отметил Петр. - person Jeff; 02.09.2014
comment
@Jeff Я интерпретировал вопрос больше в том смысле, почему вам разрешено явно открывать / закрывать поток в любое время; а почему тогда проверка ошибок а не исключений. - person bolov; 02.09.2014
comment
Это больше касается открытых/закрытых аспектов, и я думаю, что я все еще не согласен с вашими утверждениями больше/ни меньше. Выполнение объявления/открытия, затем проверка или объявление, маска исключения, а затем открытие более неуклюже, чем создание неудачного экземпляра. Я также задаюсь вопросом, действительно ли возможность перенаправить дескриптор файла что-то покупает. Затраты на создание/уничтожение дескриптора пользовательского пространства ничтожны по сравнению с базовыми системными вызовами, а descoping==closed предотвращает небольшой класс глупых ошибок доступа. - person Jeff; 02.09.2014
comment
@Jeff есть случаи, когда открытие обработчика файлов не позволяет другим процессам открывать этот файл. Возможность закрыть дескриптор в любое время, а не ждать уничтожения объекта, имеет решающее значение. Насчет перенаправления согласен, не большой плюс, но все же есть возможность. - person bolov; 02.09.2014

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

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)
{
}
person Piotr Skotnicki    schedule 02.09.2014
comment
Это очень хороший момент, но он по-прежнему не связывает маскирование исключений с созданием экземпляра и оставляет открытой возможность доступа к неоткрытым файлам, которые должны быть перехвачены во время выполнения. - person Jeff; 02.09.2014
comment
@Jeff: кажется, я вообще не понимаю вашего вопроса, закрытие недопустимо открытого потока не вызывает проблем: если операция завершается сбоем (в том числе, если перед вызовом не было открыто ни одного файла), для потока устанавливается флаг состояния failbit. - person Piotr Skotnicki; 02.09.2014
comment
Извините, я имел в виду, что доступ к файловому потоку никогда не будет работать до открытия после конструкции закрытия или без открытия, но что статус потока (или включенное исключение) должен будет вызвать ошибку времени выполнения вместо использования переменной области действия. время жизни, чтобы предотвратить это во время компиляции. - person Jeff; 02.09.2014

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

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

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

person James Kanze    schedule 02.09.2014

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

person Changming Sun    schedule 20.09.2016