所以,我一直在做一些图书馆开发并陷入两难境地。图书馆是私人的,所以我无法分享,但我觉得这可能是一个有意义的问题。
这种困境本身就是为什么库中的资源处理类没有默认构造函数的问题。该类处理特定的文件结构,这并不重要,但我们可以调用类Quake3File。
然后请求实现默认构造函数和“适当的”Open / Close方法。我的思路是RAII风格,即如果你创建了一个类的实例化,你必须给它一个它处理的资源。这样做可以确保任何和所有成功构建的句柄有效,并且IMO可以消除一整类错误。
我的建议是保持一个(智能)指针然后不必实现打开/关闭并打开一罐蠕虫,用户在免费商店创建类以“打开”它并删除是你想“关闭”它。当它超出范围时,使用智能指针甚至会“关闭”它。
这就是冲突的来源,我喜欢模仿类的STL设计,因为这使我的类更容易使用。因为我正在创建一个基本上处理文件的类,如果我以std :: fstream为指导,那么我不确定是否应该实现默认构造函数。整个std :: fstream层次结构的事实让我看到了Yes,但我自己的想法是No。
所以问题或多或少都有:
希望我的问题得到理解。感谢。
答案 0 :(得分:5)
可以说有两类RAII课程:"总是有效的"和"可能是空的"类。由于我在此解释的几个原因,标准库(或Boost等近标准库)中的大多数类属于后一类。通过"始终有效",我的意思是必须构造成有效状态的类,然后在销毁之前保持有效。并且通过"可能是空的",我的意思是可以在无效(或空)状态下构造的类,或者在某些时候变为无效(或空)。在这两种情况下,RAII原则仍然存在,即类处理资源并实现对其的自动管理,因为它在销毁时释放资源。因此,从用户的角度来看,他们都享有对泄漏资源的相同保护。但是有一些关键的区别。
首先要考虑的是,我能想到的几乎所有资源的一个关键方面是资源获取总是会失败。例如,您可能无法打开文件,无法分配内存,无法建立连接,无法为资源创建上下文等等。因此,您需要一种方法来处理此潜在的故障。在"始终有效" RAII类,你别无选择,只能通过从构造函数中抛出异常来报告失败。在"可能是空的"在类中,您可以选择通过将对象保留为空状态来报告该失败,或者可以抛出异常。这可能是IO流库使用该模式的主要原因之一,因为他们决定在其类中抛出异常抛出一个可选特性(可能是因为许多人对过多使用异常保持沉默)。
要考虑的第二件事是"始终有效"班级不能成为可移动的班级。将资源从一个对象移动到另一个对象意味着使源对象"为空"。这意味着"始终有效" class必须是不可复制的和不可移动的,这可能会给用户带来一些麻烦,也可能限制你自己提供易于使用的界面的能力(例如,工厂功能等) 。这还需要用户在需要移动对象时在freestore上分配对象。
(适用EDIT)强> 如下面的DyP所指出的,你可以拥有一个永远有效的"可移动的类,只要您可以将对象置于可破坏状态。换句话说,对象的任何其他后续使用都是UB,只有破坏才会表现良好。然而,仍然存在一个强制执行"始终有效"资源的灵活性会降低,给用户带来一些烦恼。 (结束编辑)
显然,正如你所指出的,"始终有效"通常,类的实现会更加简单,因为您不需要考虑资源为空的情况。换句话说,当你实现一个"也许是空的"在每个成员函数中,您必须检查资源是否有效(例如,文件是否打开)。但请记住,"易于实施"不是指定特定界面选择的有效理由,界面面向用户。但是,对于用户方面的代码,这个问题也是如此。当用户处理"可能为空时"对象,他总是要检查有效性,这可能会变得麻烦和容易出错。
另一方面,"始终有效" class必须完全依赖异常机制来报告其错误(即错误条件不会因为"总是有效的")的假设而消失,因此可能会在其实现中带来一些有趣的挑战。通常,您必须为涉及该类的所有函数提供强大的异常安全保证,包括实现代码和用户端代码。例如,如果您假定对象始终有效且#34;,并且您尝试失败的操作(如读取超出文件末尾),则需要回滚该操作并带来对象回到原来的有效状态,强制执行你永远有效的"假定。通常,用户在相关时将被迫做同样的事情。这可能与您正在处理的资源类型兼容也可能不兼容。
(适用EDIT)强> 正如下面的DyP所指出的那样,这两种类型的RAII类之间存在灰色阴影。因此,请注意,此解释描述了两个极端对立或两个一般分类。我并不是说这是一个黑白分明。显然,许多资源都有不同程度的有效性和#34; (例如,无效的文件处理程序可能处于"未打开状态或#34;达到文件结束状态"状态,可以以不同方式处理,即像& #34;总是打开","可能在EOF",文件处理程序类)。 (结束编辑)
资源句柄是否真的有默认构造函数?
RAII类的默认构造函数通常被理解为将对象创建为"空"状态,意味着它们仅对"可能是空的"的实施方式。
在处理文件的类上实现默认构造函数有什么好方法?只是将内部状态设置为无效状态,如果用户通过不给它资源而错过它,会导致未定义的行为吗?想要沿着这条路走下去似乎很奇怪。
我遇到过的大多数资源都有一种自然的表达方式来表达空虚"或者"无效",无论是空指针,空文件句柄,还是仅标记状态为有效的标志。所以,这很容易。但是,这并不意味着滥用课程应该触发"未定义的行为"。设计一个这样的课程是绝对可怕的。正如我之前所说,可能会出现错误情况,并使课程“始终有效”。不会改变这个事实,只会改变你处理它们的方式。在这两种情况下,您都必须检查错误情况并报告它们,并完全指定您的类的行为以防它们发生。您不能只是说"如果出现问题,代码有未定义的行为"",您必须在出现错误情况时指定您的类的行为(以某种方式)周期。
为什么STL使用默认构造函数实现fstream层次结构?遗产原因?
首先,IO流库不是STL(标准模板库)的一部分,但这是一个常见的错误。无论如何,如果您阅读上面的解释,您可能会理解为什么IO流库选择以它的方式执行操作。我认为它基本上归结为避免异常作为其实现的必要的基本机制。他们允许例外作为一种选择,但不要强制执行,我认为这对许多人来说一定是必须的,特别是在它写完之后,可能还是今天。
答案 1 :(得分:4)
我认为每个案例都应该单独考虑,但对于文件类我肯定会考虑引入“无效状态”,如“文件无法打开”(或“无文件”)附加到包装处理程序类“)。
例如,如果您没有此“无效文件”状态,则将强制文件加载方法或函数为无法打开文件的情况引发异常。我不喜欢这样,因为调用者应该在文件加载代码周围使用大量的try/catch
包装器,而是一个很好的布尔检查就可以了。
// *** I don't like this: ***
try
{
File f1 = loadFile("foo1");
}
catch(FileException& e)
{
...handle load failure, e.g. use some defaults for f1
}
doSomething();
try
{
File f2 = loadFile("foo2");
}
catch(FileException& e)
{
...handle load failure for f2
}
我更喜欢这种风格:
File f1 = loadFile("foo");
if (! f1.valid())
... handle load failure, e.g. use some default settings for f1...
doSomething();
File f2 = loadFile("foo2");
if (! f2.valid())
... handle load failure
此外,使File
类可移动也是有意义的(因此您也可以将File
个实例放入容器中,例如std::vector<File>
),在这种情况下,移动文件实例必须具有“无效”状态。
因此,对于File
类,我会考虑引入无效状态。
我还为原始资源编写了一个RAII模板包装器,我也在那里实现了一个无效状态。同样,这也可以正确地实现移动语义。
答案 2 :(得分:1)
至少IMO,你对这个问题的看法可能比iostream中显示的更好。就个人而言,如果我今天从头开始创建iostreams的模拟,它可能将没有默认的ctor并且单独open
。当我使用fstream时,我几乎总是将文件名传递给ctor而不是默认构造,然后使用open
。
对于像这样的类具有默认ctor的几乎唯一的一点是,它使得将它们更容易地放入集合中。通过移动语义和放置对象的能力,这变得不那么引人注目了。它从来没有真正必要,现在几乎无关紧要。