为什么有时从构造函数调用虚拟函数却不能正常工作?

时间:2018-08-06 07:33:25

标签: c++ winapi vtable virtual-method

一般的经验法则是不要从构造函数中调用虚函数,因为它可能导致不可预测的行为。那么为什么有时会起作用?

我最近写了几个带有纯虚函数的基类,并偶然在构造函数中包括了对这些函数的间接调用。我意识到了自己的错误并予以纠正,但是其中一个有效,而另一个则没有。

这是起作用的类的定义:

template <typename TWindow>
class dialog_base
{
    static INT_PTR CALLBACK dlg_proc_internal
        (HWND,
         UINT,
         WPARAM,
         LPARAM);
protected:
    dialog_base
        (const LPCWSTR templateName,
         const HWND parent)
        {
        CREATESTRUCT create;
        create.lpCreateParams = this;
        m_hwnd = CreateDialogParam(
            hinstance_, templateName, parent, dlg_proc_internal,
            reinterpret_cast<LPARAM>(&create));
        }

    HWND m_hwnd;
    virtual INT_PTR CALLBACK dlg_proc
        (UINT,
         WPARAM,
         LPARAM) = 0;
public:
    virtual ~dialog_base()
        {
        DestroyWindow(m_hwnd);
        }

    HWND GetHandle() const;
    void show() const;
};

在此类中,DialogBoxParam函数调用dlg_proc_internal,并传递WM_NCCREATE消息:

template <typename TWindow>
INT_PTR dialog_base<TWindow>::dlg_proc_internal
    (HWND hWnd,
     UINT msg,
     WPARAM wParam,
     LPARAM lParam)
    {
    dialog_base<TWindow>* pThis;
    if (msg == WM_NCCREATE)
        {
        pThis = static_cast<dialog_base<TWindow>*>(reinterpret_cast<
            CREATESTRUCT*>(lParam)->lpCreateParams);
        SetLastError(0);
        if (!SetWindowLongPtr(
            hWnd, GWLP_USERDATA,
            reinterpret_cast<LONG_PTR>(pThis)) && GetLastError() != 0)
            return 0;
        }
    else
        {
        pThis = reinterpret_cast<dialog_base<TWindow>*>(
            GetWindowLongPtr(hWnd, GWLP_USERDATA));
        }
    return pThis
               ? pThis->dlg_proc(msg, wParam, lParam)
               : DefWindowProc(hWnd, msg, wParam, lParam);
    }

此函数检索作为最后一个参数传递给CreateDialogParam的指针,并将其存储在窗口中,以便以后在对该函数的调用中可以再次检索它。 然后,它错误地调用了纯虚函数dlg_proc而不是返回-似乎在子类的构造函数中也可以正常工作。

我创建了一个几乎相同的不同类,除了它叫CreateWindowEx而不是CreateDialogParam。指针参数以几乎相同的方式传递,并用于调用纯虚函数。这次,它失败了,正如人们所期望的那样。那么这两种情况有什么区别?

编辑:

也许我应该澄清一下。我不是在问“为什么不能从构造函数中调用虚拟成员?”。我在问一个问题,为什么在构造对象之前解析虚拟成员的过程有时会创建没有发生错误并调用正确函数的情况。

2 个答案:

答案 0 :(得分:3)

从构造函数调用virtual函数在C ++中具有完全可预测的行为,就像在.Net和Java中具有完全可预测的行为一样。但是,这不是相同行为。

在C ++中,virtual函数在调用时按对象的类型调度。其他一些语言将使用预期类型的对象。两者都是可行的选择,都有风险,但是由于这是一个C ++问题,因此我将重点介绍C ++风险。

在C ++中,virtual函数可以是 pure 虚拟的。问题中的dlg_proc就是这样一个纯虚函数。这些是在基类中声明的,但是(没有)在基类中定义。尝试调用未定义的函数是未定义行为。编译器完全可以自由地做自己喜欢的事情。

一个可能的实现是编译器只是调用随机的其他函数。这可能是remove(filename)。它也可能是派生类的替代。但是还有其他百万种可能的结果,包括崩溃和挂起。因此,我们不要试图预测会发生什么,只是说:不要从ctor调用 pure 虚拟函数。

脚注: 实际上,您可以为纯虚函数提供主体。语言允许。

答案 1 :(得分:1)

CreateDialog...()(和DialogBox...())不会通过dwInitParam将其WM_(NC)CREATE参数值传递给您的消息过程。它通过WM_INITDIALOG传递,您没有处理。只有CreateWindow/Ex()通过lpParam将其WM_(NC)CREATE参数值传递给消息过程。 这是记录的行为!

但更重要的是,您手动CREATESTRUCT传递给CreateDialogParam()。这不是必需的,尤其是因为您没有在CREATESTRUCT处理程序中处理额外的WM_NCCREATE。当系统向窗口发出WM_(NC)CREATE时,传递给lParam的{​​{1}}被包裹在系统提供的 CreateWindow/Ex()中。因此,即使CREATESTRUCT将其CreateDialogParam()作为dwInitParam传递给lParam,这是未记录的行为,顺便说一句),您仍然不会在消息过程内正确获取CreateWindowEx()指针,因为您没有处理可能存在2个单独的dialog_base*的问题。因此,由于{em>任何原因而使用CREATESTRUCT指针时,您的代码具有未定义的行为,因为您没有将该指针值正确地传递到消息过程中。

您需要将pThis指针直接传递到this而不进行包装,并且您需要处理CreateDialogParam()而不是WM_INITDIALOG。然后您的虚拟方法应该表现出预期的效果(即,它将不会分派到派生类,因为WM_NCCREATE是在基类构造函数的上下文中处理的。)

此外,使用WM_INITDIALOG(或DefWindowProc())时,请勿在消息过程(或派生的替代)中调用CreateDialog...()DialogProc文档中对此进行了专门说明:

  

尽管对话框过程类似于窗口过程,但它不得调用DialogBox...()函数来处理不需要的消息。不需要的消息由对话框窗口过程内部处理。

尝试以下方法:

DefWindowProc