2013-09-09

C++1yに提案されている不透明エイリアス(opaque alias)

[PDF注意] N3741では、C++1yに向けて、不透明エイリアス(opaque alias)が提案されている。

不透明エイリアスとは、Strong typedefなどとも呼ばれてきた機能で、typedefに似ているが、別の型として認識される別名の宣言機能だ。

C++には、Cから受け継いだtypedef指定子という機能がある。また、最近はもう少しまともな文法の、エイリアス宣言がある。どちらも機能も、「typedef名」という型に対する別名を宣言する。

typedef int Integer ; // typedef指定子
using Number = int ; // エイリアス宣言

typedef名は、ソースコード中に、型名で意味を記述させることができる。例えば、年齢とかお金とか身長とかだ。

using Age = int ;
using JPY = int ;
using Height = int ;

これらの値は、意味的に別の単位であり、混ぜあわせて演算したくはない。年齢とお金と身長の値を足し引きすることに、通常は意味がない。

しかし、typedef名は、あくまで型に別の名前をつけるだけであり、型自体はおなじだ。そのため、そのような意味的に防ぎたい単位の混同を防ぐことができない。

int main ()
{
    using Age = int ;
    using Height = int ;

    Age age = 0x20 ;
    Height height = 156 ;

    int x = age + height ; // エラーではない
}

また、異なる型として認識されないという事は、関数のオーバーロードの実引数や型テンプレート実引数としても使えないことを意味する。

using Age = int ;
using Height = int ;

void f( Age ) { }
void f( Height ) { } // エラー、ODR違反

しかし、単純に別の型にすればよいというわけではない。もし、typedef機能が単純に別の型を宣言するとしたら、以下のような問題が起こる。

int main()
{
    using Age = int ; // typedef名ではなく、Ageという名前の別の型を宣言するものとする。

    Age age = 0x20 ; // エラー
}

なぜなら、単純に別の型を定義できる場合、規格は当然、そのようなユーザー定義型とint型の標準変換(暗黙の型変換)を定義していないので、標準変換は起こらない。

では、このような方法で宣言された別の型名は、元の型名との標準変換を許せばいいのか。その型だけを取り扱う場合や、関数のオーバーロードやテンプレートのインスタンス化だけならそれでもいいが、それ以外の点では、元の木阿弥である。

int main()
{
    using Age = int ; // 別の型名を宣言する。ただし、標準変換がうごくものとする
    using Height = int ;

    Age age = 0x20 ; // OK
    Height height = 156 ; // OK

    age + height ; // エラーとはならない
}

では、明示的にキャストをした場合のみ型変換を行う場合はどうだろう。ある種の問題は解決してくれるが、完全な解決にはならない。

異なる単位の間で計算をしたこともある。しかし、その型で許されている演算子をすべて適用したいわけではない。たとえば、ニュートンの法則により、力は、質量と加速度をかけることで求めることができる。

using Force = double ; // 力
using Mass = double ; // 質量
using Accel = double ; // 加速度

Force calc_force( Mass m, Accel a )
{
    return m + a ; // エラーにはならない。
} 

このコードは、残念ながらコンパイルエラーにも実行時エラーにもならない。また、このコードを警告する静的解析ツールも存在しない。なぜならば、C++にはそのような意味を表現する文法がないからだ。

と、こう考えると、単に別の型を定義するだけでは不十分で、万人が納得できる一つの機能を提供することは不可能である。標準変換や、許可する操作について、ユーザー側が柔軟に指定できるような文法が必要だ。

しかし、なぜ新しい言語機能が必要なのか。我々には古き良きクラスがあるではないか。そう考える読者もいるだろう。たしかに、クラスを使えば、暗黙の型変換や許可する操作についても、望みのままの挙動を実装できる。問題は、面倒だという事だ。

独自の実装の文字列クラスとか、独自の実装の多精度整数クラスを実装するのなら分かる。しかしこの問題は、単に既存の型と同じように機能しつつ、別の型として認識されてほしい問題なのだ。

したがって、クラスで実装するとなると、元の型からprivateで派生されたクラスで、大部分のメンバーを、単に転送するだけの退屈で機械的なコードを書かなければならない。たとえば。std::stringとは別の型として認識されて欲しいクラスを考えてみよう。

class my_string : private std::string
{
public :
    using std::string::string ; // 継承コンストラクター

    // 基本クラスのネストされた型名を網羅して宣言
    using iterator = std::string::iterator ;
    // 省略

    // 基本クラスのメンバー関数を網羅して転送
    iterator begin() noexcept { return std::string::begin() ; }
    // 省略
    
} ;

このように、機械的で退屈な転送を山ほど書かなければならない。C++11では、継承コンストラクターにより、コンストラクターだけは自動で転送できるが、所詮コンストラクターだけだ。もちろん、perfect fowardingなど、考慮しなければならないことが山ほどある。例えば、人間の名前を表現するために、ファーストネーム、ミドルネーム、ラストネームを混同しないように、型名を分けるとすると、このようなクラス名だけ違う中身は同じラッパークラスを3つも書かなければならない。これは人間のすることではない。

明らかに、新しい言語機能が必要である。その新しい言語機能として提案されているのが、不透明エイリアス(opaque alias)だ。

現段階で提案されている不透明エイリアスの文法は、エイリアス宣言を拡張したものだ。

using identifier = access-specifier type-id opaque-definition

アクセス指定子は、クラスのと同じで、public, protected, privateとなる。publicは暗黙の型変換を許可する。privateは暗黙の型変換を許可しない。protectedは、opaque-definitionに従う。

アクセス指定子がpublicとprivateの場合の不透明エイリアスは使い方は以下のようになる。

int main()
{
    using Age = public int ;

    Age age = 0x20 ; // OK

    using Height = private int ;
    Height h1 = 156 ; // エラー
    Height h2 = static_cast<Height>(156) ; // OK、明示的なキャストはできる
}

もちろん、関数のオーバーロードや、テンプレートのインスタンス化でも、別の型として扱われる。

using type = public int ;

void f( int ) { }
void f( type ) { } // OK、別の型

template < typename T >
struct temp { } ;

int main()
{
    temp< int > t1 ; // intに対する特殊化
    temp< type > t2 ; // 上とは異なる、typeに対する特殊化、
    std::is_same< int, type >::value ; // false
}

publicとprivateは、暗黙の型変換を許すか許さないかという二択になっている。しかし、実際には、もっと細かく指定したい場合がある。例えば、以下のコードはどうなるべきなのか。

int main()
{
    using Age = public int ;

    Age age = 0x20 ;
    auto x = 1 + age ; // 型は何か?
    auto y = +age ; // 型は何か?
}

このような場合、式を評価した結果の型は、一体どうなるべきなのか。ある者は、Ageになって欲しいだろうが、ある者はintになってほしいだろう。また、全く別の型になってほしいかもしれない。場合により望みの挙動は異なる。

opaque-definitionは、そのような細やかな指定を可能にする機能だ。

using Age = public int
{
    Age operator +( Age, int ) = default ;
    Age operator +( int, Age ) = default ;
    Age operator +() = default ;
    Age operator +( Age, double ) = delete ;
    Age operator +( double, Age ) = delete ;
} ;

int main()
{
    Age age = 0x20 ;
    auto x = age + 1 ; // 型はAge
    auto y = age + 0.1 ; // エラー、delete定義されている

} 

このように、望みのシグネチャを指定することができる。定義として= defaultを指定すれば、実装に愚直な転送コードを自動的に生成させることができる。もちろん、何か別のことがやりたければ、関数のように明示的な定義を与えることもできる。また、=delete定義により、一部の操作を禁止することもできる。

とまあ、まだ提案の段階であり、より厳密な機能は、これから何年もかけて議論の上で設計していく事になるが、以上が、現在C++1yに提案されている不透明エイリアスの基本的な使い方だ。

あまり紹介したくないが、opaque definitionの一部機能を実現しているCプリプロセッサーを悪用した極めて邪悪なライブラリである、DESALT_NEWTYPEが、でちまるさんにより書かれている。

dechimal/desalt · GitHub

No comments: