Как мне `std::bind` нестатический член класса к функции обратного вызова Win32 `WNDPROC`?

Я пытаюсь привязать нестатический член класса к стандартному WNDPROC функция. Я знаю, что могу просто сделать это, сделав член класса статическим. Но, как изучающий C++11 STL, я очень заинтересован в том, чтобы сделать это с помощью инструментов под заголовком <functional>.

Мой код выглядит следующим образом.

class MainWindow
{
    public:
        void Create()
        {
            WNDCLASSEXW WindowClass;
            WindowClass.cbSize          = sizeof(WNDCLASSEX);
            WindowClass.style           = m_ClassStyles;
            WindowClass.lpfnWndProc     = std::function<LRESULT(HWND, UINT, WPARAM, LPARAM)>
                                            (   std::bind(&MainWindow::WindowProc, 
                                                *this,
                                                std::placeholders::_1,
                                                std::placeholders::_2,
                                                std::placeholders::_3,
                                                std::placeholders::_4));
            WindowClass.cbClsExtra      = 0;
            WindowClass.cbWndExtra      = 0;
            WindowClass.hInstance       = m_hInstance;
            WindowClass.hIcon           = LoadIconW(m_hInstance, MAKEINTRESOURCEW(IDI_WINDOW));
            WindowClass.hCursor         = LoadCursor(NULL, IDC_ARROW);
            WindowClass.hbrBackground   = (HBRUSH) COLOR_WINDOW;
            WindowClass.lpszMenuName    = MAKEINTRESOURCEW(IDR_MENU);
            WindowClass.lpszClassName   = m_ClassName.c_str();
            WindowClass.hIconSm         = LoadIconW(m_hInstance, MAKEINTRESOURCEW(IDI_WINDOW_SMALL));
            RegisterClassExW(&WindowClass);
            m_hWnd = CreateWindowEx(/*_In_      DWORD*/     ExtendedStyles,
                                    /*_In_opt_  LPCTSTR*/   m_ClassName.c_str(),
                                    /*_In_opt_  LPCTSTR*/   m_WindowTitle.c_str(),
                                    /*_In_      DWORD*/     m_Styles,
                                    /*_In_      int*/       m_x,
                                    /*_In_      int*/       m_y,
                                    /*_In_      int*/       m_Width,
                                    /*_In_      int*/       m_Height,
                                    /*_In_opt_  HWND*/      HWND_DESKTOP,
                                    /*_In_opt_  HMENU*/     NULL,
                                    /*_In_opt_  HINSTANCE*/ WindowClass.hInstance,
                                    /*_In_opt_  LPVOID*/    NULL);

        }

    private:
        LRESULT CALLBACK WindowProc(_In_ HWND hwnd,
                                    _In_ UINT uMsg,
                                    _In_ WPARAM wParam,
                                    _In_ LPARAM lParam)
        {
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
};

Когда я запускаю его как есть, он выдает сообщение об ошибке:

Error: no suitable conversion function from "std::function<LRESULT(HWND, UINT, WPARAM, LPARAM)>" to "WNDPROC".

person hkBattousai    schedule 10.08.2013    source источник


Ответы (2)


Хотя JohnB уже подробно объяснил, почему это невозможно, вот общее решение проблемы, которую вы пытаетесь решить: Предоставление доступа к экземпляру класса статическому члену класса.

Руководящий принцип решения заключается в том, что указатель экземпляра должен храниться таким образом, чтобы он был доступен для статического члена класса. При работе с окнами дополнительная память окна является хорошим местом для хранения этой информации. Запрашиваемый объем дополнительной памяти окна указывается через WNDCLASSEXW::cbWndExtra, а доступ к данным предоставляется через SetWindowLongPtr и GetWindowLongPtr.

  1. Сохраните указатель экземпляра в области дополнительных данных окна после построения:

    void Create()
    {
        WNDCLASSEXW WindowClass;
        // ...
        // Assign the static WindowProc
        WindowClass.lpfnWndProc = &MainWindow::StaticWindowProc;
        // Reserve space to store the instance pointer
        WindowClass.cbWndExtra  = sizeof(MainWindow*);
        // ...
        RegisterClassExW(&WindowClass);
        m_hWnd = CreateWindowEx( /* ... */ );
    
        // Store instance pointer
        SetWindowLongPtrW(m_hWnd, 0, reinterpret_cast<LONG_PTR>(this));
    }
    
  2. Получите указатель экземпляра из статической оконной процедуры и вызовите функцию-член оконной процедуры:

    static LRESULT CALLBACK StaticWindowProc( _In_ HWND hwnd,
                                              _In_ UINT uMsg,
                                              _In_ WPARAM wParam,
                                              _In_ LPARAM lParam )
    {
        // Retrieve instance pointer
        MainWindow* pWnd = reinterpret_cast<MainWindow*>(GetWindowLongPtrW(hwnd, 0));
        if ( pWnd != NULL )  // See Note 1 below
            // Call member function if instance is available
            return pWnd->WindowProc(hwnd, uMsg, wParam, lParam);
        else
            // Otherwise perform default message handling
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    

    Подпись члена класса WindowProc такая же, как и в предоставленном вами коде.

Это один из способов реализовать желаемое поведение. Реми Лебо предложил вариант, преимущество которого состоит в том, что все сообщения перенаправляются через член класса WindowProc:

  1. Выделите место в окне дополнительных данных (то же, что и выше):

    void Create()
    {
        WNDCLASSEXW WindowClass;
        // ...
        // Assign the static WindowProc
        WindowClass.lpfnWndProc = &MainWindow::StaticWindowProc;
        // Reserve space to store the instance pointer
        WindowClass.cbWndExtra  = sizeof(MainWindow*);
        // ...
    
  2. Передать указатель экземпляра на CreateWindowExW:

        m_hWnd = CreateWindowEx( /* ... */,
                                 static_cast<LPVOID>(this) );
        // SetWindowLongPtrW is called from the message handler
    }
    
  3. Извлеките указатель экземпляра и сохраните его в области дополнительных данных окна при первом сообщении (WM_NCCREATE) отправляется в окно:

    static LRESULT CALLBACK StaticWindowProc( _In_ HWND hwnd,
                                              _In_ UINT uMsg,
                                              _In_ WPARAM wParam,
                                              _In_ LPARAM lParam )
    {
        // Store instance pointer while handling the first message
        if ( uMsg == WM_NCCREATE )
        {
            CREATESTRUCT* pCS = reinterpret_cast<CREATESTRUCT*>(lParam);
            LPVOID pThis = pCS->lpCreateParams;
            SetWindowLongPtrW(hwnd, 0, reinterpret_cast<LONG_PTR>(pThis));
        }
    
        // At this point the instance pointer will always be available
        MainWindow* pWnd = reinterpret_cast<MainWindow*>(GetWindowLongPtrW(hwnd, 0));
        // see Note 1a below
        return pWnd->WindowProc(hwnd, uMsg, wParam, lParam);
    }
    

Примечание 1. Указатель экземпляра сохраняется в области дополнительных данных окна после создания окна, а lpfnWndProc устанавливается до создания. Это означает, что StaticWindowProc будет вызываться, пока указатель экземпляра еще недоступен. Как следствие, оператор if внутри StaticWindowProc требуется, чтобы сообщения во время создания (например, WM_CREATE) правильно обрабатывались.

Примечание 1a. Ограничения, указанные в примечании 1, не применяются к альтернативной реализации. Указатель экземпляра будет доступен начиная с первого сообщения, и, следовательно, для всех сообщений будет вызываться член класса WindowProc.

Примечание 2. Если вы хотите уничтожить экземпляр класса C++ при уничтожении базового HWND, WM_NCDESTROY — это место, где это можно сделать; это последнее сообщение, отправляемое любому окну.

person IInspectable    schedule 10.08.2013
comment
Вместо вызова SetWindowLongPtr() после CreateWindowEx() вы можете передать указатель экземпляра самой функции CreateWindowEx(), а затем вызвать SetWindowLongPtr() в обработчике сообщений WM_NCCREATE. - person Remy Lebeau; 10.08.2013
comment
@Remy Спасибо за указание на альтернативную реализацию. Я обновил ответ, чтобы включить ваше предложение. - person IInspectable; 10.08.2013
comment
Если вы используете GWLP_USERDATA с SetWindowLongPtr(), вам вообще не нужно устанавливать cbWndExtra окна. - person Remy Lebeau; 10.08.2013
comment
@Remy Это правда, однако, GWLP_USERDATA сегодня в основном бесполезен, поскольку все и их собака (ab) используют его для хранения своих ценных данных (например, при создании подклассов). Всякий раз, когда вы управляете регистрацией оконного класса, более надежно использовать эту «частную» область. Технически разницы нет - обе области доступны для всех. Однако дополнительная оконная память не так воспринимается как общественное достояние, как GWLP_USERDATA. - person IInspectable; 10.08.2013
comment
Ты неправ. Это возможно. Я полагаю, вы не слышали о thunking. - person user2345215; 02.01.2014
comment
@ user2345215 Прочитайте вопрос. Прочитайте мой ответ в контексте. Постигать. Не превращайте это в соревнование по размахиванию членами. - person IInspectable; 02.01.2014
comment
@IInspectable: вы сослались на другой ответ, где доказательство ошибочно, пространство может быть создано путем создания исполняемого кода во время выполнения. Итак, моя точка зрения остается в силе. Кстати, я задал вопрос специально из-за тебя, по поводу const. Выруби себя. - person user2345215; 02.01.2014
comment
@user2345215 user2345215 Почему бы вам тогда не прокомментировать этот другой ответ и не проголосовать за этот другой ответ вместо моего? Вы видите недостаток? Кроме того, создание исполняемого кода во время выполнения нарушит работу некоторых антивирусных приложений. - person IInspectable; 02.01.2014

Думаю, вы не можете этого сделать, так как WNDPROC означает указатель на функцию. Любой указатель на функцию можно преобразовать в std::function, но не каждый std::function представляет собой указатель на функцию.

Доказательство невозможности вашего плана: технически WNDPROC представляет собой только адрес вызываемой функции в памяти. Следовательно, переменная типа WNDPROC не содержит «пробела» для хранения информации о связанных параметрах.

Это та же проблема, что и в следующем примере:

typedef void (* callbackFn) ();

struct CallingObject {
    callbackFn _callback;

    CallingObject (callbackFn theCallback) : _callback (theCallback) {
    }

    void Do () {
       _callback ();
    }
};

void f () { std::cout << "f called"; }
void g () { std::cout << "g called"; }
void h (int i) { std::cout << "h called with parameter " << i; }

int main () {
    CallingObject objF (f); objF.Do (); // ok
    CallingObject objG (g); objG.Do (); // ok

}

Тем не менее, чтобы вызвать h из CallingObject с некоторым значением параметра, определенным во время выполнения, вы должны сохранить значение параметра в статической переменной, а затем написать функцию-оболочку, вызывающую h с этим значением в качестве аргумента.

По этой причине функции обратного вызова обычно принимают аргумент типа void *, куда вы можете передавать произвольные данные, необходимые для расчета.

person JohnB    schedule 10.08.2013