ぷろみん

プログラミング的な内容を扱ってます

std::vectorのようなコピーコストの高いクラスに参照を使ってはいけないシチュエーション

概要

RVO(Return Value Optimization)の話です。特別に触れませんがNRVO(Named RVO)も最適化されるものとして話を進めています。

昔の効率の良いコード

void Initialize(std::vector<int>& buffer) {
    // bufferに色々な計算結果を詰める
    buffer.emplace_back(5);
}

昔は新しいデータ列を作成する際、アロケートの際には上記のような関数が使われていました。
それ故に下記のような分かり難い記述が散らかることになります。

std::vector<int> buffer;

// これくらいならまだ良いけど
Initialize(buffer);

// 生成に必要な引数が増えると何がなんだか
Create(buffer, elementA, elementIndex, combineBuffer);

// 使う
Use(buffer);

しかし、これには仕方ない事情がありました。
std::vectorを返すのがスマートだと分かっていても、そんな大きなものを返すとコピーコストが高いことが予想されるからです。

今の効率の良いコード

// 引数によってbufferが生成されていることがはっきり分かる
auto buffer = Create(elementA, elementIndex, combineBuffer);
// 使う
Use(buffer);

ここでRVOを知らない人はbufferの型がstd::vectorというのを見てびっくりします。
関数を見てみましょう。

std::vector<int> Create(...) {
    std::vector<int> result;
    // 色々な計算結果を詰める
    result.emplace_back(5);

    return result;
}

確かにRVOが効かなかった昔のコンパイラならば最悪のコードです。膨大なバッファのコピーが無意味に走る。
しかし、今の私たちにはRVOがあります。
上記コードは以下のコードと同等と解釈されます。

std::vector<int> buffer;

// 色々な計算結果を詰める
buffer.emplace_back(5);

// 使う
Use(buffer);

つまり、バッファポインタのコピーコストや関数の呼び出しコストすらも無く同様の処理を記述できるようになったのです。
昔のコードよりも高速に処理される上に遥かに高い可読性を持つこの初期化方法は想像以上に多くの可能性を私たちに提供します。

まず、コピーコストを無視できるので何らかのクラスを生成している箇所を機械的に分離できるようになります。
長い関数は大抵途中でなんらかのデータ列を生成していることが多いので、その部分だけでも分離することで効率を落とさずに可読性を上げることができます。

もうすでにRVOを行わないコンパイラは少ないと思っています。
加えて、最近の規格ではRVOを保証するものも出て来ているので上記のような書き方をしない理由はないと思います。