2011-05-29

C++0xの新機能、finalとoverride

gcc 4.7は、finalとoverrideを実装している。

finalとは、クラスの派生と、virtual関数のオーバーライドを禁止するための機能である。

struct B final { } ;
struct D : B { } ; // エラー

struct C { virtual void f() final { } } ;
struct D : C
{
    void f() { } // エラー、C::fはオーバーライドできない
} ;

これ以上説明する必要がないくらいに分かりやすい機能だ。現実的には、最底辺の基本クラスのvirtual関数のオーバーライドを禁止にするということはない。なぜならば、それは普通の関数とほぼかわりないからだ。あえて言えば、派生クラスで、同じシグネチャの関数を定義できなくする程度の意味しかない。大方、以下のような目的に使われるだろう。

struct A { virtual void f() = 0 ; } ;
struct B : A
{
    // 最終的な実装
    virtual void f() final
    { /* 実装 */}
} ;

// Bからは、まだ派生できるが、オーバーライドはさせない。
struct C : B { } ;

overrideは、「俺はオーバーライドするぞジョジョッ!」と明示的に叫ぶための機能である。overrideを指定しておきながら、実際にオーバーライドをしていないvirtual関数はエラーとなる。

struct Base { virtual void hoge() { } } ;

struct Fail : Base
{
    void hage() override { } // エラー、オーバーライドしていない
} ;

よくみると、Failのメンバー関数の名前は、hogeではなくhageになっている。そのため、hageは関数をオーバーライドしない。よってエラーとなる。

ご存知のように、virtual関数をオーバーライドするさいには、virtualキーワードは必要ない。しかし問題は、名前を間違えたり、仮引数や戻り値の型を間違えたりすると、別の関数になってしまう。

struct Base { virtual void func(int) { } } ;

struct Fail : Base
{
    // Base::hoge()をオーバーライドしたい
    void fucn(int) ; // 名前間違い
    void hoge(long) ; // 仮引数の型間違い
} ;

コンパイラーは喜んでこのコードを受け取り、別の関数として解釈してしまう。コンパイルエラーにならないのはもちろんのこと、警告を発するのも難しいのだ。たとえvirtualキーワードを使っていても、問題は変わらない。普通の関数ではなく、新しいvirtual関数を導入するだけだからだ。

間違いを防ぐためには、オーバーライドするということを明示的に示さなければならない。overrideはそのためのキーワードである。

完全を期すために付け加えると、finalとoverrideを組み合わせることもできる。

struct B { virtual void f() = 0 ; } ;
struct D : B
{
    void f() final override { }
} ;

意味はもちろん、finalとoverrideの両方である。

この新機能は、純粋にコンパイル時の些細なエラーを見つけやすくするための機能である。

この便利な機能がここ至るまでには、かなりの紆余曲折を経ている。そもそも、この機能はもともと、attributeを使って実現されるはずだったのだ。

// C++0xでは却下された機能
struct B { virtual void f() = 0 ; } ;
class C [[final]]
{
    [[final, override]] void f() { }
} ;

しかし、色々と議論した挙句、標準機能は独自のキーワードを与えられてしかるべきだというコンセンサスが得られた。しかし、一体どんなキーワードを使えばいいというのか。キーワードの追加は、互換性の敵である。既存のコードで使われていない識別子を選ぶ必要があるが、あまりに変な名前では、意味が分からない。finalというのは、他の言語の同等機能に与えられているキーワードである。しかし、さすがにfinalなどというキーワードを今更C++に付け加えることはできない。既存のコードを破壊してしまうからである。

苦渋の末、標準化委員会は、Contextual Keywordを使う決断をした。これは、ある特定の文脈で現れた時のみ、意味をなすキーワードである。

たとえば、C++の文法上、クラス指定子のクラス名の後に識別子が来ることはない。

class Name /*ここに識別子は使えない*/ : Base { } ;

とすれば、その場所だけで有効なキーワードを作ることができる。これは文法全体には影響しないので、finalという識別子は、他の場所では問題なく使える。

struct final final { } final ;
struct X final { } ;

本来、この機能にはもう一つ、hidingというキーワードが加わるはずだった。これは、attributeからcontextual keywordに移行するときに、newを再利用することになった。そして、最後の最後になって、ドラフトから削られた。

このhidingは、名前を隠すということを明示的に主張する機能である。

struct A
{
    int value ;
    void f( int ) ;
} ;
struct B : A
{
    int value ; // A::valueを隠す
    void f( double ) { } // A::fを隠す
} ;

int main()
{
    B b ;
    b.value = 0 ; // b.B::balue
    b.f( 0 ) ; // b.B::f
}

この、派生クラスのスコープが基本クラスのスコープ内の名前を隠すという挙動は、時として不思議な結果を招くことがある。また、うっかりして基本クラスのメンバーを隠してしまわないとも限らない。そのため、隠すということを明示的に主張するための、hidingが、finalやoverrideと同時に提案された。hidingが指定された名前が、実際には何も隠していない場合は、エラーとなる。

// C++0xでは却下された機能
struct A { int value ; }
struct B : A
{
    int vlaue new ; // エラー
} ;

B::vlaueは、typoである。単純な間違いだが、現実に起こりうる間違いである。しかもコンパイラーは警告を発することができない。hidingは、overrideと同じように、明示的な隠す主張を行えるようにした。

また、このoverrideとhidingを明示的に使わなければエラーとなる、explicitも提案された。

// C++0xでは却下された機能
struct A
{
    int value ;
    virtual void f() = 0 ;
} ;

struct B explicit : A
{
    int value ; // エラー、newが指定されていない
    void f() { } // エラー、overrideが指定されていない
} ;

何が問題だったかというと、hidingは、文法上、かなり広範な範囲で起こるのだが、既存の文法を虱潰しに対応させるだけの余裕がなかったのである。たとえば、using宣言が関係する場合はどうなるのか。

struct A { void f(int) ; } ;

struct B : A
{
    using A::f ;
    void f(double) ;
} ;

この場合、名前は隠されていない。しかし、using宣言で名前を隠すこともできる。

struct A { void f(int) ; } ;
struct B { void f(double) ; } ;

struct C : A, B
{
    using A::f ; // B::fを隠す
}

この機能が提案されたのは、規格がほぼ固まった後であり、到底対応する時間はなかった。また、hidingのために、newキーワードを再利用するというのは、あまり賛同を得られなかった。結局、色々と議論した挙句、今、早急にhidingを言語仕様に取り入れるのは、将来のためによろしくないと結論した。そこで、hidingが取り除かれ、それに関連して、explicitも取り除かれた。なぜなら、hidingなしでexplicitだけあっても、片手落ちであるし、将来hidingを取り入れた時に、深刻な互換性の問題を引き起こすからである。

色々あって、本来の機能の半分以上がそがれてしまい、finalとoverrideだけが残ったのである。

No comments: