【问题标题】:Calling virtual function from destructor - any workaround?从析构函数调用虚函数 - 任何解决方法?
【发布时间】:2016-02-29 21:10:00
【问题描述】:

我正在将现有库从 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 的两个析构函数非常相似——但“仅”两个实例不足以证明重构/泛化的合理性。非常感谢那些回答的人。

【问题讨论】:

  • 您可能有一个包含 DeviceImplementation 的 Device,其中 DeviceImplementation 不是从 Device 继承,而是 DeviceInterface 具有默认构造函数、create 和 destroy 函数。是的,这就像一个“pimpl”(我猜在这种情况下这是一个正确的设计决策)。
  • RAII 观点:让每个(派生)对象在其析构函数中进行适当的清理,不要从基类调用(虚拟)清理代码。
  • @all:感谢您的回答/指点!一切似乎都指向(伪)pimpl 方向,所以我将沿着这条路线走,看看它如何适应更大的图景。我有点担心将我的组合设备/设备实现子类化的能力。无论如何,我会在进行建议的修改后尝试发布更新。
  • 我有点惭愧。我刚刚注意到有一个非常非常相似的问题here on SO - 我怎么会错过我不知道的。但是,两者都指向“代码重复”方向,即让每个析构函数执行清理(并因此检查那里的条件,而不是在基类中)。

标签: c++


【解决方案1】:

您能否将对象的作用域生命周期控制与对象本身分开,从而允许您使用 RAII?

我尝试按照您的示例,其中基类实际上是逻辑 (x),派生类是实现(x 和打开/关闭),而保护类管理打开/关闭调用的生命周期。

#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(但我令人费解的问题似乎并没有吸引这么多的流量,所以这个答案很可能会被低估)。所以有时可能很残酷:)
  • @Tibo 不客气。从您的回答中,我无法判断它是否回答了问题。如果是,请考虑接受它作为答案。
【解决方案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;
};

【讨论】:

  • 谢谢,这听起来很合法。我试试看。
【解决方案3】:

您如何将您的设备句柄包装在它自己的类中,该类公开以下方法?

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

这些的实现细节是handle特有的,你当前的类只需要调用上面两个方法并保留一个ADeviceHandle的成员吗?

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-06-07
    • 1970-01-01
    • 1970-01-01
    • 2021-10-20
    • 2021-05-18
    • 2018-02-08
    • 2017-08-13
    相关资源
    最近更新 更多