this adjustment

MSVC 컴파일러에서 상속시 발생할 수 있는 this 포인터 문제


Callback 인터페이스를 제공하는 클래스를 디자인하던 중 발생한 this 포인터 문제에 대해서 정리하기 위해서 작성하였음. 빌드 환경은 윈도우 + MSVC컴파일러.

this 포인터의 잘못된 참조

목표는 몇 가지 콜백함수를 등록하고, 특정 상황에 맞게 호출할 수 있는 인터페이스를 제공하는 클래스를 디자인하는 것이다. 여러 방법 중 해당 클래스를 상속하여 인터페이스를 이용하는 방식을 선택하였다.

먼저 상속관계에서 콜백을 호출하는 간단한 테스트를 진행하였고, 그 과정에서 문제를 발견하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ICallback
{
protected:
typedef void(ICallback::* Callback)();

void CallProc(Callback proc)
{
(this->*proc)();
}
};

class Test : public SomeClasses, public ICallback
{
int data{ 10 };
public:
void RegisterFoo()
{
CallProc((Callback)&Test::foo);
}

void foo()
{
printf("%d\n", this->data);
}
};

int main()
{
Test* t = new Test;
t->RegisterFoo();
}

위 코드에서 Test클래스는 ICallback클래스 말고도 다른 클래스들을 상속할 수 있다. 이때 경우에 따라서 Test::foo()의 결과가 달라진다. 어떤 경우에는 정확한 값인 10을 출력하지만 다른 경우에는 쓰레기값을 출력한다.

이러한 결과는 this포인터를 잘못 참조할 경우 나타난다. 즉, tthis(ICallback*)tthis가 서로 다른경우 문제가 발생한다.

예상되는 문제의 원인은 ICallback::CallProc()에서 콜백을 호출하는 과정에서 this포인터의 implicit down casting 이 이뤄진다. 이 부분에서 문제가 발생하는 것 같다.



버그?

이 과정에서 신기한점을 발견하였다. 멤버함수 포인터를 struct, class, union중 하나의 멤버로 전방선언을 하면 콜백호출과정에서 this가 정확하게 조정된다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ICallback;
struct
{
typedef void(ICallback::* Callback)(); // 전방선언
};

class ICallback
{
...
};

class Test : public SomeClasses, public ICallback
{
...
};

...
...
...

왜 단순한 전방선언만으로 문제를 해결할 수 있는지는 아직도 이유를 모르겠다.



this 포인터

클래스 인스턴스의 this포인터가 가리키는 대상은 클래스 구조에 따라 달라진다. 하위 클래스의 this포인터는 항상 클래스 데이터의 시작 메모리 주소이지만, 클래스 구조에 따라 메모리에 데이터가 적층되는 순서와 종류가 다르기 때문에 그 대상이 달라진다. 일반적인 경우를 제외하고 상속구조에서의 this포인터가 참조하는 메모리는 몇 가지 규칙에 의해 정해진다.
MSVC에서 실험한 결과는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
class BaseA
{ ... };
class BaseB
{ ... };
class Derived : public BaseA, public BaseB
{ ... };

Derived* d = new Derived;
BaseA* ba = (BaseA*)d;
BaseB* bb = (BaseB*)d;
  1. BaseA, BaseB둘 다 vtable이 없는 경우에는 상속의 역순으로 메모리에 적층된다.
1
2
3
4
5
6
7
//    ┌───────────────┐ ◄──── this of d and ba
// │ data of BaseA │
// ├───────────────┤ ◄──── this of bb
// │ data of BaseB │
// ├───────────────┤
// │data of Derived│
// └───────────────┘
  1. BaseA혹은 BaseBvtable을 가지고 있다면 해당 클래스는 후순위에 메모리에 적층된다. 만약 둘 다 가지고 있다면 1에서처럼 상속 순서에 영향을 받는다.
1
2
3
4
5
6
7
8
9
10
11
//    ┌─────────────────┐ ◄──── this d and ba
// │ vtable of BaseA │
// ├─────────────────┤
// │ data of BaseA │
// ├─────────────────┤ ◄──── this of bb
// │ vtable of BaseB │
// ├─────────────────┤
// │ data of BaseB │
// ├─────────────────┤
// │ data of Derived │
// └─────────────────┘

이 예제에서 처럼 this포인터를 dba가 공유하고 bb는 다른 위치를 참조한다. 이렇기 때문에 알맞은 vtable을 참조할 수 있다. 그리고 만약 필요하다면 thunk를 통해 this위치를 조정한다.



this adjustment

다시 문제로 돌아와서, 콜백호출과정에서 this의 잘못된 참조를 해결하기 위해서는 this를 강제로 조정해줘야 한다는 생각이 들었다. 위에서 본것과 같이 this를 명시적으로 캐스팅한다면 그 값이 정확하다는 점을 통해서 문제를 해결하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ICallback
{
protected:
template <typename T>
void CallProc(void(T::* proc)())
{
((T*)this->*proc)();
}
};

class Test : public SomeClasses, public ICallback
{
int data{ 10 };
public:
void RegisterFoo()
{
CallProc(&Test::foo);
}
// ...
};

이제 단순 호출에는 문제없지만, 내가 디자인하고자 했던 인터페이스 클래스는 콜백리소스를 직접 관리해야했다.

해당 콜백을 올바르게 호출하기 위한 this adjustment 정보와 콜백을 함께 관리하고, 콜백호출시 해당 정보를 이용하여 this를 알맞게 조절하도록 하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ICallback
{
typedef void(ICallback::* Callback)();
struct CallbackInfo
{
Callback proc;
std::ptrdiff_t thisAdjustment;
}
std::vector<CallbackInfo> infos;

protected:
template <typename T>
void RegisterCallback(Callback proc)
{
std::intptr_t adjustment{(std::intptr_t)this - (std::intptr_t)((T*)this)};
infos.emplace_back(proc, adjustment);
}

void CallProc(size_t i)
{
ICallback* adjThis{ (ICallback*)((std::uintptr_t)this - infos[i].thisAdjustment) };
adjThis->*infos[i].proc)();
}
};

콜백호출 시 this의 값이 조정되는 up/down casting 대신 값이 변하지 않는 type casting 을 사용하여 this의 값을 직접 조정하였다.
이제 기존에 문제가 발생하던 상황에도 문제없이 콜백을 호출한다.

굳이 콜백을 호출할 때 마다 std::ptrdiff_tthis를 조정하는 대신 직접 (T*)this를 저장해서 사용해도 될것이다.


Author

Joyus.Gim

Posted on

2022-05-01

Updated on

2022-07-19

Licensed under

Comments