私は最近、方程式ソルバーで奇妙な動作に出くわしました。これにより、移動セマンティクスと RVO がどのように連携するかを本当に理解しているかどうか自問するようになりました。
このフォーラムには関連する質問がたくさんあり、これに関する一般的な説明もたくさん読みました。しかし、私の問題は非常に具体的であるように思われるので、誰かが私を助けてくれることを願っています.
関連する構造体は全体的に少し複雑ですが、少なくとも次のように分類されます。
struct Foo
{
Bar* Elements;
Foo(void) : Elements(nullptr)
{
cout << "Default-constructing Foo object " << this << endl;
}
Foo(Foo const& src) : Elements(nullptr)
{
cout << "Copying Foo object " << &src << " to new object " << this << endl;
if (src.Elements != nullptr)
{
Allocate();
copy (src.Elements, src.Elements + SIZE, Elements);
}
}
Foo(Foo&& src) : Elements(nullptr)
{
cout << "Moving Foo object " << &src << " into " << this << endl;
Swap(src);
}
~Foo(void)
{
cout << "Destructing Foo object " << this << endl;
Deallocate();
}
void Swap(Foo& src)
{
cout << "Swapping Foo objects " << this << " and " << &src << endl;
swap(Elements, src.Elements);
}
void Allocate(void)
{
Elements = new Bar[SIZE]();
}
void Deallocate(void)
{
delete[] Elements;
}
Foo& operator=(Foo rhs)
{
cout << "Assigning another Foo object to " << this << endl;
Swap(rhs);
return *this;
}
Foo& operator+=(Foo const& rhs)
{
cout << "Adding Foo object " << &rhs << " to " << this << endl;
// Somehow adding rhs to *this
cout << "Added Foo object" << endl;
return *this;
}
Foo operator+(Foo rhs) const
{
cout << "Summing Foo objects" << endl;
return rhs += *this;
}
static Foo Example(void)
{
Foo result;
cout << "Creating Foo example object " << &result << endl;
// Somehow creating an 'interesting' example
return result;
}
};
次の短いプログラムを考えてみましょう。
int main()
{
Foo a = Foo::Example();
cout << "Foo object 'a' is stored at " << &a << endl;
Foo b = a + a;
cout << "Foo object 'b' is stored at " << &b << endl;
}
これらは、このコードを実行する前の私の期待でした:
- この
Example
メソッドはローカルFoo
オブジェクトをインスタンス化し、デフォルトの ctorが呼び出されます。 Example
Foo
ローカルオブジェクトを値で返します。ただし、このコピーはRVOのために省略されると思います。- コピー ctorへの後続の呼び出しも最適化される可能性があります。代わり
a
に、 内の一時オブジェクトのアドレスが与えられる場合がありますExample
。 - 式を評価するために、左側のオペランドでメソッドが呼び出されます
a + a
。operator+
- 右側のオペランドは値で渡されるため、ローカル コピーを作成する必要がある場合があります。
- メソッド内では、参照渡し
operator+=
でそのコピーに対して呼び出されます。*this
- ここで、呼び出し元のメソッド
operator+=
の return ステートメントにジャンプして、再び同じローカル コピーへの参照を返します。operator+
- 参照されたオブジェクトは、最終的に値によって返されます。ここでは、ローカル コピーの値を保持する必要があるだけなので
b
(以前のステップ 2 と 3 で発生したように)、別のコピー省略が予想されます。 - オブジェクト
a
とオブジェクトの両方b
が最終的にスコープ外になるため、それらのデストラクタを呼び出します。
(少なくとも私にとって) 驚くべき観察結果は、ステップ 8 でディープ コピーが最適化されていないことです (使用されたコンパイラ オプションに関係なく)。代わりに、出力は次のようになります。
01 Default-constructing Foo object 0x23fe20
02 Creating Foo example object 0x23fe20
03 Foo object 'a' is stored at 0x23fe20
04 Copying Foo object 0x23fe20 to new object 0x23fe40
05 Summing Foo objects
06 Adding Foo object 0x23fe20 to 0x23fe40
07 Added Foo object
08 Copying Foo object 0x23fe40 to new object 0x23fe30
09 Destructing Foo object 0x23fe40
10 Foo object 'b' is stored at 0x23fe30
11 Destructing Foo object 0x23fe30
12 Destructing Foo object 0x23fe20
の次の小さな変更は、operator+
まったく違いがないように見えました。
Foo operator+(Foo rhs) const
{
cout << "Summing Foo objects" << endl;
rhs += *this;
return rhs;
}
ただし、今回の結果は完全に異なります。
01 Default-constructing Foo object 0x23fe20
02 Creating Foo example object 0x23fe20
03 Foo object 'a' is stored at 0x23fe20
04 Copying Foo object 0x23fe20 to new object 0x23fe40
05 Summing Foo objects
06 Adding Foo object 0x23fe20 to 0x23fe40
07 Added Foo object
08 Moving Foo object 0x23fe40 into 0x23fe30
09 Swapping Foo objects 0x23fe30 and 0x23fe40
10 Destructing Foo object 0x23fe40
11 Foo object 'b' is stored at 0x23fe30
12 Destructing Foo object 0x23fe30
13 Destructing Foo object 0x23fe20
明らかに、コンパイラはxvaluerhs
として認識され(明示的に を記述した場合と同様に)、代わりにmove ctorを呼び出します。return move(rhs += *this);
さらに、-fno-elide-constructors
オプションを使用すると、常に次のようになります。
01 Default-constructing Foo object 0x23fd30
02 Creating Foo example object 0x23fd30
03 Moving Foo object 0x23fd30 into 0x23fe40
04 Swapping Foo objects 0x23fe40 and 0x23fd30
05 Destructing Foo object 0x23fd30
06 Moving Foo object 0x23fe40 into 0x23fe10
07 Swapping Foo objects 0x23fe10 and 0x23fe40
08 Destructing Foo object 0x23fe40
09 Foo object 'a' is stored at 0x23fe10
10 Copying Foo object 0x23fe10 to new object 0x23fe30
11 Summing Foo objects
12 Adding Foo object 0x23fe10 to 0x23fe30
13 Added Foo object
14 Moving Foo object 0x23fe30 into 0x23fe40
15 Swapping Foo objects 0x23fe40 and 0x23fe30
16 Moving Foo object 0x23fe40 into 0x23fe20
17 Swapping Foo objects 0x23fe20 and 0x23fe40
18 Destructing Foo object 0x23fe40
19 Destructing Foo object 0x23fe30
20 Foo object 'b' is stored at 0x23fe20
21 Destructing Foo object 0x23fe20
22 Destructing Foo object 0x23fe10
私が信じていることから、コンパイラは行かなければなりません
- RVO (可能であれば)、または
- 建設を移動する(可能であれば)、または
- コピー構築(そうでなければ)、
その順序で。だから私の質問は: 誰かが私に説明してもらえますか、ステップ 8 で実際に何が起こるのか、なぜ上記の優先順位の規則が適用されないのか (または、もしそうなら、ここに表示されていないものは何ですか)? 冗長な例で申し訳ありませんが、事前に感謝します。
-std=c++11
現在、最適化をオフにしてgcc mingw-w64 x86-64 v.4.9.2を使用しています。
ps - 適切な OO コードの書き方とカプセル化の方法について私にアドバイスしたいという衝動を抑えてください ;-)