[C++] 클래스의 멤버 함수를 가리키는 멤버 함수 포인터

반응형

함수 포인터( function pointer )는 함수를 지시하는 포인터( pointer )입니다.

 

[C++] 함수를 전달하기 위한 함수 포인터( function pointer )

함수 포인터( function pointer )포인터( pointer )란 객체의 메모리 주소를 저장하는 변수입니다.이러한 포인터 중에, 함수의 메모리 주소를 저장하는 포인터를 함수 포인터라고 합니다.int var{ 5 };int* pt

codingbonfire.tistory.com

그리고, 이러한 함수 포인터 중에서 클래스의 멤버 함수를 가리키는 포인터를 멤버 함수 포인터( member function pointer )라고 합니다.

 

윗글에서, 함수 포인터는 함수 타입과 * 기호를 사용해서 함수 포인터를 정의한다고 했습니다.

 

int add(int a, int b){  // 일반 함수
    return a + b;
}

int main(){

    // add 함수의 타입은 int(int,int)
    static_assert( 
        is_same_v< decltype( add ), int(int,int) > == true);

    int (*pFunc1)(int, int) = &add;  // 일반 함수 포인터
    cout << (*pFunc1)(2, 3) << endl;   // 5
}

위의 add 함수의 타입은 int(int, int)입니다.

그리고, 이러한 타입의 함수를 가리키는 함수 포인터는 int(*pFunc1)(int, int)와 같이 정의할 수 있습니다.

 

참고로, 위의 decltype(add)add 함수의 타입을 나타냅니다.

그리고, std::is_same_v는 주어진 두 타입이 같은 타입인지 확인할 수 있는 함수 템플릿입니다.

 

이와 마찬가지로, 멤버 함수 포인터는 멤버 함수의 타입과 * 기호를 사용해서 정의됩니다.

class CSomething{
    int m_nValue{};
public:
    CSomething() = default;
    CSomething(int val) : m_nValue(val){}
    
    int add(int a, int b){ // 멤버 함수
        return m_nValue + a + b;
    }
};

int add(int a, int b){  // 일반 함수
    return a + b;
}

int main(){

    // CSomething::add 함수의 타입
    static_assert( 
        is_same_v< decltype( CSomething::add ), int( CSomething::*)(int,int) > == true);

    // 멤버 함수 포인터의 정의
    int (CSomething::*pFunc2)(int, int) = CSomething::add;
    
    CSomething c1(10);
    cout << (c1.*pFunc2)(2, 3) << endl;   // 5
}

위에서 보는 것처럼, add 멤버 함수의 타입은 int( CSomething::* )(int, int)입니다.

따라서, 이 멤버 함수를 가리키는 멤버 함수 포인터는 다음과 같습니다.

// 비-정적 멤버 함수 포인터의 정의
int (CSomething::*pFunc2)(int, int) = &CSomething::add;

// & 연산자를 사용하지 않아도 괜찮음
int (CSomething::*pFunc3)(int, int) = CSomething::add;

그리고, 이 멤버 함수 포인터가 가리키는 add 함수는 일반 함수가 아니라, 클래스의 멤버 함수이기 때문에, 클래스명 CSomething과 범위 지정 연산자( scope resolution operator ) ::를 사용해서 add 함수가 멤버 함수임을 명시해야 합니다.

 

위의 첫 번째 문장에선 멤버 함수 포인터를 초기화하기 위해, 주소 연산자 &를 사용하여 멤버 함수의 주소를 구했지만, & 연산자를 사용하지 않더라도 컴파일러는 함수명을 암시적으로 함수의 주소로 변환하기 때문에 오류가 되지 않습니다.

 

역참조 연산자 *를 통한 멤버 함수 호출

함수 포인터에 역참조 연산자 *를 사용해, 가리키고 있는 함수를 호출하는 것과 마찬가지로, 멤버 함수 포인터에 * 연산자를 사용해 멤버 함수를 호출할 수 있습니다.

그렇지만, 비-정적 멤버 함수( non-static member function )는 클래스 객체를 통해서만 호출할 수 있다는 점이, 일반 함수 포인터를 사용할 때와 다른 부분입니다.

int main(){

    int (*pFunc1)(int, int) = add;  // 일반 함수 포인터
    cout << (*pFunc1)(2, 3) << endl;   // 5
    
    // 비-정적 멤버 함수 포인터
    int (CSomething::*pFunc2)(int, int) = CSomething::add;

    CSomething c1(10);  // 클래스 객체
    cout << (c1.*pFunc2)(2, 3) << endl;   // 5
}

그리고, 함수 포인터의 경우에는 역참조 연산자를 사용하지 않더라도, 컴파일러가 알아서 함수 포인터가 가리키는 함수를 호출하지만, 멤버 함수 포인터로부터 멤버 함수를 호출할 때는 반드시 역참조 연산자 *를 사용해야 합니다.

int main(){

    int (*pFunc1)(int, int) = add;  // 일반 함수 포인터
    cout << pFunc1(2, 3) << endl;   // 역참조 연산자를 사용하지 않을 수 있음
    
    // 비-정적 멤버 함수 포인터
    int (CSomething::*pFunc2)(int, int) = CSomething::add;

    CSomething c1(10);  // 클래스 객체
    cout << (c1.pFunc2)(2, 3) << endl;   // error!. 역참조 연산자를 반드시 사용해야 됨
}

▼출력

error: 'class CSomething' has no member named 'pFunc2'

멤버 함수 포인터에 역참조 연산자를 사용하지 않으면, 컴파일러는 위의 pFunc2를 클래스 CSomething의 멤버 함수로 오해해서, 존재하지 않는 함수를 호출한 것에 대한 오류를 발생시키기 때문입니다.

 

 

그리고, 객체를 명시해야 멤버 함수 포인터를 통해서 그 객체의 멤버 함수에 접근할 수 있으므로, 이러한 멤버 함수 포인터를 클래스 내부에서 사용하려면, this 포인터를 사용해야 합니다.

class CSomething{
    int m_nValue;
    
public:

    CSomething(int val) : m_nValue(val){}
    
    int add(int a, int b){	// 멤버 함수
        return m_nValue + a + b;
    }

    int add2(int a, int b){
        
        // 멤버 함수 포인터 정의
        // 클래스 내의 함수이므로 add 앞에 something:: 을 붙일 필요없습니다.
        int (CSomething::*pFunc)(int, int) = add;
        
        // this 객체를 명시
        return (this->*pFunc)(a,b);
    }
};

this는 포인터( pointer )이므로, 위의 (this->*pFunc)(a, b) 대신, this에 역참조 연산자를 사용해서, (*this.*pFunc)(a, b) 형태로 클래스의 멤버 함수를 호출할 수도 있습니다.

 

참고로, this 포인터는 비-정적 멤버 함수가 호출될 때, 컴파일러가 전달해 주는 객체의 포인터입니다.

따라서, 정적 멤버 함수가 호출될 때는 매개변수를 통해서 객체를 전달받지 않으면, 멤버 함수 포인터를 사용할 수 없습니다.

 

멤버 함수 포인터를 사용한 예

당연히 멤버 함수의 주소를 저장하는 멤버 함수 포인터 또한 클래스 멤버가 될 수 있습니다.

이러한 멤버 함수 포인터는 주로 멤버 함수에 다른 멤버 함수를 전달하거나, 다음에 실행될 것으로 예상되는 멤버 함수의 주소를 저장했다가, 필요한 경우 그 멤버 함수를 호출하는 용도로 사용됩니다.

 

아래의 예문은, 설정된 기능을 멤버 함수 포인터에 저장했다가, doAction 함수를 호출하면 그 설정된 기능을 실행하는 클래스를 보여줍니다.

enum Action{    // 범위없는 enum
    act_play,
    act_pause,
    act_stop,
};

class CAction{
private:
    bool play(){ std::cout << "playing...\n"; }
    bool pause(){ std::cout << "pause...\n"; }
    bool stop(){ std::cout << "stopped...\n"; }

    //bool (*m_funcPtr)();    // 함수 포인터
    bool (CAction::*m_funcPtr)();    // 멤버 함수 포인터

    void setFuncPtr( bool (CAction::*ptr)() ){	// 멤버 함수의 전달
        m_funcPtr = ptr;
    }

public:
    CAction() : m_funcPtr(stop){}   // 멤버 초기화 리스트

    void setAction( Action ac){
        switch(ac){
            case act_play: setFuncPtr(play); break;
            case act_pause: setFuncPtr(pause); break;
            case act_stop: setFuncPtr(stop); break;
        }
    }

    bool doAction(){
        return (this->*m_funcPtr)();
    }
};

int main(){

    CAction action;
    action.setAction( act_play);
    action.doAction();
}

위에서 주의할 점은, 멤버 함수 포인터 m_funcPtr의 타입이 bool (*)()이 아니라 bool ( CAction::*)()이라는 것입니다.

이것은 ( 눈에는 그렇게 보이지 않지만 ) 멤버 함수 play의 타입이 bool ( CAction::*)()이기 때문입니다.

 

위 예문에서, 멤버 함수 포인터 타입의 별명( alias )을 만들면, 좀 더 읽기 쉬운 코드를 작성할 수 있습니다.

using FuncPtr = bool (CAction::*)();	// 멤버 함수 포인터의 별명

FuncPtr m_funcPtr;    // 멤버 함수 포인터 정의

void setFuncPtr( FuncPtr ptr ){
    m_funcPtr = ptr;
}

 

정적 멤버 함수 포인터( static member function pointer )

정적 멤버 함수는 클래스 내부에서 정의된다는 것을 제외하면, 일반 함수와 차이가 없습니다.

class CSomething{
public:

    static int add(int a, int b){	// 정적 멤버 함수
    	cout << "static function called\n";
        return a + b;
    }
};

int add(int a, int b){  // 일반 함수
    return a + b;
}

int main(){

    static_assert( 
        is_same_v< decltype( add ), int(int,int) > == true);

    static_assert( 
        is_same_v< decltype( CSomething::add ), int(int,int) > == true);
}

일반 함수 add와 정적 멤버 함수 CSomething::add의 타입은 int (int, int)로 동일합니다.

그렇기 때문에, 정적 멤버 함수 포인터가 클래스의 정적 멤버 함수를 가리킨다는 점을 제외하면 일반 함수 포인터와 마찬가지로 정의됩니다.

int main(){

    int (*pFunc1)(int, int) = add;  // 일반 함수 포인터
    cout << pFunc1(2, 3) << endl;   // 5
    
    // 정적 멤버 함수 포인터
    int (*pFunc2)(int, int) = CSomething::add;
    cout << pFunc2(2, 3) << endl;   // 5 
}

위의 일반 함수 포인터 pFunc1과 정적 멤버 함수 포인터 pFunc2는 초기화 표현식만 다를 뿐, 선언하는 방법이나 사용법은 완전히 동일합니다.

 

정리

  • 멤버 함수 포인터는 멤버 함수의 타입과 * 기호로 정의되는 함수 포인터입니다.
  • 멤버 함수 포인터에 역참조 연산자를 사용하여 클래스 객체의 멤버 함수에 접근할 수 있습니다.
  • 비-정적 멤버 함수 포인터가 가리키는 함수에 접근하려면, 클래스 객체를 통해야 합니다.
  • 정적 멤버 함수 포인터는 일반 함수 포인터와 차이가 없습니다.

 

 

이 글과 관련 있는 글들

표현식의 구체적인 타입을 알려주는 decltype

클래스 생성자의 멤버 초기화 리스트( member initializer list )

몇 가지 값만 가질 수 있는 타입: unscoped enum

호출 가능한 객체를 저장하는 std::function

 

 

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유