함수 포인터로 함수명 변경하기 (완결)

1. 함수 포인터로 함수명 변경하기 (완성)

지난 번에 함수 포인터로 함수명을 변경하지 못했다. 그 원인은 클래스 내부 함수의 주소는 클래스 주소 + 클래스 내부 함수 주소 로 되어있기 때문이다.

이후에 간간히 내 작업을 하면서 방법을 고민했다. 그러던중에 stl에서 지원하는 함수 포인터가 있다는 말을 들었다.

1.1. std::function

C++11에서부터 지원하는 이것은

  1. 함수 포인터의 반환값이 명시적으로 같은 타입이 아니면 컴파일 에러 발생 (오히려 그게 더 안전할 수도 있긴하다.)
  2. 오로지 함수만 호환이 가능하다. (람다, 멤버함수 포인터, 람다함수등 호환불가능)

의 단점을 보완하고

  1. 반환값의 암시적 형변환이 가능하다.
  2. 멤버함수, 람다, 람다함수등 호환가능

의 장점을 지닌다.

1.2. std::bind

일련의 인자들로 함수에 바인딩시킨 함수 객체를 반환하는 함수 템플릿


std::placeHolder를 활용하여 추가 인수 bind bind의 인자 대신 std::placeHolders::_1, _2, _3... _N을 입력


클래스 멤버함수를 바인딩할 경우, 해당 객체를 받아야한다. 문제는 객체를 그대로 인자로 넘겨주면 복사가 발생하기에 std::ref 를 이용하여 참조로 넘겨야한다.



이것을 이용해서 이전에 내가 만들려던 상속받은 클래스의 함수 이름을 변경하는 기능을 만들 수 있었다.


template <typename T>  
class Container {
public: 
     virtual T* Push() { //... }
     
protected: //.... 
}; 
     
class Object {
public: //...
};

class ObjectContainer 
    :public Container<Object> {
public: 
    typedef Object* (Container::*Push)();
    
    Push PushObject = &Container::Push;
};

int main() {
    ObjectContainer obj_container;  
    obj_container.PushOjbect();
}

이게 이전의 코드다. 이 코드의 문제는 컴파일러가 멤버함수를 호출하기위한 클래스의 주소를 모른다는 점이다. 이를 알려줄 수는 있지만,


(obj_container.*(obj_container->PushOjbect))();


로 명시해줘야기에 적어도 내가 보기엔 좋지못했다.

하지만 std::bind의 두번째인자에 객체를 알려주기에 이런 문제점을 해결할 수 있다.

template <typename T>
class Container
{
protected:
    virtual void Push() { printf("Container::Push \n"); }
};

class Object
{
public:
    //...
};

class ObjectContainer
    :public Container<Object>
{
public:
    ObjectContainer() {
        f = std::bind(&ObjectContainer::Push, std::ref(*this));
        f();
    }

    std::function<void()> f;
};

int main()
{	
    ObjectContainer obj_container;
    return 0;
}

2. 성능 비교

음...이걸 만들면서 대체품으로 생각했던게 상속받는 클래스안에 그냥 해당 함수를 호출하는 함수로 래핑시키는게 있었다.


나쁘지않았고, 오히려 그게 더 정답이었다고 생각했다. 그래도 일단 만들어보자했으니 만들었고, 이 두 개는 무슨 차이가 있을까싶어서 비교해보았다.


class ObjectContainer
    :public Container<Object>
{
public:

    ObjectContainer() {
        f = std::bind(&ObjectContainer::Push, std::ref(*this));
        cout << sizeof(f) << endl;
        cout << sizeof(&ObjectContainer::Push) << endl;
        cout << sizeof(&ObjectContainer::ObjectPush) << endl;
    }

    std::function<void()> f;
    

    void ObjectPush() { Push(); }
};

int main()
{	
    ObjectContainer obj_container;

    obj_container.f();
    obj_container.ObjectPush();


    getchar();

    return 0;
}

2.1. 디스어셈블리로 비교

나는 디스어셈블리를 볼 줄 모른다. 그냥 어셈블리어는 명령어가 단순하니 이런 뜻이겠거니~하고 유추할뿐.


아니라면 어쩔 수 없지ㅎㅎ 라는 가벼운 느낌으로 비교하는거니 넘겨도 된다. 애초에 이렇게 비교하는게 맞는지도 모르고..

2.1.1. 선언시

void ObjectPush() { Push(); }
000000013FE92B20  mov         qword ptr [rsp+8],rcx  
000000013FE92B25  push        rbp  
000000013FE92B26  push        rdi  
000000013FE92B27  sub         rsp,0E8h  
000000013FE92B2E  lea         rbp,[rsp+20h]  
000000013FE92B33  mov         rdi,rsp  
000000013FE92B36  mov         ecx,3Ah  
000000013FE92B3B  mov         eax,0CCCCCCCCh  
000000013FE92B40  rep stos    dword ptr [rdi]  
000000013FE92B42  mov         rcx,qword ptr [rsp+108h]  
000000013FE92B4A  mov         rax,qword ptr [this]  
000000013FE92B51  mov         rax,qword ptr [rax]  
000000013FE92B54  mov         rcx,qword ptr [this]  
000000013FE92B5B  call        qword ptr [rax]  
000000013FE92B5D  lea         rsp,[rbp+0C8h]  
000000013FE92B64  pop         rdi  

음 정확하게는 모르겠지만, ObjectPush의 메모리를 할당하는 것같다.


문제는 function이다. 어쨋든 function도 변수라서 큰 ...이걸 std::bind를 호출하는 순간이 문제다. stl인 function.h으로 들어가보면 알겠지만, 이거 완전 템플릿으로 떡칠해놨다.

큰 가락만 파악하고 포기했었는데, Binder라는 템플릿 클래스가 있다.


  1. Binder는 당연히 함수의 주소와 객체(멤버함수라면), 함수의 인자를 저장한다.

  2. _Compressed_pair에 객체 주소와 인자들이 저장된다. _Compressed_pair는

    template <class First, class... _Types>
    class Binder{

    _Compressed_pair<decay_t<First>, tuple<decay_t<_Types>...>>

    //... }

로 선언되어있는데, 이 decay_t는


using type = conditional_t<is_array_v<_Ty1>,
	add_pointer_t<remove_extent_t<_Ty1>>,
	conditional_t<is_function_v<_Ty1>,
		add_pointer_t<_Ty1>,
		remove_cv_t<_Ty1>>>;    


으로 정의되어있다, 지금은 일단 패스하겠다. 이해도 못할 뿐더러 알아도 크게 상관이 없을 것같다.


  1. _Compressed_pair는 <함수의 주소, 가변인자들...>을 가공해서 저장한다.
  2. 가공이 정상적으로 완료되면 Binder를 리턴
  3. std::function은 =연산자 오버로딩을 이용해서....


여기서부터 도저히 모르겠다. 구조도 모르겠다. union을 저장공간으로 이용하는 곳부터 이해가 되질 않았기에 멈추었다.

말이 다른 곳으로 새어버렸는데, 말하고 싶은 것은 함수 하나를 저장하는 것이 자식 클래스에서 함수를 하나 생성하는 것보다 몇배나 많은 연산이 필요하다는 것이다.


2.1.2. 호출시

obj_container.f();
000000013FA74C64  lea         rcx,[rbp+20h]  
000000013FA74C68  call        std::_Func_class<void>::operator() (013FA716C7h)  
obj_container.ObjectPush();
000000013FA74C6D  lea         rcx,[obj_container] 
000000013FA74C71  call        ObjectContainer::ObjectPush (013FA71244h)  

호출은 차이가 없다. 동일.

2.1.3. 메모리

다음으로 비교한 것은 용량이었다. 과연 우리의 std::function의 크기는 얼마나 될까? sizeof로 비교해본 결과...

  1. 멤버함수의 크기는 8바이트다.
  2. std::function의 크기는 64바이트다.

....용량면에서 std::function은 멤버함수보다 무려 8배나 더 크다.

3. 정리

음...std::function이 함수포인터보다 좋지않다는 결론을 쓰기는 했지만, 그렇다고 std::function의 강점을 무시할 수는 없다. 개인적인 프로젝트를 만들면서 std::function을 썼는데, 확실히 함수 포인터보다는 쓰기도 쉽고 확장성이 더 좋다.

이 경우에는, 내가 용도에 맞지않게 썼다는 점이 맞을 것이다. 함수 포인터는 함수 포인터의 용도가, std::function에는 그에 맞는 용도가 있다.

'프로그래밍 > 프로그래밍 관련' 카테고리의 다른 글

std::tuple을 알아보자.  (0) 2018.04.26
Lambda (람다)  (0) 2018.04.25
Epoll  (0) 2018.03.22
함수포인터로 함수명 변경하기  (0) 2018.03.22
declspec에 대해  (0) 2018.02.23
더보기

댓글,

Lowpoly

게임 서버 프로그래머 지망생