가상 소멸자를 사용하지 않아야 할 때?


48

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

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

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

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

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


6
그리고 만약 당신의 클래스가 상속받지 않으면 어떻게 될까요? 많은 표준 라이브러리 클래스를 살펴보십시오. 상속되지 않도록 설계된 가상 함수는 거의 없습니다.
일부 프로그래머 친구

4
또한 대부분의 경우 소멸자는 가상이어야합니다. 아니. 전혀. 구성을 선호하지 않고 상속을 남용하는 사람 만이 그렇게 생각합니다. 소수의 기본 클래스와 가상 기능만으로 전체 응용 프로그램을 보았습니다.
Matthieu M.

1
@underscore_d 일반적인 구현에서는 이러한 모든 암묵적인 내용이 가상화되고 최적화되지 않는 한 모든 다형성 클래스에 대해 추가 코드가 생성됩니다. 일반적인 ABI 내에서 각 클래스마다 하나 이상의 vtable이 포함됩니다. 수업의 레이아웃도 변경해야합니다. 공용 인터페이스의 일부와 같은 클래스를 게시 한 후에는 다시 인터페이스를 변경하면 ABI 호환성이 손상 될 수 있기 때문에 안정적으로 돌아올 수 없습니다. 인터페이스 계약으로서 일반적으로 가상화를 기대하는 것은 좋지 않기 때문입니다.
FrankHB

1
@underscore_d "컴파일시"라는 문구는 정확하지 않지만 가상 소멸자가 사소하거나 constexpr을 지정할 수 없기 때문에 여분의 코드 생성을 피하기 어렵다는 것을 의미합니다. 런타임 성능에 다소 영향을 미칩니다.
FrankHB

2
@underscore_d "포인터"는 빨간 청어 인 것 같습니다. 멤버에 대한 포인터 일 수 있습니다 ( 정의에 의한 포인터 가 아님 ). 일반적인 ABI의 경우 멤버에 대한 포인터는 기계적인 단어 (일반 포인터)에 맞지 않는 경우가 많으며 클래스를 비다 형성에서 다형성으로 변경하면 포인터의 크기가이 클래스의 멤버로 변경되는 경우가 많습니다.
FrankHB

답변:


41

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

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

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

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

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

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

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

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


23

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

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

BTW,

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

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


7
구체적 # 2 +1, 다형성 삭제없이 . 기본 포인터를 통해 소멸자를 호출 할 수없는 경우 가상 클래스가 불필요하고 중복됩니다. 특히 클래스가 이전에 가상이 아닌 경우 RTRT로 새로 부풀어 오른 것입니다. Herb Sutter가 조언했듯이, 이것을 위반하는 사용자를 막기 위해 기본 클래스의 dtor를 보호되고 비 가상적이게 만들면 파생 된 소멸자에 의해서만 / 후에 호출 할 수 있습니다.
underscore_d

@underscore_d 이럴 그 내가 상속의 존재 나는 그것이 필요하지 않습니다 있는지 확인 할 수있을 때 가상 생성자가 필요가없는 유일한 경우로, 답을 놓친 중요한 포인트
formerlyknownas_463035818

14

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

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

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)을 권장합니다.


9

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


강력한 마감 기한을 보장하기 위해 동적 메모리와 같은 많은 리소스를 사용할 수 없기 때문에 소멸자는 하드 실시간 시스템에서 처음 사용해서는 안됩니다
.

9
@MarcoA. 소멸자는 언제 동적 메모리 할당을 의미합니까?
chbaker0

@ chbaker0 'like'를 사용했습니다. 그들은 단순히 내 경험에 사용되지 않습니다.
Marco A.

6
동적 메모리를 하드 실시간 시스템에서 사용할 수 없다는 것도 말이되지 않습니다. 고정 할당 크기 및 할당 비트 맵을 사용하여 미리 구성된 힙이 해당 비트 맵을 스캔하는 데 걸리는 시간에 메모리를 할당하거나 메모리 부족 조건을 반환한다는 것을 증명하는 것은 매우 사소한 일입니다.
MSalters

내가 생각하게 만드는 @msalters : 각 작업의 비용이 유형 시스템에 저장된 프로그램을 상상해보십시오. 실시간 보장의 컴파일 타임 점검을 허용합니다.
Yakk

5

다음은 가상 소멸자를 사용하지 않는 좋은 예입니다. 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의 소멸자가 가상으로 만들어지면 상황이 바뀝니다.

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


If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.ut 다른 사람들은 가상의 방법에 전혀 신경 쓰지 않고 클래스와 상속을 사용하여 재사용 가능한 멤버와 행동의 연속적인 계층을 구축 할 수 있었던 Good Old Days를 기억합니까? C'mon, Scott. 핵심 포인트를 얻었지만 "종종"실제로 도달합니다.
underscore_d

3

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

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

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

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


1

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


3
그 이유는 하위 분류 + 다형성의 사용 때문입니다. 가상 소멸자는 동적 해상도가 필요한 경우에만 필요합니다. 즉 마스터 클래스에 대한 참조 / 포인터 / 무엇이 실제로 서브 클래스의 인스턴스를 참조 할 수 있습니다.
Michel Billaud

2
@MichelBillaud는 실제로 가상 dtor 없이도 다형성을 가질 수 있습니다. 가상 dtor는 다형성 삭제, 즉 delete기본 클래스에 대한 포인터 호출에만 필요합니다 .
chbaker0

1

vtable을 생성하는 생성자에 오버 헤드가 있습니다 (다른 가상 함수가없는 경우에는 항상 가상 소멸자가 있어야합니다). 그리고 다른 가상 함수가 없다면 객체를 필요한 것보다 하나의 포인터 크기로 만듭니다. 크기가 커지면 작은 물체에 큰 영향을 줄 수 있습니다.

vtable을 가져 와서이를 통해 디렉토리에서 함수를 호출하는 추가 메모리 읽기가 있습니다.이를 통해 소멸자가 호출 될 때 가상이 아닌 소멸자보다 오버 헤드가 발생합니다. 물론 결과적으로 소멸자를 호출 할 때마다 약간의 추가 코드가 생성됩니다. 이것은 컴파일러가 실제 유형을 추론 할 수없는 경우를위한 것입니다. 실제 유형을 추론 할 수있는 경우 컴파일러는 vtable을 사용하지 않고 소멸자를 직접 호출합니다.

당신은 해야 그것은 그것을 창조에서, 당신은 가상 소멸자를 필요로 무엇을 입력 알고있는 코드보다 다른 엔티티에 의해 파괴 / 생성 할 수 있습니다 경우 클래스가 특히 기본 클래스로 구성되어있는 경우 가상 소멸자를 가지고있다.

확실하지 않으면 가상 소멸자를 사용하십시오. "올바른 소멸자가 호출되지 않음"으로 인한 버그를 찾는 것보다 문제로 나타나는 경우 가상을 제거하는 것이 더 쉽습니다.

즉 , 다음과 같은 경우 가상 소멸자가 없어야 합니다. 1. 가상 기능이 없습니다. 2. 클래스에서 파생하지 마십시오 ( finalC ++ 11에 표시 하면 컴파일러가 클래스 에서 파생하려고하는지 알 수 있습니다).

대부분의 경우 "많은 내용"이 없으면 (만일 1MB 문자열을 만드는 데 시간이 다소 걸릴 것입니다. 최소 1MB의 데이터가 현재 위치한 곳에서 복사). 1MB 문자열을 파괴하는 것은 150B 문자열을 파괴하는 것보다 나쁘지 않으며, 둘 다 문자열 스토리지를 할당 해제해야하며, 그다지 많지 않기 때문에 소요되는 시간은 일반적으로 동일합니다. "독 패턴"-프로덕션에서 실제 응용 프로그램을 실행하는 방법은 아닙니다].

요컨대, 작은 오버 헤드가 있지만 작은 물체의 경우 차이가 생길 수 있습니다.

컴파일러는 경우에 따라 가상 룩업을 최적화 할 수 있기 때문에 벌칙 일뿐입니다.

성능, 메모리 풋 프린트 등 벤치 마크 및 프로파일 및 측정과 관련하여 항상 대안과 결과를 비교하고 가장 많은 시간 / 메모리가 소비되는 위치를 확인하고 90 %의 최적화를 시도하지 마십시오. 많이 실행되지 않는 코드 [대부분의 응용 프로그램은 실행 시간에 큰 영향을 미치는 코드의 약 10 %, 전혀 영향을 미치지 않는 코드의 90 %]을 갖습니다. 높은 수준의 최적화 수준에서이 작업을 수행하면 컴파일러에서 좋은 작업을 수행 할 수 있다는 이점이 있습니다. 반복하고 다시 확인하고 단계적으로 개선하십시오. 영리하지 말고 특정 유형의 응용 프로그램에 대해 많은 경험이 없으면 중요하지 않은 것과 중요하지 않은 것을 알아 내려고 시도하십시오.


1
"vtable을 생성하기 위해 생성자에서 오버 헤드가 발생합니다." -vtable은 일반적으로 컴파일러에 의해 클래스별로 "만들어집니다". 생성자는 생성중인 오브젝트 인스턴스에 포인터를 저장하는 오버 헤드 만 갖습니다.
Tony

또한 ... 나는 조기 최적화를 피하는 것이 중요하지만, 반대로 You **should** have a virtual destructor if your class is intended as a base-class과도 단순화 및 조기 비관 화 입니다. 이것은 누군가가 base에 대한 포인터를 통해 파생 클래스를 삭제할 수있는 경우에만 필요합니다. 많은 상황에서 그렇지 않습니다. 당신이, 알고있는 경우 다음 물론, 오버 헤드가 발생합니다. 실제 호출을 컴파일러가 정적으로 해결할 수있는 경우에도 btw가 항상 추가됩니다. 그렇지 않으면, 사람들이 당신의 물건으로 할 수있는 일을 적절히 통제 할 때 가치가 없습니다
underscore_d
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.