【发布时间】:2016-02-29 21:10:00
【问题描述】:
我正在将现有库从 Windows 移植到 Linux。一些类包含特定于操作系统的代码。我决定不使用 Pimpl (*),所以我选择了一个简单的 DeviceBase、DeviceWin32 和 DeviceLinux 层次结构。 DeviceBase 包含基本的东西:高级逻辑、公共函数和(受保护的)虚拟 doX() 和 doY 函数(如 Herb Sutter 在他的 Virtuality 文章中所述)。
原始代码(下面的x() 代表所有具有特定平台行为的函数,例如close()、open() 等):
class Device
{
public:
Device() : _foo(42) {}
~Device() {
if (!isValid())
close();
}
bool isValid() const { return _handle != NULL && _handle != INVALID_HANDLE; }
void x() {
checkPrecondsForX();
DWORD ret = DoSomethingForWindows(&_handle);
[...]
checkPostcondsForX();
}
private:
int _foo;
HANDLE _handle;
};
新代码:
class DeviceBase
{
public:
DeviceBase() : _foo(42) {}
virtual ~DeviceBase() {
if (!isValid()) close();
}
virtual bool isValid() const = 0;
void x() {
checkPrecondsForX();
doX();
checkPostcondsForX();
}
protected:
virtual void doX() = 0;
private:
int _foo;
};
class DeviceWin32
{
public:
DeviceWin32() : DeviceBase(), _handle(NULL) {}
virtual ~DeviceWin32() {}
virtual bool isValid() const { return _handle != NULL && _handle != INVALID_HANDLE; }
protected:
void doX() {
DWORD ret = DoSomethingForWindows(&_handle);
[...]
}
private:
HANDLE _handle;
};
class DeviceLinux
{
public:
DeviceLinux() : DeviceBase(), _fd(0) {}
virtual ~DeviceLinux() {}
virtual bool isValid() const { return _fd != 0; }
protected:
void doX() {
int ret = _do_smth_posix(&_fd);
[...]
}
private:
int _fd;
};
一切都很顺利,直到我遇到了析构函数。我天真地认为,像其他功能一样,它可以开箱即用。测试很快证明我错了,实际上是因为纯虚函数调用而崩溃了。我很快就弄清楚了原因:这是完全可以预料的,记录在案并讨论了here 和elsewhere(有关更多示例,请参见本页上的相关问题列)。
我很担心,因为从概念上讲,“如果设备打开则关闭”的逻辑属于基类。实现细节是它的表示方式(HANDLE vs int)和实际关闭它的函数,因此我想看看基类中的逻辑。
但是,在我的情况下,我找不到可行的解决方案。诸如“不要这样做”之类的事情确实有助于理解出了什么问题,我发现的大多数解决方法都归结为duplicating the logic of the base destructor 或using a helper object(但后者不起作用,因为我们需要访问存储的数据在派生类中)。
有趣的是,我在构造时没有遇到同样的问题,因为对 open() 的调用不会在构造函数中发生(这会转换为使用 C++ FAQ lite 中解释的两步构造)。这是理想的,因为我们可以有一个未打开的设备,并在某个时间点打开它之前传递它。然而,设备必须在使用后关闭,即使发生异常(因此 RAII 是唯一合理的解决方案)。
(可能值得注意的是,DeviceXXX 类不是最终的(尽管它们可以按原样使用)并且确实有一个派生类 - AsyncDevice。我不知道这是否会对潜在的解决方案。)
是的,到写这个问题的时候,我本可以:
不再使用 Pimpl 成语
编写了两个完全独立的实现(在某些库中可以看到,例如William Woodall's Serial library)
在两个析构函数中复制了
isValid()检查并收工表示 RAII id 无论如何都不好,并要求用户明确地关闭()设备(毕竟,他们打开了它,所以他们应该在负责关闭它)。
在quite similar question here on SO,作者问“这是真的吗?”。现在我在问:“有没有人知道不意味着完全重新设计库(例如 Pimpl)和/或逻辑重复的解决方案”?如果这听起来像吹毛求疵,我很抱歉,但我不想错过(几乎)显而易见的事情。
(*) 这听起来可能很愚蠢 - 如果我有,我显然 不会 面临这个特定问题。不过,也许还有其他一些人。但无论如何,我只是不想浪费一个学习的机会。
更新:到目前为止,3 个答案推动了使用两个单独的类来管理不同的问题(我没有明确指出这一点 - 感谢您指出这一点)。我正在尝试这种方法,尽管我觉得管理第二次继承会稍微困难一些(AsyncDevice,现在可能需要更多工作)。我们会看到我的结局。
更新 2:我终于后退了一步,并在目前的情况下做出了我认为是明智的决定:接受代码重复,并将要求分离关注点的答案放在我的枕头下未来使用。因此DeviceWin32 和DeviceLinux 的两个析构函数非常相似——但“仅”两个实例不足以证明重构/泛化的合理性。非常感谢那些回答的人。
【问题讨论】:
-
您可能有一个包含 DeviceImplementation 的 Device,其中 DeviceImplementation 不是从 Device 继承,而是 DeviceInterface 具有默认构造函数、create 和 destroy 函数。是的,这就像一个“pimpl”(我猜在这种情况下这是一个正确的设计决策)。
-
RAII 观点:让每个(派生)对象在其析构函数中进行适当的清理,不要从基类调用(虚拟)清理代码。
-
@all:感谢您的回答/指点!一切似乎都指向(伪)pimpl 方向,所以我将沿着这条路线走,看看它如何适应更大的图景。我有点担心将我的组合设备/设备实现子类化的能力。无论如何,我会在进行建议的修改后尝试发布更新。
-
我有点惭愧。我刚刚注意到有一个非常非常相似的问题here on SO - 我怎么会错过我不知道的。但是,两者都指向“代码重复”方向,即让每个析构函数执行清理(并因此检查那里的条件,而不是在基类中)。
标签: c++