가상 기능과 vtable은 어떻게 구현됩니까?


109

우리는 모두 C ++에 어떤 가상 함수가 있는지 알고 있지만, 어떻게 심층적으로 구현 될까요?

vtable을 수정하거나 런타임에 직접 액세스 할 수 있습니까?

vtable이 모든 클래스에 대해 존재합니까, 아니면 하나 이상의 가상 기능이있는 클래스에만 존재합니까?

추상 클래스는 적어도 하나의 항목의 함수 포인터에 대해 단순히 NULL을 가지고 있습니까?

단일 가상 기능을 사용하면 전체 수업 속도가 느려지나요? 아니면 가상 함수에 대한 호출 만? 그리고 가상 기능이 실제로 덮어 쓰여 졌는지 여부에 따라 속도가 영향을 받습니까, 아니면 가상 기능이있는 한 효과가 없습니다.


2
로 걸작 Inside the C++ Object Model을 읽을 것을 제안하십시오 Stanley B. Lippman. (섹션 4.2, 페이지 124-131)
smwikipedia

답변:


123

가상 기능은 어떻게 심층적으로 구현됩니까?

에서 "C ++ 가상 함수는" :

프로그램에 가상 함수가 선언 될 때마다 av-table이 클래스에 대해 생성됩니다. v-table은 하나 이상의 가상 기능을 포함하는 클래스의 가상 기능에 대한 주소로 구성됩니다. 가상 함수를 포함하는 클래스의 객체는 메모리에있는 가상 테이블의 기본 주소를 가리키는 가상 포인터를 포함합니다. 가상 함수 호출이있을 때마다 v-table을 사용하여 함수 주소를 확인합니다. 하나 이상의 가상 함수를 포함하는 클래스의 객체는 메모리의 객체 맨 처음에 vptr이라는 가상 포인터를 포함합니다. 따라서이 경우 개체의 크기는 포인터의 크기만큼 증가합니다. 이 vptr은 메모리에있는 가상 테이블의 기본 주소를 포함합니다. 가상 테이블은 클래스에 따라 다릅니다. 포함 된 가상 함수의 수에 관계없이 클래스에 대해 하나의 가상 테이블 만 있습니다. 이 가상 테이블에는 클래스의 하나 이상의 가상 기능에 대한 기본 주소가 포함됩니다. 객체에서 가상 함수가 호출 될 때 해당 객체의 vptr은 메모리에서 해당 클래스에 대한 가상 테이블의 기본 주소를 제공합니다. 이 테이블은 해당 클래스의 모든 가상 함수 주소를 포함하므로 함수 호출을 해결하는 데 사용됩니다. 이것이 가상 함수 호출 중에 동적 바인딩이 해결되는 방법입니다. 해당 개체의 vptr은 메모리에서 해당 클래스에 대한 가상 테이블의 기본 주소를 제공합니다. 이 테이블은 해당 클래스의 모든 가상 함수 주소를 포함하므로 함수 호출을 해결하는 데 사용됩니다. 이것이 가상 함수 호출 중에 동적 바인딩이 해결되는 방법입니다. 해당 개체의 vptr은 메모리에서 해당 클래스에 대한 가상 테이블의 기본 주소를 제공합니다. 이 테이블은 해당 클래스의 모든 가상 함수 주소를 포함하므로 함수 호출을 해결하는 데 사용됩니다. 이것이 가상 함수 호출 중에 동적 바인딩이 해결되는 방법입니다.

vtable을 수정하거나 런타임에 직접 액세스 할 수 있습니까?

일반적으로 나는 대답이 "아니오"라고 믿는다. vtable을 찾기 위해 약간의 메모리 맹 글링을 할 수는 있지만 여전히 함수 서명이 어떻게 호출되는지 알 수 없습니다. 이 기능 (언어가 지원하는)으로 달성하려는 모든 것은 vtable에 직접 액세스하거나 런타임에 수정하지 않고도 가능해야합니다. 또한 C ++ 언어 사양 vtables가 필요하다고 지정 하지 않지만 대부분의 컴파일러가 가상 함수를 구현하는 방법입니다.

vtable이 모든 객체에 대해 존재합니까, 아니면 하나 이상의 가상 기능이있는 객체에만 존재합니까?

사양이 처음에 vtables를 필요로하지 않기 때문에 여기서 대답은 "구현에 따라 다릅니다" 라고 생각 합니다. 그러나 실제로 모든 최신 컴파일러는 클래스에 가상 함수가 하나 이상있는 경우에만 vtable을 생성한다고 생각합니다. vtable과 관련된 공간 오버 헤드와 가상 함수와 비가 상 함수 호출과 관련된 시간 오버 헤드가 있습니다.

추상 클래스는 적어도 하나의 항목의 함수 포인터에 대해 단순히 NULL을 가지고 있습니까?

대답은 언어 사양에 의해 지정되지 않았으므로 구현에 따라 다릅니다. 순수 가상 함수를 호출하면 정의되지 않은 경우 (일반적으로 그렇지 않은 경우) 정의되지 않은 동작이 발생합니다 (ISO / IEC 14882 : 2003 10.4-2). 실제로는 vtable에 함수에 대한 슬롯을 할당하지만 주소를 할당하지 않습니다. 이로 인해 vtable이 불완전하여 파생 된 클래스가 함수를 구현하고 vtable을 완료해야합니다. 일부 구현은 단순히 vtable 항목에 NULL 포인터를 배치합니다. 다른 구현에서는 어설 션과 유사한 작업을 수행하는 더미 메서드에 대한 포인터를 배치합니다.

추상 클래스는 순수 가상 함수에 대한 구현을 정의 할 수 있지만 해당 함수는 정규화 된 ID 구문으로 만 호출 할 수 있습니다 (즉, 메서드 이름에 클래스를 완전히 지정합니다. 파생 클래스). 이것은 사용하기 쉬운 기본 구현을 제공하는 동시에 파생 클래스가 재정의를 제공하도록 요구합니다.

단일 가상 함수를 사용하면 전체 클래스가 느려지거나 가상 함수에 대한 호출 만 느려 집니까?

이것은 내 지식의 가장자리에 도달하고 있으므로 내가 틀렸다면 누군가 나를 도와주세요!

내가 생각 하는 시간 성능을 가상 함수 대 가상이 아닌 함수를 호출에 관련된 히트 클래스의 경험에서 가상하다 만 기능을. 클래스의 공간 오버 헤드는 어느 쪽이든 있습니다. vtable이있는 경우 객체 당 하나 가 아니라 클래스 당 하나만 있습니다.

가상 기능이 실제로 재정의되었는지 여부에 따라 속도가 영향을 받습니까? 아니면 가상 기능이있는 한 효과가 없습니까?

재정의 된 가상 함수의 실행 시간이 기본 가상 함수를 호출하는 것에 비해 감소한다고 생각하지 않습니다. 그러나 파생 클래스와 기본 클래스에 대해 다른 vtable을 정의하는 것과 관련된 클래스에 대한 추가 공간 오버 헤드가 있습니다.

추가 자료 :

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (복귀 시스템을 통해)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable


2
컴파일러가 불필요한 vtable 포인터를 필요하지 않은 객체에 넣는 것은 Stroustrup의 C ++ 철학과 일치하지 않습니다. 규칙은 요청하지 않는 한 C에없는 오버 헤드가 발생하지 않으며 컴파일러가이를 깨뜨리는 것은 무례하다는 것입니다.
Steve Jessop

3
가상 함수가 없을 때 vtable을 사용하는 것을 진지하게 생각하는 컴파일러는 어리석은 일이라는 데 동의합니다. 그러나 나는 내가 아는 한 C ++ 표준이 그것을 요구하지 않는다는 점을 지적하는 것이 중요하다고 느꼈다. 따라서 그것에 의존하기 전에 경고를 받았다.
Zach Burlingame

8
가상 기능조차도 비 가상적으로 호출 할 수 있습니다. 이것은 실제로 매우 일반적입니다. 객체가 스택에 있으면 범위 내에서 컴파일러는 정확한 유형을 알고 vtable 조회를 최적화합니다. 이는 동일한 스택 범위에서 호출해야하는 dtor의 경우 특히 그렇습니다.
MSalters

1
나는 적어도 하나의 가상 함수가있는 클래스가 전체 클래스를위한 것이 아니라 모든 객체가 vtable을 가지고 있다고 믿습니다.
Asaf R

3
일반적인 구현 : 각 객체에는 vtable에 대한 포인터가 있습니다. 클래스는 테이블을 소유합니다. 구성 마법은 기본 ctor가 완료된 후 파생 된 ctor에서 vtable 포인터를 업데이트하는 것으로 구성됩니다.
MSalters

31
  • vtable을 수정하거나 런타임에 직접 액세스 할 수 있습니까?

이식성이 좋지는 않지만 더러운 속임수에 신경 쓰지 않는다면 확실합니다!

경고 :이 기술은 어린이, 969 세 미만의 성인 또는 Alpha Centauri의 작은 털복숭이 동물은 사용하지 않는 것이 좋습니다 . 부작용으로는 에서 튀어 나온 악마 , 모든 후속 코드 검토에서 필수 승인자로서 Yog-Sothoth 가 갑작스럽게 등장 하거나 IHuman::PlayPiano()기존의 모든 인스턴스에 소급 추가되는 것 등 이 있습니다.]

내가 본 대부분의 컴파일러에서 vtbl *는 객체의 처음 4 바이트이고 vtbl 내용은 단순히 멤버 포인터의 배열입니다 (일반적으로 선언 된 순서대로 기본 클래스의 첫 번째). 물론 다른 가능한 레이아웃이 있지만 일반적으로 관찰 한 것입니다.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

이제 몇 가지 헛소리를하려고 ...

런타임에 클래스 변경 :

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

모든 인스턴스의 메서드 교체 (클래스 몽키 패칭)

이것은 vtbl 자체가 아마도 읽기 전용 메모리에 있기 때문에 조금 더 까다 롭습니다.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

후자는 mprotect 조작으로 인해 바이러스 검사기와 링크가 깨어나서 주목할 가능성이 높습니다. NX 비트를 사용하는 프로세스에서는 실패 할 수 있습니다.


6
흠. 이것이 현상금을 받았다는 것이 불길한 느낌입니다. 나는 그되지 평균 @Mobilewits 않는 희망은 ... 그런 헛소리는 실제로 좋은 아이디어입니다 생각
puetzk

1
"윙킹"보다는 명확하고 강하게이 기술을 사용하지 않는 것이 좋습니다.
einpoklum

" VTBL 내용은 단순히 부재 포인터 배열이다 "실제로 균일하게 이격 될 일이 다른 항목으로 레코드 (구조체)의
curiousguy

1
어느 쪽이든 볼 수 있습니다. 함수 포인터는 서명이 다르므로 포인터 유형도 다릅니다. 그런 의미에서 그것은 실제로 구조와 같습니다. 그러나 다른 상황에서는 vtbl 인덱스의 개념이 유용합니다 (예 : ActiveX는 typelibs에서 이중 인터페이스를 설명하는 방식으로 사용). 이것은 배열과 유사한 뷰입니다.
puetzk

17

단일 가상 기능을 사용하면 전체 수업 속도가 느려지나요?

아니면 가상 함수에 대한 호출 만? 그리고 가상 기능이 실제로 덮어 쓰여 졌는지 여부에 따라 속도가 영향을 받습니까, 아니면 가상 기능이있는 한 효과가 없습니다.

가상 함수를 사용하면 이러한 클래스의 객체를 다룰 때 하나 이상의 데이터 항목을 초기화, 복사해야하는 한 전체 클래스의 속도가 느려집니다. 6 명 정도의 구성원이있는 클래스의 경우 그 차이는 무시할 만합니다. 단일 char멤버 만 포함 하거나 멤버가 전혀없는 클래스의 경우 차이가 눈에 띄게 나타날 수 있습니다.

그 외에도 가상 함수에 대한 모든 호출이 가상 함수 호출이 아니라는 점에 유의하는 것이 중요합니다. 알려진 유형의 객체가있는 경우 컴파일러는 정상적인 함수 호출을위한 코드를 내보낼 수 있으며, 그럴 경우 해당 함수를 인라인 할 수도 있습니다. 기본 클래스의 개체 또는 일부 파생 클래스의 개체를 가리킬 수있는 포인터 또는 참조를 통해 다형성 호출을 수행 할 때만 vtable 간접 지정이 필요하고 성능 측면에서 비용을 지불해야합니다.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

하드웨어가 취해야하는 단계는 기능 덮어 쓰기 여부에 관계없이 본질적으로 동일합니다. vtable의 주소는 객체, 적절한 슬롯에서 검색된 함수 포인터 및 포인터가 호출하는 함수에서 읽습니다. 실제 성능 측면에서 분기 예측은 약간의 영향을 미칠 수 있습니다. 예를 들어, 대부분의 객체가 주어진 가상 함수의 동일한 구현을 참조하는 경우 분기 예측기가 포인터가 검색되기 전에 호출 할 함수를 올바르게 예측할 가능성이 있습니다. 그러나 어떤 함수가 일반적인 함수인지는 중요하지 않습니다. 대부분의 객체가 덮어 쓰지 않은 기본 케이스에 위임되거나 대부분의 객체가 동일한 하위 클래스에 속하므로 동일한 덮어 쓰기 케이스에 위임 될 수 있습니다.

깊은 수준에서 어떻게 구현됩니까?

모의 구현을 사용하여 이것을 시연하는 jheriko의 아이디어가 마음에 듭니다. 그러나 C를 사용하여 위의 코드와 유사한 것을 구현하여 낮은 수준을 더 쉽게 볼 수 있습니다.

부모 클래스 Foo

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

파생 클래스 Bar

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

함수 f 가상 함수 호출 수행

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

보시다시피 vtable은 대부분 함수 포인터를 포함하는 메모리의 정적 블록입니다. 다형성 클래스의 모든 객체는 동적 유형에 해당하는 vtable을 가리 킵니다. 이는 또한 RTTI와 가상 함수 간의 연결을 더 명확하게합니다. 클래스가 가리키는 vtable을보고 클래스가 어떤 유형인지 확인할 수 있습니다. 위의 내용은 다중 상속과 같이 여러 방법으로 단순화되었지만 일반적인 개념은 건전합니다.

경우 arg유형 인 Foo*당신이 가지고 arg->vtable있지만, 실제로 유형의 목적은 Bar, 당신은 여전히의 올바른 주소를 얻을 vtable. 그 이유 vtable는는 호출 vtable되거나 base.vtable올바른 형식의 표현식에 관계없이 항상 개체 주소의 첫 번째 요소 이기 때문 입니다.


"다형성 클래스의 모든 객체는 자체 vtable을 가리 킵니다." 모든 객체에 자체 vtable이 있다는 말입니까? AFAIK vtable은 동일한 클래스의 모든 객체간에 공유됩니다. 내가 틀렸다면 알려주세요.
Bhuwan

1
@Bhuwan : 아니요, 맞습니다 : 유형 당 하나의 vtable 만 있습니다 (템플릿의 경우 템플릿 인스턴스화 당 일 수 있음). 나는 다형성 클래스의 각 객체가 적용되는 vtable을 가리키고 있으므로 각 객체에는 그러한 포인터가 있지만 동일한 유형의 객체에 대해서는 동일한 테이블을 가리 킵니다. 아마 나는 이것을 바꿔야 할 것입니다.
MvG

1
@MvG " 동일한 유형의 객체는 가상 기본 클래스를 사용하여 기본 클래스를 생성하는 동안이 아닌" 동일한 테이블을 가리 킵니다 ! (아주 특별한 경우)
curiousguy

1
@curiousguy : 특히 가상 기반의 주요 응용 프로그램이 다중 상속이므로 모델링하지 않았기 때문에 "위의 내용은 여러면에서 단순화되었습니다"라는 파일을 작성했습니다. 그러나 의견을 주셔서 감사합니다. 더 깊이가 필요한 사람들을 위해 여기에있는 것이 유용합니다.
MvG

3

일반적으로 함수에 대한 포인터 배열 인 VTable을 사용합니다.


2

이 답변은 커뮤니티 위키 답변에 통합되었습니다.

  • 추상 클래스는 적어도 하나의 항목의 함수 포인터에 대해 단순히 NULL을 가지고 있습니까?

이에 대한 대답은 그것이 지정되지 않았다는 것입니다. 순수 가상 함수를 호출하면 정의되지 않은 경우 (보통 그렇지 않은 경우) 정의되지 않은 동작이 발생합니다 (ISO / IEC 14882 : 2003 10.4-2). 일부 구현은 단순히 vtable 항목에 NULL 포인터를 배치합니다. 다른 구현에서는 어설 션과 유사한 작업을 수행하는 더미 메서드에 대한 포인터를 배치합니다.

추상 클래스는 순수 가상 함수에 대한 구현을 정의 할 수 있지만 해당 함수는 정규화 된 ID 구문으로 만 호출 할 수 있습니다 (즉, 메서드 이름에 클래스를 완전히 지정합니다. 파생 클래스). 이것은 사용하기 쉬운 기본 구현을 제공하는 동시에 파생 클래스가 재정의를 제공하도록 요구합니다.


또한 추상 클래스가 순수 가상 기능에 대한 구현을 정의 할 수 있다고 생각하지 않습니다. 정의에 따르면 순수 가상 함수에는 본문이 없습니다 (예 : bool my_func () = 0;). 그러나 일반 가상 기능에 대한 구현을 제공 할 수 있습니다.
Zach Burlingame

순수 가상 기능에는 정의가있을 수 있습니다. Scott Meyers의 "Effective C ++, 3rd Ed"항목 # 34, ISO 14882-2003 10.4-2 또는 bytes.com/forum/thread572745.html
Michael Burr

2

함수 포인터를 클래스 멤버로 사용하고 정적 함수를 구현으로 사용하거나 구현을 위해 멤버 함수 및 멤버 함수에 대한 포인터를 사용하여 C ++에서 가상 함수의 기능을 다시 만들 수 있습니다. 두 메서드 사이에는 표기상의 이점 만 있습니다. 사실 가상 함수 호출은 그 자체로 표기상의 편의 일뿐입니다. 사실 상속은 단지 표기상의 편의 일뿐입니다. 상속을 위해 언어 기능을 사용하지 않고도 모두 구현할 수 있습니다. :)

아래는 테스트되지 않은, 아마도 버그가 많은 코드이지만 아이디어를 보여주기를 바랍니다.

예 :

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};

void(*)(Foo*) MyFunc;이것은 자바 구문입니까?
curiousguy

아니요, 함수 포인터에 대한 C / C ++ 구문입니다. "함수 포인터를 사용하여 C ++에서 가상 함수의 기능을 다시 만들 수 있습니다." 그것은 끔찍한 구문이지만 자신을 C 프로그래머라고 생각하면 익숙 할 것입니다.
jheriko

ac 함수 포인터는 다음과 같습니다. int ( PROC) (); 클래스 멤버 함수에 대한 포인터는 다음과 같습니다. int (ClassName :: MPROC) ();
Menace

1
@menace, 당신은 거기에 약간의 구문을 잊어 버렸습니다 ... 당신은 아마도 typedef를 생각하고 있습니까? typedef int (* PROC) (); 그래서 int (* foo) () 대신 PROC foo를 나중에 할 수 있습니까?
jheriko 2015 년

2

간단하게 만들려고합니다. :)

우리는 모두 C ++에 어떤 가상 함수가 있는지 알고 있지만, 어떻게 심층적으로 구현 될까요?

이것은 특정 가상 기능의 구현 인 함수에 대한 포인터가있는 배열입니다. 이 배열의 인덱스는 클래스에 대해 정의 된 가상 함수의 특정 인덱스를 나타냅니다. 여기에는 순수 가상 기능이 포함됩니다.

다형성 클래스가 다른 다형성 클래스에서 파생되면 다음과 같은 상황이 발생할 수 있습니다.

  • 파생 클래스는 새로운 가상 함수를 추가하거나 재정의하지 않습니다. 이 경우이 클래스는 vtable을 기본 클래스와 공유합니다.
  • 파생 클래스는 가상 메서드를 추가하고 재정의합니다. 이 경우에는 추가 된 가상 함수가 마지막으로 파생 된 항목을 지나서 시작하는 인덱스가있는 자체 vtable을 가져옵니다.
  • 상속의 여러 다형성 클래스. 이 경우 우리는 두 번째와 다음 염기 사이의 인덱스 이동과 파생 클래스의 인덱스를 가지고 있습니다.

vtable을 수정하거나 런타임에 직접 액세스 할 수 있습니까?

표준 방식이 아닙니다. 액세스 할 수있는 API가 없습니다. 컴파일러에는 액세스 할 수있는 일부 확장 또는 개인 API가있을 수 있지만 이는 확장 일 수 있습니다.

vtable이 모든 클래스에 대해 존재합니까, 아니면 하나 이상의 가상 기능이있는 클래스에만 존재합니까?

하나 이상의 가상 함수 (소멸자 포함)가 있거나 vtable ( "다형성")이있는 클래스를 하나 이상 파생하는 것들만.

추상 클래스는 적어도 하나의 항목의 함수 포인터에 대해 단순히 NULL을 가지고 있습니까?

그것은 가능한 구현이지만 오히려 실행되지는 않습니다. 대신 일반적으로 "순수 가상 함수 호출"과 같은 것을 인쇄하고 수행하는 함수가 abort()있습니다. 생성자 또는 소멸자에서 추상 메서드를 호출하려고하면 해당 호출이 발생할 수 있습니다.

단일 가상 기능을 사용하면 전체 수업 속도가 느려지나요? 아니면 가상 함수에 대한 호출 만? 그리고 가상 기능이 실제로 덮어 쓰여 졌는지 여부에 따라 속도가 영향을 받습니까, 아니면 가상 기능이있는 한 효과가 없습니다.

속도 저하는 통화가 직접 통화로 해결되는지 가상 통화로 해결되는지에 따라 달라집니다. 그리고 다른 건. :)

포인터 또는 객체에 대한 참조를 통해 가상 함수를 호출하면 항상 가상 호출로 구현됩니다. 컴파일러는 런타임에이 포인터에 어떤 종류의 객체가 할당되는지, 그리고 그것이 이 메서드가 재정의 된 클래스입니다. 두 가지 경우에만 컴파일러가 가상 함수에 대한 호출을 직접 호출로 확인할 수 있습니다.

  • 값 (값을 반환하는 함수의 결과 또는 변수)을 통해 메서드를 호출하는 경우-이 경우 컴파일러는 객체의 실제 클래스가 무엇인지 의심하지 않으며 컴파일 타임에 "하드-리졸 브"할 수 있습니다. .
  • 가상 메서드가 final호출되는 포인터 또는 참조가있는 클래스에서 선언 된 경우 ( C ++ 11에서만 ). 이 경우 컴파일러는이 메서드가 더 이상 재정의 될 수 없으며이 클래스의 메서드 일 수만 있음을 알고 있습니다.

가상 호출에는 두 포인터를 역 참조하는 오버 헤드 만 있습니다. RTTI (다형성 클래스에만 사용할 수 있음)를 사용하는 것은 가상 메서드를 호출하는 것보다 느립니다. 두 가지 방법으로 동일한 것을 구현하는 경우를 찾을 수 있습니다. 예를 들어,을 시도하는 것보다 더 빠른 호출 기능을 제공하는 virtual bool HasHoof() { return false; }대로만 정의한 다음 재정의합니다 . 이는 실제 포인터 유형과 원하는 클래스 유형에서 경로를 빌드 할 수 있는지 확인하기 위해 경우에 따라 반복적으로 클래스 계층 구조를 살펴 봐야하기 때문입니다. 가상 호출은 항상 동일하지만 두 포인터를 역 참조합니다.bool Horse::HasHoof() { return true; }if (anim->HasHoof())if(dynamic_cast<Horse*>(anim))dynamic_cast


2

다음은 최신 C ++에서 가상 테이블 의 실행 가능한 수동 구현입니다. 잘 정의 된 의미 체계, 해킹 및 void*.

참고 : .*->*보다 다른 사업자 있습니다 *->. 멤버 함수 포인터는 다르게 작동합니다.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}

1

각 객체에는 멤버 함수의 배열을 가리키는 vtable 포인터가 있습니다.


1

이 모든 답변에서 여기에 언급되지 않은 것은 기본 클래스가 모두 가상 메서드를 갖는 다중 상속의 경우입니다. 상속 클래스에는 vmt에 대한 여러 포인터가 있습니다. 결과적으로 이러한 개체의 각 인스턴스 크기가 더 커집니다. 가상 메서드가있는 클래스는 vmt에 대해 4 바이트가 추가로 있다는 것을 누구나 알고 있지만 다중 상속의 경우 가상 메서드가있는 각 기본 클래스에 대해 4를 곱한 값이 포인터 크기입니다.


0

Burly의 대답은 다음 질문을 제외하고 여기에서 정확합니다.

추상 클래스는 적어도 하나의 항목의 함수 포인터에 대해 단순히 NULL을 가지고 있습니까?

대답은 추상 클래스에 대해 가상 테이블이 전혀 생성되지 않는다는 것입니다. 이 클래스의 개체를 만들 수 없으므로 필요가 없습니다!

즉, 다음이있는 경우 :

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

pB를 통해 액세스되는 vtbl 포인터는 클래스 D의 vtbl이됩니다. 이것이 바로 다형성이 구현되는 방식입니다. 즉, pB를 통해 D 메소드에 액세스하는 방법입니다. 클래스 B에는 vtbl이 필요하지 않습니다.

아래 Mike의 의견에 대한 응답으로 ...

내 설명의 B 클래스에 D로 재정의되지 않은 가상 메서드 foo () 와 재정의 된 가상 메서드 bar () 가있는 경우 D의 vtbl은 B의 foo () 및 자체 bar ()에 대한 포인터를 갖습니다. . B에 대해 생성 된 vtbl이 아직 없습니다.


이것은 두 가지 이유로 올바르지 않습니다. 1) 추상 클래스는 순수 가상 메소드에 추가하여 일반 가상 메소드를 가질 수 있으며 2) 순수 가상 메소드는 선택적으로 완전한 이름으로 호출 할 수있는 정의를 가질 수 있습니다.
Michael Burr

맞습니다. 모든 가상 메서드가 순수 가상이면 컴파일러가 vtable을 최적화 할 수 있다고 생각했습니다 (정의가 없는지 확인하기 위해 링커를 형성하는 데 도움이 필요함).
Michael Burr

1
" 대답은 추상 클래스에 대해 가상 테이블이 전혀 생성되지 않는다는 것입니다. "틀 렸습니다. " 이 클래스의 개체를 만들 수 없기 때문에 필요가 없습니다! "틀 렸습니다.
curiousguy

나는 더 vtable에 대한이 있다는 이론적 근거 따를 수 B 있어야 필요하지합니다. 일부 메서드에 (기본) 구현이 있다고해서 vtable에 저장해야하는 것은 아닙니다. 하지만 난 그냥 코드 (이 컴파일하기 위해 약간의 수정을 모듈로)를 통해 실행 gcc -S다음을 c++filt명확하게하기위한 vtable에이 B거기에 포함. vtable이 클래스 이름 및 상속과 같은 RTTI 데이터도 저장하기 때문일 수 있습니다. 에 필요할 수 있습니다 dynamic_cast<B*>. 심지어 -fno-rtti멀리 VTABLE 이동하지 않습니다. 로 clang -O3대신 gcc갑자기 사라 졌어요.
MvG

@MvG " 일부 메서드에 (기본) 구현이 있다고해서 vtable에 저장해야한다는 의미는 아닙니다. "예, 그렇습니다.
curiousguy

0

내가 조금 일찍 만든 개념의 매우 귀여운 증명 (상속 순서가 중요한지 확인하기 위해); C ++ 구현이 실제로 거부하는지 알려주세요 (내 버전의 gcc는 익명 구조체 할당에 대한 경고 만 표시하지만 버그입니다). 궁금합니다.

CCPolite.h :

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h :

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c :

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

산출:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

가짜 개체를 할당하지 않기 때문에 어떤 파괴도 할 필요가 없습니다. 소멸자는 동적으로 할당 된 개체의 범위 끝에 자동으로 배치되어 개체 리터럴 자체와 vtable 포인터의 메모리를 회수합니다.

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