2009-05-20

rvalue referenceについて

VC10のrvalue referenceの実装が、*thisを含まないという但し書きがあるのをみて、ぱっと*thisへの参照がlvalueなのかrvalueなのかが問題なのだというのを思いつけなかったのは、情けない。これは自分のrvalue referenceへの理解が足りないためだ。rvalue referenceについても、よく知っているはずだったのに、実に情けない。そこで、rvalue referenceについて解説してみることにした。

まず、lvalueとrvalueの違いについて述べる。この違いを、言葉で説明するのは難しい。むしろ、コードで説明した方が分かりやすい。

int f()
{ return 0 ; }

void g()
{
        int a = 0 ;
        a ; // lvalue

        0 ; // rvalue
        int(0) ; // rvalue
        f() ; // rvalue
}

なんとなく違いが分かってくれたと思う。この違いが目に見える形で現れるのは、参照型に代入をしようとした時だ。

int f()
{ return 0 ; }

void g()
{
        int a = 0 ;
        int & r = a ; // OK

        int & r1 = 0 ; // Error
        int & r2 = int(0) ; // Error
        int & r3 = f() ; // Error

}

rvalueを、C++03の参照に代入しようとすると、エラーになる。なぜならば、C++03の参照は、lvalueへの参照(lvalue reference)だからだ。

ところで、rvalueは、const lvalue referenceでキャプチャすることができる。これは実に不思議な仕様だ。私はlvalueとrvalueを理解する前にも、常々、違和感を持っていた。

int f() { return 0 ; }
void call_by_reference( int & ) { }
void call_by_const_reference( int const & ) { }

void g()
{
        int a = 0 ;

        call_by_reference( a ) ; // OK
        call_by_const_reference( a ) ; // OK

        int & r = a ;

        call_by_reference( r ) ; // OK
        call_by_const_reference( r ) ; // OK

        call_by_reference( 0 ) ; // Error, rvalue referenceは非constな参照では受けられない
        call_by_const_reference( 0 ) ; // OK

        call_by_reference( int(0) ) ; // Error, rvalue referenceは非constな参照では受けられない
        call_by_const_reference( int(0) ) ; // OK

        call_by_reference( f() ) ; // Error, rvalue referenceは非constな参照では受けられない
        call_by_const_reference( f() ) ; // OK
}

私は、lvalueとrvalueを理解していない時でも、これが疑問だった。なぜだ。aという式と、0とかf()という式は、全然別物じゃないか(つまりrvalueになる) なぜconst referenceならば代入できるのだ。確かに、constであれば、問題はないに違いない。でも何か違う気がする。

ところで、std::auto_ptrというものがある。以下のように使える。

std::auto_ptr< int > f( std::auto_ptr< int > ptr )
{ return ptr ; }

void g()
{
        std::auto_ptr< int > ptr( new int(0) ) ;
        std::auto_ptr< int > ret = f( ptr ) ; // OK, ただし、その理由は謎である。
}

なぜこれが通るのか。f()の戻り値はrvalueとなっている。constでしか受けられないはずだ。どうやって安全にretに代入しているのだろうか。私は、このコードのコンパイルを通すための、実に驚嘆すべきauto_ptrの実装方法を知っているが、ここに記すには余白が足りない。どうしても知りたい方は、Nicolai JosuttisのThe C++ Standard Libraryを読めば、実装方法が乗っている。簡単に言うと、Template Argument DeductionとOverload Resolutionの微妙な差異を利用して、この場合にだけ動く型変換の状況を作り出し、別のクラスに変換してやってから、ポインタを取り出している。それはさておき――

この、rvalueをconstな参照で受けられるという不思議な仕組みは、じつに奇妙な挙動を引き起こす。それに、実際、制限も多い。というわけで、C++0xからは、rvalue referenceというものが入るようになった。これにともない、以前の、C++03の参照は、lvalue referenceと呼ばれるようになる。

int f()
{ return 0 ; }

void call_by_rvalue_reference( int && ) { }

void g()
{
        int && r1 = 0 ; // OK
        int && r2 = int(0) ; // OK
        int && r3 = f() ; // OK

        call_by_rvalue_reference( 0 ) ; //OK
        call_by_rvalue_reference( int(0) ) ; //OK
        call_by_rvalue_reference( f() ) ; //OK
}

これは、rvalueを捕捉するための参照だ。だから、rvalue referenceと呼ばれている。const_castでconstを外すのは、大抵の環境で動くだろうが、それは危険なコードである。

rvalue referenceは、関数のオーバーロードにも使える。

int f() { return 0 ; }

void call( int & ref ) ; // #1
void call( int && ref ) ; // #2

void g()
{
        int a = 0 ;
        call( a ) ; // #1 が呼ばれる。

        // これらは、#2が呼ばれる。
        call( 0 ) ;
        call( int(0) ) ;
        call( f() ) ;
}

rvalue referenceとは、基本的にこれだけの事である。しかし、ここで説明を終えてしまっては、rvalue referenceの必要性が分からないと思われるので、Move Semanticと呼ばれている概念を説明しようと思う。たとえば以下のコード――

class Foo
{
     int * ptr ;

public :
        Foo()
        {
                ptr = new int[10000] ;
        }
        ~Foo()
        { delete[] ptr ; }

        Foo( Foo const & r )
        {
                ptr = new int[10000] ;
                std::copy( r.ptr, r.ptr + 10000, ptr ) ;
        }

} ;


Foo f() { return Foo() ;}

void g()
{
        Foo a ;
        Foo b( a ) ; // #1
        Foo ret( f() ) ; // #2
}

#1では、メモリの確保、コピーが発生する。それは仕方がないことだ。なぜなら、コピーしたいのだから。しかし、#2の場合は違う。f()の戻り値であるrvalueは、どうせこの後すぐに、消される運命なのだ。なんでわざわざメモリの確保とコピーを行わなければならないのか。f()の戻り値のポインタをそのまま、retに代入しても、何の問題もない。

この場合、ポインタだけ渡すなどという操作を、Move Semanticsという。実際、メモリを確保してコピーするよりも、ポインタをすげ替えた方がいいからだ。このように、rvalueからlvalueへ、moveすることは、たいていの場合、全く問題がない。しかし、C++03では、これを実現する文法がない。なぜなら、rvalueを区別して変数に束縛(bind)することができないからだ。これまでは、const lvalue referenceとしてbindしてきた。C++0xでは、rvalue referenceがあるので、rvalueの場合を考慮してオーバーロードできる。

class Foo
{
     int * ptr ;

public :
        Foo()
        {
                ptr = new int[10000] ;
        }
        ~Foo()
        { delete[] ptr ; }

        Foo( Foo const & r )
        {
                std::cout << "コピーするよ~、遅いよ~、我慢してね~" << std::endl ;
                ptr = new int[10000] ;
                std::copy( r.ptr, r.ptr + 10000, ptr ) ;
        }

        Foo( Foo && r )
        {
                std::cout << "たんなるポインタのすげ替えだよ~、早いよ~" << std::endl ;
                ptr = r.ptr ;
                r.ptr = null_ptr ;
        }

} ;


Foo f() { return Foo() ;}

void g()
{
        Foo a ;
        Foo b( a ) ; // #1
        Foo ret( f() ) ; // #2
}

ごらんの通りだ。

そのほかにも、Forwardingという概念がある。これは、詳しくは解説しないが、関数オブジェクトのラッパを作る時に、ラッパに渡されたlvalueとrvalueは、区別してそのまま、実際の関数オブジェクトに渡したいという希望がある。C++03では、容易にできなかったことだが、C++0xでは、rvalue referenceのおかげでできるようになった。

以上がrvalue referenceの簡単な解説である。rvalue referenceの導入により、C++0xでは、lvalueとrvalueの区別が明確になるだろう。C++03では、違いを表現する文法が欠如しているために、プログラマは、lvalueとrvalueの違いを、実感することができなかった。そのため、理解もできず、いろいろとわけの分からないコンパイルエラーに遭遇したことも多いだろうと思う。C++0xでは明確に区別できるので、そういう混乱も起こらなくなり、より学びやすくなる。私がC++を学び始めた時にC++0xになっていれば、どんなにか学習が容易だったことだろうか。

ところで、実は*thisへのrvalue referenceというものがある。これはどういうものか。

non-staticなメンバ関数が、メンバ変数を参照できるのは、魔法でも何でもない。プログラマからは見えないものの、メンバ関数には、this参照への引数が、暗黙のうちに含まれているのだ。ちなみに、thisは、プログラマから見るとポインタだが、実際には参照であるべきものだ。thisがポインタであるのは、単にthisが参照よりも早く導入されたという、歴史的な理由でしかない。

ところで、そのクラスの実際のオブジェクトも、lvalueの時と、rvalueの時がある。そういう場合、区別できるようにしようというものだ。

struct Foo
{
    void func() & ; // #1 *thisはlvalue reference
    void func() && ; // #2 *thisはrvalue reference
} ;

Foo f() { return Foo() ; }

void g()
{
    Foo a ;
    a.func() ; // #1が呼ばれる。

    f().func() ; // #2が呼ばれる。
}

これはもちろん、メンバ関数限定である。尚、通常の何も指定しないメンバ関数は、lvalue referenceの*thisを引数に持つものとして解釈される。つまり、#1と同じだ。この新しい修飾は、ref-qualifierと呼ばれていて、cv-qualifierの後、exception-specificationの前に記述できる。つまり、void Foo::func() const volatile && throw() という順番になる。

また、ref-qualifierを明示的に指定しないメンバ関数と、ref-qualifierを指定するメンバ関数で、引数名前などが同じメンバ関数のオーバーロードを記述することはできない。これは、明示的に指定しないメンバ関数とlvalue referenceのメンバ関数にとどまらず、rvalue referenceであってもそうだ。

struct Foo
{
    void f() ; // Error
    void f() & ; // Error

    void g () ; // Error
    void g() && ; // Error

    void h() & ; // OK
    void h() && ; // OK
} ;

この仕様は不思議だが、プログラマをしてlvalueとrvalueの違いに気づかせるには、むしろ、いい仕様かもしれない。

ところで、VC9だと、以下のコードのコンパイルが通ってしまうのだが、いいのだろうか。

struct Foo
{
    void func() {  }
} ;

Foo f() { return Foo() ; }

void g()
{
        f().func() ;
}

ペーパーにあったこのコードもコンパイルが通ってしまう。(もちろん、ref-qualifierをなくした状態で、だが。)

struct S {
  S* operator &() &;            // Selected for lvalues only
  S& operator=(S const&) &;     // Selected for lvalues only
};

int main() {
  S* p = &S();                  // Error!
  S() = S();                    // Error!
}

追記:ペーパーにあるコードは、おそらく、ref-qualifierが導入された暁には、*thisがlvalueの場合だけ呼び出されるメンバ関数を書くことが出来るという意味だろう。

9 comments:

Anonymous said...

http://cpplover.blogspot.com/2008/10/hasxxx.html
に間違ってコメントしてしまいました。

上から5つ目のコードの
void call_by_rvalue_reference( int & ) { }

void call_by_rvalue_reference( int && ) { }
の間違いじゃないんですか?

江添亮 said...

おっと、間違えた。修正修正。
しかし、よりにもよって、has_xxxの実装方法に誤爆するとは、
またマニアックなものを読んでますな。しかも、そこでもtypoを見つけてしまった。

Anonymous said...

> ところで、VC9だと、以下のコードのコンパイルが通ってしまうのだが、いいのだろうか。
VC は昔からデフォルトでは右辺値を参照(従来の左辺値参照)にバインドできるように
なっています。これは標準から外れた言語拡張にあたるもので、 /Za オプションで無効に
できます。

江添亮 said...

なるほど、しかし以下のコードは通らないんですね。
うーん、inconsistencyの香りが。

void f( int & ref ) { }
void g() { f( 0 ) ; }

Anonymous said...

些細なことですが
×補足
○捕捉

江添亮 said...

キャプチャ、捕捉、束縛、bind。
うーん、気がついたら、四種類も言葉を使っている。なんと統一感のない。

egtra said...

投稿できなかったようなのでもう1度書きます。f().func()はC++03の仕様として問題ないはずです。ざっと見たところ、3.10に僅かに触れられているのみでしたが。S* p = &S();とS() = S();も、C++03ではエラーにできなかったがref修飾子でエラーにできるという話だろうと思います。

江添亮 said...

確認しなくては。
しかし、3.10はあまり分かりやすい記述とは思えないなぁ。

江添亮 said...

なるほどなるほど。