从析构函数调用虚函数 - 任何解决方法?

时间:2015-11-26 17:27:54

标签: c++

我正在将现有的库从Windows移植到Linux。一些类包含特定于操作系统的代码。我决定不使用Pimpl(*),所以我选择了一个简单的DeviceBaseDeviceWin32DeviceLinux层次结构。 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;
};

一切顺利,直到我遇到了析构函数。我天真地认为,与其他功能一样,它可以开箱即用。这些测试很快证明了我的错误,并且实际上是在进行纯虚函数调用时崩溃了。我花了很长时间才弄明白为什么:这完全是预期的,记录在案并讨论hereelsewhere(更多示例请参见本页相关问题栏目。)

我很担心,因为从概念上讲,如果设备被打开则关闭设备的逻辑"属于基类。实现细节是它的表示方式(HANDLE vs int)以及实际关闭它的函数,因此我希望看到基类中的逻辑。

但是,在我的情况下,我找不到可行的解决方案。喜欢"不要这样做"帮助理解出现了什么问题,我发现的大多数变通方法归结为duplicating the logic of the base destructorusing 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 :我终于退后一步,根据情况做出了我认为是明智的决定:接受代码重复,并保留答案,要求在我的枕头下分离关注点未来的使用。因此DeviceWin32DeviceLinux的两个析构函数非常相似 - 但只有两个实例"仅#34;不足以证明重构/概括的合理性。非常感谢那些回答的人。

3 个答案:

答案 0 :(得分:2)

你能否将对象的范围生命周期与对象本身分开控制,允许你使用RAII?

我试图按照你的例子,其中基类实际上是逻辑(x),派生类是实现(x和open / close),而guard类管理打开/关闭调用的生命周期。

#include <iostream>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
namespace ExampleOpenCloseScope
{
    static DWORD DoSomethingForWindows(HANDLE *noonePassesHandlesLikeThis) { *noonePassesHandlesLikeThis = reinterpret_cast<HANDLE>(1); std::wcout << L"DoSomethingForWindows()\n";  return 0; }
    static void WindowsCloseHandle(HANDLE /*handle*/) { std::wcout << L"WindowsCloseHandle()\n"; }
    class DeviceBase
    {
        int _x;
    public:
        bool isValid() const { return isValidImpl(); }
        void x() { checkPrecondsForX(); doX(); checkPostcondsForX(); }
        bool open() { return openHandle(); }
        void close() { closeHandle(); }
    private:
        virtual bool isValidImpl() const = 0;
        virtual void checkPrecondsForX() = 0;
        virtual void doX() = 0;
        virtual void checkPostcondsForX() = 0;

        virtual bool openHandle() = 0;
        virtual void closeHandle() = 0;
    protected:
        DeviceBase() : _x(42) {}
    public:
        virtual ~DeviceBase() = 0 {}
    };

    class DeviceWin32 : public DeviceBase  
    {
    private:
        HANDLE _handle;
        virtual bool isValidImpl() const override { return _handle != NULL; }
        virtual void checkPrecondsForX() override { std::wcout << L"DeviceWin32::checkPrecondsForX()\n"; }
        virtual void doX() override { std::wcout << L"DeviceWin32::doX()\n"; }
        virtual void checkPostcondsForX() override { std::wcout << L"DeviceWin32::checkPostcondsForX()\n"; }
        virtual bool openHandle() override { std::wcout << L"DeviceWin32::openHandle()\n"; if (_handle == NULL) return DoSomethingForWindows(&_handle) == ERROR_SUCCESS; return true; }
        virtual void closeHandle() override { std::wcout << L"DeviceWin32::closeHandle()\n"; if (_handle != NULL) WindowsCloseHandle(_handle); _handle = NULL; }
    public:
        DeviceWin32() : _handle(NULL) {}
        virtual ~DeviceWin32() { std::wcout << L"DeviceWin32::~DeviceWin32()\n"; }
    };

    static int _do_smth_posix(int *fd) { *fd = 1; std::wcout << L"_do_smth_posix()\n"; return 0; }
    static void _posix_close_fd(int /*fd*/) { std::wcout << L"_posix_close_fd\n"; }

    class DeviceLinux : public DeviceBase
    {
    private:
        int _fd;
        virtual bool isValidImpl() const override { return _fd != 0; }
        virtual void checkPrecondsForX() override { std::wcout << L"DeviceLinux::checkPrecondsForX()\n"; }
        virtual void doX() override { std::wcout << L"DeviceLinux::doX()\n"; }
        virtual void checkPostcondsForX() override { std::wcout << L"DeviceLinux::checkPostcondsForX()\n"; }
        virtual bool openHandle() override { std::wcout << L"DeviceLinux::openHandle()\n"; if (_fd == -1) return _do_smth_posix(&_fd) == 0; return true; }
        virtual void closeHandle() override { std::wcout << L"DeviceLinux::closeHandle()\n"; if (_fd != -1) _posix_close_fd(_fd); _fd = -1; }
    public:
        DeviceLinux() : _fd(-1) {}
        virtual ~DeviceLinux() { std::wcout << L"DeviceLinux::~DeviceLinux()\n"; }
    };

    class DeviceGuard
    {
        DeviceBase *_device;
        bool _open;
    public:
        DeviceGuard(DeviceBase *device) : _device(device) { _open = _device->open(); }
        ~DeviceGuard() { try { if (_open) _device->close(); _open = false; } catch (...) { std::wcerr << L"This ain't good\n"; } }

        DeviceGuard(DeviceGuard const &) = delete;
        DeviceGuard & operator=(DeviceGuard const &) = delete;
    };

    enum OS
    {
        Windows,
        Linux
    };
    static OS GetOs() { return OS::Windows; }
    void TestDevice(DeviceBase *device)
    {
        DeviceGuard guard(device);
        device->x();
    }
    void Test()
    {
        std::wcout << L"===ExampleOpenCloseScope.Test()===\n";

        DeviceBase *device;
        if (GetOs() == Windows) device = new DeviceWin32();
        else device = new DeviceLinux();

        TestDevice(device);
        delete device;
        std::wcout << L"exiting ExampleOpenCloseScope.Test()\n";
    }
}

输出结果为:

===ExampleOpenCloseScope.Test()===
DeviceWin32::openHandle()
DoSomethingForWindows()
DeviceWin32::checkPrecondsForX()
DeviceWin32::doX()
DeviceWin32::checkPostcondsForX()
DeviceWin32::closeHandle()
WindowsCloseHandle()
DeviceWin32::~DeviceWin32()
exiting ExampleOpenCloseScope.Test()

答案 1 :(得分:2)

这可能不是你想要的答案,但无论如何我觉得发布它很重要。

您的问题域中至少有两个不同的问题。一个是设备的生命周期,另一个是内部状态。

正如您所知,c ++中的好习惯是给每个班级一个关注点或“工作”。

因此,对你不满意,正确的解决方案是将分开的关注,在这种情况下转换为拥有虚拟device_handle的非虚拟device_concept类,然后可以来自。

这样:

  // concern 1 : internal state
  struct device_concept {
    virtual ~device_concept();
    virtual bool is_open() const = 0;
    virtual void close() = 0;
    virtual void doX() = 0;
  };

struct windows_device : public device_concept {... };
struct linux_device : public device_concept {... };

// concern 2 : lifetime
struct device_handle
{
#if WINDOWS
  device_handle() 
  : _ptr(new windows_device, &close_and_delete)
  {}
#else ... etc
#endif    

  // non virtual functions deferring to virtual ones
  void doX()
  {
    _ptr->doX();
  }    

  private:
    static void close_and_delete(device_concept* p) {
      if (p && p->is_open()) {
        p->close();
      }
      delete p;
    }
    std::unique_ptr<device_concept, void(*)(device_concept*)> _ptr;
};

答案 2 :(得分:1)

如何将设备句柄包装在自己的类中,以暴露以下方法?

public class ADeviceHandle {
    virtual bool IsValid() = 0;
    void* GetHandle() = 0;
}

这些的实现细节是特定于句柄的,您当前的类只需要调用上面两个方法并保留ADeviceHandle的成员吗?