【问题标题】:C++ :: Calling Member function via pointer before constructor exitC++ :: 在构造函数退出之前通过指针调用成员函数
【发布时间】:2020-05-14 02:47:26
【问题描述】:

我处于在构造函数执行期间将指向成员函数的指针共享到外部的情况。并且可以随时调用成员函数,甚至在构造函数完成之前。我假设它可能导致未定义的行为。 这种情况出现在以下情况。

ButtonCtrl::ButtonCtrl(int id, HWND hwnd_parent, int x, int y,
                       int w, int h, const string& text) 
{
    this->hwnd = CreateWindowExW(0, L"BUTTON", this->wtext.c_str(),
                        WS_CHILD | WS_VISIBLE | WS_TABSTOP,
                        x, y, w, h, hwnd_parent, (HMENU)id, 
                        GetModuleHandle(NULL), NULL);

    this->wndproc_org = (WNDPROC) SetWindowLongW(this->hwnd, GWL_WNDPROC, 
                                      (LONG) &ButtonCtrl :: wndproc_new);
}

这里 wndproc_new 是静态类方法,但认为它不是静态的。现在在 SetWindowLongW 方法中,我将 Button 的窗口过程替换为我的窗口过程 (wndproc_new )。由于按钮已经使用 CreateWindowExW 创建,它的消息循环可能已经开始。现在它可以随时调用我的 wndproc_new。甚至在这个构造函数完成之前。所以我的问题是,我会得到未定义的行为还是有效?

【问题讨论】:

  • 您可以在构造函数完成之前调用成员函数,前提是您不读取这些函数中未初始化的成员。
  • 它不能在任何时候调用wndproc_new,在构造函数完成之前不能调用。因为窗口过程(回调)只能在几个 api 调用中调用(GetMessage, PeekMessage..)。
  • 作为旁注 - 需要使用 SetWindowLongPtrWGWLP_WNDPROC,以及你的子类按钮是什么?为什么不通过SetWindowSubclass
  • 现在它可以随时调用我的 wndproc_new。 - 这是错误的假设,无论如何你都没有 UB
  • @RbMm 我使用 SetWindowLongW 因为我的目标是 32 位。我想具体而不是依赖宏观。同样,我没有声明 UNICODE 而是使用 W 或 A 函数。 无论如何你都没有 UB(?) ?

标签: c++ winapi


【解决方案1】:

正式的,即使从语言的角度来看,你也可以从构造函数体中调用成员函数甚至虚函数。因为您的类的虚拟表(如果它具有虚拟函数)将在构造函数主体之前初始化。另一个问题 - 您是否已经在此时初始化所有实例。我不会从构造函数中调用它,不是因为这是错误的,UB 等(否),而是因为风格原因。

现在它可以随时调用我的 wndproc_new。甚至在这个构造函数完成之前

这是错误的。 wndproc_new 不是中断。它不能在任何时候被调用。只有当您调用一些具体的 api(如 GetMessagePeekMessageWSendMessageCreateWindowEx 等)可以调用您的窗口过程回调时,系统才能调用此回调。您的 wndproc_new 将仅在此 api 调用中被调用,而不是从随机位置调用。

还需要使用SetWindowLongPtrWGWLP_WNDPROC,即使是32 位代码。我强烈怀疑需要更改默认的 WC_BUTTON 窗口程序。但是,如果真的需要子类 - 最好使用 SetWindowSubclassAtlThunk_* 之类的方法


一般规则 - 仅在您完全准备好接听来电时设置回调。在任何时候(尽管在具体情况下 wndproc_new 只能在几个 api 调用中调用,并且只能从您当前的线程调用)。无论如何,在构造函数体开始执行后 - 可能调用类的任何成员函数

【讨论】:

  • "它不能在任何时候被调用。只有当你调用一些具体的api如GetMessagePeekMessageW时,系统才能调用这个回调" - 这个不准确。窗口过程可以在任何时候被虚拟调用。例如,CreateWindow 可以。对SendMessage 的任何线程内调用也是如此。而GWLP_WNDPROC 是公共界面的一部分,所有的赌注都没有了。
  • @IInspectable - 不,它不能在任何时间调用。甚至实际上。只有内部一些系统api调用。我不是说 *onlyGetMessagePeekMessageW 中。在CreateWindowEx 里面当然也是(而且总是如此)。当然也可以在SendMessage 里面
  • CreateWindow 不分派传入的已发送消息。这使得声明“只有当您调用一些具体的api(如[...] CreateWindowEx [...] Dispatches传入的已发送消息)时,系统才能调用此回调”是错误的。
  • @IInspectable - 感觉CreateWindow 可以调用我们的窗口过程(比如我们为this->hwnd 创建子窗口(尽管为按钮创建子窗口异常)。调度传入的发送消息 i> - 一般意义上,它只是调用窗口过程回调
【解决方案2】:

我非常感谢您的回答和所有评论。但我没有找到我要找的东西。可能我无法解释我的问题。但是 Evg 的第一条评论是简单直接的答案。

您可以在构造函数完成之前调用成员函数,前提是 您不会在这些函数中读取未初始化的成员。 – EVG

我搜索并阅读了一些文档并测试、调试了一些代码。我发现了一些我想在这里分享的东西,并且可以详细说明答案。答案可能看起来偏离轨道,但最终我们会走上正轨。

当代码编译成员函数时,像其他普通函数一样编译。但正如我们所知道的特殊的第一个参数是 this 指针。

我们可以在没有对象的情况下获得指向成员函数的指针吗?

是的,在下面的代码中,我们得到指向没有任何对象的成员函数的指针(不要抱怨强制转换,编译器会这样做)。这表示成员函数不属于对象(半真句)。但是,将成员函数绑定到对象的是 this 指针。它是一种带有函数指针变量的 C 结构,其中函数的第一个参数是指向 C 结构本身的指针。

#include <iostream>
using namespace std;

class A
{
    public:
        int test_fun()
        {
            cout << "in test_fun" << endl;
            return 42;
        }
};

typedef int (*INT_FUN_PTR)();
int main()
{
    INT_FUN_PTR test_fun_ptr = (INT_FUN_PTR) &A::test_fun;
    int i = test_fun_ptr();
    return 0;
}

但是当我们将成员函数指针转换为普通函数指针时,我们会收到编译器警告。让我们忽略它。

我们可以像其他普通函数一样调用这个成员函数吗 指针?

答案是肯定的和否定的。我们可以,但我们永远不应该像其他普通函数一样调用它。由于上面的代码编译它运行正常,但是如果我们调试并查看 test_fun 函数的堆栈,我们可以看到 this 指针包含在堆栈中并且它包含垃圾值,因为我们这样做了不使用任何对象表示法调用 test_fun。我们不能使用 this 指针。

this 指针如何入栈?

可能是因为函数的调用约定(和/或编译器)。根据微软的文档, __thiscall 是成员函数的调用约定。并且 this 指针通过寄存器 ECX 传递,而不是在 x86 架构上的堆栈上。

如果我们用一些参数定义 test_fun 会怎样?

以下是带有 int 参数的 test_fun 的重新定义版本。

#include <iostream>
using namespace std;

class A
{
    public:
        int test_fun(int i)
        {
            cout << "in test_fun" << endl;
            return i;
        }
};

typedef int (*INT_FUN_PTR)(int);
int main()
{
    int i = 0;
    int* p = &i;
    INT_FUN_PTR test_fun_ptr = (INT_FUN_PTR) &A::test_fun;
    *p = test_fun_ptr(42);
    //test_fun_ptr(42); // program crash
    return 0;
}

现在如果我们调试这段代码,我们可以看到调用 test_fun 后主函数的堆栈变得杂乱无章。如果我们称它为第二次程序会崩溃。如果我们在第一次调用 test_fun 后分配内存并使用它,程序会崩溃。 因为调用约定,因为我们将 test_fun 称为普通函数。

现在让我们尝试使用 __cdecl 更改 test_fun 的调用约定

#include <iostream>
using namespace std;

class A
{
    public:
        int __cdecl test_fun(int i)
        {
            // here stack get cluttered
            cout << "in test_fun" << endl;
            return i;
        }
};

typedef int (__cdecl *INT_FUN_PTR)(int);
int main()
{
    int i = 0;
    int* p = &i;
    INT_FUN_PTR test_fun_ptr = (INT_FUN_PTR) &A::test_fun;
    *p = test_fun_ptr(42);
    test_fun_ptr(42); // program will not crash
    return 0;
}

这一次 test_fun 函数的堆栈变得杂乱无章。我很惊讶,是否可以通过指定任何调用约定将成员函数用作普通函数?但这不是本次讨论的一部分。

正如我们所见,成员函数就像具有特殊调用约定的普通函数一样。它总是在那里。它通过 this 指针绑定到对象。当对象被创建时,为成员分配的内存(在堆栈或堆上)和指向内存的指针被推送到注册。构造函数是第一个被调用的函数。我们可以第一次通过构造函数访问 this 指针。如果我们能够在构造函数调用之前获得 this 指针(可能是通过根本不会调用构造函数的 malloc),我们甚至可以在构造函数调用之前调用成员函数。提供不使用未初始化的成员,甚至我们可以在那里初始化成员。

总之,是的,在构造函数完成之前调用成员函数是完全正常的,只要不使用未初始化的成员即可。

【讨论】:

    猜你喜欢
    • 2010-12-01
    • 2012-09-28
    • 2015-12-05
    • 1970-01-01
    相关资源
    最近更新 更多