함수 포인터를 통해 C ++ 클래스 메서드 호출


113

클래스 멤버 함수에 대한 함수 포인터를 얻고 나중에 특정 개체를 사용하여 해당 멤버 함수를 호출하는 방법은 무엇입니까? 다음과 같이 쓰고 싶습니다.

class Dog : Animal
{
    Dog ();
    void bark ();
}


Dog* pDog = new Dog ();
BarkFunction pBark = &Dog::bark;
(*pBark) (pDog);

또한 가능하면 포인터를 통해 생성자를 호출하고 싶습니다.

NewAnimalFunction pNew = &Dog::Dog;
Animal* pAnimal = (*pNew)();    

이것이 가능합니까? 그렇다면 선호하는 방법은 무엇입니까?


1
개체 멤버 함수를 호출하고 단순히 개체에 대한 포인터를 전달하려는 경우 여전히 '이유'를 이해하지 못합니다. 사람들이 클래스를 더 잘 캡슐화 할 수 있기 때문에 모든 클래스가 상속하는 인터페이스 클래스를 만드는 것은 어떨까요?
Chad

많은 사람들이 boost :: function을 사용하여 원시 멤버 포인터 메커니즘을 숨기지 만 명령 패턴과 같은 것을 구현하는 데 유용 할 수 있습니다.
CB Bailey

9
왜 그 개를 동적으로 할당합니까? 그런 다음 개체도 수동으로 삭제해야합니다. 이것은 Java, C # 또는 기타 유사한 언어에서 왔지만 여전히 C ++와 싸우는 것처럼 보입니다. 일반 자동 개체 ( Dog dog;)가 원하는 것일 가능성이 더 높습니다.
sbi

1
@Chad : 나는 대부분 동의하지만 참조를 전달하는 데 더 많은 비용이 드는 경우가 있습니다. 일부 if / else 계산을 기반으로 함수를 호출 할 수있는 것보다 특정 유형의 데이터 (파싱, 계산 등)를 반복하는 루프를 고려해보십시오. / else는 루프에 들어가기 전에 이러한 검사를 수행 할 수 있는지 확인합니다.
에릭

1
멤버 함수에 대한 함수 포인터 도 참조하십시오 .
jww

답변:


126

자세한 내용은 다음 을 읽으십시오 .

// 1 define a function pointer and initialize to NULL

int (TMyClass::*pt2ConstMember)(float, char, char) const = NULL;

// C++

class TMyClass
{
public:
   int DoIt(float a, char b, char c){ cout << "TMyClass::DoIt"<< endl; return a+b+c;};
   int DoMore(float a, char b, char c) const
         { cout << "TMyClass::DoMore" << endl; return a-b+c; };

   /* more of TMyClass */
};
pt2ConstMember = &TMyClass::DoIt; // note: <pt2Member> may also legally point to &DoMore

// Calling Function using Function Pointer

(*this.*pt2ConstMember)(12, 'a', 'b');

23
놀랍게도 그들이 이것을 결정했다는 것은 : 효과 *this.*pt2Member가있을 것입니다. *우선 순위가 더 높습니다 .*... 개인적으로, 나는 여전히 썼을 것 this->*pt2Member입니다. 연산자가 하나 적습니다.
Alexis Wilke 2014

7
왜 초기화 pt2ConstMember해야 NULL합니까?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

@AlexisWilke 왜 놀랍습니까? 직접 객체 (포인터가 아님)의 경우이므로이 (가) 더 높은 우선 순위를 갖기를 (object.*method_pointer)원합니다 *.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

@ TomášZato, 내가 착각하지 않았다면 (그리고 내가 그럴 수도 있습니다), this당신이 적용하는 것이 무엇이든 .*(하위) 클래스의 인스턴스에 대한 포인터가되어야 한다는 것을 보여주기 위해 사용됩니다 . 그러나 이것은 여기에 링크 된 다른 답변과 리소스를 기반으로 추측하는 새로운 구문입니다. 나는 그것을 더 명확하게하기 위해 편집을 제안하고있다.
c1moore

1
오 이런, 100을 축하합니다!
Jonathan Mee

57

클래스 멤버 함수에 대한 함수 포인터를 얻고 나중에 특정 개체를 사용하여 해당 멤버 함수를 호출하는 방법은 무엇입니까?

로 시작하는 것이 가장 쉽습니다 typedef. 멤버 함수의 경우 유형 선언에 클래스 이름을 추가합니다.

typedef void(Dog::*BarkFunction)(void);

그런 다음 메서드를 호출하려면 ->*연산자 를 사용합니다 .

(pDog->*pBark)();

또한 가능하다면 포인터를 통해 생성자를 호출하고 싶습니다. 이것이 가능합니까? 그렇다면 선호하는 방법은 무엇입니까?

나는 당신이 이와 같은 생성자로 작업 할 수 있다고 생각하지 않습니다-ctor와 dtor는 특별합니다. 그런 종류의 작업을 수행하는 일반적인 방법은 기본적으로 생성자를 호출하는 정적 함수 인 팩토리 메서드를 사용하는 것입니다. 예제는 아래 코드를 참조하십시오.

기본적으로 설명하는대로 코드를 수정했습니다. 아래에 몇 가지주의 사항이 있습니다.

#include <iostream>

class Animal
{
public:

    typedef Animal*(*NewAnimalFunction)(void);

    virtual void makeNoise()
    {
        std::cout << "M00f!" << std::endl;
    }
};

class Dog : public Animal
{
public:

    typedef void(Dog::*BarkFunction)(void);

    typedef Dog*(*NewDogFunction)(void);

    Dog () {}

    static Dog* newDog()
    {
        return new Dog;
    }

    virtual void makeNoise ()
    {
        std::cout << "Woof!" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    // Call member function via method pointer
    Dog* pDog = new Dog ();
    Dog::BarkFunction pBark = &Dog::makeNoise;

    (pDog->*pBark)();

    // Construct instance via factory method
    Dog::NewDogFunction pNew = &Dog::newDog;

    Animal* pAnimal = (*pNew)();

    pAnimal->makeNoise();

    return 0;
}

이제 다형성의 마법 덕분 Dog*에 일반적으로 a 대신 a 를 사용할 수 있지만 Animal*함수 포인터의 유형 은 클래스 계층 구조의 조회 규칙을 따르지 않습니다 . 따라서 Animal 메서드 포인터는 Dog 메서드 포인터와 호환되지 않습니다. 즉 Dog* (*)(), 유형의 변수에를 할당 할 수 없습니다 Animal* (*)().

정적 newDog메서드는 단순히 새 인스턴스를 만들고 반환하는 팩토리의 간단한 예입니다. 정적 함수이므로 일반 typedef(클래스 한정자 없음)이 있습니다.

위의 답변을 통해 필요한 것을 달성하는 더 좋은 방법이 없는지 궁금합니다. 이런 종류의 작업을 수행하는 몇 가지 특정 시나리오가 있지만 문제에 더 잘 맞는 다른 패턴이있을 수 있습니다. 달성하려는 것을보다 일반적인 용어로 설명하면 하이브 마인드가 훨씬 더 유용 할 수 있습니다!

위와 관련하여 Boost bind 라이브러리 및 기타 관련 모듈이 매우 유용하다는 것은 의심 할 여지가 없습니다 .


10
저는 10 년 넘게 C ++를 사용해 왔으며 정기적으로 새로운 것을 배우고 있습니다. ->*전에 들어 본 적이 없지만 이제는 필요하지 않기를 바랍니다. :)
Thomas

31

여기에서 한 가지 문제는 일반 함수 포인터가 아닌 " 멤버 포인터 "가 필요하다는 것을 아무도 설명하지 않았다고 생각 합니다.

함수에 대한 멤버 포인터는 단순히 함수 포인터가 아닙니다. 구현 측면에서 컴파일러는 단순 함수 주소를 사용할 수 없습니다. 일반적으로 역 참조 할 개체를 알 때까지 호출 할 주소를 알지 못하기 때문입니다 (가상 함수를 생각해보세요). this물론 암시 적 매개 변수 를 제공하려면 객체를 알아야합니다 .

당신이 그것들이 필요하다고 말 했으므로 이제 나는 당신이 정말로 그들을 피해야한다고 말할 것입니다. 진지하게, 회원 포인터는 고통입니다. 동일한 목표를 달성하는 객체 지향 디자인 패턴을 boost::function보거나 위에서 언급 한대로 어떤 것을 사용하는 것이 훨씬 더 정상적 입니다.

해당 함수 포인터를 기존 코드에 제공하고 있으므로 정말 간단한 함수 포인터 가 필요한 경우 함수를 클래스의 정적 멤버로 작성해야합니다. 정적 멤버 함수는을 이해하지 못 this하므로 객체를 명시 적 매개 변수로 전달해야합니다. 함수 포인터가 필요한 오래된 C 코드로 작업하기 위해이 줄을 따라 특이하지 않은 관용구가있었습니다.

class myclass
{
  public:
    virtual void myrealmethod () = 0;

    static void myfunction (myclass *p);
}

void myclass::myfunction (myclass *p)
{
  p->myrealmethod ();
}

myfunction실제로는 정상적인 함수 이기 때문에 (범위 문제는 제쳐두고), 함수 포인터는 일반적인 C 방식으로 찾을 수 있습니다.

편집 -이러한 종류의 메서드를 "클래스 메서드"또는 "정적 멤버 함수"라고합니다. 비 멤버 함수와의 주요 차이점은 클래스 외부에서 참조하는 경우 ::범위 확인 연산자를 사용하여 범위를 지정해야한다는 것 입니다. 예를 들어, 함수 포인터를 얻으려면 사용 &myclass::myfunction하고 호출하려면 use myclass::myfunction (arg);.

이런 종류의 것은 원래 C ++가 아닌 C 용으로 설계된 이전 Win32 API를 사용할 때 매우 일반적입니다. 물론이 경우 매개 변수는 일반적으로 포인터가 아닌 LPARAM 또는 유사하며 일부 캐스팅이 필요합니다.


일반적으로 C 스타일 함수를 의미하는 경우 'myfunction'은 표준 함수가 아닙니다. 'myfunction'은 더 정확하게 myclass의 메소드라고 불립니다. 클래스의 메소드는 'this'포인터가 아닌 C 스타일 함수가없는 것을 가지고 있다는 점에서 일반 함수와 다릅니다.
에릭

3
부스트를 사용하도록 조언하는 것은 극적입니다. 메서드 포인터를 사용하는 실질적인 이유가 있습니다. 나는 부스트를 대안으로 언급하는 것을 신경 쓰지 않지만 누군가가 모든 사실을 모르고 다른 사람이 그것을 사용해야한다고 말하는 것을 싫어합니다. 부스트는 비용이 듭니다! 그리고 이것이 임베디드 플랫폼이라면 가능한 선택이 아닐 수도 있습니다. 이 외에도 나는 당신의 글을 정말 좋아합니다.
에릭

@Eric-두 번째 요점에서 저는 "Bust를 사용하라"고 말하려는 의도가 없었으며 실제로 Boost를 직접 사용한 적이 없습니다. 의도는 (3 년 후 내가 아는 한) 사람들이 대안을 찾고 몇 가지 가능성을 나열해야한다는 것이 었습니다. "또는 무엇이든"은 목록이 완전하지 않음을 나타냅니다. 멤버 포인터는 가독성에 비용이 듭니다. 간결한 소스 표현은 또한 런타임 비용을 위장 할 수 있습니다. 특히 메서드에 대한 멤버 포인터는 비가 상 메서드와 가상 메서드를 모두 처리해야하며 어떤 메서드를 알아야합니다.
Steve314

@Eric-그뿐만 아니라 이러한 문제가 멤버 포인터를 사용할 수없는 이유입니다. 적어도 과거에는 Visual C ++에 멤버 포인터 형식을 나타내는 방법에 대한 추가 단서가 필요했습니다. 나는 임베디드 시스템에 정적 함수 접근 방식을 사용합니다. 포인터의 표현은 다른 함수 포인터와 동일하고 비용이 분명하며 이식성 문제가 없습니다. 그리고 정적 멤버 함수에 의해 래핑 된 호출은 호출이 가상인지 여부를 (컴파일 타임에) 알고 있습니다. 가상 메서드에 대한 일반적인 vtable 조회 외에는 런타임 검사가 필요하지 않습니다.
Steve314

@Eric-첫 번째 요점에서는 정적 멤버 함수가 C 스타일 함수와 정확히 같지 않다는 것을 알고 있지만 (따라서 "범위 문제는 제쳐두고") 이름을 포함해야 할 것입니다.
Steve314

18
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference

2
이어야합니다 : (pDog-> * doSomething) (); // pDog가 포인터 인 경우 // (pDog. * doSomething) (); // () 연산자가 더 높은 우선 순위를 가지므로 pDog가 참조이면-> * 및. *.
Tomek

12

최소 실행 가능 예

main.cpp

#include <cassert>

class C {
    public:
        int i;
        C(int i) : i(i) {}
        int m(int j) { return this->i + j; }
};

int main() {
    // Get a method pointer.
    int (C::*p)(int) = &C::m;

    // Create a test object.
    C c(1);
    C *cp = &c;

    // Operator .*
    assert((c.*p)(2) == 3);

    // Operator ->*
    assert((cp->*p)(2) == 3);
}

컴파일 및 실행 :

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

Ubuntu 18.04에서 테스트되었습니다.

괄호의 순서를 변경하거나 생략 할 수 없습니다. 다음은 작동하지 않습니다.

c.*p(2)
c.*(p)(2)

GCC 9.2는 다음과 함께 실패합니다.

main.cpp: In function int main()’:
main.cpp:19:18: error: must use ‘.*’ or ‘->*’ to call pointer-to-member function in p (...)’, e.g. ‘(... ->* p) (...)’
   19 |     assert(c.*p(2) == 3);
      |

C ++ 11 표준

.*->*있는 하나의 운영 이 목적을 위해 C ++에 도입하고, C.에 존재하지

C ++ 11 N3337 표준 초안 :

  • 2.13 "연산자와의 부호"에 포함 된 모든 사업자의 목록을 가지고 .*->*.
  • 5.5 "Pointer-to-member 연산자"는 그들이하는 일을 설명합니다.

11

메서드에서 함수 포인터 (메서드 포인터가 아님)를 만드는 방법을 배우기 위해 여기에 왔지만 여기에 대한 답변은 해결책을 제공하지 않습니다. 내가 생각해 낸 것은 다음과 같습니다.

template <class T> struct MethodHelper;
template <class C, class Ret, class... Args> struct MethodHelper<Ret (C::*)(Args...)> {
    using T = Ret (C::*)(Args...);
    template <T m> static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

#define METHOD_FP(m) MethodHelper<decltype(m)>::call<m>

따라서 예를 들어 이제 다음을 수행합니다.

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = METHOD_FP(&Dog::bark);
(*bark)(&dog); // or simply bark(&dog)

편집 :
C ++ 17을 사용하면 더 나은 솔루션이 있습니다.

template <auto m> struct MethodHelper;
template <class C, class Ret, class... Args, Ret (C::*m)(Args...)> struct MethodHelper<m> {
    static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

매크로없이 직접 사용할 수 있습니다.

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = MethodHelper<&Dog::bark>::call;
(*bark)(&dog); // or simply bark(&dog)

수정자가있는 메서드의 경우 다음과 같은 const전문화가 더 필요할 수 있습니다.

template <class C, class Ret, class... Args, Ret (C::*m)(Args...) const> struct MethodHelper<m> {
    static Ret call(const C* object, Args... args) {
        return (object->*m)(args...);
    }
};

6

함수 포인터를 사용하여 멤버 함수를 호출 할 수없는 이유는 일반 함수 포인터가 일반적으로 함수의 메모리 주소 일 뿐이 기 때문입니다.

멤버 함수를 호출하려면 다음 두 가지를 알아야합니다.

  • 호출 할 멤버 함수
  • 사용해야하는 인스턴스 (멤버 함수)

일반 함수 포인터는 둘 다 저장할 수 없습니다. C ++ 멤버 함수 포인터는 a)를 저장하는 데 사용되므로 멤버 함수 포인터를 호출 할 때 인스턴스를 명시 적으로 지정해야합니다.


1
나는 이것을 찬성했지만 OP가 "어떤 인스턴스"로 무엇을 언급하고 있는지 알지 못하는 경우를 대비하여 설명 포인트를 추가 할 것입니다. 내재 된 'this'포인터를 설명하기 위해 확장합니다.
에릭

6

클래스 멤버에 대한 함수 포인터는 boost :: function 사용에 정말 적합한 문제입니다. 작은 예 :

#include <boost/function.hpp>
#include <iostream>

class Dog 
{
public:
   Dog (int i) : tmp(i) {}
   void bark ()
   {
      std::cout << "woof: " << tmp << std::endl;
   }
private:
   int tmp;
};



int main()
{
   Dog* pDog1 = new Dog (1);
   Dog* pDog2 = new Dog (2);

   //BarkFunction pBark = &Dog::bark;
   boost::function<void (Dog*)> f1 = &Dog::bark;

   f1(pDog1);
   f1(pDog2);
}

2

새 객체를 만들려면 위에서 언급 한대로 new 배치를 사용하거나 클래스에서 객체의 복사본을 만드는 clone () 메서드를 구현하도록 할 수 있습니다. 그런 다음 위에서 설명한대로 멤버 함수 포인터를 사용하여이 복제 메서드를 호출하여 개체의 새 인스턴스를 만들 수 있습니다. 복제의 장점은 때때로 객체의 유형을 모르는 기본 클래스에 대한 포인터로 작업 할 수 있다는 것입니다. 이 경우 clone () 메서드를 사용하는 것이 더 쉬울 수 있습니다. 또한 clone ()을 사용하면 원하는 경우 개체의 상태를 복사 할 수 있습니다.


클론은 비용이 많이들 수 있으며 성능이 문제가되거나 우려되는 경우 OP는이를 피하려고 할 수 있습니다.
에릭

0

std :: function 및 std :: bind ..

나는 이벤트 유형 (그냥 const unsigned int, 큰 네임 스페이스 범위 열거 형)을 해당 이벤트 유형에 대한 핸들러 벡터에 매핑하는 unorder_map에 핸들러 벡터를 저장하는이 EventManager 클래스를 작성했습니다.

EventManagerTests 클래스에서 다음과 같이 이벤트 핸들러를 설정했습니다.

auto delegate = std::bind(&EventManagerTests::OnKeyDown, this, std::placeholders::_1);
event_manager.AddEventListener(kEventKeyDown, delegate);

다음은 AddEventListener 함수입니다.

std::vector<EventHandler>::iterator EventManager::AddEventListener(EventType _event_type, EventHandler _handler)
{
    if (listeners_.count(_event_type) == 0) 
    {
        listeners_.emplace(_event_type, new std::vector<EventHandler>());
    }
    std::vector<EventHandler>::iterator it = listeners_[_event_type]->end();
    listeners_[_event_type]->push_back(_handler);       
    return it;
}

다음은 EventHandler 유형 정의입니다.

typedef std::function<void(Event *)> EventHandler;

그런 다음 EventManagerTests :: RaiseEvent로 돌아가서 다음을 수행합니다.

Engine::KeyDownEvent event(39);
event_manager.RaiseEvent(1, (Engine::Event*) & event);

EventManager :: RaiseEvent에 대한 코드는 다음과 같습니다.

void EventManager::RaiseEvent(EventType _event_type, Event * _event)
{
    if (listeners_.count(_event_type) > 0)
    {
        std::vector<EventHandler> * vec = listeners_[_event_type];
        std::for_each(
            begin(*vec), 
            end(*vec), 
            [_event](EventHandler handler) mutable 
            {
                (handler)(_event);
            }
        );
    }
}

작동합니다. EventManagerTests :: OnKeyDown에서 호출을받습니다. 벡터를 삭제해야 정리 시간이 오지만 일단 삭제하면 누수가 없습니다. 이벤트를 발생시키는 데는 내 컴퓨터에서 약 5 마이크로 초가 소요됩니다. 이는 2008 년경입니다. 정확히 초고속은 아니지만. 내가 그것을 알고 있고 울트라 핫 코드에서 사용하지 않는 한 충분히 공정합니다.

내 자신의 std :: function 및 std :: bind를 굴려서 속도를 높이고 싶고 정렬되지 않은 벡터 맵 대신 배열 배열을 사용하고 싶지만 멤버 함수를 저장하는 방법을 알지 못했습니다. 포인터를 호출하고 호출되는 클래스에 대해 아무것도 모르는 코드에서 호출합니다. 속눈썹의 대답은 매우 흥미로워 보입니다 ..

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.