2009-08-20

decltypeの訓練

こんにちは、脳内に優れたC++0xコンパイラをインストール済みのみなさん。今回は、あなたの脳内コンパイラが規格準拠かどうかを、さらにテストしてみましょう。

まずは、decltypeの決まりごとからです。優秀なみなさんのことですから、decltypeの決まりごとぐらい、当然暗記しているのは分かっていますが、念のために引用しておきます。

The type denoted by decltype(e) is defined as follows:
— if e is an unparenthesized id-expression or a class member access (5.2.5), decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;
— otherwise, if e is a function call (5.2.2) or an invocation of an overloaded operator (parentheses around e are ignored), decltype(e) is the return type of the statically chosen function;
— otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;
— otherwise, decltype(e) is the type of e.

まず、ひとつめの検証からいきましょう。人間コンパイラを自負している優秀なみなさんのことですから、C++0xドラフト規格の書かれている神聖なる言語、英語ぐらいは当然読めることかと思いますが、一応、翻訳します。

decltype(e)の型は、以下のように定義される。

いよいよ実際に特訓していきます。まずは最初の条件からです。ワクワクしますね。

もし、eが無括弧のid-expressionか、class member accessであれば、decltype(e)の型はeという名前の要素の型である。もし、そのような要素がないか、あるいは、eがオーバーロード関数群であれば、プログラムはill-formedである。

この条件に合致するコードとは、以下のようなものです。ちゃんと自前の脳内コンパイラでコンパイルしてから、答え合わせしてくださいね。id-expressionか、class member accessのどちらなのか、decltypeの型は何なのか。

char a ;
decltype(a) ;

int b ;
decltype(b) ;

int * p ;
decltype(p) ;

int i ;
int & r = i ;
decltype(r) ;

int && rr = i ;
decltype(rr) ;

int const c ;
decltype(c) ;

int volatile v ;
decltype(v) ;

int const volatile * const volatile cv ;
decltype(cv) ;

struct D {} d ;
decltype(d) ;

struct E { typedef int type ; } ;
decltype(E::type) ;

void func(int x, int y) ;
decltype(func) ;

struct F{ int a ; } f, * fp ;
decltype(f.a) ;
decltype(fp->a) ;

答えです。

// id-expressions

char a ;
decltype(a) ; // char

int b ;
decltype(b) ; // int

int i ;
int & r = i ;
decltype(r) ; // int &

int && rr = i ;
decltype(rr) ; // int &&

int const c ;
decltype(c) ; // int const

int volatile v ;
decltype(v) ; // int volatile

int const volatile * const volatile cv ;
decltype(cv) ; // int const volatile * const volatile

struct D {} d ;
decltype(d) ; // struct D

// qualified-id
struct E { typedef int type ; } ;
decltype(E::type) ; // int

void func(int, int) ;
decltype(func) ; // void (int, int)


// class member access

struct F{ int a ; } f, * fp ;
decltype(f.a) ; // int
decltype(fp->a) ; // int

では、ill-formedな例を挙げてみましょう。なぜill-formedなのか、脳内コンパイルのエラーメッセージを読んでくださいね。

decltype(Nudge_Nudge) ;

void func(char) ;
void func(int) ;

decltype(func) ;

おわかりでしょうか。では注目の答えです。

// エラー:Nudge_Nudgeという識別子は定義されていない。
decltype(Nudge_Nudge) ;

void func(char) ;
void func(int) ;

// エラー:funcには複数のオーバーロード関数がある。
decltype(func) ;

だいたい、チョメチョメなんて存在するわけがないですからね。幻想ですよ。

さて、次の条件に行きましょう。ゾクゾクしますね。

それ以外の場合で、eが関数呼び出しか、オーバーロード演算子の呼び出し(括弧は無視される)であれば、decltype(e)は、静的に選ばれた関数の戻り値の型である。

簡単ですね。では、関数呼び出しの例を挙げますので、さっそく脳内コンパイルにかかりましょう。

void a(void) ;
decltype(a()) ;

int b(int, int) ;
decltype( b(0,0) ) ;

int c(int) ;
float c(float) ;
double c(double) ;

decltype( c(0) ) ;
decltype( c(1.0f) ) ;
decltype( c(1.0) ) ;
decltype( c('a') ) ;

いかがですか。あなたの脳内コンパイラのパフォーマンスはどうでしょう。何分もかかったりしていませんか。それでは答えです。

void a(void) ;
decltype(a()) ; // void

int b(int, int) ;
decltype( b(0,0) ) ; // int

int c(int) ; // #1
float c(float) ; // #2
double c(double) ; // #3

decltype( c(0) ) ; // int(#1が選ばれる)
decltype( c(1.0f) ) ; // float(#2が選ばれる)
decltype( c(1.0) ) ; // double(#3が選ばれる)
decltype( c('a') ) ; // int(charからintへの暗黙の型変換の結果、#1が選ばれる)

関数呼び出しは、戻り値の型になります。最後のdecltypeは、charからintへの暗黙の型変換が行われています。decltype(e)のeのexpressionが評価されることはありません。ただし、評価された場合の型は、評価しなくても静的に判断できます。したがって、関数定義は必要ないのです。

では、オーバーロード演算子の呼び出しに移りましょう。脳内コンパイルスタート!

decltype(1 + 1) ;
decltype(1 + 1.0f) ;
decltype(1.0f + 1.0) ;
decltype(1 + 1 * 1 / 1) ;
decltype((1 + 1)) ;

int i = 0 ;
decltype(++i) ;

struct A{} ; struct B{} ;
struct C{} ; struct D {} ;

C operator + (A, B) ;
D operator + (A, C) ;
D operator + (C, A) ;

A a ; B b ; C c ; D d ;

decltype(a + b) ;
decltype(a + c) ;
decltype(a + b + a) ;

int *p ;
decltype(*p) ;
decltype(&p) ;

さあ、この辺は少々難しいかも知れません。脳内コンパイラの規格準拠度が試されます。

decltype(1 + 1) ; // int
decltype(1 + 1.0f) ; // float
decltype(1.0f + 1.0) ; // double
decltype(1 + 1 * 1 / 1) ; // int
decltype((1 + 1)) ; // int

int i = 0 ;
decltype(++i) ;// int

struct A{} ; struct B{} ;
struct C{} ; struct D {} ;

C operator + (A, B) ; // #1
D operator + (A, C) ; // #2
D operator + (C, A) ; // #3

A a ; B b ; C c ; D d ;

decltype(a + b) ; // C(#1が選ばれる)
decltype(a + c) ; // D(#2が選ばれる)

// +演算子は左結合なので、(a + b) + aと解釈される。
// a + b の型は、#1が選ばれ、C、
// C型の値 + aは、#3が選ばれ、D。
decltype(a + b + a) ;// D

int *p ;
decltype(*p) ; // int &
decltype(&p) ; // int **

+-*/といった二項演算子は、数の型変換を行った後のオペランドの型を返します。++演算子は、オペランドの型を返します。それさえ分かっていれば単純ですね。

ポインタの苦手な人は、最後で頭がカーネルパニックを起こすかも知れません。しかし、落ち着いて考えれば、簡単に分かることです。

さあ、いよいよ三つ目の条件です。ドキドキしますね。

それ以外の場合で、eがlvalueならば、decltype(e)はT &である。Tはeの型である。

もちろん、ここまで読み進めることができるほどの優秀な脳内コンパイラをお持ちのみなさんならば、解説の必要はないとは思いますが、それでも念のために解説します。括弧が無い場合は、すでに、最初の条件に合致しています。すると、残るのは、括弧でくくられている場合です。例をどうぞ。

int a ;
decltype((a)) ;
decltype(((a))) ;
decltype((((a)))) ;

int i ;
int & r = i ;
int && rr = i ;
decltype((r)) ; 
decltype((rr)) ;

Brain Compiler ver 2.0 initiating... done.
parsing... done.
lexical analyzing... done.
semantical analyzing... done.
wasting brain resources...... remaining ∞

int a ;
decltype((a)) ; // int &
decltype(((a))) ; // int &
decltype((((a)))) ; // int &

int i ;
int & r = i ;
int && rr = i ;
decltype((r)) ; // int &
decltype((rr)) ; // int &

括弧は一つだろうが二つだろうが三つだろうが、結果は同じです。もちろん、脳細胞の数は有限ですから、脳内コンパイラがパース可能な括弧数も、現実的には有限ですが、理論上は無限です。

参照に関しては、すこし厄介です。参照の参照は認められていません。では、最後のコードはill-formedではないかというと、そうではないのです。実は、参照というのは、lvalue referenceもrvalue referenceも、lvalueなのです。従って、括弧内で使った場合は、lvalueになります。

さあ、いよいよ最後です。もうそろそろクタクタでしょうか。

それ以外の場合、decltype(e)はeの型である。

さて、これも数が多いのですが、ほんの一例を挙げていきましょう。あなたの脳はマルチプロセッサでしょうか。脳内OSはプリエンプティブなスレッドをサポートしていますか。

decltype(0) ;
decltype((0)) ;
decltype(0u) ;
decltype(0.0f) ;
decltype(0.0) ;

decltype("hello,world") ;
decltype(L"hello,world") ;
decltype(u"hello,world") ;
decltype(U"hello,world") ;


struct Foo
{
    Foo()
    {
        decltype(this) ;
    }
} ;

decltype([]{}) ;
decltype([]{}()) ;

さあ、お待ちかねの答えです。

// rvalues

decltype(0) ; // int
decltype((0)) ; // int
decltype(0u) ; // unsigned int
decltype(0.0f) ; // float
decltype(0.0) ; // double


// literals

decltype("hello,world") ; // char const [12]
decltype(L"hello,world") ; // wchar_t const [12]
decltype(u"hello,world") ; // char16_t const [12]
decltype(U"hello,world") ; // char32_t const [12]


// this
struct Foo
{
    Foo()
    {
        decltype(this) ; // struct Foo *
    }
} ;

// lambda expression

decltype([]{}) ; // 規定されていないユニークな型
decltype([]{}()) ; // void

最後の条件に合致する例としては、まず、rvalueが挙げられます。次に、リテラルの類です。thisはrvalue expressionで、3.10(Lvalues and rvalues)でlvalueであると言及されていないので、この条件に含まれます。lambda expressionもそうです。

あなたの脳内コンパイラの規格準拠度はどうでしたか。

9 comments:

zak said...

脳内コンパイラがwarning吐いたので・・・
1)
int && r = i ;
decltype(i) ; // int &&
ー> decltype(r)の間違いですよね。

2)
void a(void) ;
decltype(a) ; // void
ー> decltype(a()); ならvoid。原文ママならvoid(void)。

脳内コンパイラが華麗に間違えたところ
int && rr = i ;
decltype((rr)) ; // int &
ill-formedだとばかり。何てことだ。

非常に面白かったです。
脳内コンパイラの動作時間は5分くらい。・・・遅い。

tiger said...

こういうの助かります(なんせ、0x 読む時間が取れないもんで…:勉強しなきゃってプレッシャーも感じるけど(^^;)。
a + b + a は、operator + (B, A) が定義されていないので、エラーだと思うです。
もし国語力のキッチリした系統からの C++本って出れば、面白そう。
#コメントのレイアウト、ちょっとキツイっすね…。(タブキー移動しないと…)

tiger said...

a + b + a -> c + a じゃんかよ・・・寝てるね>俺
コメントは、どうしても、S/N 比が下がっちゃうなー。すんません

DigitalGhost said...

え、decltype(E::type)って通るのですか? 5.1.1では、qualified-idがvariableかfunctionである場合以外に、id-expressionとなるようなqualified-idがあるような記述は読み取れなかったのですが、どこかで言及されていますか?

江添亮 said...

ん?
言っている意味がよく分からないのですが。
すべてのqualified-idはid-expressionですし、
E::typeはqualified-idだと思うのですが。

DigitalGhost said...

> すべてのqualified-idはid-expressionですし、
規格の5.1.1のp6に、
A nested-name-specifier that names a class, (snip) is a qualified-id; (snip).
The result is the member. The type of the result is the type of the member.
The result is an lvalue if the member is a static member function or a data member.
とありますが、ここにはqualified-idがtypeである場合について言及されていないので、
id-expressionには、typeであるqualified-idは含まれないんじゃないかと思いました。

江添亮 said...

そこの省略している部分には、
>and then followed by the name of a member of either that class (9.2) or one of its base classes (Clause 10)
と書いてあるのですが。
Nested type namesもクラスのメンバーです。

DigitalGhost said...

なるほど、ありがとうございます。こういうときにカタコト程度にしか英語が読めないは不便です…

iz said...

int i;
int&& rr = i;
はバインドできないのでコンパイル通りません