## _purecall
msdn에 나와있는 정의
👉 기본 순수 가상 함수 호출 오류 처리기입니다.
순수 가상 구성원 함수를 호출하면 컴파일러가 이 함수를 호출하는 코드를 생성합니다
__purecall과 가상 함수 테이블 및 잡다한 개념에 대해서 가끔씩 생각나서 정리, 테스트한 내용을 기록해본다...
#include <iostream>
class Base
{
public:
virtual void Print()
{
std::cout << "Base의 Print -> Base " <<std::endl;
}
int GetX() { return _x; }
private:
int _x = 0x11223344;
int _y = 0xaabbccdd;
};
class Derived : public Base
{
public:
virtual void Print() override
{
std::cout << "Base의 Print -> Derived 재정의" << std::endl;
}
virtual void Print2()
{
std::cout << "Derived의 Print2 -> Derived" << std::endl;
}
private:
int _DerivedX = 0x99887766;
};
int main()
{
Derived d;
d.GetX();
return 0;
}
에서 Derived 클래스의 객체를 하나 생성하고 그 과정을 확인한다.








1.의 코드를 조금 변경해서. Base의 Print를 순수 가상 함수로 만들었다.
class Base
{
public:
virtual void Print() abstract = 0;
int GetX() { return _x; }
private:
int _x = 0x11223344;
int _y = 0xaabbccdd;
};
class Derived : public Base
{
public:
virtual void Print() override
{
std::cout << "Base의 Print -> Derived 재정의" << std::endl;
}
private:
int _DerivedX = 0x99887766;
};
int main()
{
Derived d;
d.GetX();
return 0;
}
가상 함수 테이블과 변수를 세팅하는 과정은 1과 동일하다. (따라서 간략하게 적겠음)



Base의 Print()는 순수 가상 함수로 선언되었기 때문에 Base::Print() 대신
pure_call이 들어간 것을 확인할 수 있다.

따라서 👉 순수 가상 함수가 호출되면 _purecall handler가 실행되고, run-time error (purecall Error)가 발생할 수 있다.
class Base
{
public:
~Base()
{
std::cout << "Base 소멸자" << std::endl;
}
virtual void Print() abstract = 0;
int GetX() { return _x; }
private:
int _x = 0x11223344;
int _y = 0xaabbccdd;
};
class Derived : public Base
{
public:
~Derived()
{
std::cout << "Derived 소멸자" << std::endl;
}
virtual void Print() override
{
std::cout << "Base의 Print -> Derived 재정의" << std::endl;
}
private:
int _DerivedX = 0x99887766;
};
int main()
{
Derived d;
d.GetX();
return 0;
}





값이 바뀜을 확인할 수 있었다.
결국
생성자를 호출하는 과정에서 순수 가상 함수로 존재하는 부분을 호출
소멸자를 호출하는 과정에서 순수 가상 함수로 존재하는 부분을 호출
의 경우에 __purecall 이 호출될 수 있다로 인지하면 될까??또는 어떤 경우가 있을까??
방법은 바로 범위 연산자를 호출하는 것이다. 범위 연산자를 호출하면 가상함수도 비가상함수처럼 바로 call이 된다.
class CParent
{
public:
void Func()
{
VFunc();
CParent::VFunc();
}
virtual void VFunc()
{
std::cout << "CParent::VFunc()\n";
}
};
class CChild : public CParent
{
public:
virtual void VFunc() override
{
std::cout << "CChild::VFunc()\n";
}
};
void main()
{
CChild c;
CParent* pP = &c;
pP->Func();
pP->VFunc();
pP->CParent::VFunc();
}

②의 경우
뒤의 코드도 동일하게 호출된다.

#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
//Wrapper();
}
~Base()
{
Wrapper();
}
void Wrapper()
{
Print();
}
virtual void Print() = 0;
int x = 0;
};
class Derived : public Base
{
public:
virtual void Print() {
cout << "Drived Print\n";
}
};
int main()
{
Derived d;
}
RTTI 사용 옵션을 끄고 Release 최적화없음 진행
우리가 가상 소멸자를 사용하는 이유는
다음과 같은 상황에서 문제가 되기 때문이다
class CParent
{
public:
~CParent() { printf("~CParent\n"); }
// virtual ~CParent() { printf("Virtual ~CParent\n"); }
};
class CChild : public CParent
{
public:
~CChild() { printf("~CChild\n"); }
// virtual ~CChild() { printf("~CChild\n"); }
};
void main()
{
CParent* pP = new CChild;
// .... 사용
delete pP; // !! new CChild지만 CParent의 소멸자만 호출됨!!
}
이 상황에서는 자식 클래스의 소멸자가 호출되지 않기 때문에 자식 클래스의 데이터에 대한 정리가 이루어지지 않는다. 그렇기 때문에 소멸자를 virtual로 사용해서 완전하게 정리가 되도록 해야한다.
그렇다면 소멸자를 virtual로 만들어줬을 때 동작은 어떻게 되는 것일까?
virtual로 소멸자를 호출하도록 했을 때를 살펴보았다.
new/delete 에서 확인했듯 new / delete로 동적할당/해제를 진행했을 때 자식 클래스의 생성자가 먼저 호출되지만 부모부터 실행된다.
①
main에서 CParent* pP = new Child 실행 시
②
Child의 생성자 내부
③ 
Parent의 생성자 내부.
④
Parent의 생성자 호출 후 대입된 가상 함수 테이블 주소
⑤
Child의 생성자 호출 후 대입된 가상 함수 테이블 주소
정리해보면 다음과 같다.
여기서 각각 0x0037347C와 0x00373480 에 등록된 함수를 찾아보면 다음과 같다.


⑥ 
delete pP;가 실행됐을 때이다.
scalar_deleting_desructor 함수 호출 시 push되는 값은 내 생각엔 flag같은 값이지 않을까..싶다.
delete를 하게되면 가상함수 테이블을 이용해 등록되었던 CChild::scalar_deleting_destructor 함수가 실행된다. 이 함수는 위 링크(new/delete)에서 확인했듯이 (소멸자 호출 + 동적 할당 메모리 해제)를 수행한다.
현재(Release, 최적화 없음)에서는 scalar_deleting_destructor 또는 vector_deleting_destructor (소멸자 호출 + 동적 할당 메모리 해제를 수행하는 함수)는 가상 소멸자 또는 new / delete를 수행하는 클래스에 대해 컴파일러가 자동으로 멤버 함수로 추가해버리는 것인 것 같다.
또한 결국 virtual 소멸자는 소멸자가 재정의되는 것이 아니다.
요약
- virtual 소멸자를 사용하면 가상 함수 테이블에 (소멸자 호출 + 동적 할당된 메모리 해제를 수행하는 함수, 멤버 함수) 가 등록이 된다.
- 동적할당에 대해서 delete 시 이 가상 함수 테이블에 등록된 함수를 사용한다.
- delete 연산자 실행 시 내부에서 가상함수 테이블 참조를 통해 객체에 대한 정리(소멸자 호출 + 동적 할당 메모리 해제를 수행하는 함수의 실행)이 이루어진다. 그렇게 원래 new했던 클래스에 맞는 소멸자가 호출이 된다.
1,2번같은 경우는 일반적으로 알고 있는 것과 동일한 형태이다..
3번을 통해서는 다중 상속에서 vfptr이 여러개가 존재할 수 있다는 것을 알 수 있다.
class CParent1
{
public:
virtual void VFunc1() {}
int m_iParent1 = 0x11111111;
};
class CParent2
{
public:
virtual void VFunc2() {}
int m_iParent2 = 0x22222222;
};
class CChild : public CParent1, public CParent2
{
int m_iChild = 0x33333333;
};
int main()
{
CChild c;
}
이 코드에서는 다음과 같이 c객체가 2개의 vfptr을 가진다.

여기서 CParent1과 CParent2로부터 받고 설정된 vfptr이 같은 것은 결국 저 테이블을 통해 호출되는 함수의 내용이 동일하기 때문에 (VFunc1()과 VFunc2()의 함수 코드 자체가 동일하기 때문에) 동일한 테이블을의 함수를 참조하는 것으로 보인다.
또한
class CParent1
{
public:
virtual void VFunc1() { printf("P1\n"); }
int m_iParent1 = 0x11111111;
};
class CParent2
{
public:
virtual void VFunc2() { printf("P2\n"); }
int m_iParent2 = 0x22222222;
};
class CChild : public CParent1, public CParent2
{
public:
virtual void VFunc1() { printf("C1\n"); }
virtual void VFunc2() { printf("C2\n"); }
virtual void VFuncC1() { printf("CC1\n"); }
virtual void VFuncC2() { printf("CC2\n"); }
int m_iChild = 0x33333333;
};
int main()
{
CChild c;
}

4-a의 경우엔 가상함수와 관련된... 에서 다뤘던 내용을 설명하고 있다.
class CParent1
{
public:
int m_iParent1 = 0x11111111;
};
class CParent2
{
public:
virtual void VFunc2() = 0;
int m_iParent2 = 0x22222222;
};
class CParent3
{
public:
int m_iParent3 = 0x33333333;
};
class CParent4
{
public:
virtual void VFunc4() = 0;
int m_iParent4 = 0x44444444;
};
class CChild : public CParent1, public CParent2, public CParent3, public CParent4
{
public:
virtual void VFunc2() { printf("VF2\n"); }
virtual void VFunc4() { printf("VF4\n"); }
virtual void VFuncC() { printf("Child\n"); }
int m_iChild = 0x55555555;
};
int main()
{
CChild c;
CParent1* pP1 = &c;
CParent2* pP2 = &c;
CParent3* pP3 = &c;
CParent4* pP4 = &c;
printf("&c : %p\n", &c);
printf("p1 : %p\n", pP1);
printf("p2 : %p\n", pP2);
printf("p3 : %p\n", pP3);
printf("p4 : %p\n", pP4);
}
MSVC 기준 그려지는 메모리는 다음과 같다.
vfptr배치를 위한 메모리 위치가 변경되었을 뿐, 상속 순서의 실제 의미는 객체 생성 시 생성자가 불리는 순서를 의미하기때문에 생성자의 호출 순서가 바뀌진 않았다.
✨ 결국 컴파일러는 객체의 시작 위치에 vfptr이 존재하게 만들기 위해 메모리 내부의 클래스 위치를 조정한다.
class CParent
{
public:
virtual void VFuncP() = 0;
int m_parent = 0x11111111;
};
class CChild : public virtual CParent
{
public:
virtual void VFuncP() { void* p = this; }
// virtual void VFuncC() {} // ① 주석 버전 / ② 주석 해제 버전
int m_child = 0x22222222;
};
int main()
{
CChild c;
}
①번의 경우 다음과 같은 메모리 구조를 가진다.

하지만 ②와 같이 가상함수가 새롭게 선언되는 경우, 가상 상속에서 vfptr이 메모리 시작 위치에 존재하지 않으면 메모리 시작 위치에 vfptr을 새로 만들게 된다.
CChild 객체 c는 2개의 vfptr을 가지며
CChild* c = new CChild;
c->VFuncC();
c->VFuncP();
delete c;
다음과 같이 실행했을 때 VFuncC과 VFuncP을 각각 다른 vfptr을 통해 호출하는 것을 확인.
