実装の課題:std::move と std::forward の置き換え

C++11 で移動セマンティクスが導入されたとき、2 つの重要なヘルパー関数 std::move も追加されました。 と std::forward .オブジェクトを気にしなくなった、または汎用コードで値カテゴリを伝播する必要がないことを手動で示したい場合、それらは不可欠です.そのため、私は過去に数え切れないほどそれらを使用しました.

ただし、それらは関数です .プレーンで古い標準ライブラリ関数。

これには複数の理由で問題があります。

まず、一部のプログラマーは哲学的な理由でそれらを嫌っています:なぜ言語に必要なものを入れるのか ライブラリへの機能 ?なぜ std::forward<T>(foo) なのか >>foo のような組み込みの代わりに 、過去に提案されたものはどれですか?

次に、それらを使用するには関数呼び出しが必要です (duh)。これは、デバッガーを使用し、std::move() の標準ライブラリ定義を常にステップ実行している場合に面倒です。 、また、最適化を有効にしていない場合、実行時にパフォーマンスに影響を与える可能性があります.言語機能にはこれらの問題はありません.

第三に、これが私が嫌いな主な理由です。コンパイル時に影響があります。私は現在、メタプログラミングを多用するライブラリに取り組んでいますが、すでにコンパイル時間が大幅に増加しています。テスト スイート全体を約 5 秒 (~12K 行) で完了します。std::move を使い始めると と std::forward 、最初に <utility> を含める必要があります それらが定義されている場所 (ヘッダーの大部分は <type_traits> 以外は必要ありません 、 <cstddef> など)。#include <utility> だけの空の C++ ファイル 250ms かかります (つまり、テスト スイートのコンパイル時間の 5%)、約 3,000 行のコードを取り込みます。これに、名前の検索、オーバーロードの解決、テンプレートのインスタンス化のコストを使用するたびに追加すると、コンパイル時間はさらに 50ms .

これらの問題は実際には問題ではないと思うかもしれませんが、気にする必要はありません.

std::move を置き換えています

std::move(obj) obj の値が不要になったことを示します 他の何かがそれを自由に盗むことができます.しかし、std::move()は何ですか 実際にする ?

標準ライブラリの実装をコピーして少しクリーンアップすると、次のようになります:

template<typename T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept
{
    return static_cast<std::remove_reference_t<T>&&>(t);
}

それは本質的に美化された static_cast です .私たちが行っているのは、一部を取り入れていることです 参照 - 左辺値または右辺値、const または非 const - 右辺値参照にキャストします。

これは理にかなっています。

Type obj = std::move(other_obj); と書くと オーバーロードの解決で move コンストラクター Type(Type&& other) を呼び出す必要があります コピー コンストラクタ Type(const Type& other) の代わりに .したがって、単純に引数を右辺値参照にキャストし、コンパイラーに処理させます。

std::move() を置き換える 書く代わりに:

#include <utility>
…
Type obj = std::move(other_obj);

私たちは書いています:

// no #include necessary
…
Type obj = static_cast<Type&&>(other_obj);

#include いいえ 、関数呼び出しなし、なし。

それは簡単でした。 std::forward を見てみましょう .

std::forward を置き換えています

std::forward 完全転送の一部として使用されます。ここでは、一連の引数を取り、それらを別の関数に渡したいと考えています。

#include <utility>

template <typename Fn, typename ... Args>
void call(Fn fn, Args&&... args)
{
    // Forward the arguments to the function.
    fn(std::forward<Args>(args)...);
}

左辺値を渡すときは、fn() が必要です 左辺値で呼び出されます。右辺値を渡すときは、fn() が必要です 単純に fn(args...) と書くだけです。 十分ではありません:関数内で、右辺値引数は右辺値参照パラメーターを作成します。これは、名前が付けられているように、それ自体が左辺値です!

同じ理由で、まだ std::move() を呼び出す必要があります 右辺値参照を扱う場合:

Type& operator=(Type&& other)
{
    // Need move here, otherwise we'd copy.
    Type tmp(std::move(other));
    swap(*this, tmp);
    return *this;
}

other の間 は右辺値参照であり、参照には名前があり、左辺値です。右辺値参照を右辺値として扱うには、std::move() が必要です。 – static_cast を行う

とにかく、短い話:転送するときは、左辺値の参照をそのままにしておく必要がありますが、 std::move() 右辺値参照。これがまさに std::forward です します;見てみましょう:

template<typename T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept
{
    return static_cast<T&&>(t);
}

template<typename T>
constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept
{
    static_assert(!std::is_lvalue_reference_v<T>);
    return static_cast<T&&>(t);
}

std::forward の 2 つのオーバーロードがあります。 .

最初のものは左辺値参照を取り、static_cast<T&&> を返します .なぜなら T は左辺値参照であり、参照折りたたみルールが開始され、T&& T と同じです (左辺値参照)。これは、左辺値参照を取り込んで、左辺値参照を返すだけであることを意味します。

2 つ目は右辺値参照を取り、static_cast<T&&> も返します。 .なぜなら T は右辺値参照であり、参照折りたたみルールが開始され、T&& T と同じです (右辺値参照).これは、まだ右辺値参照を受け取り、右辺値参照を出力していることを意味します.ただし、返された右辺値参照には、右辺値にする名前がありません!

しかし、待ってください。両方のオーバーロードの forward の実装は同一です 、では、次のことだけを行ってみませんか?

template <typename T>
constexpr T&& forward(T&& t) noexcept
{
    return static_cast<T&&>(t);
}

それはうまくいきません。関数内ではすべての参照が左辺値であることを思い出してください。明示的な引数 forward<Arg>(arg) を記述しています。 左辺値を右辺値参照に渡そうとしますが、これはコンパイルされません。そして、テンプレート引数の推論で常に左辺値を推定します。

左辺値と右辺値が多かったので、要約すると:

  • 3,000 行の C++ が含まれています。
  • コンパイラは名前検索を実行して std::forward を見つける必要があります .
  • コンパイラは、2 つの forward の間でオーバーロード解決を実行する必要があります。 オーバーロード。
  • コンパイラは、選択したオーバーロードをインスタンス化する必要があります。
  • コンパイラは std::forward を使用したかどうかをチェックする必要があります

すべては static_cast のために 同じタイプ

そうです、std::forward<Arg>(arg) の代わりです ちょうど static_cast<Arg&&>(arg) です :

template <typename Fn, typename ... Args>
void call(Fn fn, Args&&... args)
{
    // Forward the arguments to the function.
    fn(static_cast<Args&&>(args)...);
}

引数が左辺値参照の場合、左辺値を生成する左辺値参照にキャストしています。引数が右辺値参照の場合、右辺値を生成する右辺値参照にキャストしています (これは名前)

以上です。

テンプレート パラメーターとして型がない場合 (C++20 より前のラムダを使用しているため)、decltype() を使用することもできます。 :

auto call = [](auto fn, auto&&... args) {
    // Forward the arguments to the function.
    fn(static_cast<decltype(args)>(args)...);
};

static_cast<decltype(x)>(x) って変だな ノーオペレーションではありませんが、... C++.

自己文書化コード

この時点で、static_cast<Arg>(arg) と言っている人もいます。 std::forward<Arg>(arg) に比べてかなり読みにくい .2 番目のケースでは、何かを転送していることは明らかです。最初のケースでは、右辺値参照が左辺値である理由と、C++ でプログラミングすることを選択した理由を説明する必要があります。

そして、私は完全に同意します。そのため、私はマクロを使用します:

// static_cast to rvalue reference
#define MOV(...) \ 
  static_cast<std::remove_reference_t<decltype(__VA_ARGS__)>&&>(__VA_ARGS__)

// static_cast to identity
// The extra && aren't necessary as discussed above, but make it more robust in case it's used with a non-reference.
#define FWD(...) \
  static_cast<decltype(__VA_ARGS__)&&>(__VA_ARGS__)

…

Type obj = MOV(other_obj);

…

fn(FWD(args)...);

なんてこった!

私は知っています、私は知っています、マクロは悪であり、マクロを使用することは悪です。適切で最新の C++ ガイドラインに従い、代わりにテンプレートと関数とオーバーロードを使用する必要があります (そもそも問題の原因となった)。

気にしません。

Bjarne – 私が思うに – かつてマクロの使用は言語の欠陥の指標であると言いました.そしてこれはまさに std::move および std::forward は:言語の小さな欠陥の指標です。マクロを使用して、私ができる唯一の方法で修正しています。そして、欠陥が修正されるまで、これらのマクロを使用し続けます (おそらく決して起こらないでしょう)。

私だけではないことに注意してください。マクロまたは static_cast を直接使用するさまざまなプロジェクトがあります。

それは実際的なことです。