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

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, этот оператор позволяет использовать его неправильно, как в примере. Или я что-то упускаю?

4b9b3361

Ответ 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 в конструкторе