2009-12-25

ref-qualifier: rvalue referenceの完全を期すための補足

rvalue reference 完全解説で、rvalue referenceは、ほぼ解説した。ところが、うっかりしたことに、ひとつだけ忘れていた事項があったので、それを解説する。

それは、非静的なメンバ関数に、ref-qualifierがつけられるというものだ。例を挙げる。

struct Foo
{
    void f() & {}
    void f() && {}
}

いったいこれは何なのか。これは、thisに対する修飾である。thisは、C++では、歴史上の理由でポインタになっているが、本来、参照となるべきであった。thisが指し示すのは、そのクラスのオブジェクトである。

struct Foo
{
    void f() {}
} ;



int main()
{
    Foo a, b ;

    a.f() ;// thisは、aを指す。
    b.f() ;// thisは、bを指す。
}

つまり、非静的なメンバ関数には、暗黙の内に、thisという引数が渡されていると考えても良い。実際、規格上は、そのような扱いになっている。というのも、thisもoverload resolutionの際に考慮されるのだ。C++03では、非静的なメンバ関数には、cv修飾ができる。これはなんに対する修飾かというと、thisに対する修飾である。

struct Foo
{
    void f() { std::cout << "none" << std::endl ;}
    void f() const { std::cout << "const" << std::endl ;}
    void f() volatile { std::cout << "volatile" << std::endl ;}
    void f() const volatile { std::cout << "const volatile" << std::endl ;}
} ;



int main()
{
    Foo a ; Foo const b ;
    Foo volatile c ; Foo const volatile d ;

    a.f() ;// none
    b.f() ;// const
    c.f() ;// volatile
    d.f() ;// const volatile
}

コンパイラは、thisのcv修飾(cv-qualifier)を静的に決定できるので、当然、このoverload resolutionも、静的に解決出来るのである。

今回、これに参照修飾(ref-qualifier)が加わる。以下のように使える。

struct Foo
{
    void f() & { std::cout << "*this* is lvalue reference." << std::endl ;}
    void f() && { std::cout << "*this* is rvalue reference." << std::endl ;}
} ;

// 関数の戻り値はrvalue
Foo g() { return Foo() ; }


int main()
{
    Foo a ;

    a.f() ;// lvalue reference.
    std::move(a).f() ;// rvalue reference.

    g().f() ; // rvalue reference.
}

コンパイラは、ある型がlvalueなのかrvalueなのかを、静的に決定できるので、当然、overload resolutionも静的に決定出来るのである。

ちなみに、何も書かないと、lvalue reference修飾したものとみなされる。

もちろん、cv-qualifierも、メンバ関数の型として扱われるので、同名で引数が同じメンバ関数の型は、8種類あることになる。cv-qualifierとref-qualifierは、どちらもoverload resolutionの際に考慮される。

たぶん、これでrvalue refereneに対する解説は、完全になったことと思う。

5 comments:

redboltz said...

b.f();は lvalue referenceとなるのでは無いでしょうか?
void test(Foo &&arg);
void test(Foo &arg);
があったとして、test(b)は、test(Foo &arg)の呼び出しとなるのと同様に。
また、a.f()をstd::moveのあとに呼び出していますが、これは、moveされた
データメンバへのアクセスがない(そもそもこの例ではデータメンバがないが)
から安全ということですよね?

江添亮 said...

>b.f();は lvalue referenceとなるのでは無いでしょうか?
よく考えればそうでした。修正します。

std::move()を、何かコア言語の機能のように勘違いしていませんか?
あくまでSemanticsです。
std::moveは、static_cast(a) をしているにすぎないのです。
rvalue referenceも、単なるreferenceに過ぎないのです。
Move Semanticsとは、たんにlvalueとrvalueを、movableなフラグとして使っているに過ぎないのです。

その他は、lvalue referenceの場合と、何も変わりありません。
lvalue referenceで、データメンバにアクセスしたからと言って、そのオブジェクトがその後使えなくなるとは限らないでしょう。
もちろん、参照しているわけですから、publicなメンバ変数に対して、破壊的な書き換えもできるわけです。

江添亮 said...

std::moveは、static_cast(a) をしているにすぎないのです。
うっかりangle bracketが消えてしまいました。

std::moveは、static_cast< Foo && >(a) をしているにすぎないのです。

江添亮 said...

やはり、コンパイルして確認したいものですね。
gccも、まだ対応していないのです。
C++0x Support in GCC- GNU Project - Free Software Foundation (FSF)

redboltz said...

std::moveが、static_castに過ぎない件、理解していたのですが、
以下の勘違いをしたまま、コメントしてしまいました。

確か、元のサンプルは、
Foo &&b = std::move(a);
でしたよね?
私は、コメントの2点目を書いているときは、これを、以下のように勘違いしてしまいました。
Foo b = std::move(a);
この場合、Fooの右辺値参照を引数とするコンストラクタ(move constructor)
が呼ばれると思います。
そして、このような処理を行った後でも、move constructorの内容によっては、
aを継続して利用することが可能だが、プログラミングスタイルとしては、
推奨されないと思うので、より適切なサンプルの記述法があるのでは?
などと思っておりました。

いずれにしても、最初の勘違いに基づく話です。
お騒がせしました。