파생 클래스의 함수에 대한 C ++ "가상"키워드. 그게 필요 할까?


221

아래에 주어진 구조체 정의와 함께 ...

struct A {
    virtual void hello() = 0;
};

접근법 # 1 :

struct B : public A {
    virtual void hello() { ... }
};

접근법 # 2 :

struct B : public A {
    void hello() { ... }
};

hello 함수를 재정의하는 두 가지 방법 사이에 차이점이 있습니까?


65
C ++ 11에서는 "void hello () override {}"를 작성하여 가상 메서드를 재정의하고 있음을 명시 적으로 선언 할 수 있습니다. 기본 가상 메소드가 존재하지 않으면 컴파일러가 실패하고 하위 클래스에 "가상"을 배치하는 것과 동일한 가독성을 갖습니다.
ShadowChaser

실제로 gcc의 C ++ 11에서 기본 클래스가 hello () 메소드가 가상임을 지정했기 때문에 파생 클래스에서 void hello () override {}를 작성하는 것이 좋습니다. 즉, 파생 클래스 에서 virtual이라는 단어 를 gcc / g ++에 사용할 필요는 없습니다. (RPi 3에서 gcc 버전 4.9.2를 사용하고 있습니다.) 어쨌든 파생 클래스의 메소드에 virtual 키워드를 포함시키는 것이 좋습니다.
Will

답변:


183

그들은 정확히 같습니다. 첫 번째 접근 방식은 더 많은 타이핑이 필요하고 잠재적으로 더 명확하다는 점 외에는 차이점이 없습니다.


25
이것은 사실이지만 Mozilla C ++ 이식성 안내서 는 항상 "가상 컴파일러"를 사용하지 않으면 경고를 표시하므로 항상 가상을 사용하는 것이 좋습니다. 안타깝게도 그러한 컴파일러의 예는 언급되지 않았습니다.
세르게이 타체 노프

5
또한 명시 적으로 가상으로 표시하면 소멸자를 가상으로 만들도록 상기시키는 데 도움이됩니다.
lfalin

1
언급 만, 가상 소멸자
Atul

6
@SergeyTachenov는에 따른 클리포드 에의 의견을 자신의 대답 같은 컴파일러의 예는 armcc에서입니다.
Ruslan

4
새로운 이식성 안내서 @Rasmi 가 여기 있지만 override키워드 를 사용하는 것이 좋습니다 .
Sergei Tachenov

83

함수의 '가상 성'은 암시 적으로 전파되지만 사용하는 적어도 하나의 컴파일러는 다음과 같은 경우 경고를 생성합니다. virtual 키워드를 명시 적으로 사용하지 않으면 하므로 컴파일러를 조용하게 유지하려는 경우에만 사용할 수 있습니다.

virtual키워드를 포함한 순전히 문체 적 관점에서 그 기능이 가상이라는 사실을 사용자에게 명확하게 '광고'합니다. 이것은 A의 정의를 확인하지 않고도 B를 하위 클래스로 분류하는 모든 사람에게 중요합니다. 딥 클래스 계층의 경우 특히 중요합니다.


12
이것은 어느 컴파일러입니까?
James McNellis

35
@James : armcc (ARM 장치 용 ARM 컴파일러)
Clifford

55

virtual키워드는 파생 클래스에서 필요하지 않습니다. 다음은 C ++ Draft Standard (N3337) (강조 광산)의 지원 문서입니다.

10.3 가상 기능

2 가상 멤버 함수 경우 vf클래스로 선언 Base하고, 클래스에 Derived직접 또는 간접적으로 유래의 Base멤버 함수, vf동일한 이름의 파라미터 타입 목록 (8.3.5), CV-자격 및 REF - 규정 ( Base::vf선언 된 것과 동일하거나 존재하지 않는 경우) Derived::vf가상 ( 선언되었는지 여부 )도 무시 Base::vf합니다.


5
이것은 지금까지 가장 좋은 대답입니다.
환상적인 Mr Fox

33

아니요, virtual파생 클래스의 가상 함수 재정 의 키워드는 필요하지 않습니다. 그러나 가상 함정을 재정의하지 못하는 관련 함정을 언급 할 가치가 있습니다.

재정실패 는 파생 클래스에서 가상 함수를 재정의하려고하지만 서명에 오류가있어 새롭고 다른 가상 기능을 선언 할 경우 발생합니다. 이 함수는 기본 클래스 함수 의 오버로드 이거나 이름이 다를 수 있습니다. virtual파생 클래스 함수 선언에서 키워드 를 사용하는지 여부에 관계없이 컴파일러는 기본 클래스에서 함수를 재정의하려는 의도를 알 수 없습니다.

그러나이 함정은 고맙게도 C ++ 11 명시 적 재정의 언어 기능 으로 해결되었으며 ,이를 통해 소스 코드는 멤버 함수가 기본 클래스 함수를 재정의하도록 의도되었음을 명확하게 지정할 수 있습니다.

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

컴파일러는 컴파일 타임 오류를 발생시키고 프로그래밍 오류는 즉시 명백합니다 (아마도 Derived의 함수 float가 인수로 사용 되어야 함 ).

WP : C ++ 11을 참조하십시오 .


11

"가상"키워드를 추가하면 가독성이 향상되므로 좋은 방법이지만 반드시 그럴 필요는 없습니다. 기본 클래스에서 virtual로 선언되고 파생 클래스에서 동일한 서명을 갖는 함수는 기본적으로 "가상"으로 간주됩니다.


7

virtual파생 클래스에서 작성 하거나 생략 할 때 컴파일러에는 차이가 없습니다 .

그러나이 정보를 얻으려면 기본 클래스를 살펴 봐야합니다. virtual따라서 인간 에게이 기능이 가상이라는 것을 보여주고 싶다면 파생 클래스에도 키워드 를 추가하는 것이 좋습니다 .


2

템플릿이 있고 기본 클래스를 템플릿 매개 변수로 사용하기 시작하면 상당한 차이가 있습니다.

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

그것의 재미있는 부분은 지금 인터페이스와 비 인터페이스 함수를 정의 할 수 있다는 것입니다 나중에 클래스를 정의 할 수 있습니다. 이는 라이브러리 간의 인터 워킹 인터페이스에 유용합니다 ( 단일 라이브러리 의 표준 설계 프로세스로 이것을 사용하지 마십시오 ). 모든 수업에 이것을 허용하는 데 비용이 들지 않습니다.typedef . 원한다면 B .

이 작업을 수행하면 복사 / 이동 생성자를 템플릿으로 선언 할 수도 있습니다. 다른 인터페이스에서 구성 할 수 있으면 다른 B<>유형 간에 '캐스트'할 수 있습니다.

그것은 당신에 대한 지원을 추가해야하는지 의문이다 const A&에서 t_hello(). 이 재 작성의 일반적인 이유는 주로 성능상의 이유로 상속 기반 전문화에서 템플릿 기반 전문화로 이동하지 않기 때문입니다. 이전 인터페이스를 계속 지원하면 이전 사용법을 거의 감지하지 못하거나 막을 수 없습니다.


1

virtual키워드를 재정의 할 기본 클래스의 기능을 추가해야합니다. 귀하의 예 struct A에서 기본 클래스입니다. virtual파생 클래스에서 해당 함수를 사용하는 것은 아닙니다. 그러나 파생 클래스가 기본 클래스 자체가되기를 원하며 해당 함수를 재정의 할 수 있기를 원한다면 virtual거기 에 배치해야 합니다.

struct B : public A {
    virtual void hello() { ... }
};

struct C : public B {
    void hello() { ... }
};

여기에서를 C상속 받으 B므로 B기본 클래스가 아니며 파생 클래스이기도합니다 C. 상속 다이어그램은 다음과 같습니다.

A
^
|
B
^
|
C

따라서 virtual자식이있을 수있는 잠재적 인 기본 클래스 안에 함수 앞에를 배치해야합니다 . virtual자녀가 귀하의 기능을 무시할 수 있습니다. virtual파생 클래스 내부에서 함수 앞에 함수 를 배치하는 데 아무런 문제 가 없지만 필수는 아닙니다. 그러나 누군가 파생 클래스에서 상속하려면 메소드 재정의가 예상대로 작동하지 않는다는 것을 기쁘게 생각하지 않기 때문에 권장됩니다.

따라서 virtual클래스에 기본 클래스의 기능을 재정의해야하는 자식이 없는지 확실하지 않으면 상속과 관련된 모든 클래스에서 함수 앞에 놓으십시오 . 좋은 습관입니다.


0

자식 클래스에 대한 Virtual 키워드를 확실히 포함시킬 것입니다.

  • 나는. 가독성.
  • ii. 이 자식 클래스는 더 많이 파생됩니다. 추가 파생 클래스의 생성자 가이 가상 함수를 호출하지 않도록하십시오.

1
그는 하위 기능을 가상으로 표시하지 않으면 나중에 하위 클래스에서 파생 된 프로그래머가 해당 기능이 실제로 가상임을 알지 못하고 (기본 클래스를 보지 않았기 때문에) 건설 중에 호출 할 수 있음을 의미한다고 생각합니다 ( 올바른 일을 할 수도 있고하지 않을 수도 있습니다).
PfhorSlayer
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.