将函数指针从一种类型转换为另一种类型的最佳方法是什么?

Adr*_*ica 6 c++ winapi function-pointers reinterpret-cast

我已经在 Stack Overflow 上搜索了答案,但我没有得到任何特定于这个问题的信息:只有关于使用各种类型转换运算符的一般情况。因此,恰当的情况是使用 Windows GetProcAddress()API 调用检索函数地址时,该调用返回类型为 的函数指针FARPROC,其中:typedef INT_PTR (__stdcall *FARPROC)();

问题是,所寻求的实际函数很少(如果有的话)具有这个实际签名,如下面的 MRCE 代码所示。在这段代码中,我展示了将返回值转换为适当类型的函数指针的各种不同尝试,除了第四种方法之外的所有方法都被注释掉了:

#include <Windows.h>
#include <iostream>

typedef DPI_AWARENESS_CONTEXT(__stdcall* TYPE_SetDPI)(DPI_AWARENESS_CONTEXT); // Function pointer typedef
static DPI_AWARENESS_CONTEXT __stdcall STUB_SetDpi(DPI_AWARENESS_CONTEXT) { return nullptr; } // Dummy 'stub' function
static DPI_AWARENESS_CONTEXT(__stdcall* REAL_SetDpi)(DPI_AWARENESS_CONTEXT) = STUB_SetDpi; // Func ptr to be assigned

using std::cout;    using std::endl;

int main()
{
    HINSTANCE hDll = LoadLibrary("User32.dll");
    if (!hDll) {
        cout << "User32.dll failed to load!\n" << endl;
        return 1;
    }
    cout << "User32.dll loaded succesfully..." << endl;

    // (1) Simple assignment doesn't work ...
//  REAL_SetDpi = GetProcAddress(hDll, "SetThreadDpiAwarenessContext");
    // (2) Using 'C'-style cast does work, but it is flagged as 'evil' ...
//  REAL_SetDpi = (TYPE_SetDPI)GetProcAddress(hDll, "SetThreadDpiAwarenessContext");
    // (3) Using reinterpret_cast: seems OK with clang-cl but MSVC doesn't like it ...
//  REAL_SetDpi = reinterpret_cast<TYPE_SetDPI>(GetProcAddress(hDll, 
    // (4) Using a temporary plain "void *": OK with MSVC but clang-cl complains ...
    void* tempPtr = GetProcAddress(hDll, "SetThreadDpiAwarenessContext");
    REAL_SetDpi = reinterpret_cast<TYPE_SetDPI>(tempPtr);
    // (5) Using a union (cheating? - but neither clang-cl nor MSVC give any warning!) ...
//  union {
//      intptr_t(__stdcall* gotProc)(void);
//      TYPE_SetDPI usrProc; // This has the 'signature' for the actual function.
//  } TwoProcs;
//  TwoProcs.gotProc = GetProcAddress(hDll, "SetThreadDpiAwarenessContext");
//  REAL_SetDpi = TwoProcs.usrProc;

    if (REAL_SetDpi == nullptr) cout << "SetThreadDpiAwarenessContext function not found!" << endl;
    else                        cout << "SetThreadDpiAwarenessContext function loaded OK!" << endl;

    FreeLibrary(hDll);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

对于 5 个选项中的每一个,由clang-cl本地MSVC编译器和本地编译器给出的各种错误/警告消息如下:

// (1) Simple assignment doesn't work ...
REAL_SetDpi = GetProcAddress(hDll, "SetThreadDpiAwarenessContext");

clang-cl -> error :  assigning to 'DPI_AWARENESS_CONTEXT (*)(DPI_AWARENESS_CONTEXT) __attribute__((stdcall))'
  (aka 'DPI_AWARENESS_CONTEXT__ *(*)(DPI_AWARENESS_CONTEXT__ *)') from incompatible type 'FARPROC' 
  (aka 'long long (*)()'): different number of parameters (1 vs 0)
Visual-C -> error C2440:  '=': cannot convert from 'FARPROC' to 
  'DPI_AWARENESS_CONTEXT (__cdecl *)(DPI_AWARENESS_CONTEXT)'
  message :  This conversion requires a reinterpret_cast, a C-style cast or function-style cast
Run Code Online (Sandbox Code Playgroud)

这个错误(当然)是预料之中的,但让我感到困惑的是为什么将MSVC我的函数显示为__cdecl我已经明确声明它__stdcall

// (2) Using 'C'-style cast does work, but it is flagged as dangerous ...
REAL_SetDpi = (TYPE_SetDPI)GetProcAddress(hDll, "SetThreadDpiAwarenessContext");

clang-cl -> warning :  use of old-style cast [-Wold-style-cast]
Visual-C -> warning C4191:  'type cast': unsafe conversion from 'FARPROC' to 'TYPE_SetDPI'
            warning C4191:   Calling this function through the result pointer may cause your program to fail
Run Code Online (Sandbox Code Playgroud)

一般来说,我努力在我的代码中完全避免旧的、'C' 风格的强制转换!在我被迫在“不相关”对象之间进行转换的地方,我使用显式reinterpret_cast运算符,因为如果出现问题,这些运算符更容易在代码中进行跟踪。因此,对于情况 3:

// (3) Using reinterpret_cast: seems OK with clang-cl but MSVC doesn't like it ...
REAL_SetDpi = reinterpret_cast<TYPE_SetDPI>(GetProcAddress(hDll, "SetThreadDpiAwarenessContext"));

clang-cl -> No error, no warning!
Visual-C -> warning C4191:  'reinterpret_cast': unsafe conversion from 'FARPROC' to 'TYPE_SetDPI'
            Calling this function through the result pointer may cause your program to fail
Run Code Online (Sandbox Code Playgroud)

在这里,MSVC警告与 C 样式转换几乎相同。也许我可以接受这个,但案例 4 让事情变得更有趣:

// (4) Using a temporary plain "void *": OK with MSVC but clang-cl complains ...
void* tempPtr = GetProcAddress(hDll, "SetThreadDpiAwarenessContext");
REAL_SetDpi = reinterpret_cast<TYPE_SetDPI>(tempPtr);

clang-cl -> warning :  implicit conversion between pointer-to-function and pointer-to-object is a Microsoft extension
            [-Wmicrosoft-cast]
            warning :  cast between pointer-to-function and pointer-to-object is incompatible with C++98
            [-Wc++98-compat-pedantic]
Run Code Online (Sandbox Code Playgroud)

在这里,MSVC没有给出警告——但我觉得我只是在“愚弄”编译器!我看不出这与案例 3 中的代码有何不同的整体效果。

// (5) Using a union (cheating? - but neither clang-cl nor MSVC give any warning!) ...
union {
    intptr_t(__stdcall* gotProc)(void);
    TYPE_SetDPI usrProc; // This has the 'signature' for the actual function.
} TwoProcs;
TwoProcs.gotProc = GetProcAddress(hDll, "SetThreadDpiAwarenessContext");
REAL_SetDpi = TwoProcs.usrProc;
Run Code Online (Sandbox Code Playgroud)

我确实将此作为答案发布(现已撤回),@前已知为_463035818指出这是官方未定义行为和/或不允许C++(上述评论员给出的链接)。

我目前使用哪个选项?

好吧,因为我的软件是专门面向 Windows 的,所以我使用最后一个(选项 4)有两个原因:(1)clang-cl警告是“最不可怕的”;(2) 我认为这MSVC可能是编译/构建 Windows 应用程序的最佳“中介”。

编辑:自从第一次发布这个问题,并“审查”了各种评论和建议后,我现在已经在我的代码中更改了这种类型转换的所有实例(即,从通过加载的函数指针GetProcAddress)到使用以下转换'function',在我的全局头文件中定义:

template<typename T> T static inline FprocPointer(intptr_t(__stdcall* inProc)(void)) {
    __pragma(warning(suppress:4191)) // Note: no semicolon after this expression!
    return reinterpret_cast<T>(inProc);
}
Run Code Online (Sandbox Code Playgroud)

如果我将来需要(或希望)改变它们的工作方式,这允许轻松/快速地定位任何此类演员。

为什么这有关系?

也许没有!然而,在我的代码的其他地方,当使用通过加载的函数指针时,我遇到了意外崩溃GetProcAddress()——不是任何标准WinAPI调用,而是来自我自己的 DLL 的函数,作为插件模块加载。下面的代码片段显示了一个潜在的案例:

// --------------------------------------------------------------------------------------------------------------------
// These two routines are the 'interceptors' for plug-in commands; they check active plug-ins for handlers or updaters:

static int      plin;   //! NOTA BENE:  We use this in the two functions below, as the use of a local 'plin' loop index
                        //  is prone to induce stack corruption (?), especially in MSVC 2017 (MFC 14) builds for x86.

void BasicApp::OnUpdatePICmd(uint32_t nID, void *pUI)
{
//! for (int plin = 0; plin < Plugin_Number; ++plin) { // Can cause problems - vide supra
    for (plin = 0;  plin < Plugin_Number;  ++plin) {
        BOOL mEbl = FALSE;  int mChk = -1;
        if ((Plugin_UDCfnc[plin] != nullptr) && Plugin_UDCfnc[plin](nID, &mEbl, &mChk)) {
            CommandEnable(pUI, mEbl ? true : false);
            if (mChk >= 0) CmdUISetCheck(pUI, mChk);
            return;
        }
    }
    CommandEnable(pUI, false);
    return;
}

void BasicApp::OnPluginCmd(uint32_t nID)
{
//! for (int plin = 0; plin < Plugin_Number; ++plin) { // Can cause problems - vide supra
    for (plin = 0; plin < Plugin_Number; ++plin) {
        piHandleFnc Handler = nullptr;  void *pParam = nullptr;
        if ((Plugin_CHCfnc[plin] != nullptr) && Plugin_CHCfnc[plin](nID, &Handler, &pParam) && (Handler != nullptr)) {
            Handler(pParam);
            return;
        }
    }
    return;
}
Run Code Online (Sandbox Code Playgroud)

请注意, Plugin_UDCfncPlugin_CHCfnc是函数指针数组,如上所述加载。

最后,我的问题是什么?

双重:

  1. 忽略警告是否“安全”?
  2. 有没有更好的方法,使用标准库(我仍然习惯使用它)——也许像std::bind()

任何帮助、建议或建议将不胜感激。

编辑:我将本机MSVC编译器用于我的“发布”构建(使用/Wall),并在代码中(本地)显式禁用了一些特定警告。有时,我会通过clang-cl编译器运行我的整个代码库,以寻找其他可能的狡猾代码的警告(实际上非​​常有用)。

Ale*_*iev 4

我认为 C/C++ 缺乏通用函数指针类型,就像void*通用对象指针类型一样。

\n

一般来说,只要您不调用错误的函数指针类型,就支持从一种函数指针转换为另一种函数指针。请参阅[expr.reinterpret.cast]/6

\n
\n

函数指针可以显式转换为不同类型的函数指针。

\n
\n

将一种函数指针类型转换为另一种函数指针类型时发出的警告通常很有用。进行此类转换可能会导致调用具有错误签名的函数。此类问题可能仅影响特定的 CPU 架构,或者仅在特定的操作系统版本中才会被注意到,因此在初始测试后可能并不明显。标准只是说unspecified,请参阅[expr.reinterpret.cast]/6

\n
\n

除了将 \xe2\x80\x9cpointer 类型的纯右值转换为 T1\xe2\x80\x9d 到 \xe2\x80\x9cpointer 类型的 T2\xe2\x80\x9d (其中 T1 和 T2 是函数类型)并返回到它的原始类型产生原始指针值,这种指针转换的结果是未指定的。

\n
\n

是否void*可以转换为函数指针类型以及它是否具有足够的位是特定于实现的。不过,对于 Windows 来说确实如此。我不会赞同针对一般问题的 Windows 特定习惯。请参阅\n [expr.reinterpret.cast]/8

\n
\n

有条件地支持将函数指针转换为对象指针类型,反之亦然。

\n
\n

使用联合的类型双关会引发严格别名问题(什么是严格别名规则?),因此这不是智胜编译器的好方法。

\n

因此,请接受您的GetProcAddress调用附近或包装中的本地警告抑制GetProcAddess。使用reinterpret_cast

\n

如果您打算使用辅助函数将一种函数类型转换为另一种函数类型而不发出警告,请确保仅在GetProcAddress类似场景中使用它,即当您使用某些通用签名来临时存储函数指针时,但该签名不是一个实际的签名——不要通过非预期的类型调用函数指针。

\n

对不起。

\n