c++ 本の虫 「* thisの右辺値参照」とは何ですか?




本の虫 rvalue (3)

clangのC ++ 11ステータスページで 「* thisのための右辺値参照」という提案に出会いました。

右辺値参照についてかなり読んで理解しましたが、私はこれについて知っているとは思いません。 私はまた、その用語を使ってウェブ上で多くのリソースを見つけることができませんでした。

このページにプロポーザルのペーパーへのリンクがあります: N2439 (* move semanticsを* thisに拡張する)、しかし私はそこから多くの例を得ていない。

この機能は何ですか?


クラスに2つの関数があり、両方とも同じ名前とシグネチャを持つとしましょう。 しかし、そのうちの1つはconstとして宣言されています。

void SomeFunc() const;
void SomeFunc();

クラスインスタンスがconstではない場合、オーバーロード解決は非constバージョンを優先的に選択します。 インスタンスがconst場合、ユーザーはconstバージョンのみを呼び出すことができます。 thisポインタはconstポインタなので、インスタンスは変更できません。

これに対するr値の参照は、別の選択肢を追加することを可能にします。

void RValueFunc() &&;

これにより、ユーザーが適切なr値を介してそれを呼び出す場合にのみ呼び出すことができる関数を持つことができます。 これがObject型の場合

Object foo;
foo.RValueFunc(); //error: no `RValueFunc` version exists that takes `this` as l-value.
Object().RValueFunc(); //calls the non-const, && version.

このようにして、オブジェクトがr値を介してアクセスされているかどうかに基づいて動作を特殊化することができます。

r値参照バージョンと非参照バージョンの間ではオーバーロードできません。 つまり、メンバー関数名がある場合、そのすべてのバージョンはこれにl / r-value修飾子を使用するか、または使用しません。 これはできません。

void SomeFunc();
void SomeFunc() &&;

あなたはこれをしなければなりません:

void SomeFunc() &;
void SomeFunc() &&;

この宣言によって*thisの型が変わることに注意してください。 これは、 &&バージョンがすべてr値参照としてメンバーにアクセスすることを意味します。 そのため、オブジェクト内から簡単に移動することが可能になります。 プロポーザルの最初のバージョンで示されている例は次のとおりです(注:以下はC ++ 11の最終バージョンでは正しくない場合があります。これは最初のプロポーザルの "r-value"プロポーザルから直接です)。

class X {
   std::vector<char> data_;
public:
   // ...
   std::vector<char> const & data() const & { return data_; }
   std::vector<char> && data() && { return data_; }
};

X f();

// ...
X x;
std::vector<char> a = x.data(); // copy
std::vector<char> b = f().data(); // move

まず、「* thisのref-qualifiers」は単なる「マーケティングステートメント」です。 *thisの型は変わることはありません。この記事の最後をご覧ください。 この表現で理解するのは簡単です。

次に、次のコードは、関数†の 「暗黙のオブジェクトパラメータ」のref修飾子に基づいて呼び出す関数を選択します。

// t.cpp
#include <iostream>

struct test{
  void f() &{ std::cout << "lvalue object\n"; }
  void f() &&{ std::cout << "rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // lvalue
  test().f(); // rvalue
}

出力:

$ clang++ -std=c++0x -stdlib=libc++ -Wall -pedantic t.cpp
$ ./a.out
lvalue object
rvalue object

すべてのことは、関数が呼び出されるオブジェクトが右辺値(たとえば、名前のない一時変数)であるという事実を利用できるようにするために行われます。 さらなる例として、次のコードを取ります。

struct test2{
  std::unique_ptr<int[]> heavy_resource;

  test2()
    : heavy_resource(new int[500]) {}

  operator std::unique_ptr<int[]>() const&{
    // lvalue object, deep copy
    std::unique_ptr<int[]> p(new int[500]);
    for(int i=0; i < 500; ++i)
      p[i] = heavy_resource[i];

    return p;
  }

  operator std::unique_ptr<int[]>() &&{
    // rvalue object
    // we are garbage anyways, just move resource
    return std::move(heavy_resource);
  }
};

これはちょっと工夫が必要かもしれませんが、あなたはアイデアを得るべきです。

cv修飾子constvolatile )とref修飾子&&& )を組み合わせることができます。

注:多くの標準的な引用符とここからのオーバーロード解決の説明!

†これがどのように機能するのか、そしてなぜ@Nicol Bolasの答えが少なくとも部分的に間違っているのかを理解するには、C ++標準を少し掘り下げる必要があります。それだけに興味がある。

どの関数が呼び出されるのかは、 オーバーロード解決と呼ばれるプロセスによって決まります。 このプロセスはかなり複雑なので、私達は私達にとって重要な部分にだけ触れます。

まず、メンバー関数のオーバーロード解決がどのように機能するのかを確認することが重要です。

§13.3.1 [over.match.funcs]

p2候補関数のセットには、同じ引数リストに対して解決されるメンバー関数と非メンバー関数の両方を含めることができます。 引数リストとパラメーター・リストがこの異種セット内で比較可能であるように、メンバー関数は、メンバー関数が呼び出されたオブジェクトを表す暗黙オブジェクト・パラメーターと呼ばれる追加のパラメーターを持つと見なされます 。 [...]

p3同様に、必要に応じて、コンテキストは、操作対象のオブジェクトを示すための暗黙のオブジェクト引数を含む引数リストを構成できます。

なぜメンバーと非メンバーの関数を比較する必要さえあるのでしょうか。 オペレータの過負荷、それが理由です。 このことを考慮:

struct foo{
  foo& operator<<(void*); // implementation unimportant
};

foo& operator<<(foo&, char const*); // implementation unimportant

あなたは確かに以下の関数にfree関数を呼んでもらいたいですね。

char const* s = "free foo!\n";
foo f;
f << s;

そのため、メンバー関数と非メンバー関数がいわゆるoverload-setに含まれています。 解決をより簡単にするために、標準引用符の太字部分が存在します。 さらに、これは私たちにとって重要なことです(同じ節)。

p4非静的メンバ関数の場合、暗黙的オブジェクトパラメータの型は次のとおりです。

  • ref修飾子なしで、または& ref 修飾子付きで宣言された関数については、「 cv Xへの左辺値参照」

  • && ref修飾子で宣言された関数については、「 cv Xへの右辺値参照」

ここで、 Xは関数がメンバーとなるクラス、 cvはメンバー関数宣言のcv修飾です。 [...]

p5オーバーロードの解決中、暗黙のオブジェクトパラメータ[...]は、対応する引数の変換が次の追加規則に従うので、そのアイデンティティを保持します。

  • 暗黙のオブジェクトパラメータの引数を保持するために一時オブジェクトを導入することはできません。 そして

  • 型の一致を達成するためにユーザー定義の変換を適用することはできません。

[...]

(最後のビットは、メンバー関数(または演算子)が呼び出されるオブジェクトの暗黙的な変換に基づいてオーバーロードの解決を不正にできないことを意味しています。)

この記事の先頭にある最初の例を見てみましょう。 前述の変換後、オーバーロードセットは次のようになります。

void f1(test&); // will only match lvalues, linked to 'void test::f() &'
void f2(test&&); // will only match rvalues, linked to 'void test::f() &&'

暗黙のオブジェクト引数を含む引数リストは、オーバーロードセットに含まれるすべての関数のパラメータリストと照合されます。 我々の場合、引数リストはそのオブジェクト引数のみを含みます。 その様子を見てみましょう。

// first call to 'f' in 'main'
test t;
f1(t); // 't' (lvalue) can match 'test&' (lvalue reference)
       // kept in overload-set
f2(t); // 't' not an rvalue, can't match 'test&&' (rvalue reference)
       // taken out of overload-set

セット内のすべてのオーバーロードがテストされた後に1つのみが残り、オーバーロードの解決が成功し、その変換されたオーバーロードにリンクされている関数が呼び出されます。 2回目の 'f'の呼び出しも同様です。

// second call to 'f' in 'main'
f1(test()); // 'test()' not an lvalue, can't match 'test&' (lvalue reference)
            // taken out of overload-set
f2(test()); // 'test()' (rvalue) can match 'test&&' (rvalue reference)
            // kept in overload-set

ただし、 ref-qualifierを指定しなかった(そして関数がオーバーロードされなかった)場合、 f1 右辺値(まだ§13.3.1一致します。

p5 [...] ref修飾子なしで宣言された非静的メンバ関数には、追加の規則が適用されます。

  • 暗黙のオブジェクトパラメータがconst修飾されていなくても、引数が暗黙のオブジェクトパラメータの型に変換できるのであれば、右辺値をパラメータにバインドできます。
struct test{
  void f() { std::cout << "lvalue or rvalue object\n"; }
};

int main(){
  test t;
  t.f(); // OK
  test().f(); // OK too
}

さて、@ Nicolの答えが少なくとも部分的に間違っている理由について。 彼は言い​​ます:

この宣言によって*thisの型が変わることに注意してください。

それは間違っています、 *this常に左辺値です:

§5.3.1 [expr.unary.op] p1

単項*演算子は間接演算を実行します。適用先の式はオブジェクト型へのポインタ、または関数型へのポインタとなり、結果は指すオブジェクトまたは関数を参照する左辺値になります。

§9.3.2 [class.this] p1

非静的(9.3)メンバ関数の本体では、キーワードthisは、その値が関数が呼び出されたオブジェクトのアドレスであるprvalue式です。 クラスXメンバー関数内のthisの型はX*です。 [...]


左辺値参照修飾子形式のための追加のユースケースがあります。 C ++ 98には、右辺値であるクラスインスタンスに対して非constメンバ関数を呼び出すことを許可する言語があります。 これは、rvaluenessの概念そのものに反し、組み込み型がどのように機能するかから逸脱する、あらゆる種類の奇妙さにつながります。

struct S {
  S& operator ++(); 
  S* operator &(); 
};
S() = S();      // rvalue as a left-hand-side of assignment!
S& foo = ++S(); // oops, dangling reference
&S();           // taking address of rvalue...

左辺値修飾子はこれらの問題を解決します。

struct S {
  S& operator ++() &;
  S* operator &() &;
  const S& operator =(const S&) &;
};

これで、演算子は左辺値のみを受け入れて、組み込み型の演算子のように機能します。







qualifiers