std::moveなしでC++を移動する
Move in C++ without a std:move (andreasfertig.com)
要約
C++において、不要なコピー操作はパフォーマンスの敵であり、return value optimization (RVO) や named return value optimization (NRVO) がコピーを省略する上で重要である。C++11以降、暗黙的な移動(implicit move)も導入されたが、特定のケースでは手動でのstd::moveが必要だった。しかし、C++23ではこれらのケースでも暗黙的な移動が行われるようになり、std::moveを稀にしか使わないという原則をさらに強く支持する。
全文翻訳
C++でstd::moveを使わずにムーブする
以前の投稿「std::moveは稀にしか使うべきではない理由」で、std::moveは稀にしか使うべきではないと述べました。今日の投稿では、このアドバイスの利点、すなわちデフォルトで最高のパフォーマンスを引き出す方法を示したいと思います。
Return Value Optimization
パフォーマンスの最悪の敵の一つは、不要なコピー操作です。
Return Value Optimization (RVO) については、きっと耳にしたことがあるでしょう。これは、可能であれば常に目指すべきものです。RVOとは、返されるオブジェクトが関数内のスタック上で作成されるのではなく、値が最終的に配置される呼び出し側で作成されることを意味します。これにより、コピーとムーブが不要になります。標準では、これをコピーエリジョン(copy elision)と呼んでいます。なぜなら、標準はコンパイラ最適化について言及しないからです。
C++17以降、以下のケースではコピーエリジョンが保証されます。
Apple RVO()
{
return {};
}
これは純粋なRVOです。次に、Named Return Value Optimization (NRVO) があります。
Apple NRVO()
{
Apple res{};
return res;
}
後者は保証されたコピーエリジョンの対象ではありません。しかし、おそらくここでもコピーやムーブのコストはかからないでしょう。
コピーの代わりにムーブする
コピーエリジョンの次に良いのは、オブジェクトをムーブすることです。C++11以降、言語には暗黙的なムーブが発生するいくつかの場所がありました。
Apple Fun(Apple val)
{
return val;
}
このコードでは、結果のオブジェクトはパラメータからムーブされます。
しかし、言語にはもう少し複雑なケースもありました。
最初に共有したいのは、以下のケースです。
Apple Cat(Apple&& val)
{
return val;
}
rvalue参照パラメータを受け取り、受け取ったばかりのオブジェクトを返す関数があります。このコードはコンパイルはしますが、C++20より前では、戻り値のコピー構築が発生していました。ただし、GCCのような標準に準拠したコンパイラの場合です。あまり準拠していないClangコンパイラは、ムーブ構築を行います。そう、時にはルールに従わない方が良いこともあるのです。
私がさらに嫌いだったもう一つの例は、以下のものです。
Apple&& Dog(Apple&& val)
{
return std::move(val);
}
ここでもrvalue参照パラメータを受け取る関数がありますが、今回はrvalue参照が返されます。今回は、戻り値を手動でムーブしないとコードはコンパイルされません。これは私の(std::moveを稀にしか使わないという)アドバイスに反します。
公平を期すために言えば、両方のケースで最良の結果を得るには、戻り値のムーブが必要です。したがって、両方のケースが私の(std::moveを稀にしか使わないという)アドバイスに反します。
コンパイラをC++23モードに切り替えると、両方のケースで暗黙的なムーブが発生します。std::moveは不要になります。これで、私の「std::moveを稀にしか使わない」というルールをさらに実践できるようになります!
Andreas