重构 MFC 消息映射以包含完全限定的成员函数指针

Posted

技术标签:

【中文标题】重构 MFC 消息映射以包含完全限定的成员函数指针【英文标题】:Refactor MFC message maps to include fully qualified member function pointers 【发布时间】:2018-07-16 13:32:48 【问题描述】:

我有一个代码库,其中 MFC 消息映射以这种形式编写:

BEGIN_MESSAGE_MAP(SomeForm, BaseForm)
    ON_COMMAND(CID_ButtonAction, OnButtonAction)
END_MESSAGE_MAP()

这在 MSVC 中编译得很好。当我想在 Clang 中编译相同的代码时,我得到一个 call to non-static member function without an object argument 错误,因为 OnButtonAction 不是指定成员函数指针的正确形式。代码很容易修复:

    ON_COMMAND(CID_ButtonAction, &SomeForm::OnButtonAction)

或者我们可以使用 BEGIN_MESSAGE_MAP() 宏中的 ThisClass typedef:

    ON_COMMAND(CID_ButtonAction, &ThisClass::OnButtonAction)

到目前为止一切都很好...唯一的问题是我在很多单独的文件中有数百个这样的消息映射条目。有什么工具可以解决这个问题吗?一些晦涩难懂的 Visual Studio 魔法?还是可以在这里通过正则表达式使用替换?

【问题讨论】:

唯一支持编译 MFC 代码的编译器是 Microsoft 的编译器,它带有与您的 MFC 版本匹配的 Visual Studio。不支持其他编译器。即使你解决了这个问题,未来也会有更多的问题需要解决。如果事情确实出错了,你就靠自己了。 Microsoft 不提供支持。 我的动机是能够通过 VS 的 Clang Power Tools 插件使用 clang-tidy,我需要为此编译代码 - 仅此而已。我不会在生产中使用 clang 编译的二进制文件,因此不涉及风险/需要支持。此外,此修复实际上会提高代码质量,使其更符合标准。 那你应该问如何在 MFC 中使用 clang-tidy。这个问题是问如何用 Clang 编译 MFC 代码。这些是不同的问题,他们可能有不同的答案。 所以您毕竟正在尝试使用 Clang 编译 MFC。这根本不受支持,我什至不确定,它是否合法。您必须仔细阅读 EULA 以进行验证。 请在您发布的问题中包含实际的错误文本。 【参考方案1】:

最后我想出了一个从 MinGW 运行的 sed 命令:

sed -b -i -re '/^BEGIN_MESSAGE_MAP/,/^END_MESSAGE_MAP//(BEGIN_MESSAGE_MAP|\/\/)/!s/(.*),\s0,/\1, \&ThisClass::/;' *.cpp

解释它的作用:

-b 将文件视为二进制文件(可选,在 Windows 中保留行尾)* -re 支持扩展正则表达式 -i 就地替换 /^BEGIN_MESSAGE_MAP/,/^END_MESSAGE_MAP/ 仅匹配这两个字符串之间的文本 /!s 替换命令将忽略您之前匹配的任何内容 /\(BEGIN_MESSAGE_MAP\|\/\/\)/ 匹配要忽略的行开头(消息映射的第一行或注释掉的行) /(.*),\s0,/\1, \&ThisClass::/, &ThisClass:: 替换行上的最后一个逗号,后跟 0+ 个空格

示例输入:

BEGIN_MESSAGE_MAP(SomeForm, BaseForm)
    ON_COMMAND(CID_ButtonAction, OnButtonAction)
    ON_NOTIFY_EX(CID_Notify, 0, OnNotify)
END_MESSAGE_MAP()

输出:

BEGIN_MESSAGE_MAP(SomeForm, BaseForm)
    ON_COMMAND(CID_ButtonAction, &ThisClass::OnButtonAction)
    ON_NOTIFY_EX(CID_Notify, 0, &ThisClass::OnNotify)
END_MESSAGE_MAP()

这很好用,对于大约 500 个文件,我只需要在已经使用类方法成员符号的地方进行两次手动调整。可以调整 sed 命令来解决这个问题(例如,检查该行的最后一个逗号是否后跟 &),但这对于我的目的来说已经足够了。

编辑 - 添加了-b 选项。这将文件视为二进制文件。在 Windows 上,这可以防止用 Unix 替换原始换行符 - 如果不启用此选项,任何已处理文件的 git diff 看起来就像整个文件已被删除并再次添加。

【讨论】:

【参考方案2】:

错误消息有点奇怪,我想它与 Visual Studio 和 CLANG 之间在处理源代码方面的差异有关。

我使用的编译器是 Visual Studio 2005,我正在开发一个 MFC 应用程序,因此 Visual Studio 2005 的 MFC 源代码非常方便。我用相同的解决方案快速浏览了 Visual Studio 2015,看起来 MFC 头文件是相似的。所以我将把它建立在 Visual Studio 2005 MFC 的基础上。

位于 afxmsg_.h 中的ON_COMMAND() 宏定义如下:

#define ON_COMMAND(id, memberFxn) \
     WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v, \
        static_cast<AFX_PMSG> (memberFxn) ,
        // ON_COMMAND(id, OnBar) is the same as
        //   ON_CONTROL(0, id, OnBar) or ON_BN_CLICKED(0, id, OnBar)

AFX_PMSG在文件afxwin.h中定义为:

// pointer to afx_msg member function
#ifndef AFX_MSG_CALL
#define AFX_MSG_CALL
#endif
typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);

CCmdTarget 类是一个基类,从它派生出其他类,例如 CWndCWinThread 以及其他使用消息映射的 MFC 类。

所以ON_COMMAND() 宏将static_cast&lt;&gt; 用于应该是窗口或线程目标的基类。也许其他更博学的人可以提供关于编译器在做什么以及 C++ 语言规范将如何处理此构造的实际解释。

但是,在更实际的情况下,我建议您编写自己的 ON_COMMAND() 宏版本并将此版本插入到解决方案的每个项目中的 StdAfx.h 文件中。我选择了 StdAfx.h 文件,因为每个项目只有一个文件,而且它是单个修改可以影响多个编译单元的中心点。

在所有各种包含之后的文件底部和关闭已包含的头文件测试的#endif之前,添加以下源代码行。

#undef ON_COMMAND

#define ON_COMMAND(id, memberFxn) \
     WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v, \
        static_cast<AFX_PMSG> (&ThisClass :: memberFxn) ,
        // ON_COMMAND(id, OnBar) is the same as
        //   ON_CONTROL(0, id, OnBar) or ON_BN_CLICKED(0, id, OnBar)

这有两个作用。

首先,它取消定义ON_COMMAND() 宏的当前定义,以便您可以将其替换为您自己的。

其次,它使用类方法成员符号表示方法指针。我无法使用 CLANG 进行测试,但是它应该执行与您所说的手动操作相同的源文本替换。

ON_COMMAND(CID_ButtonAction, &SomeForm::OnButtonAction)

ThisClassBEGIN_MESSAGE_MAP() 指令中指定的类的类型定义(例如BEGIN_MESSAGE_MAP(CFrameworkWnd, CWin)),由BEGIN_MESSAGE_MAP() 宏生成,如下所示:

#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
    PTM_WARNING_DISABLE \
    const AFX_MSGMAP* theClass::GetMessageMap() const \
         return GetThisMessageMap();  \
    const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \
     \
        typedef theClass ThisClass;                        \
        typedef baseClass TheBaseClass;                    \
        static const AFX_MSGMAP_ENTRY _messageEntries[] =  \
        

我用 Visual Studio 测试了这种方法,一切都编译得很好,它适用于 Visual Studio 2005。

请注意,可能还有其他消息映射宏可能需要类似的解决方法,因为 static_cast&lt;AFX_PMSG&gt; 的使用似乎在大多数消息映射宏中很常见。

一个奇怪的区别

对此,afxmsg_.h 中各种宏的一个奇怪区别是使用类方法指针表示法的一整套宏。一个例子如下:

#define ON_WM_PAINT() \
     WM_PAINT, 0, 0, 0, AfxSig_vv, \
        (AFX_PMSG)(AFX_PMSGW) \
        (static_cast< void (AFX_MSG_CALL CWnd::*)(void) > ( &ThisClass :: OnPaint)) ,

查看一些特定的事件宏,它们似乎重用了 ON_CONTROL() 宏,因此除了 ON_COMMAND() 宏之外替换该宏会影响特定于控制的 MFC 宏集。

// Combo Box Notification Codes
#define ON_CBN_ERRSPACE(id, memberFxn) \
    ON_CONTROL(CBN_ERRSPACE, id, memberFxn)

总结

使用这种用您自己的版本覆盖默认宏的方法,包含文件 afxmsg_.h 似乎包含需要更改的内容的列表。似乎有两组 MFC 宏需要替换版本,靠近文件顶部的一组(以 ON_COMMAND() 开头)和靠近包含文件 afxmsg_.h 底部的一些宏。

例如,ON_MESSAGE() 宏需要更改为:

// for Windows messages
#define ON_MESSAGE(message, memberFxn) \
     message, 0, 0, 0, AfxSig_lwl, \
        (AFX_PMSG)(AFX_PMSGW) \
        (static_cast< LRESULT (AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM) > \
        (&ThisClass :: memberFxn)) ,

我想知道为什么会有多种样式(可能是由于多年来不同的人添加了新的宏,而不愿意更改现有的宏?)。我很好奇为什么在过去二十年的某个时候这个问题没有得到解决,因为 MFC 至少可以追溯到 Visual Studio 6.x,并且有机会使宏统一。例如,Visual Studio 2005 的发布将是一个好时机。也许是担心与庞大的 Visual Studio 6.x MFC 代码库的向后兼容性?

现在我知道为什么要使用量身定制的特定 static_cast&lt;&gt;。它允许检测具有错误或不匹配接口签名的类方法以及编译错误。因此,C 风格的强制转换是通过 AFX_MSGMAP_ENTRY 中函数指针的定义来纠正问题,static_cast&lt;&gt; 是通过在方法接口与实际接口不同时发出编译器错误来捕获由于接口缺陷导致的程序员错误预计。

【讨论】:

这适用于 Clang。这里的问题是我的消息映射不只使用 ON_COMMAND 而是另外大约 30 个我必须以这种方式重写的宏。 Clang 编译将依赖于修改后的 afxmsg_.h,它有几个缺点 - 对其他机器的可移植性和 MFC 更新时破坏的编译。 Microsoft 自己建议在消息映射宏中使用完全限定的成员函数名称,因此我将尝试提出一个正则表达式来正确修复它。不错的答案,谢谢。 @pablo285 我很确定微软不会在这个领域进行 MFC 更新,这会破坏可追溯到 1990 年代的数十年源代码。我的 MFC 代码最初是用 Visual Studio 6.x 编写的,可以在 Visual Studio 2017 中很好地编译,因此它们似乎确实在不涉及 C++ 标准的领域努力实现向后兼容性。我想知道 VS 6.X 是否允许类方法指针,所以微软从那以后就不得不允许它?有 /Za 和 /Zc Visual Studio C++ 编译器选项可以修改 VS 编译器行为。

以上是关于重构 MFC 消息映射以包含完全限定的成员函数指针的主要内容,如果未能解决你的问题,请参考以下文章

为啥即使从类内部获取成员函数指针值也需要类名限定?

C ++ - 通过getter函数在单独的类中从对象指针映射访问成员函数

MFC如何添加系统消息处理函数?

字符串和成员函数指针的 C++ 映射

离线成员函数定义是不是需要完全限定的类名到全局范围?

指向 cv 和/或 ref 限定成员函数的指针的 typedef