我正在阅读Stroustrup的C ++(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,则此运算符允许它像示例中那样被滥用。或者我错过了什么?
答案 0 :(得分:28)
FILE*
可以从你喜欢的RAII类中提取,它就会被误用。它是operator FILE*()
还是FILE* getRawPtr()
方法,或者其他什么,它可以在temporarty对象上调用,使得结果在返回后无效。
但是,在C ++ 11中,你可以通过禁止对临时对象的这种调用来使这更加安全,如下所示:
operator FILE* () & { return p; }
// Note this -----^
有关Morwenn在评论中提供的工作方式的有用链接:What is "rvalue reference for *this"?
答案 1 :(得分:7)
由于经验的缘故,思维从1997年起已经发生了很大变化,现在的一个主要建议是不要因为这样的问题而使用隐式转换运算符。以前认为有一个隐式转换运算符可以更容易地改进现有代码,但是当函数破坏资源时会导致问题,因为RAII包装类不知道它。
现代约定是提供对底层指针的访问,但是给它一个名称,以便至少它是显式的。它不会捕捉到所有可能的问题,但可以更容易地查找潜在的违规行为。例如std::string
c_str()
:
std::string myString("hello");
callFunctionTakingACharPointer(myString.c_str());
// however...
delete myString.c_str(); // there's no way of preventing this
答案 2 :(得分:4)
我不明白为什么他使用运算符FILE *(){return p;}。
运营商的原因是为使用FILE *的API提供访问/兼容性。实现的问题在于它允许客户端代码与您给出的代码类似。
这将导致以下列方式使用File_ptr:
FILE* p = File_ptr("myfile.txt", "r");
实际上没有。当可以执行此操作时,类定义不会产生这样的代码。你应该避免编写这种代码(正如你通常需要编写代码来避免生命周期管理问题一样)。
您问题中的RAII示例是设计不佳的示例。可以避免使用转换运算符。
我会用FILE *const File_ptr::get() const
访问者替换它。这种改变并没有消除这个问题,但是它更容易在客户端代码中看到你从类中返回一个const指针(即“ClientCode,请不要删除它”)。
答案 3 :(得分:4)
不遵循rule of 3(更不用说5),
因此将函数声明为Bar* createBarFromFile(File_ptr ptr)
会发生意外情况(调用此函数后文件将被关闭)
它需要定义一个拷贝构造函数和一个拷贝赋值构造函数。对于规则5,它还需要移动变体
但是如果我被迫使用FILE*
作为成员字段,我更喜欢使用std::unique_ptr<FILE, int (__cdecl *)(FILE *)>
并在构造函数中传递&fclose