話のネタはありながら、気が付けば木曜日となり、どうやら毎週木曜日に書くという習慣が自分の中に根付いたようです。

さて今日はC++の話です。ActiveBasicもバージョン5で色々と変わりそうですが、C++も目下新規格の制定に向けて話が進んでいます。その新規格は現在のところ、C++0xと呼ばれています。勿論200x年中の制定を見込まれているからですが、最近ではC++09という言葉もちらほら見かけ始めている気もします。

なお、そんなC++0xの様子はC++の標準化委員会のページで窺い知ることができます。 http://www.open-std.org/jtc1/sc22/wg21/ JTC1/SC22/WG21 – The C++ Standards Committee

その中でも今回紹介したいのはムーブセマンティクス (move semantics)と右辺値参照 (rvalue reference)です。

まずは次のコードをご覧ください。stringは参照カウント及びCopy on Writeを使っておらず、コピーコンストラクタ、コピー代入演算子では常に文字列のコピーを行うものとします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
string operator +(const string& lhs, const string& rhs)
{
    string tmp(lhs);
    tmp += rhs; //operator +=は定義されているとする
    return tmp;
}
 
void func1()
{
    string s1 = "hello, ";
    string s2 = "world.";
    string sc;
 
    sc = s1 + s2;
}

上のコードではs1 + s2の式によって呼び出されたoperator +の中でtmpから戻り値オブジェクト、外で戻り値オブジェクトからscへと2度もコピーが発生することになります(ただし現行のC++では、RVO/NRVOと呼ばれる戻り値オブジェクトの作成を省略する最適化が認められており、その場合は1回で済みます)。

なぜコピーが良くないかというと、当然のことながら文字列のコピーに時間的なコストがかかるからです。それを避けるにはtmpが中に持っている文字列のメモリへのポインタをそのまま(文字列のコピーを行わずに)scまで連れてくるようにすれば良いのです。これがムーブセマンティクスの発想です。

ここでムーブセマンティクスの提案を見てみましょう。そこでは次のようにstringクラスを定義しています。

move proposalのstring motivationの項からです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class string
{
public:
    // copy semantics
    string(const string& s)
        : data_(new char[s.size_]), size_(s.size_)
        {memcpy(data_, s.data_, size_);}
    string& operator=(const string& s)
        {if (this != &s)
        {
            if (size_ < s.size_)
                // get sufficient data buffer
            size_ = s.size_;
            memcpy(data_, s.data_, size_);
        }
        return *this;
}
    // move semantics
    string(string&& s)
        : data_(s.data_), size_(s.size_) {s.data_ = 0; s.size_ = 0;}
    string& operator=(string&& s) {swap(s); return *this;}
    // ...
private:
    char* data_;
    size_t size_;
    // ...
};

copy semanticsではmemcpyを使っていることから文字列をコピーしていることが見て取れます。一方、move semanticsでは(メンバ初期化子やswap関数を使っているため直接目に見えませんが)単にメンバ変数であるdata_とsize_の代入だけで済ませています。これなら無駄な文字列のコピーをせずに済みます。しかしその代わりに元のオブジェクトsは破壊されていることに注意が必要です。これがムーブ(移動)といわれる所以です。

ムーブセマンティクスの実現に用いられているのが「右辺値参照」です。右辺値参照はアンサパンドを2つ重ねて表現します。上の例でもムーブコンストラクタとムーブ代入演算子の宣言で仮引数の型がstring&&となっています。なおアンサパンド1つ (stirng&)で表す従来の参照は、区別のため左辺値参照と呼びます。

左辺値参照・右辺値参照の区別でうまい具合に移動されては困る場合にはコピーが行われます。コピーコンストラクタ・代入演算子では左辺値参照、ムーブコンストラクタ・代入演算子では右辺値参照と使い分けていますが、左辺値参照は左辺値、右辺値参照は右辺値を指すという風に多重定義解決するという規則を設けるだけで良いのです。

そもそも左辺値とは、およそ(constでなければ)代入演算子の左辺に置けるオブジェクトです。一方、右辺値は代入演算子の右辺に置けるものということで、一時オブジェクト(関数の戻り値もそう)、数値リテラルなどがあります(ただし当然ながら左辺値も右辺値として扱えます)。

右辺値として挙げたものは(左辺値だったもの以外)すぐに寿命を迎え破棄されるので、いくら変更を加えようと全く困りません。そこで右辺値からは移動を行って構わないということになります。一方左辺値は(大抵の場合)すぐに寿命を迎えず、移動されては困るのでコピーを行うという具合です。

なお、最初の例でoperator +内のtmpは左辺値ですが、局所変数をreturnで返す際には右辺値参照で参照できるという規則が提案の中にあり、戻り値オブジェクトへは移動が行われます。


長くなってきたのでとりあえず今日はこれくらいにしておきます。たぶん次回へ続きます。


スポンサード リンク

この記事のカテゴリ

  • ⇒ ムーブセマンティクスと右辺値参照