[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が、でちまるさんにより書かれている。
No comments:
Post a Comment