가상 소멸자를 사용하지 않아야 할 때? 필요한 이유를 언급했습니다.

가상 소멸자에 대해 여러 번 검색했으며 가상 소멸자의 목적과 가상 소멸자가 필요한 이유를 언급했습니다. 또한 대부분의 경우 소멸자는 가상이어야합니다.

그렇다면 질문은 : 왜 C ++이 모든 소멸자를 기본적으로 가상으로 설정하지 않습니까? 또는 다른 질문에서 :

언제 가상 소멸자를 사용할 필요가 없습니까?

어떤 경우에 가상 소멸자를 사용해서는 안됩니까?

필요하지 않더라도 가상 소멸자를 사용하는 경우 비용은 얼마입니까?



답변

가상 소멸자를 클래스에 추가하는 경우 :

  • 현재의 모든 C ++ 구현에서 해당 클래스의 모든 객체 인스턴스는 런타임 유형에 대한 가상 디스패치 테이블에 대한 포인터를 저장해야하며 해당 가상 디스패치 테이블 자체는 실행 가능 이미지에 추가됩니다.

  • 가상 디스패치 테이블의 주소가 프로세스 전체에서 반드시 유효하지는 않으므로 공유 메모리에서 이러한 오브젝트를 안전하게 공유하지 못하게 할 수 있습니다.

  • 임베디드 가상 포인터를하는 몇몇 공지 된 입력 또는 출력 형식과 일치하는 메모리 레이아웃 클래스를 생성 좌절 (예를 들어, 그래서이 Price_Tick*수신 UDP 패킷 내에서 적합하게 정렬 된 메모리에 직접 겨냥하여 데이터를 분석 / 액세스 또는 변경하는 데, 또는 수 new발신 패킷에 데이터를 쓰기 위해 이러한 클래스를 배치 )

  • 소멸자 호출 자체는 (특정 조건 하에서) 가상으로 발송되어 오프라인이 될 수 있지만, 비가 상 소멸자는 호출자에게 사소하거나 무관 한 경우 인라이닝되거나 최적화되지 않을 수 있습니다

“상속되지 않기”라는 주장은 위에서 설명한 것처럼 실제적인 방식으로 더 나쁘지 않은 가상 소멸자를 항상 갖지 않는 실질적인 이유는 아닙니다. 그러나 비용을 지불 할 때 중요한 기준이되는 것이 더 나쁩니다. 기본적으로 클래스를 기본 클래스로 사용하려는 경우 가상 소멸자가 있어야합니다 . 항상 필요한 것은 아니지만 파생 클래스 소멸자가 기본 클래스 포인터 또는 참조를 사용하여 호출되는 경우 실수로 정의되지 않은 동작없이 계층 구조의 클래스를 더 자유롭게 사용할 수 있습니다.

“대부분의 경우 소멸자는 가상이어야합니다”

그렇지 않습니다 … 많은 수업에는 그러한 필요가 없습니다. 불필요하다고 생각되는 곳이 너무 많지만 표준 라이브러리를 보거나 부스트를 말하면 가상 소멸자가없는 많은 클래스가 있음을 알 수 있습니다. 부스트 1.53에서 494 중에서 72 명의 가상 소멸자를 계산합니다.


답변

어떤 경우에 가상 소멸자를 사용해서는 안됩니까?

  1. 상속 받고 싶지 않은 구체적인 클래스.
  2. 다형성 삭제가없는 기본 클래스의 경우. 클라이언트는 Base에 대한 포인터를 사용하여 다형성을 삭제할 수 없어야합니다.

BTW,

어떤 경우 가상 소멸자를 사용해야합니까?

다형성 삭제가있는 기본 클래스의 경우


답변

필요하지 않더라도 가상 소멸자를 사용하는 경우 비용은 얼마입니까?

도입 비용 어떤 클래스 가상 기능과 같이, 객체별로 저장되어있는 가상 포인터의 초기 비용 (목적에 따라 또는 생략) (또는 상속 클래스 정의의 일부)는 아마도 매우 가파르다 :

struct Integer
{
    virtual ~Integer() {}
    int value;
};

이 경우 메모리 비용은 상대적으로 엄청납니다. 클래스 인스턴스의 실제 메모리 크기는 이제 64 비트 아키텍처에서 종종 다음과 같습니다.

struct Integer
{
    // 8 byte vptr overhead
    int value; // 4 bytes
    // typically 4 more bytes of padding for alignment of vptr
};

Integer클래스 는 총 4 바이트가 아닌 16 바이트입니다. 백만 개의 메모리를 어레이에 저장하면 16MB의 메모리 사용이 발생합니다. 일반적인 8MB L3 CPU 캐시 크기의 두 배이며 이러한 어레이를 반복적으로 반복하는 것은 4MB에 해당하는 것보다 몇 배 느릴 수 있습니다 추가 캐시 누락 및 페이지 결함으로 인해 가상 포인터가없는 경우

그러나 객체 당 가상 포인터 비용은 더 많은 가상 기능으로 증가하지 않습니다. 클래스에 100 개의 가상 멤버 함수를 가질 수 있으며 인스턴스 당 오버 헤드는 여전히 단일 가상 포인터입니다.

가상 포인터는 일반적으로 오버 헤드 관점에서보다 즉각적인 관심사입니다. 그러나 인스턴스 당 가상 포인터 외에 클래스 당 비용이 있습니다. 가상 함수가있는 각 클래스 vtable는 가상 함수 호출이 이루어질 때 실제로 호출해야하는 함수 (가상 / 동적 디스패치)에 주소를 저장 하는 메모리에 메모리를 생성합니다 . 는 vptr다음 인스턴스에 저장이 클래스 별 가리 킵니다 vtable. 이 오버 헤드는 일반적으로 덜 걱정되지만 복잡한 코드베이스에서 수천 개의 클래스에 대해이 오버 헤드를 불필요하게 지불 한 경우 바이너리 크기를 vtable늘리고 약간의 런타임 비용을 추가 할 수 있습니다. 믹스에서 더 많은 가상 기능.

Java 사용자 정의 유형은 암묵적으로 중앙 object기본 클래스 에서 상속 되며 Java의 모든 함수는 암시 적으로 가상 이기 때문에 성능이 중요한 영역에서 일하는 Java 개발자는 이러한 종류의 오버 헤드를 잘 이해합니다 (박싱과 관련하여 종종 설명 됨). ) 다르게 표시되지 않는 한 본질적으로 결과적으로 Java Integer도 마찬가지로 vptr인스턴스 당 연관된 이 스타일 메타 데이터 의 결과로 64 비트 플랫폼에서 16 바이트의 메모리를 요구하는 경향이 있으며 일반적으로 Java int에서는 런타임을 지불하지 않고 단일과 같은 것을 클래스로 랩핑하는 것이 불가능 합니다. 성능 비용.

그렇다면 질문은 : 왜 C ++이 모든 소멸자를 기본적으로 가상으로 설정하지 않습니까?

C ++은 “지불하는대로 지불”종류의 사고 방식과 C에서 상속 된 많은 베어 메탈 하드웨어 기반 디자인으로 성능을 선호합니다. vtable 생성 및 동적 디스패치에 필요한 오버 헤드를 불필요하게 포함하고 싶지는 않습니다. 모든 단일 클래스 / 인스턴스. 성능이 C ++과 같은 언어를 사용하는 주요 이유 중 하나가 아닌 경우, 많은 C ++ 언어가 성능이 자주 발생하는 것보다 안전하지 않고 더 어렵 기 때문에 다른 프로그래밍 언어의 이점을 얻을 수 있습니다. 그러한 디자인을 선호하는 주요 이유.

언제 가상 소멸자를 사용할 필요가 없습니까?

꽤 자주. 클래스가 상속되도록 설계되지 않은 경우 가상 소멸자가 필요하지 않으며 필요하지 않은 항목에 대해 가능한 큰 오버 헤드를 지불하게됩니다. 마찬가지로 클래스가 상속되도록 설계되었지만 기본 포인터를 통해 하위 유형 인스턴스를 삭제하지 않더라도 가상 소멸자가 필요하지 않습니다. 이 경우 안전한 비 가상적 소멸자를 다음과 같이 정의하는 것이 안전합니다.

class BaseClass
{
protected:
    // Disallow deleting/destroying subclass objects through `BaseClass*`.
    ~BaseClass() {}
};

어떤 경우에 가상 소멸자를 사용해서는 안됩니까?

가상 소멸자 를 사용해야 할 때 실제로 다루기가 더 쉽습니다 . 코드베이스에서 훨씬 더 많은 클래스가 상속을 위해 설계되지 않을 것입니다.

std::vector그 다음이 기본 포인터 삭제 문제 (경향 바와 같이, 예를 들어, 상속 할 수 있도록 설계되지 않으며, 일반적으로 (매우 불안정 디자인) 상속하지 않아야 std::vector서투른뿐만 아니라 의도적으로 가상 소멸자 방지) 개체 슬라이스 문제 경우 파생 클래스는 새로운 상태를 추가합니다.

일반적으로 상속 된 클래스는 퍼블릭 가상 소멸자 또는 보호 된 비가 상 클래스를 가져야합니다. 에서 C++ Coding Standards장 50 :

50. 기본 클래스 소멸자를 공개 및 가상 또는 보호 및 비 가상화로 만듭니다. 삭제하거나 삭제하지 않습니다. 그것이 문제입니다 :베이스베이스에 대한 포인터를 통한 삭제가 허용되어야한다면베이스의 소멸자는 퍼블릭 및 가상이어야합니다. 그렇지 않으면 보호되고 비 가상적이어야합니다.

C ++가 암시 적으로 강조하는 경향 중 하나는 (디자인이 실제로 취하기 쉽고 어색하고 어쩌면 안전하지 않기 때문에) 상속은 나중에 생각하기 위해 설계된 메커니즘이 아니라는 생각입니다. 다형성을 고려한 확장 성 메커니즘이지만 확장 성이 필요한 위치에 대한 예측이 필요한 메커니즘입니다. 결과적으로, 기본 클래스는 상속 계층 구조의 근본으로 설계되어야하며, 사전에 그러한 예측없이 나중에 나중에 생각할 것이 아닙니다.

기존 코드를 재사용하기 위해 단순히 상속하고자하는 경우에는 컴포지션 재사용 원칙 (Composite Reuse Principle)을 권장합니다.


답변

왜 C ++에서 기본적으로 모든 소멸자를 가상으로 설정하지 않습니까?
추가 스토리지 비용 및 가상 메소드 테이블 호출. C ++은 부담이 될 수있는 시스템, 저 지연, RT 프로그래밍에 사용됩니다.


답변

다음은 가상 소멸자를 사용하지 않는 좋은 예입니다. From Scott Meyers :

클래스에 가상 함수가 포함되어 있지 않은 경우, 이는 기본 클래스로 사용되지 않음을 나타냅니다. 클래스를 기본 클래스로 사용하지 않으려는 경우 소멸자를 가상으로 만드는 것은 일반적으로 나쁜 생각입니다. ARM에 대한 논의를 기반으로이 예제를 고려하십시오.

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

short int가 16 비트를 차지하는 경우 Point 객체는 32 비트 레지스터에 맞을 수 있습니다. 또한 Point 객체는 C 또는 FORTRAN과 같은 다른 언어로 작성된 함수에 32 비트 수량으로 전달 될 수 있습니다. 그러나 Point의 소멸자가 가상으로 만들어지면 상황이 바뀝니다.

가상 구성원을 추가하는 순간 해당 클래스의 가상 테이블을 가리키는 가상 포인터가 클래스에 추가됩니다.


답변

가상 소멸자는 런타임 비용을 추가합니다. 클래스에 다른 가상 메소드가없는 경우 비용이 특히 큽니다. 가상 소멸자는 하나의 특정 시나리오에서만 필요하며, 기본 클래스에 대한 포인터를 통해 객체가 삭제되거나 파괴됩니다. 이 경우 기본 클래스 소멸자는 가상이어야하며 파생 클래스의 소멸자는 내재적으로 가상입니다. 소멸자가 가상 ​​일 필요가없는 방식으로 다형성 기본 클래스가 사용되는 몇 가지 시나리오가 있습니다.

  • 파생 클래스의 인스턴스가 힙에 할당되지 않은 경우 (예 : 스택 또는 다른 객체 내부에만) (초기화되지 않은 메모리와 배치 연산자 new를 사용하는 경우 제외)
  • 파생 클래스의 인스턴스가 힙에 할당되었지만 삭제가 가장 파생 된 클래스에 대한 포인터를 통해서만 발생하는 경우 (예 std::unique_ptr<Derived>:)가 있고 다형성은 비 소유 포인터 및 참조를 통해서만 발생합니다. 또 다른 예는 객체를 사용하여 객체를 할당하는 경우 std::make_shared<Derived>()입니다. 사용하기 괜찮 std::shared_ptr<Base>뿐만 아니라만큼 초기 포인터가이었다로 std::shared_ptr<Derived>. 공유 포인터에는 반드시 가상 기본 클래스 소멸자에 의존하지 않는 소멸자 (삭제 자)에 대한 자체 동적 디스패치가 있기 때문입니다.

물론, 위에서 언급 한 방식으로 만 객체를 사용하는 규칙은 쉽게 깨질 수 있습니다. 따라서 Herb Sutter의 조언은 “기본 클래스 소멸자는 공개 및 가상이거나 보호되고 비 가상적이어야합니다.” 이런 식으로 누군가가 비가 상 소멸자를 사용하여 기본 클래스에 대한 포인터를 삭제하려고하면 컴파일 타임에 액세스 위반 오류가 발생합니다.

그런 다음 다시 (공개) 기본 클래스가 아닌 클래스가 있습니다. 개인적으로 추천하는 것은 finalC ++ 11 이상 으로 만드는 것 입니다. 사각 페그로 설계되면 원형 페그로 잘 작동하지 않을 가능성이 있습니다. 이것은 기본 클래스와 파생 클래스 사이의 명시 적 상속 계약, 구체적 기본 클래스가 아닌 추상에 대한 NVI (가상 인터페이스) 디자인 패턴, 보호 된 멤버 변수의 혐오에 대한 선호와 관련이 있습니다. 하지만 이러한 견해는 어느 정도 논란의 여지가 있습니다.


답변

소멸자 선언은 상속 가능 virtual하도록 만들 때만 필요합니다 class. 일반적으로 표준 라이브러리의 클래스 (예 std::string:)는 가상 소멸자를 제공하지 않으므로 하위 클래스 화를위한 것이 아닙니다.