program tip

클래스 멤버 함수를 콜백으로 전달하려면 어떻게해야합니까?

radiobox 2020. 11. 13. 08:02
반응형

클래스 멤버 함수를 콜백으로 전달하려면 어떻게해야합니까?


함수 포인터를 콜백으로 전달해야하는 API를 사용하고 있습니다. 클래스에서이 API를 사용하려고하는데 컴파일 오류가 발생합니다.

생성자에서 수행 한 작업은 다음과 같습니다.

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

이것은 컴파일되지 않습니다-다음 오류가 발생합니다.

오류 8 오류 C3867 : 'CLoggersInfra :: RedundencyManagerCallBack': 함수 호출에 인수 목록이 없습니다. '& CLoggersInfra :: RedundencyManagerCallBack'을 사용하여 멤버에 대한 포인터를 만듭니다.

나는 사용 제안을 시도했다 &CLoggersInfra::RedundencyManagerCallBack-나를 위해 작동하지 않았다.

이것에 대한 제안 / 설명 ??

VS2008을 사용하고 있습니다.

감사!!


멤버 함수 포인터는 "this"개체 인수를 예상하기 때문에 일반 함수 포인터처럼 처리 할 수 ​​없기 때문에 작동하지 않습니다.

대신 다음과 같이 정적 멤버 함수를 전달할 수 있습니다. 이는 이와 관련하여 일반 비 멤버 함수와 같습니다.

m_cRedundencyManager->Init(&CLoggersInfra::Callback, this);

함수는 다음과 같이 정의 할 수 있습니다.

static void Callback(int other_arg, void * this_pointer) {
    CLoggersInfra * self = static_cast<CLoggersInfra*>(this_pointer);
    self->RedundencyManagerCallBack(other_arg);
}

이것은 간단한 질문이지만 대답은 놀랍도록 복잡합니다. 짧은 대답은 std :: bind1st 또는 boost :: bind로하려는 작업을 수행 할 수 있다는 것입니다. 더 긴 대답은 다음과 같습니다.

컴파일러는 & CLoggersInfra :: RedundencyManagerCallBack을 사용하도록 제안하는 것이 정확합니다. 첫째, RedundencyManagerCallBack이 멤버 함수 인 경우 함수 자체는 CLoggersInfra 클래스의 특정 인스턴스에 속하지 않습니다. 클래스 자체에 속합니다. 이전에 정적 클래스 함수를 호출 한 적이 있다면 동일한 SomeClass :: SomeMemberFunction 구문을 사용하는 것을 눈치 채 셨을 것입니다. 함수 자체는 특정 인스턴스가 아닌 클래스에 속한다는 의미에서 '정적'이므로 동일한 구문을 사용합니다. 기술적으로 말하면 함수를 직접 전달하지 않기 때문에 '&'가 필요합니다. 함수는 C ++의 실제 객체가 아닙니다. 대신 기술적으로 함수의 메모리 주소, 즉 함수의 명령이 메모리에서 시작되는 위치에 대한 포인터를 전달합니다.

그러나 이것은이 경우 문제의 절반에 불과합니다. 내가 말했듯이 RedundencyManagerCallBack 함수는 특정 인스턴스에 '속하지'않습니다. 그러나 특정 인스턴스를 염두에두고 콜백으로 전달하려는 것 같습니다. 이를 수행하는 방법을 이해하려면 멤버 함수가 실제로 무엇인지 이해해야합니다. 추가 숨겨진 매개 변수가있는 일반 클래스에 정의되지 않은 일반 함수입니다.

예를 들면 :

class A {
public:
    A() : data(0) {}
    void foo(int addToData) { this->data += addToData; }

    int data;
};

...

A an_a_object;
an_a_object.foo(5);
A::foo(&an_a_object, 5); // This is the same as the line above!
std::cout << an_a_object.data; // Prints 10!

How many parameters does A::foo take? Normally we would say 1. But under the hood, foo really takes 2. Looking at A::foo's definition, it needs a specific instance of A in order for the 'this' pointer to be meaningful (the compiler needs to know what 'this' is). The way you usually specify what you want 'this' to be is through the syntax MyObject.MyMemberFunction(). But this is just syntactic sugar for passing the address of MyObject as the first parameter to MyMemberFunction. Similarly when we declare member functions inside class definitions we don't put 'this' in the parameter list, but this is just a gift from the language designers to save typing. Instead you have to specify that a member function is static to opt out of it automatically getting the extra 'this' parameter. If the C++ compiler translated the above example to C code (the original C++ compiler actually worked that way), it would probably write something like this:

struct A {
    int data;
};

void a_init(A* to_init)
{
    to_init->data = 0;
}

void a_foo(A* this, int addToData)
{ 
    this->data += addToData;
}

...

A an_a_object;
a_init(0); // Before constructor call was implicit
a_foo(&an_a_object, 5); // Used to be an_a_object.foo(5);

귀하의 예로 돌아 가면 이제 명백한 문제가 있습니다. 'Init'는 하나의 매개 변수를 취하는 함수에 대한 포인터를 원합니다. 그러나 & CLoggersInfra :: RedundencyManagerCallBack은 두 개의 매개 변수를 취하는 함수에 대한 포인터입니다. 이는 일반 매개 변수와 비밀 'this'매개 변수입니다. 따라서 여전히 컴파일러 오류가 발생하는 이유는 다음과 같습니다 (부수 메모 : Python을 사용해 본 적이 있다면 이러한 종류의 혼란이 모든 멤버 함수에 'self'매개 변수가 필요한 이유입니다).

이를 처리하는 장황한 방법은 원하는 인스턴스에 대한 포인터를 보유하고 매개 변수를받는 'run'또는 'execute'(또는 '()'연산자를 오버로드)와 같은 멤버 함수를 갖는 특수 객체를 만드는 것입니다. 멤버 함수의 경우 저장된 인스턴스의 해당 매개 변수를 사용하여 멤버 함수를 호출하기 만하면됩니다. 그러나 이것은 원시 함수 포인터가 아닌 특수 객체를 가져 오기 위해 'Init'를 변경해야하며 Init이 다른 사람의 코드 인 것처럼 들립니다. 그리고이 문제가 발생할 때마다 특별한 클래스를 만들면 코드가 부풀어 오른다.

이제 마지막으로 좋은 솔루션 인 boost :: bind 및 boost :: function, 여기에서 각각에 대한 문서를 찾을 수 있습니다.

boost :: bind docs , boost :: function 문서

boost :: bind를 사용하면 함수와 해당 함수에 대한 매개 변수를 가져 와서 해당 매개 변수가 '잠긴'새 함수를 만들 수 있습니다. 따라서 두 개의 정수를 추가하는 함수가있는 경우 boost :: bind를 사용하여 매개 변수 중 하나가 5라고 잠긴 새 함수를 만들 수 있습니다.이 새 함수는 하나의 정수 매개 변수 만 취하고 항상 5를 추가합니다. 그것에. 이 기술을 사용하면 숨겨진 'this'매개 변수를 특정 클래스 인스턴스로 '고정'하고 원하는대로 하나의 매개 변수 만 취하는 새 함수를 생성 할 수 있습니다 (숨겨진 매개 변수는 항상 첫 번째입니다.매개 변수 및 일반 매개 변수는 그 뒤에 순서대로 표시됩니다). 예를 들어 boost :: bind 문서를 살펴보고 멤버 함수에 사용하는 방법에 대해서도 구체적으로 설명합니다. 기술적으로 std :: bind1st라는 표준 함수도 사용할 수 있지만 boost :: bind가 더 일반적입니다.

물론 한 가지만 더 있습니다. boost :: bind는 당신을 위해 좋은 boost :: function을 만들 것입니다. 그러나 이것은 기술적으로 Init이 원하는 것과 같은 원시 함수 포인터가 아닙니다. 고맙게도 boost는 여기 StackOverflow에 설명 된대로 boost :: function을 원시 포인터로 변환하는 방법을 제공합니다 . 이것을 구현하는 방법은이 답변의 범위를 벗어나지 만 흥미 롭습니다.

이것이 터무니없이 어렵게 느껴지더라도 걱정하지 마십시오. 귀하의 질문은 C ++의 어두운 모서리 몇 개와 교차하며 일단 학습하면 boost :: bind가 매우 유용합니다.

C ++ 11 업데이트 : boost :: bind 대신 이제 'this'를 캡처하는 람다 함수를 사용할 수 있습니다. 이것은 기본적으로 컴파일러가 동일한 것을 생성하도록하는 것입니다.


이 답변은 위의 의견에 대한 답변이며 VisualStudio 2008에서는 작동하지 않지만 최신 컴파일러에서는 선호되어야합니다.


한편 당신은 더 이상 무효 포인터를 사용할 필요가 없습니다 이후 부스트 필요도 없다 std::bind하고 std::function사용할 수 있습니다. void 포인터와 비교할 때 한 가지 장점은 반환 유형과 인수가 다음을 사용하여 명시 적으로 지정되기 때문에 유형 안전성입니다 std::function.

// std::function<return_type(list of argument_type(s))>
void Init(std::function<void(void)> f);

그런 다음 함수 포인터를 만들고 std::bindInit에 전달할 수 있습니다.

auto cLoggersInfraInstance = CLoggersInfra();
auto callback = std::bind(&CLoggersInfra::RedundencyManagerCallBack, cLoggersInfraInstance);
Init(callback);

std::bind멤버, 정적 멤버 및 멤버가 아닌 함수와 함께 사용 하는 완전한 예 :

#include <functional>
#include <iostream>
#include <string>

class RedundencyManager // incl. Typo ;-)
{
public:
    // std::function<return_type(list of argument_type(s))>
    std::string Init(std::function<std::string(void)> f) 
    {
        return f();
    }
};

class CLoggersInfra
{
private:
    std::string member = "Hello from non static member callback!";

public:
    static std::string RedundencyManagerCallBack()
    {
        return "Hello from static member callback!";
    }

    std::string NonStaticRedundencyManagerCallBack()
    {
        return member;
    }
};

std::string NonMemberCallBack()
{
    return "Hello from non member function!";
}

int main()
{
    auto instance = RedundencyManager();

    auto callback1 = std::bind(&NonMemberCallBack);
    std::cout << instance.Init(callback1) << "\n";

    // Similar to non member function.
    auto callback2 = std::bind(&CLoggersInfra::RedundencyManagerCallBack);
    std::cout << instance.Init(callback2) << "\n";

    // Class instance is passed to std::bind as second argument.
    // (heed that I call the constructor of CLoggersInfra)
    auto callback3 = std::bind(&CLoggersInfra::NonStaticRedundencyManagerCallBack,
                               CLoggersInfra()); 
    std::cout << instance.Init(callback3) << "\n";
}

가능한 출력 :

Hello from non member function!
Hello from static member callback!
Hello from non static member callback!

또한를 사용 std::placeholders하면 콜백에 인수를 동적으로 전달할 수 있습니다 (예 : f에 문자열 매개 변수가있는 경우 return f("MyString");in을 사용할 수 있음 Init).


어떤 논쟁이 필요 Init합니까? 새로운 오류 메시지는 무엇입니까?

C ++의 메서드 포인터는 사용하기가 약간 어렵습니다. 메서드 포인터 자체 외에도 인스턴스 포인터를 제공해야합니다 (귀하의 경우 this). 아마도 Init그것은 별도의 주장으로 기대할 수 있습니까?


클래스 멤버 함수에 대한 포인터는 함수에 대한 포인터와 다릅니다. 클래스 멤버는 암시 적 추가 인수 ( this 포인터)를 사용하고 다른 호출 규칙을 사용합니다.

API가 비 멤버 콜백 함수를 예상하는 경우이를 전달해야합니다.


m_cRedundencyManager멤버 함수를 사용할 수? 대부분의 콜백은 일반 함수 또는 정적 멤버 함수를 사용하도록 설정됩니다. 한 번 봐 가지고 이 페이지에 대한 자세한 내용은 C ++ FAQ 라이트에 있습니다.

업데이트 : 제공 한 함수 선언 m_cRedundencyManager은 다음 형식의 함수를 예상하고 있음을 보여줍니다 void yourCallbackFunction(int, void *). 따라서이 경우 멤버 함수는 콜백으로 허용되지 않습니다. 정적 멤버 함수 작동 할 수 있지만 귀하의 경우에 허용되지 않는 경우 다음 코드도 작동합니다. 에서 악의 캐스트를 사용합니다 void *.


// in your CLoggersInfra constructor:
m_cRedundencyManager->Init(myRedundencyManagerCallBackHandler, this);

// in your CLoggersInfra header:
void myRedundencyManagerCallBackHandler(int i, void * CLoggersInfraPtr);

// in your CLoggersInfra source file:
void myRedundencyManagerCallBackHandler(int i, void * CLoggersInfraPtr)
{
    ((CLoggersInfra *)CLoggersInfraPtr)->RedundencyManagerCallBack(i);
}

네크 로맨싱.
지금까지의 답변이 조금 불분명하다고 생각합니다.

예를 들어 보겠습니다.

픽셀 배열 (ARGB int8_t 값 배열)이 있다고 가정합니다.

// A RGB image
int8_t* pixels = new int8_t[1024*768*4];

이제 PNG를 생성하려고합니다. 이렇게하려면 toJpeg 함수를 호출합니다.

bool ok = toJpeg(writeByte, pixels, width, height);

여기서 writeByte는 콜백 함수입니다.

void writeByte(unsigned char oneByte)
{
    fputc(oneByte, output);
}

여기서 문제 : FILE * 출력은 전역 변수 여야합니다.
다중 스레드 환경 (예 : http-server)에서는 매우 나쁩니다.

따라서 콜백 서명을 유지하면서 출력을 비전 역 변수로 만드는 방법이 필요합니다.

떠오르는 즉각적인 해결책은 멤버 함수가있는 클래스를 사용하여 에뮬레이트 할 수있는 클로저입니다.

class BadIdea {
private:
    FILE* m_stream;
public:
    BadIdea(FILE* stream)  {
        this->m_stream = stream;
    }

    void writeByte(unsigned char oneByte){
            fputc(oneByte, this->m_stream);
    }

};

그리고

FILE *fp = fopen(filename, "wb");
BadIdea* foobar = new BadIdea(fp);

bool ok = TooJpeg::writeJpeg(foobar->writeByte, image, width, height);
delete foobar;
fflush(fp);
fclose(fp);

그러나 예상과 달리 이것은 작동하지 않습니다.

그 이유는 C ++ 멤버 함수가 C # 확장 함수처럼 구현되기 때문입니다.

그래서 당신은

class/struct BadIdea
{
    FILE* m_stream;
}

static class BadIdeaExtensions
{
    public static writeByte(this BadIdea instance, unsigned char oneByte)
    {
         fputc(oneByte, instance->m_stream);
    }

}

따라서 writeByte를 호출하려면 writeByte의 주소뿐만 아니라 BadIdea 인스턴스의 주소도 전달해야합니다.

따라서 writeByte 프로 시저에 대한 typedef가 있고 다음과 같이 보입니다.

typedef void (*WRITE_ONE_BYTE)(unsigned char);

그리고 다음과 같은 writeJpeg 서명이 있습니다.

bool writeJpeg(WRITE_ONE_BYTE output, uint8_t* pixels, uint32_t 
 width, uint32_t height))
    { ... }

두 주소 멤버 함수를 한 주소 함수 포인터 (writeJpeg 수정없이)에 전달하는 것은 근본적으로 불가능하며 그 주위에는 방법이 없습니다.

The next best thing that you can do in C++, is using a lambda-function:

FILE *fp = fopen(filename, "wb");
auto lambda = [fp](unsigned char oneByte) { fputc(oneByte, fp);  };
bool ok = TooJpeg::writeJpeg(lambda, image, width, height);

However, because lambda is doing nothing different, than passing an instance to a hidden class (such as the "BadIdea"-class), you need to modify the signature of writeJpeg.

The advantage of lambda over a manual class, is that you just need to change one typedef

typedef void (*WRITE_ONE_BYTE)(unsigned char);

to

using WRITE_ONE_BYTE = std::function<void(unsigned char)>; 

And then you can leave everything else untouched.

You could also use std::bind

auto f = std::bind(&BadIdea::writeByte, &foobar);

But this, behind the scene, just creates a lambda function, which then also needs the change in typedef.

So no, there is no way to pass a member function to a method that requires a static function-pointer.

But lambdas are the easy way around, provided that you have control over the source.
Otherwise, you're out of luck.
There's nothing you can do with C++.

Note:
std::function requires #include <functional>

However, since C++ allows you to use C as well, you can do this with libffcall in plain C, if you don't mind linking a dependency.

Download libffcall from GNU (at least on ubuntu, don't use the distro-provided package - it is broken), unzip.

./configure
make
make install

gcc main.c -l:libffcall.a -o ma

main.c:

#include <callback.h>

// this is the closure function to be allocated 
void function (void* data, va_alist alist)
{
     int abc = va_arg_int(alist);

     printf("data: %08p\n", data); // hex 0x14 = 20
     printf("abc: %d\n", abc);

     // va_start_type(alist[, return_type]);
     // arg = va_arg_type(alist[, arg_type]);
     // va_return_type(alist[[, return_type], return_value]);

    // va_start_int(alist);
    // int r = 666;
    // va_return_int(alist, r);
}



int main(int argc, char* argv[])
{
    int in1 = 10;

    void * data = (void*) 20;
    void(*incrementer1)(int abc) = (void(*)()) alloc_callback(&function, data);
    // void(*incrementer1)() can have unlimited arguments, e.g. incrementer1(123,456);
    // void(*incrementer1)(int abc) starts to throw errors...
    incrementer1(123);
    // free_callback(callback);
    return EXIT_SUCCESS;
}

And if you use CMake, add the linker library after add_executable

add_library(libffcall STATIC IMPORTED)
set_target_properties(libffcall PROPERTIES
        IMPORTED_LOCATION /usr/local/lib/libffcall.a)
target_link_libraries(BitmapLion libffcall)

or you could just dynamically link libffcall

target_link_libraries(BitmapLion ffcall)

Note:
You might want to include the libffcall headers and libraries, or create a cmake project with the contents of libffcall.


I can see that the init has the following override:

Init(CALLBACK_FUNC_EX callback_func, void * callback_parm)

where CALLBACK_FUNC_EX is

typedef void (*CALLBACK_FUNC_EX)(int, void *);

This question and answer from the C++ FAQ Lite covers your question and the considerations involved in the answer quite nicely I think. Short snippet from the web page I linked:

Don’t.

Because a member function is meaningless without an object to invoke it on, you can’t do this directly (if The X Window System was rewritten in C++, it would probably pass references to objects around, not just pointers to functions; naturally the objects would embody the required function and probably a whole lot more).

참고URL : https://stackoverflow.com/questions/400257/how-can-i-pass-a-class-member-function-as-a-callback

반응형