API での右辺値参照のガイドライン

ACCU で、いつどのポインター型を使用するか、またその理由について講演する予定です。

それに取り組んでいる間に、インターフェースでの右辺値参照のガイドラインをいくつか作成しましたが、これは話に合わないので、ここに書きます.

右辺値参照を関数パラメーターとして使用する必要があるのはいつですか?

戻り型としていつ?

ref 修飾されたメンバー関数とは何ですか?また、いつ、どのように使用する必要がありますか?

1つずつ取り組みましょう。

右辺値参照パラメータ

一部の関数は右辺値参照を取ります:

void foo(T&& param);

この関数は右辺値参照を取らないことに注意してください:

template <typename T>
void foo(T&& param);

Tだから は関数のテンプレート パラメータです。転送参照への変換にはさまざまなルールが適用されます .これは、完全な転送というまったく別の目的を達成するために使用されるハックです。詳細については、たとえばこちらをご覧ください。

では、なぜ関数は右辺値参照を取るのでしょうか?

私は 3 つのユース ケースを特定しました。1 つは一般的なもの、もう 1 つはまれなもの、もう 1 つは役に立つかもしれない実験的なものです。

1.パフォーマンスのための右辺値 + 左辺値参照パラメーターの組み合わせ

これの最も良い例は std::vector<T>::push_back() です :

void push_back(const T& obj);
void push_back(T&& obj);

別の場所に保存したいパラメーターがある場合、適切なガイドラインは、そのパラメーターを値で渡し、最終的な場所に移動することです。

その古典的な例はコンストラクタです:

explicit person(std::string name)
: name_(std::move(name))
{}

そうすれば、左辺値を渡すときは、コピー (パラメーターへの) と移動 (最終的な場所への) に対して支払いを行い、右辺値を渡すと、移動 (パラメーターへの) と移動 (への移動) に対して支払います。最終的な場所)

const T& を取る 左辺値と右辺値の両方の (最終的な場所への) コピーがあり、T&& を渡します。 左辺値を渡すのを防ぐだけです.So with T どちらも機能し、移動は一般的に安価であるため、大きなマイナス面はありません.

ただし、完全なソリューションは左辺値をコピーして右辺値を移動するため、パラメーターから最終的な場所への追加の移動に料金を支払う必要はありません。これを実現するには、左辺値と右辺値の両方に対して関数をオーバーロードする必要があります。 /P>

しかし、これは 2^n につながります n の重複した関数 は引数の数であるため、次の場合にのみ実行する必要があります:

  • このコードのパフォーマンスは非常に重要です。
  • 扱っているタイプ (ジェネリック コード) がわからないか、移動するのにコストがかかる (つまり、移動コンストラクタがないため)。
  • パラメーターはほとんどありません。

person で たとえば、パフォーマンスはおそらく重要ではなく、std::string であることがわかっています。 安価に移動できるため、コンストラクターを複製する必要はありません。

しかし std::vector<T>::push_back() では 3 つの条件がすべて真であるため、2 つのオーバーロードが存在します。

2.条件付き移動の右辺値参照パラメーター

Move は単なるコピーの最適化ではなく、所有権の譲渡という重要なセマンティックな意味を持っています。

std::unique_ptr から所有権を取得したい関数を考えてみましょう .そのためには、引数から移動する必要があります。

これにより、パラメータ タイプに 3 つのオプションが提供されます。

  • std::unique_ptr<T>&
  • std::unique_ptr<T>
  • std::unique_ptr<T>&&

左辺値参照を取得するのは悪い考えです。関数が戻った後、ポインターが移動元の状態になることは呼び出し元には明らかではありません。また、関数は実際の右辺値 (一時値) を取得できません。

呼び出し元が std::move() と記述する必要があるため、値で取得することが機能します。 既存のオブジェクト (左辺値) を移動するとき。これには再び追加の移動操作が伴いますが、通常は無視できます。

右辺値参照パラメーターは呼び出し元と同じですが、内部的に余分な移動がないため、優れているように見えます.しかし、この関数を検討してください:

void foo(std::unique_ptr<T>&&) { /* do nothing */ }

この関数は実際には引数から移動しないため、呼び出し後も同じままです!

したがって、ここでの右辺値参照パラメータのセマンティクスはわずかに異なります:条件付き 関数が所有権を取得するかもしれませんが、そうでないかもしれません。

ただし、この使用例は非常にまれです。

3.移動を強制するための右辺値参照パラメーター

右辺値参照パラメーターには、値渡しパラメーターとの追加の違いがあります。呼び出し元に std::move() の使用を強制します。 型が実際には移動可能でない場合でも、左辺値の場合!

void foo(int&& i);
…
auto i = 42;
foo(i); // error: i is an lvalue
foo(std::move(i)); // okay

しかし、これは私たちの利点として使用できます:私が取り組んでいる新しいプロジェクトには、次の (簡略化された) コードがあります。

// some block of memory
struct memory_block
{
    std::byte* address;
    std::size_t size;
};

memory_block new_block(std::size_t size)
{
    // allocate block using ::operator new
}

void delete_block(memory_block&& block)
{
    // deallocate block using ::operator delete
}

delete_block() に注意してください memory_block を取る 右辺値参照による — 自明にコピー可能な型ですが、呼び出し元に delete_block(std::move(block)) の書き込みを強制します メモリ ブロックが使用できなくなっていることは明らかです。

最近使い始めたばかりなので、あまり経験がありませんが、試してみることをお勧めします。

Rvalue Ref 修飾メンバー関数

C++11 では、*this の ref-qualifiers という比較的わかりにくい機能が追加されました .

const でいつでもメンバー関数を修飾できます const で呼び出されるようにします。 オブジェクト。

同様に、C++11 では & で修飾できます と && 同様に、下位互換性のため、ルールが若干異なります:

  • & で修飾されたメンバー関数 右辺値で呼び出すことはできません (const で修飾されている場合を除く) ).
  • && で修飾されたメンバー関数 左辺値で呼び出すことはできません。
  • 修飾されていないメンバー関数は、左辺値と右辺値で呼び出すことができます。

通常、&& で修飾されたメンバー関数は 1 つだけではありません。 、たとえば、ただし、異なる修飾を持つ複数のオーバーロードがあります.オーバーロードの組み合わせはユースケースによって異なります.

1.値カテゴリをゲッター関数で渡す

std::optional<T> を検討してください :(おそらく) T 型のオブジェクトが含まれています .value() でアクセスできます .

いつものように、それはあなたに const T を与えます const で呼び出す場合 オブジェクト、および非 const T const 以外で呼び出した場合 オブジェクト:

std::optional<int> opt(42);
opt.value() = 43; // okay

const std::optional<int> opt(42);
opt.value() = 43; // error

そして - ご想像のとおり - T&& が得られます 右辺値と T& で呼び出された場合 左辺値で呼び出されたとき。

なぜそれが行われたのですか?

これを考慮してください:

std::optional<std::string> make();

…

std::string str = make().value();

こちら make() 右辺値オプションを返すため、文字列を外に移動しても安全です。右辺値修飾 value() のおかげで、これはまさに行われることです。 それが呼び出されています!

このユース ケースを実現するために、std::optional<T>::value() の 4 つのオーバーロードがあります。 、 const の組み合わせごとに 1 つ そして右辺値:

// assume a `get_pointer()` function that returns a pointer to the object being stored

T& value() & // non-const lvalue
{
    return *get_pointer();
}

T&& value() && // non-const rvalue
{
    return std::move(*get_pointer()); // propagate rvalue-ness
}

const T& value() const & // const lvalue
{
    return *get_pointer();
}

const T&& value() const && // const rvalue
{
    return std::move(*get_pointer()); // propagate rvalue-ness
}

std::optional のような型を記述していない限り、この使用例はクールですが、 、おそらく必要ありません。

2.メンバー関数の最適化

しかし、もっと一般的な考えが役に立つかもしれません:const があります コストのかかる計算を行うメンバー関数。結果の内部状態をコピーする必要があるかもしれません:

expensive_state foo(int arg) const
{
    expensive_state copy = my_state_;
    do_sth(copy, arg); 
    return copy;
}

右辺値で修飾されたオーバーロードは、内部状態を直接使用することでコピーを取り除くことができます - 結局、オブジェクトは一時的なものであるか、不要になったので、そのままにしておく必要はありません:

expensive_state&& foo(int arg) &&
{
    do_sth(my_state_, arg);
    return std::move(my_state_);
}

右辺値に対して特定の関数を最適化する場合、通常は 2 つのオーバーロードがあります。1 つは const です。 と 1 つの && ただし、コードを複製するだけの価値がある最適化であることを確認してください!

3.ダングリング参照を防ぐ

このブログ投稿で詳しく説明しました:if std::string_view は関数パラメータの外で使用されます。十分に注意してください!

たとえば、次の単純な getter を考えてみましょう:

std::string_view get_name() const
{
    return name_;
}

時間の経過とともに、名前を姓名に分割する必要があることが決定されました.

深夜のリファクタリングでは、ゲッターが変更されます:

std::string_view get_name() const
{
    return first_name_ + last_name_;
}

一時的な文字列へのビューを作成したので、これは大失敗です!

これを防ぐ方法の 1 つは、std::string_view への変換を無効にすることです。 右辺値の場合。現在、これがあります:

std::string::operator std::string_view() const { return …; }

2 番目のオーバーロードを追加することで、一時的な使用を防ぐことができます:

std::string::operator std::string_view() const & { return …; }
std::string::operator std::string_view() const && = delete;

そうすれば、オーバーロードの解決は、右辺値で呼び出されたときにこのオーバーロードを選択し、関数が削除されるためエラーを発行します。

オーバーロード解決の制御に関する私の連載で、関数の削除に関する詳細をお読みください。

4.オブジェクトを破棄する 1 回限りの操作をマーク

私は最近不変オブジェクトをたくさん持っているので、ビルダー パターンをよく使います:

class Foo
{
public:
    class Builder
    {
    public:
        Builder() = default; 

        void set_bar(Bar b) { … }

        void set_baz(Baz b) { … }

        Foo&& finish()
        {
            return std::move(obj);
        }

    private:
        Foo obj_;
    };

private:
    Foo() = default;
};

finish() に注意してください 関数:オブジェクトが完了すると、それは移動されます。しかし、これはビルダーを破壊します。つまり、二度と使用できなくなります。

確かに、メソッド名は finish() です 当たり前かもしれませんが、それでもメンバー関数を && で修飾します :

Foo&& finish() &&
{
    return std::move(obj);
}

次に、オブジェクトが使用不可になったことを通知されます:

auto obj = builder.finish(); // error!
auto obj2 = std::move(builder).finish(); // okay

戻り値の型としての右辺値参照

右辺値参照を戻り値の型として使用する場合、一時変数または関数ローカル変数を返すと、左辺値参照の場合と同様に、ダングリング参照が作成されます。

したがって、メンバー変数や参照パラメーターなどを返し、それらを別の場所に移動する場合にのみ、実際に適用できます。

参照パラメーターを移動する関数は 1 つだけです:std::move .

しかし、メンバー変数を移動する関数をいくつか見てきました:最近の Builder::finish() optional::value() と同様に どちらも値で返すことができるので、右辺値参照を使用する必要がありますか?

複雑です。

optional::value() で右辺値を返す 大文字と小文字が decltype() であることを保証します ただし、次のようなことを行うと、ダングリング参照が発生する可能性があります:

optional<T> foo();

auto&& val = foo().value();

関数によって返されたテンポラリは破棄されるため、val 破棄されたメンバー変数を参照します。ただし、value() の場合 T を返しました 有効期間の延長により、参照がまだ有効であることが保証されます。

一方、参照によって戻ると、1 つの余分な移動を節約できます。

では、右辺値の戻り値の型を使用する必要がありますか?

右辺値で修飾されたメンバー関数がある場合にのみ行うべきだと思いますが、その結果について考えてください。

クラス メンバーとしての右辺値参照

クラスに参照を入れないでください。operator= を書くのが難しくなります。 .

代わりに、ポインターを格納するか、できれば type_safe::object_ref<T> のように決して null にならないものを格納します。 .

結論

条件付き移動の関数パラメーターとして右辺値参照を使用して、呼び出し元に std::move() の書き込みを強制します。 、そして — const T& と一緒に オーバーロード — より効率的な入力パラメーター用。

ref 修飾されたメンバー関数を使用して、getter の値カテゴリをミラーリングし、メンバー関数を最適化し、一時的な操作を防止し、オブジェクトの内部状態を破壊するメンバー関数をマークします。

右辺値参照の戻り値の型に注意し、それらをクラスに入れないでください。


No