함수에 대한 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.