클래스의 virtual function table과 가상함수의 동적바인딩이 잘 안 됩니다.

eagle76fish의 이미지

가상함수 테이블(virtual function table)은 컴파일 타임에 생성이 되고, 가상함수 테이블의 대상인 클래스로부터 생성된 객체 포인터의 vptr은 실행시에 동적으로 결정되는 것으로 알고 있습니다. 그래서 실습코드를 만들어보던 중에 동적결합의 이상유무를 발견하게 되었습니다. 코드는 다음과 같습니다.

class TopC {
public:
virtual void QueryInterface()=0;
};

class MiddleC : public TopC {
public:
virtual void QueryInterface() {
printf("MiddleC::QueryInterface\n");
}
virtual void QueryInterface(const char* name) {
printf("MiddleC::QueryInterface(const char* name), %s\n", name);
}
};
class ImpC : public MiddleC {
public:
virtual void QueryInterface() {
printf("ImpC::QueryInterface()\n");
}
};
class OverC : public ImpC {
public:
virtual void QueryInterface() {
printf("OverC::QueryInterface()\n");
}
};

클래스의 계층구조를 위와 같이 설계하고, 다음과 같이 코드를 언급했습니다.

ImpC* PIpC2=new ImpC;
PImpC2->QueryInterface();//ImpC::QueryInterface() 호출
//PImpC2->QueryInterface("huge");//컴파일 오류
MiddleC* PMiddleC2=PImpC2;
PMiddleC2->QueryInterface();//ImpC::QueryInterface() 호출
PMiddleC2->QueryInterface("huge");//컴파일이 안되야 하는데, 호출까지 이루어짐.//MiddleC::QueryInterface()

PImpC2 포인터 변수와 PiddleC2 포인터 변수는

new ImpC

로 생성된 객체를 가리키고, 그렇기 때문에 같은 vptr을 공유합니다. 그래고 해당 vptr은 ImpC 클래스의 vtable을 가리킵니다. 그림으로 그리면 다음과 같습니다.

ImpC이 vtable
virtual void QueryInterface의 주소 <--------------------- vptr <------- PImpc2, PMiddleC2

그래서 PMiddleC2라는 포인터로 호출할 수 있는 함수는 QueryInterface() 함수로 국한돼야 하는데, 가상함수테이블에 놓이지 않은 QueryInterface(const char* name) 함수가 아무런 문제없이 호출됩니다. 가상함수 테이블을 살펴보기도 하였으나, 그 구성원리에 따르면, ImpC 클래스에서 QueryInterface() 함수가 언급됨에 따라 MiddleC에서 언급된 QueryInterface(const char* name) 함수는 이름이 가리워져 테이블에 놓이지 않을 것으로 생각하는데, 어째서 QueryInterface(const char* name) 함수가 호출되는지 궁금합니다. 그 이유를 알고 싶습니다.

peecky의 이미지

QueryInterface(const char*) 함수가 ImpC 단계에서 숨겨진거지 존재하지 않는건 아닙니다.

ImpC* 타입의 포인터로 함수를 호출하는 것과, MiddleC* 타입의 포인터로 함수를 호출하는 것은 다르게 동작합니다. QueryInterface() 함수가 가상함수이므로 런타임에서 최종적으로는 같은 함수가 호출되긴 합니다.

PMiddleC2->QueryInterface("huge"); 가 컴파일 안 될 이유는 없습니다. PMiddleC2는 MiddleC* 타입이고 MiddleC 클래스에는 엄연히 QueryInterface(const char*) 가 존재하기 때문입니다. (여기서 PMiddleC2가 실제로는 ImpC 객체를 가리키고 있다는 맥락은 고려되지 않습니다.)

그러면 MiddleC* PMiddleC2=PImpC2; 는 문제가 안되는가? 예, 하위 클래스 타입의 포인터를 상위 클래스 타입의 포인터로 사용하는 것이므로 역시 문제가 없습니다.

eagle76fish의 이미지

비가상함수는 정적바인딩되고 가상함수는 실행시에 정적바인딩되는 것으로 압니다.

MiddleC* PMiddleC2=PImpC2;

상속계층 어떤 클래스에도 비가상함수를 언급하지 않았으니 정적바인딩되는 함수는 없습니다. 그러면 바인딩에서 고려되는 것은 오직 동적바인딩이죠. PMilldeC2와 PImpC2의 vptr의 값은 같지 않습니까. 그러니까 같은 ImpC에 대한 가상함수테이블을 가리킬 것이고요. 이 가상함수테이블에는 QueryInterface(const char* name) 라는 함수를 가리키는 열이 없습니다. 가상함수를 호출할 수 있는 유일한 단서는 vptr일텐데요. 마치 컴파일이 이 함수를 정적바인딩해야할 대상으로 여기는 것 같기도 하고. 혹시 가상함수인데 파생클래스에서 재정의를 하지 않으니 정적바인딩하는 것일까요?

익명 사용자의 이미지

동적 바인딩 이전에 고려해야 할 단계가 하나 있습니다.
바로 Overload resolution이죠.

귀하의 코드에서 MiddleC 및 그 파생 클래스에는 가상 메서드가 두 개 있습니다.
그런데 이름은 QueryInterface으로 같지요.

따라서 귀하의 다른 코드가 MiddleC 및 그 파생 클래스의 객체에 대해 QueryInterface를 호출하고자 할 때,
컴파일러는 둘 중에 어떤 QueryInterface를 호출하고자 하는지 찾는 과정을 거쳐야 합니다.

https://en.cppreference.com/w/cpp/language/overload_resolution

컴파일러는 객체의 (컴파일 타임) 타입으로부터 Candidate functions의 목록을 뽑아내게 되는데,
이 때 하위 클래스의 선언은 기본 클래스의 선언을 모조리 dominate 해 버리게 됩니다.

https://en.wikipedia.org/wiki/Dominance_(C%2B%2B)

즉, ImpC에서부터 시작된 QueryInterface 이름에 대한 candidate functions은,
ImpC에서 QueryInterface라는 이름으로 단 하나의 (매개변수 없는) 메서드만 선언하였기에, 이것 하나뿐이라는 거죠.

반면 MiddleC에서부터 시작할 경우엔 매개변수 없는 것과 const char*를 받는 것, 둘 다 Candidate functions이 됩니다.

Candidate functions이 모두 모이고 나면, 컴파일러는 그 중 Best viable function을 선택하게 되고,
가상 메서드이므로 동적 바인딩이 뒤이어 일어나게 됩니다.

======

링크 드린 문서를 보시면 아시겠지만, C++에서 "함수를 호출하고자 할 때 어떤 함수가 호출되는가"는 꽤 복잡한 주제입니다.

대부분의 경우에는 이런 걸 다 알 필요가 없습니다만...

올려주신 예제와 같이 "기본 클래스에서 오버로딩 된 메서드를 하위 클래스에서 하나만 오버라이드 하는" 상황을 만들 경우엔 골치아픈 현상이 나타나게 됩니다.

peecky의 이미지

> 이 가상함수테이블에는 QueryInterface(const char* name) 라는 함수를 가리키는 열이 없습니다.
없다는 것은 어떻게 확인하셨나요? 실제로 PMiddleC2->QueryInterface("huge") 가 호출되고 있으므로 있다고 보는 것이 합당해 보입니다.

---

정적 바인딩이 되는 것은 아닌가?

PMiddleC2->QueryInterface("huge") 부분이 컴파일 될 때 컴파일러는 PMiddleC2의 내용물을 모르는 상태로 코드를 생성해야 합니다.
하지만 컴파일러 최적화가 적용된다면 코드 맥락을 파악해서 불필요한 동적 바인딩이 제거될 수도 있습니다. PMiddleC2와 PImpC2가 둘 다 지역변수이고 같은 코드블럭에 있으므로 최적화 난이도는 쉬워보입니다.
그렇지만 최적화 적용 전과 후가 최종적으로 같은 결과를 내야 하므로 동적 바인딩이 되었든 정적 바인딩이 되었든 PMiddleC2->QueryInterface("huge") 는 호출 가능하다고 볼 수 있으며, 동적 바인딩으로 동작할 경우(최적화 없이 기본 스펙대로 동작할 경우) 해당 함수는 가상함수 테이블에 존재한다고 볼 수 있습니다.

eagle76fish의 이미지

ImpC2->QueryInterface("glue");

라고 호출하면, 컴파일 오류가 납니다. 그도 그럴 것이 ImpC 클래스에 정의된 QueryInterface와 동일한 이름의 베이스 클래스 함수이기 때문에 이름이 가리워지죠. 그래서 컴파일 오류가 나는 것이고요. 스콧 마이어 선생님의 책을 보면 그렇게 설명이 돼있습니다. 이름가리기 규칙에 따라 ImpC의 가상함수테이블에 실리지 않는 것이죠.

eagle76fish의 이미지

vptr에 없으면 형식적인 타입에 대응하는 가상함수테이블에서 해당 함수를 얻어오는 것 같은데요. 아닐까요?

eagle76fish의 이미지

객체의 개념화에 대해 탐색해보니, 객체를 그림으로 설명해놓은 것을 보았는데, 그 그림은 순차적으로 다음과 같이 언급을 합니다.

객체
- vptr
- 속성1
- 속성2
- 속성3
- 메서드1
- 메서드2
- 메서드3
- 메서드4

여기서 속성은 멤버변수이고, 메서드는 멤버함수입니다. 그리고 vptr은 가상함수테이블을 가리키는 용도의 포인터입니다. 컴파일이 new ImpC라는 구문을 만날 때, 저 객체의 공간은 어떤 시간에 어떠한 방식으로 채워지는 것일까요? 저 공간 전부가 실행시에 생성되는 것이라면, 저 객체를 가리키는 PMiddleC2는 QueryInterface(const char* name) 함수를 어떻게 호출하는 것인지 궁금합니다. vptr만 유효하겠죠?
정적바인딩과 동적바인딩에서 new ImpC 구문을 컴파일이 만나면, new ImpC와 관련해서 공간을 컴파일시와 실행시에 어떻게 채워가는 것인지? PMiddleC2가 QueryInterface(const char* name)이라는 함수를 어떻게 찾을 수 있는지 궁금합니다.

eagle76fish의 이미지

동적 바인딩과 정적 바인딩이 서로 엮이는 것이군요.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.