Stroustrup RAII и оператор-оператор FILE *() = противоречие?
Я читал через Stroustrups С++ (3ed, 1997), чтобы увидеть, как он реализовал RAII, и на странице 365 я нашел это:
class File_ptr{
FILE* p;
public:
File_ptr(const char* n, const char* a){p = fopen(n, a);}
File_ptr(FILE* pp) { p = pp; }
~File_ptr() {fclose(p);}
operator FILE* () {return p;}
};
Реализация конструкторов и деструкторов очевидна и соответствует идиоме RAII, но я не понимаю, почему он использует operator FILE* () {return p;}
.
Это приведет к использованию File_ptr
следующим образом:
FILE* p = File_ptr("myfile.txt", "r");
Результат в закрытом p
, который семантически неуместен в этом случае. Кроме того, если File_ptr
предназначен для использования как RAII, этот оператор позволяет использовать его неправильно, как в примере. Или я что-то упускаю?
Ответы
Ответ 1
Похоже, это неизбежная злая цена за комфорт. Как только вы захотите, чтобы путь FILE*
был извлечен из вашего фантастического класса RAII, его можно использовать неправильно. Будет ли это метод operator FILE*()
или FILE* getRawPtr()
или что-то еще, он может быть вызван на объект temporarty, что делает результат недействительным сразу после его возврата.
В С++ 11, однако, вы можете сделать это немного более защищенным, запретив этот вызов во временном режиме, например:
operator FILE* () & { return p; }
// Note this -----^
Полезная ссылка о том, как это работает Morwenn в комментариях: Что такое " rvalue reference for * это & Quot;?
Ответ 2
Мышление довольно сильно изменилось с 1997 года в результате опыта, и одна из основных рекомендаций теперь состоит в том, чтобы не иметь неявных операторов-операторов из-за таких проблем. Ранее считалось, что лучше иметь неявный оператор преобразования, чтобы упростить дооснащение существующего кода, но это привело к проблемам, когда функция уничтожает ресурс, поскольку класс-оболочка RAII не знает об этом.
Современное соглашение - предоставить доступ к основному указателю, но дать ему имя, чтобы он, по крайней мере, был явным. Он не поймает все возможные проблемы, но облегчит борьбу за возможные нарушения. Например, с std::string
it c_str()
:
std::string myString("hello");
callFunctionTakingACharPointer(myString.c_str());
// however...
delete myString.c_str(); // there no way of preventing this
Ответ 3
Я не понимаю, почему он использует оператор FILE *() {return p;}.
Причиной оператора является предоставление доступа/совместимости для API, которые используют FILE *. Проблема с реализацией заключается в том, что он позволяет клиентскому коду аналогично тому, что вы дали в качестве примера.
Это приведет к использованию File_ptr следующим образом:
FILE* p = File_ptr("myfile.txt", "r");
Нет. Хотя вы можете это сделать, определение класса не приводит к подобному коду. Вы должны избегать написания такого кода (так как обычно вам надлежит писать код, который позволяет избежать проблем управления временем жизни).
Пример RAII в вашем вопросе - пример плохого дизайна. Оператор преобразования можно было бы избежать.
Я бы заменил его на аксессуар FILE *const File_ptr::get() const
. Это изменение не устраняет проблему, но упрощает просмотр в клиентском коде, что вы возвращаете указатель const (т.е. "ClientCode, не удаляйте это" ) из класса.
Ответ 4
что не соответствует правилу 3 (не говоря уже о 5),
поэтому объявление функции как Bar* createBarFromFile(File_ptr ptr)
приведет к неожиданным вещам (файл будет закрыт после вызова этой функции)
ему нужно определить конструктор копирования и конструктор назначения копирования. для правила 5 ему также нужны варианты перемещения
однако, если я вынужден использовать поля FILE*
как член, я предпочитаю использовать std::unique_ptr<FILE, int (__cdecl *)(FILE *)>
и передать &fclose
в конструкторе