C ++에서 언제 가상 메서드 선언에서 final을 사용해야합니까?


11

final파생 된 클래스가 가상 메서드를 재정의하는 것을 막기 위해 키워드가 사용 된다는 것을 알고 있습니다. 그러나 키워드를 실제로 메소드 와 함께 사용해야하는 경우 유용한 예를 찾을 수 없습니다 . 더욱이, 가상 메소드 사용은 프로그래머가 미래에 클래스를 확장 할 수 없기 때문에 나쁜 냄새 처럼 느껴집니다 .finalvirtualfinal

내 질문은 다음입니다.

메소드 선언 final에 실제로 사용해야 할 유용한 사례가 virtual있습니까?


Java 메소드가 최종적으로 작성되는 것과 동일한 이유로?
Ordous

Java에서는 모든 메소드가 가상입니다. 기본 클래스의 최종 방법은 성능을 향상시킬 수 있습니다. C ++에는 비가 상 메소드가 있으므로이 언어에는 해당되지 않습니다. 다른 좋은 사례를 알고 있습니까? 듣고 싶습니다. :)
메타 메이커

설명을 잘 못해요-Mike Nakis와 똑같은 말을하려고 했어요.
Ordous

답변:


12

final키워드 의 요약 : 기본 클래스 A와 파생 클래스 가 있다고 가정 해 보겠습니다 B. 함수를 f()선언 할 수있다 A같은 virtual클래스를 의미 B를 오버라이드 (override) 할 수 있습니다. 그러나 클래스 B는 더 파생 된 클래스 B가 재정의 할 수 없도록하기를 원할 수 있습니다 f(). 그때 f()와 같이 선언해야 할 때 final입니다 B. final키워드가 없으면 메소드를 가상으로 정의하면 파생 클래스가 메소드를 대체 할 수 있습니다. final키워드는이 자유에 종지부를 찍을하는 데 사용됩니다.

당신이 그런 일을해야하는 이유의 예 : 가정하자 클래스는 A가상 함수를 정의 prepareEnvelope()하고, 클래스 B고유의 가상 메서드 호출의 순서로 대체를하고 구현을 stuffEnvelope(), lickEnvelope()하고 sealEnvelope(). 클래스 B는 파생 클래스가 자체 가상 구현을 제공하기 위해 이러한 가상 메소드를 대체하도록 허용하지만 클래스 B는 파생 클래스가 대체 prepareEnvelope()하여 물건의 순서를 바꾸거나 핥거나 밀봉하거나 생략하는 것을 허용하지 않습니다 . 따라서이 경우 클래스 BprepareEnvelope()final로 선언 됩니다.


첫째, 답변에 감사드립니다. 그러나 제 질문의 첫 문장에서 쓴 내용이 아닌가? 내가 정말로 필요한 것은 언제 class B might wish that any class which is further derived from B should not be able to override f()입니까? 그러한 클래스 B와 메소드 f ()가 존재하고 잘 맞는 실제 사례가 있습니까?
메타 메이커

예, 죄송합니다. 잠깐만, 지금 예를 쓰고 있습니다.
Mike Nakis

@metamaker 당신은 간다.
Mike Nakis

1
정말 좋은 예입니다. 지금까지 가장 좋은 대답입니다. 고마워!
메타 메이커

1
그런 다음 시간을 낭비 해 주셔서 감사합니다. 인터넷에서 좋은 예를 찾을 수 없었고 검색하는 데 많은 시간을 낭비했습니다. 나는 대답에 +1을 넣을 것이지만 평판이 낮기 때문에 그것을 할 수 없습니다. :( 아마 나중에.
메타 메이커

2

디자인 관점에서 사물을 변하지 않는 것으로 표시 할 수있는 경우가 종종 있습니다. 같은 방식으로 const컴파일러 보호 기능을 제공하고 상태가 변경되지 않아야 함 final을 나타내며, 상속 계층 구조에서 동작이 더 이상 변경되지 않아야 함을 나타내는 데 사용할 수 있습니다.

차량이 플레이어를 한 위치에서 다른 위치로 이동시키는 비디오 게임을 고려하십시오. 모든 차량은 출발 전에 유효한 위치로 여행하고 있는지 확인해야합니다 (예 : 위치의 기지가 파괴되지 않았는지 확인). 비가 상 인터페이스 관용구 (NVI)를 사용하여 시작하여 차량과 상관없이이 점검을 수행 할 수 있습니다.

class Vehicle
{
public:
    virtual ~Vehicle {}

    bool transport(const Location& location)
    {
        // Mandatory check performed for all vehicle types. We could potentially
        // throw or assert here instead of returning true/false depending on the
        // exceptional level of the behavior (whether it is a truly exceptional
        // control flow resulting from external input errors or whether it's
        // simply a bug for the assert approach).
        if (valid_location(location))
            return travel_to(location);

        // If the location is not valid, no vehicle type can go there.
        return false;
    }

private:
    // Overridden by vehicle types. Note that private access here
    // does not prevent derived, nonfriends from being able to override
    // this function.
    virtual bool travel_to(const Location& location) = 0;
};

이제 우리는 게임에 비행 차량이 있고 모든 비행 차량이 필요로하는 공통점은 이륙 전에 격납고 내부의 안전 점검을 거쳐야한다는 것입니다.

여기서 우리는 final모든 비행 차량이 그러한 검사를 통과하고 비행 차량의 이러한 설계 요구 사항을 전달하도록 보장하는 데 사용할 수 있습니다 .

class FlyingVehicle: public Vehicle
{
private:
    bool travel_to(const Location& location) final
    {
        // Mandatory check performed for all flying vehicle types.
        if (safety_inspection())
            return fly_to(location);

        // If the safety inspection fails for a flying vehicle, 
        // it will not be allowed to fly to the location.
        return false;
    }

    // Overridden by flying vehicle types.
    virtual void safety_inspection() const = 0;
    virtual void fly_to(const Location& location) = 0;
};

final이러한 방식으로 사용함으로써 , 우리는 가상 계층이 아닌 인터페이스 관용구의 유연성을 효과적으로 확장하여 상속 계층 (아래의 취약한 기본 클래스 문제에 대응하는)에서도 가상 함수 자체에 균일 한 동작을 제공합니다. 또한, 우리는 존재하는 각각의 모든 비행 차량 구현을 수정하지 않고 모든 비행 차량 유형에 영향을 미치는 중심 변경을 수행하기 위해 흔들림 공간을 스스로 구매합니다.

이것은를 사용하는 하나의 예입니다 final. 가상 멤버 기능이 더 이상 재정의되는 것이 단순히 의미가없는 상황에 직면하게됩니다. 그렇게하면 취하기 쉬운 설계로 인해 설계 요구 사항이 위반 될 수 있습니다.

final디자인 / 건축 관점에서 유용한 곳 입니다.

또한 옵티마이 저가 가상 함수 호출을 실용화 할 수있는이 설계 정보를 옵티 마이저에게 제공하므로 동적 디스패치 오버 헤드를 제거하고 호출자와 피 호출자 간의 최적화 장벽을 제거 할 수 있기 때문에 더욱 유용합니다.

질문

의견에서 :

final과 virtual이 동시에 사용되는 이유는 무엇입니까?

계층 구조의 루트에있는 기본 클래스가 함수를 virtual및 로 선언하는 것은 이치에 맞지 않습니다 final. 컴파일러와 인간 독자 모두가 불필요한 경우를 피해야 virtual하는 경우 가 있기 때문에 나에게는 매우 어리석은 것처럼 보입니다 . 그러나 서브 클래스는 다음과 같이 가상 멤버 함수를 상속합니다.

struct Foo
{
   virtual ~Foo() {}
   virtual void f() = 0;
};

struct Bar: Foo
{
   /*implicitly virtual*/ void f() final {...}
};

이 경우 Bar::fvirtual 키워드를 명시 적으로 사용 하는지 여부 Bar::f는 가상 함수입니다. virtual키워드는이 경우에 선택된다. 따라서 가상 함수 (가상 함수 에만 사용할 수 있음) 이지만 Bar::f로 지정 final하는 것이 좋습니다.final

그리고 어떤 사람들은 문체 적으로 Bar::f가상 인 것을 명시 적으로 나타내기를 선호 할 수 있습니다 .

struct Bar: Foo
{
   virtual void f() final {...}
};

나에게 그것은 종류 모두 사용하는 중복의의 virtualfinal이러한 맥락 (마찬가지로 동일한 기능을 위해 지정자를 virtual하고 override)하지만,이 경우 스타일의 문제이다. 어떤 사람들은 외부 연결로 함수 선언에 virtual사용 extern하는 것과 같이 (여기에는 다른 연결 한정자가없는 경우에도) 유용한 것을 전달할 수 있습니다 .


클래스 private내 에서 메소드 를 대체하려면 어떻게합니까 Vehicle? 당신은 protected대신 의미 하지 않았습니까 ?
Andy

3
@DavidPacker private는 오버 리딩 (비트 반 직관)으로 확장되지 않습니다. 가상 함수에 대한 공개 / 보호 / 개인 지정자는 호출자에게만 적용되며 재정 의자에게는 적용되지 않습니다. 파생 클래스는 가시성에 관계없이 기본 클래스에서 가상 함수를 재정의 할 수 있습니다.

@DavidPacker protected는 좀 더 직관적 인 의미를 가질 수 있습니다. 가능한 한 가장 낮은 가시성을 달성하는 것을 선호합니다. 언어가 이런 식으로 디자인 된 이유는 개인 가상 회원 기능이 우정의 맥락 밖에서 이해가되지 않기 때문이라고 생각합니다. 단순히 전화하는 것이 아니라

비공개로 설정하면 컴파일조차하지 않는다고 생각했습니다. 예를 들어 Java도 C #도 허용하지 않습니다. C ++은 매일 나를 놀라게합니다. 10 년의 프로그래밍 후에도.
Andy

@DavidPacker 처음 만났을 때 저도 넘어졌습니다. 어쩌면 나는 protected다른 사람들을 혼란스럽게하지 않기 위해 해야 합니다. 적어도 개인 가상 기능을 여전히 재정의하는 방법을 설명하는 주석을 달았습니다.

0
  1. 어떤 함수가 호출되는지 컴파일 타임에 알 수 있기 때문에 많은 최적화가 가능합니다.

  2. "코드 냄새"라는 단어를 던지십시오. "최종"은 수업을 연장하는 것을 불가능하게하지 않습니다. "최종"단어를 두 번 클릭하고 백 스페이스를 누르고 클래스를 확장하십시오. 그러나 final은 개발자가 함수를 재정의하지 않을 것으로 기대하는 훌륭한 문서이며 최종 개발자가 그런 식으로 재정의하면 클래스가 제대로 작동하지 않을 수 있으므로 다음 개발자는 매우주의해야합니다.


왜 것 final하고 virtual지금까지 동시에 사용할 수?
Robert Harvey

1
어떤 함수가 호출되는지 컴파일 타임에 알 수 있기 때문에 많은 최적화가 가능합니다. 방법을 설명하거나 참고할 수 있습니까? 그러나 final은 개발자가 함수를 재정의하지 않을 것으로 기대하는 훌륭한 문서이며 최종 개발자가 그런 식으로 재정의하면 클래스가 제대로 작동하지 않을 수 있으므로 다음 개발자는 매우주의해야합니다. 나쁜 냄새 아니야?
메타 메이커

@RobertHarvey ABI 안정성. 다른 경우에는, 아마해야 final하고 override.
중복 제거기

- @metamaker 여기 의견에서 논의 된 동일 Q했다 programmers.stackexchange.com/questions/256778/...을 - & 재미있게만을 요구하기 때문에, 여러 가지 다른 토론에 발견했다! 기본적으로 최적화 컴파일러 / 링커가 참조 / 포인터가 파생 클래스를 나타낼 수 있고 따라서 가상 호출이 필요한지 여부를 결정할 수 있는지 여부입니다. 메소드 (또는 클래스)가 참조 자체의 정적 유형을 넘어서서 재정의 될 수없는 경우, 그들은 직접 호출 할 수 있습니다. 있다면 어떤 의심의 여지 - 자주 - 그들은 사실상 호출해야합니다. 선언 final하면 실제로 도움이 될 수 있습니다.
underscore_d
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.