함수에 대한 C ++ 11의 "최종"키워드의 목적은 무엇입니까?


143

final함수에 대한 C ++ 11 키워드 의 목적은 무엇입니까 ? 파생 클래스가 함수를 재정의하는 것을 방지한다는 것을 알고 있지만 이것이 사실이라면 비가 상 final함수 로 선언하기에 충분하지 않습니까? 내가 여기서 놓친 또 다른 것이 있습니까?


30
" 최종"함수를 가상이 아닌 것으로 선언하는 것만으로는 충분하지 않습니다. "아니오, virtual키워드 를 사용하는지 여부에 관계없이 재정의 함수는 암시 적으로 가상 입니다.
— ildjarn

13
@ildjarn 슈퍼 클래스에서 가상으로 선언되지 않은 경우에는 사실이 아닙니다. 클래스에서 파생되지 않고 비가 상 메서드를 가상 메서드로 변환 할 수 없습니다.
— Dan O

10
@ DanO 나는 당신이 재정의 할 수는 없지만 메소드를 "숨길"수 있다고 생각합니다. 사람들이 메소드를 숨기는 것을 의미하지 않기 때문에 많은 문제가 발생합니다.
— Alex Kremer

16
@DanO : 슈퍼 클래스에서 가상이 아니면 "재정의"되지 않습니다.
— ildjarn

2
다시, " 재정의 "는 여기서 가상 함수에 다형성 동작을 제공하는 특정 의미를 갖습니다. 귀하의 예에서는 func가상이 아니므로 재정의 할 것이 없으므로 override또는 로 표시 할 것이 없습니다 final.
— ildjarn

답변:


129

idljarn이 이미 주석에서 언급했듯이 누락 된 것은 기본 클래스에서 함수를 재정의 하는 경우 비가 상으로 표시 할 수 없다는 것입니다.

struct base {
   virtual void f();
};
struct derived : base {
   void f() final;       // virtual as it overrides base::f
};
struct mostderived : derived {
   //void f();           // error: cannot override!
};

감사! 이것이 내가 놓친 요점입니다. 즉, "리프"클래스조차도 함수를 재정의하고 자신을 재정의하지 않더라도 함수를 가상으로 표시해야합니다.
— lezebulon

8
@lezebulon : 수퍼 클래스가 가상으로 선언 한 경우 리프 클래스는 함수를 가상으로 표시 할 필요가 없습니다.
— Dan O

5
리프 클래스의 메소드는 기본 클래스에서 가상 인 경우 내재적으로 가상입니다. 이 암시 적 '가상'이 없으면 컴파일러가 경고해야한다고 생각합니다.
— Aaron McDaid

@AaronMcDaid : 컴파일러는 일반적으로 혼동이나 오류를 일으킬 수있는 코드에 대해 경고합니다. 나는이 문제의 원인이 될 수있는이 언어 의이 특별한 기능에 놀란 사람을 본 적이 없으므로 오류가 얼마나 유용한 지 실제로 알지 못합니다. 반대로, virtualC ++ 11은 오류를 유발할 수 있다는 사실을 잊어 버리고 C ++ 11 override은 해당 상황을 감지하고 실제로 재정의 하려는 함수가 실제로 숨길
— David Rodríguez-dribeas

1
GCC 4.9 변경 노트에서 : "실용화를 향상시키는 새로운 유형 상속 분석 모듈. 가상화는 이제 익명의 네임 스페이스와 C ++ 11 최종 키워드를 고려합니다"-구문 설탕 일뿐만 아니라 잠재적 인 최적화 이점도 있습니다.
— kfsone

126
  • 클래스가 상속되지 않도록하는 것입니다. 에서 위키 백과 :

    C ++ 11은 클래스에서 상속받지 못하거나 파생 클래스에서 메서드를 재정의하는 것을 단순히 방지하는 기능도 추가합니다. 이것은 특수 식별자 final로 수행됩니다. 예를 들면 다음과 같습니다.

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
  • 또한 파생 클래스에서 가상 함수가 재정의되지 않도록 가상 함수를 표시하는 데 사용됩니다.

    struct Base2 {
        virtual void f() final;
    };
    
    struct Derived2 : Base2 {
        void f(); // ill-formed because the virtual function Base2::f has 
                  // been marked final
    };

Wikipedia는 또한 흥미로운 지적을합니다 .

언어 키워드 도 override아닙니다 final. 그것들은 기술적으로 식별자입니다. 그들은 특정 상황에서 사용될 때만 특별한 의미를 얻습니다 . 다른 위치에서는 유효한 식별자가 될 수 있습니다.

즉, 다음이 허용됩니다.

int const final = 0;     // ok
int const override = 1;  // ok

1
고마워,하지만 내 질문은 방법으로 "최종"을 사용하는 것과 관련이 있다는 것을 언급하지 않았다
— lezebulon

당신은 @lezebulon :-) 마지막 "C ++ 11의 키워드"의 목적은 무엇인가 "를 언급 한 기능 ". (내 강조)
— Aaron McDaid

편집 했습니까? "lezebulon이 (가) x 분 전에 편집했습니다"라는 메시지가 표시되지 않습니다. 어떻게 된거 지? 제출 후 매우 빠르게 편집했을까요?
— Aaron McDaid

5
@Aaron : 게시 후 5 분 이내에 수정 한 내용은 개정 내역에 반영되지 않습니다.
— ildjarn

@Nawaz : 키워드 만 지정자가 아닌 이유는 무엇입니까? 호환성 이유로 인해 C ++ 11 이전의 기존 코드가 다른 목적으로 final & override를 사용할 수 있습니까?
— 소멸자

45

"최종"은 또한 컴파일러 최적화가 간접 호출을 우회 할 수있게합니다.

class IAbstract
{
public:
  virtual void DoSomething() = 0;
};

class CDerived : public IAbstract
{
  void DoSomething() final { m_x = 1 ; }

  void Blah( void ) { DoSomething(); }

};

"final"을 사용하면 컴파일러는 에서 또는 인라인 CDerived::DoSomething()에서 직접 호출 할 수 있습니다 Blah(). 그것 없이는 재정의 된 파생 클래스 내에서 호출 될 수 Blah()있기 때문에 내부에서 간접 호출을 생성해야합니다 .Blah()DoSomething()


29

"최종"의 시맨틱 한 측면에 추가 할 것은 없습니다.

그러나 "최종"이 그리 멀지 않은 미래에 매우 중요한 컴파일러 최적화 기술 이 될 수 있다고 chris green의 의견에 덧붙이고 싶습니다 . 그가 언급 한 간단한 경우뿐만 아니라 "최종"에 의해 "폐쇄"될 수있는보다 복잡한 실제 클래스 계층 구조에 대해서도 컴파일러는 일반적인 vtable 방식보다 더 효율적인 디스패치 코드를 생성 할 수 있습니다.

vtables의 한 가지 주요 단점은 이러한 가상 객체 (일반적인 Intel CPU에서 64 비트로 가정)에 대해 포인터 만 캐시 라인의 25 % (64 바이트 중 8 바이트)를 소비한다는 것입니다. 내가 작성하는 응용 프로그램의 종류에서 이것은 매우 심하게 아프다. (그리고 내 경험상 그것은 순수한 프로그래머 관점에서, 즉 C 프로그래머에 의한 C ++에 대한 # 1 논쟁입니다.)

C ++에서는 그리 드문 일이 아닌 극단적 인 성능이 필요한 응용 프로그램에서는 실제로 C 스타일이나 이상한 템플릿 저글링 에서이 문제를 수동으로 해결할 필요가 없으므로 대단해질 수 있습니다.

이 기술을 가상화 해제라고 합니다. 기억할 가치가있는 용어. :-)

Andrei Alexandrescu의 최근 연설이 있는데, 오늘날 이러한 상황을 해결할 수있는 방법과 "최종"이 미래에 유사한 사례를 "자동으로"해결하는 방법에 대해 잘 설명하고 있습니다 (청중과 논의).

http://channel9.msdn.com/Events/GoingNative/2013/Writing-Quick-Code-in-Cpp-Quickly


23
8은 64의 25 %입니까?
— ildjarn

6
누구나 지금 사용하는 컴파일러를 알고 있습니까?
— Vincent Fourmond

내가 말하고 싶은 것과 같은 것.
— crazii

8

비가 상 기능에는 Final을 적용 할 수 없습니다.

error: only virtual member functions can be marked 'final'

비가 상 방법을 '최종'으로 표시 할 수 있다는 것은 의미가 없습니다. 주어진

struct A { void foo(); };
struct B : public A { void foo(); };
A * a = new B;
a -> foo(); // this will call A :: foo anyway, regardless of whether there is a B::foo

a->foo()항상 전화 A::foo합니다.

그러나 A :: foo가 virtual이면 B :: foo가이를 대체합니다. 이것은 바람직하지 않을 수 있으므로 가상 기능을 최종적으로 만드는 것이 합리적입니다.

문제는 가상 함수에 final을 허용하는 이유 입니다. 계층 구조가 깊은 경우 :

struct A            { virtual void foo(); };
struct B : public A { virtual void foo(); };
struct C : public B { virtual void foo() final; };
struct D : public C { /* cannot override foo */ };

그런 다음 final얼마나 많은 재정의를 수행 할 수 있는지에 대한 '바닥'을 넣습니다. 다른 클래스는 A와 B를 확장 foo하고 해당 클래스를 재정의 할 수 있지만 클래스가 C를 확장하면 허용되지 않습니다.

따라서 'top-level'foo를 만드는 것은 의미가 없지만 아마도 final더 낮을 수도 있습니다.

(그러나 최종 단어를 확장하고 비가 상 구성원에게 재정의 할 여지가 있다고 생각합니다.하지만 다른 의미를 가질 것입니다.)


예를 들어 주셔서 감사합니다. 확실하지 않은 부분입니다. 그러나 여전히 : 최종 (및 가상) 기능을 갖는 요점은 무엇입니까? 기본적으로 함수를 재정의 할 수 없으므로 가상이라는 사실을 사용할 수 없습니다.
— lezebulon

@ lezebulon, 질문을 편집했습니다. 그러나 DanO의 대답을 알았습니다. 내가 말하려는 것에 대한 명확한 대답입니다.
— Aaron McDaid

나는 전문가가 아니지만 때로는 최상위 기능을 만드는 것이 합리적이라고 생각합니다 final. 당신이 알고있는 경우 예를 들어, 모든 원하는 Shape에의 foo()더 파생 된 형태는 수정하지 것을 미리 정의 및 명확한 - 비슷한는. 아니면 내가 틀렸고 그 경우에 더 나은 패턴을 사용합니까? 편집 : 아, 아마도 그 경우에, 최상위 레벨 foo() virtual로 시작 해서는 안되기 때문에 ? 그러나 여전히 (다형성으로) 호출 되어도 숨길 수 있습니다 Shape*.
— Andrew Cheong

8

내가 좋아하는 '최종'키워드의 사용 사례는 다음과 같습니다.

// This pure abstract interface creates a way
// for unit test suites to stub-out Foo objects
class FooInterface
{
public:
   virtual void DoSomething() = 0;
private:
   virtual void DoSomethingImpl() = 0;
};

// Implement Non-Virtual Interface Pattern in FooBase using final
// (Alternatively implement the Template Pattern in FooBase using final)
class FooBase : public FooInterface
{
public:
    virtual void DoSomething() final { DoFirst(); DoSomethingImpl(); DoLast(); }
private:
    virtual void DoSomethingImpl() { /* left for derived classes to customize */ }
    void DoFirst(); // no derived customization allowed here
    void DoLast(); // no derived customization allowed here either
};

// Feel secure knowing that unit test suites can stub you out at the FooInterface level
// if necessary
// Feel doubly secure knowing that your children cannot violate your Template Pattern
// When DoSomething is called from a FooBase * you know without a doubt that
// DoFirst will execute before DoSomethingImpl, and DoLast will execute after.
class FooDerived : public FooBase
{
private:
    virtual void DoSomethingImpl() {/* customize DoSomething at this location */}
};

1
예, 이것은 기본적으로 템플릿 방법 패턴의 예입니다. 그리고 C ++ 11 이전에는 항상 Java가했던 것처럼 C ++에 "final"과 같은 언어 기능이 있기를 바라는 것은 TMP였습니다.
— Kaitain

6

final 함수를 재정의하지 않으려는 명확한 의도를 추가하고이를 위반하면 컴파일러 오류가 발생합니다.

struct A {
    virtual int foo(); // #1
};
struct B : A {
    int foo();
};

코드가 의미하는대로 컴파일하고 B::foo재정의 A::foo합니다. B::foo그건 그렇고 가상입니다. 그러나 # 1을로 변경하면 virtual int foo() final이는 컴파일러 오류이므로 A::foo파생 클래스에서 더 이상 재정의 할 수 없습니다 .

이렇게하면 새로운 계층 구조를 "다시 열 수"없습니다. 즉 B::foo, 새로운 가상 계층 구조의 헤드에 독립적으로있을 수 있는 새로운 관련없는 기능 을 만들 수 있는 방법이 없습니다 . 함수가 최종되면 파생 클래스에서 다시 선언 할 수 없습니다.


5

마지막 키워드를 사용하면 가상 메서드를 선언하고 N 번 재정의 한 다음 '더 이상 재정의 할 수 없습니다'라고 명령 할 수 있습니다. 파생 클래스의 사용을 제한하는 데 유용하므로 "내 슈퍼 클래스를 사용하면이를 무시할 수 있지만 나에게서 파생 시키려면 할 수 없습니다!"라고 말할 수 있습니다.

struct Foo
{
   virtual void DoStuff();
}

struct Bar : public Foo
{
   void DoStuff() final;
}

struct Babar : public Bar
{
   void DoStuff(); // error!
}

다른 포스터에서 지적했듯이 비가 상 기능에는 적용 할 수 없습니다.

최종 키워드의 한 가지 목적은 우발적 인 메소드 재정의를 방지하는 것입니다. 필자의 예제에서 DoStuff ()는 파생 된 클래스가 단순히 올바른 동작을 얻기 위해 이름을 변경해야하는 도우미 함수일 수 있습니다. 최종적으로는 테스트 할 때까지 오류가 발견되지 않습니다.


1

함수에 추가 될 때 C ++의 최종 키워드는 기본 클래스에 의해 재정의되는 것을 방지합니다. 또한 클래스에 추가되면 모든 유형의 상속을 방지합니다. 최종 지정자를 사용하는 다음 예를 고려하십시오. 이 프로그램은 컴파일에 실패합니다.

#include <iostream>
using namespace std;

class Base
{
  public:
  virtual void myfun() final
  {
    cout << "myfun() in Base";
  }
};
class Derived : public Base
{
  void myfun()
  {
    cout << "myfun() in Derived\n";
  }
};

int main()
{
  Derived d;
  Base &b = d;
  b.myfun();
  return 0;
}

또한:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}

0

Mario Knezović의 답변 보충 :

class IA
{
public:
  virtual int getNum() const = 0;
};

class BaseA : public IA
{
public:
 inline virtual int getNum() const final {return ...};
};

class ImplA : public BaseA {...};

IA* pa = ...;
...
ImplA* impla = static_cast<ImplA*>(pa);

//the following line should cause compiler to use the inlined function BaseA::getNum(), 
//instead of dynamic binding (via vtable or something).
//any class/subclass of BaseA will benefit from it

int n = impla->getNum();

위의 코드는 이론을 보여 주지만 실제 컴파일러에서는 실제로 테스트되지 않았습니다. 누군가가 분해 된 출력을 붙여 넣으면 많은 감사를드립니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.