为什么构造函数的虚拟函数调用有时有效,但其他调用却无效

Why does a virtual function call from constructor work sometimes but not others?

本文关键字:其他 调用 无效 有效 构造函数 虚拟 函数调用 为什么      更新时间:2023-10-16

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

我最近写了几个带有纯虚拟函数的基类,并意外地在构造函数中包含了对这些函数的间接调用。我意识到我的错误并改正了,但其中一个有效,另一个无效。

以下是有效类的定义:

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。指针参数以大致相同的方式传递,并用于调用纯虚拟函数。这一次,正如人们所料,它失败了。那么这两种情况有什么区别呢?

编辑:

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

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

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

在C++中,virtual函数可以是虚拟函数。问题中的CCD_ 9就是这样一个纯虚拟函数。这些是在基类中声明的,但不一定在基类中定义。试图调用未定义的函数是"未定义行为"。编译人员完全可以随心所欲。

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

脚注:实际上,您可以为纯虚拟功能提供一个实体;语言允许。

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

但更重要的是,您手动将CREATESTRUCT传递给CreateDialogParam()。这是不必要的,尤其是因为您没有在WM_NCCREATE处理程序中处理额外的CREATESTRUCT。当系统向窗口发出WM_(NC)CREATE时,传递给CreateWindow/Ex()lParam被封装在CREATESTRUCT提供的系统中。因此,即使CreateDialogParam()将其dwInitParam作为lParam传递给CreateWindowEx()(这不是记录行为,BTW(,您仍然无法在消息过程中正确获取dialog_base*指针,因为您没有处理可能存在的两个单独的CREATESTRUCT。因此,当出于任何原因使用pThis指针时,您的代码具有未定义的行为,因为您没有将该指针值正确地传递到消息过程中。

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

此外,使用CreateDialog...()(或DialogBox...()(时,不要在消息过程中调用DefWindowProc()(或派生重写(。这在DialogProc文件中有明确说明:

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

试试这个:

template <typename TWindow>
class dialog_base
{
static INT_PTR CALLBACK dlg_proc_internal(HWND, UINT, WPARAM, LPARAM);
protected:
dialog_base(LPCWSTR templateName, HWND parent)
{
m_hwnd = CreateDialogParamW(hinstance_, templateName, parent, dlg_proc_internal, reinterpret_cast<LPARAM>(this));
}
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;
};
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_INITDIALOG)
{
pThis = reinterpret_cast<dialog_base<TWindow>*>(lParam);
// you CANT cancel dialog creation here when
// using CreateDialog...(), only when using
// DialogBox...()!  So, no point in doing any
// error checking on SetWindowLongPtr()...
SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pThis));
// no point in trying to call pThis->dlg_proc()
// here since it won't be dispatched to derived
// classes anyway...
return TRUE; // or FALSE, depending on your needs...
}
pThis = reinterpret_cast<dialog_base<TWindow>*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));
if (!pThis) return FALSE;
return pThis->dlg_proc(msg, wParam, lParam);
}
相关文章: