가상 기능과 vtable은 어떻게 구현됩니까? 존재합니까? 추상 클래스는 적어도 하나의

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

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

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

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

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



답변

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

에서 “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


답변

  • 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 명 정도의 구성원이있는 클래스의 경우 그 차이는 무시할 만합니다. 단일 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을 사용합니다.


답변

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

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

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

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


답변

함수 포인터를 클래스 멤버로 사용하고 정적 함수를 구현으로 사용하거나 구현을 위해 멤버 함수 및 멤버 함수에 대한 포인터를 사용하여 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; }
...
};


답변

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

우리는 모두 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