C++
セマンティクスを移動する
サーチ…
セマンティクスを移動する
移動セマンティクスは、C ++でオブジェクトを別のオブジェクトに移動する方法です。このために、古いオブジェクトを空にして、それをすべて新しいオブジェクトに配置します。
このためには、価値基準が何であるかを理解する必要があります。値の参照( T&&
Tはオブジェクト型)は、通常の参照( T&
、現在は左辺の参照と呼ばれます)とあまり変わりません。しかし、それらは2つの異なる型として動作します。したがって、移動セマンティクスを扱う際に必要となる、あるタイプまたは他のタイプを取るコンストラクタまたは関数を作成できます。
2つの異なるタイプが必要な理由は、2つの異なる動作を指定することです。左辺の参照コンストラクタはコピーに関連し、右辺の参照コンストラクタは移動に関連しています。
オブジェクトを移動するには、 std::move(obj)
を使用します。この関数は、そのオブジェクトの新しい値へのデータを盗むことができるように、オブジェクトへの値の参照を返します。これについては、以下で説明するいくつかの方法があります。
重要なのは、 std::move
を使用すると、右辺参照のみstd::move
作成されることです。言い換えれば、 std::move(obj)
ステートメントはobjの内容を変更しませんが、 auto obj2 = std::move(obj)
(おそらく)は行います。
コンストラクタの移動
このコードスニペットがあるとします。
class A {
public:
int a;
int b;
A(const A &other) {
this->a = other.a;
this->b = other.b;
}
};
コピーコンストラクタを作成する、つまりオブジェクトをコピーして新しいオブジェクトを作成する関数を作成するには、通常は上記の構文を選択します。A型の別のオブジェクトへの参照を取るAのコンストラクタがあります。このメソッド内でオブジェクトを手動でコピーします。
あるいは、 A(const A &) = default;
書くこともできA(const A &) = default;
コピーコンストラクタを使用して、自動的にすべてのメンバーをコピーします。
ただし、移動コンストラクタを作成するには、左辺値参照の代わりに右辺値参照を使用します。
class Wallet {
public:
int nrOfDollars;
Wallet() = default; //default ctor
Wallet(Wallet &&other) {
this->nrOfDollars = other.nrOfDollars;
other.nrOfDollars = 0;
}
};
古い値をzero
設定していることに注意してください。デフォルトの移動コンストラクタ( Wallet(Wallet&&) = default;
)は、 nrOfDollars
の値をPODとしてコピーします。
移動セマンティクスは元のインスタンスから「スチール」状態を許可するように設計されているため、このスチール後に元のインスタンスがどのように見えるかを検討することが重要です。この場合、値をゼロに変更しないと、ドルを2倍にすることになります。
Wallet a;
a.nrOfDollars = 1;
Wallet b (std::move(a)); //calling B(B&& other);
std::cout << a.nrOfDollars << std::endl; //0
std::cout << b.nrOfDollars << std::endl; //1
したがって、私たちは古いものからオブジェクトを構築しました。
上の例は単純な例ですが、ムーブコンストラクタの目的を示しています。リソース管理が関与している場合など、より複雑な場合にはより便利になります。
// Manages operations involving a specified type.
// Owns a helper on the heap, and one in its memory (presumably on the stack).
// Both helpers are DefaultConstructible, CopyConstructible, and MoveConstructible.
template<typename T,
template<typename> typename HeapHelper,
template<typename> typename StackHelper>
class OperationsManager {
using MyType = OperationsManager<T, HeapHelper, StackHelper>;
HeapHelper<T>* h_helper;
StackHelper<T> s_helper;
// ...
public:
// Default constructor & Rule of Five.
OperationsManager() : h_helper(new HeapHelper<T>) {}
OperationsManager(const MyType& other)
: h_helper(new HeapHelper<T>(*other.h_helper)), s_helper(other.s_helper) {}
MyType& operator=(MyType copy) {
swap(*this, copy);
return *this;
}
~OperationsManager() {
if (h_helper) { delete h_helper; }
}
// Move constructor (without swap()).
// Takes other's HeapHelper<T>*.
// Takes other's StackHelper<T>, by forcing the use of StackHelper<T>'s move constructor.
// Replaces other's HeapHelper<T>* with nullptr, to keep other from deleting our shiny
// new helper when it's destroyed.
OperationsManager(MyType&& other) noexcept
: h_helper(other.h_helper),
s_helper(std::move(other.s_helper)) {
other.h_helper = nullptr;
}
// Move constructor (with swap()).
// Places our members in the condition we want other's to be in, then switches members
// with other.
// OperationsManager(MyType&& other) noexcept : h_helper(nullptr) {
// swap(*this, other);
// }
// Copy/move helper.
friend void swap(MyType& left, MyType& right) noexcept {
std::swap(left.h_helper, right.h_helper);
std::swap(left.s_helper, right.s_helper);
}
};
割り当ての移動
左辺値参照を持つオブジェクトに値を割り当てる方法と同様に、オブジェクトをコピーすることなく、オブジェクトを別の値に移動することもできます。我々はこの移動割り当てと呼ぶ。あるオブジェクトから別の既存のオブジェクトに値を移動します。
このためには、 operator =
をオーバーロードする必要があります。これは、コピー代入のように左辺値の参照を取るのではなく、右辺値の参照をとるようにします。
class A {
int a;
A& operator= (A&& other) {
this->a = other.a;
other.a = 0;
return *this;
}
};
これは移動割り当てを定義する一般的な構文です。 operator =
をオーバーロードして、それにr値参照を与え、それを別のオブジェクトに割り当てることができます。
A a;
a.a = 1;
A b;
b = std::move(a); //calling A& operator= (A&& other)
std::cout << a.a << std::endl; //0
std::cout << b.a << std::endl; //1
したがって、オブジェクトを別のオブジェクトに割り当てることができます。
O(n²)からO(n)への複雑さを減らすためにstd :: moveを使用する
C ++ 11では、オブジェクトの移動に必要なコア言語と標準ライブラリのサポートが導入されました。アイデアは、オブジェクトoが一時的と1のときということであるO論理的に空にまだ破壊し、コピー可能残し、そのような動的に割り当てられたバッファとしてだけでピルファーO「sのリソースへの安全、その後、論理コピーを望んでいます。
コア言語のサポートは主に
右辺値参照型ビルダー
&&
、例えば、std::string&&
への右辺値参照でstd::string
、そのオブジェクトと呼ぶことを示しているが、そのリソースだけくすねすることができます一時的なものである(すなわち移動)これらのリソースを実際にコピーするのではなく、指定された他のオブジェクトからリソースを効率的に移動すると考えられる移動コンストラクタ
T( T&& )
特別なサポート移動代入演算子
auto operator=(T&&) -> T&
特別にサポートしています。これもソースから移動するはずです。
標準ライブラリのサポートは、主に<utility>
ヘッダのstd::move
関数テンプレートです。この関数は、指定されたオブジェクトへのrvalue参照を生成します。これは、一時的であるかのように、移動可能であることを示します。
コンテナの場合、実際のコピーは一般にO( n )の複雑さです。ここで、 nはコンテナ内のアイテムの数です。移動はO(1)、一定時間です。そして、その論理的にコピーするコンテナn回アルゴリズムのために、これは通常は非現実的ではO(n²)にちょうどリニアO(N)からの複雑さを軽減することができます。
Andrew Koenig氏は、 2013年9月のDr. Dobbs Journalの記事"Containers That Never Changes"では、初期化後に変数が不変なプログラミングスタイルを使用する場合、アルゴリズムの非効率性の興味深い例を紹介しました。このスタイルでは、ループは一般的に再帰を使用して表現されます。また、Collatzシーケンスの生成などのアルゴリズムでは、再帰的にコンテナを論理的にコピーする必要があります。
// Based on an example by Andrew Koenig in his Dr. Dobbs Journal article
// “Containers That Never Change” September 19, 2013, available at
// <url: http://www.drdobbs.com/cpp/containters-that-never-change/240161543>
// Includes here, e.g. <vector>
namespace my {
template< class Item >
using Vector_ = /* E.g. std::vector<Item> */;
auto concat( Vector_<int> const& v, int const x )
-> Vector_<int>
{
auto result{ v };
result.push_back( x );
return result;
}
auto collatz_aux( int const n, Vector_<int> const& result )
-> Vector_<int>
{
if( n == 1 )
{
return result;
}
auto const new_result = concat( result, n );
if( n % 2 == 0 )
{
return collatz_aux( n/2, new_result );
}
else
{
return collatz_aux( 3*n + 1, new_result );
}
}
auto collatz( int const n )
-> Vector_<int>
{
assert( n != 0 );
return collatz_aux( n, Vector_<int>() );
}
} // namespace my
#include <iostream>
using namespace std;
auto main() -> int
{
for( int const x : my::collatz( 42 ) )
{
cout << x << ' ';
}
cout << '\n';
}
出力:
42 21 64 32 16 8 4 2
ベクトルのコピーによるアイテムコピー操作の数は、1 + 2 + 3 + ... nの合計であるため、ここではおおよそO( n² )です。
具体的には、g ++およびVisual C ++コンパイラでは、上記のcollatz(42)
呼び出しによって、ベクトルコピーコンストラクタ呼び出しでCollatzシーケンスが8個のアイテムと36個のアイテムコピー操作(8 * collatz(42)
= 28個)になりました。
これらのアイテムのコピー操作はすべて 、単に値が不要なベクトルを移動するだけで削除できます。これを行うには、ベクトル型引数のconst
と参照を削除し、ベクトルを値渡しする必要があります 。関数の戻り値はすでに自動的に最適化されています。ベクトルが渡され、関数内でこれ以上使用されない呼び出しでは、実際にそれらをコピーするのではなく、 std::move
を適用してstd::move
させてください:
using std::move;
auto concat( Vector_<int> v, int const x )
-> Vector_<int>
{
v.push_back( x );
// warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
// See https://stackoverflow.com/documentation/c%2b%2b/2489/copy-elision
// return move( v );
return v;
}
auto collatz_aux( int const n, Vector_<int> result )
-> Vector_<int>
{
if( n == 1 )
{
return result;
}
auto new_result = concat( move( result ), n );
struct result; // Make absolutely sure no use of `result` after this.
if( n % 2 == 0 )
{
return collatz_aux( n/2, move( new_result ) );
}
else
{
return collatz_aux( 3*n + 1, move( new_result ) );
}
}
auto collatz( int const n )
-> Vector_<int>
{
assert( n != 0 );
return collatz_aux( n, Vector_<int>() );
}
ここで、g ++およびVisual C ++コンパイラでは、ベクトルコピーコンストラクタ呼び出しによる項目コピー操作の数は、正確に0でした。
アルゴリズムは、まだ必ずしも生成このCollatzシーケンスの長さはO(N)であり、これは非常に劇的な改良である:O(N²)→O(N)。
いくつかの言語サポートでは、おそらく移動を使用しても、その変数の初期化と最終移動の間の変数の不変性を表現し、強制します。その後、その変数の使用はエラーになります。 Alas、C ++ 14以降、C ++ではこれがサポートされていません。ループフリーのコードでは、移動後のno useは、 struct result;
と同様に、不完全なstruct
として関連する名前の再宣言によって強制的に適用できますstruct result;
これは醜いものであり、他のプログラマーには理解されそうにない。診断もかなり誤解を招く可能性があります。
要約すると、C ++言語とライブラリのサポートにより、アルゴリズムの複雑さは大幅に改善されますが、サポートの不完全性のために、コードの正確性の保証とコードの明快さを犠牲にしてconst
が提供することができます。
完全性を保つために、コピーコンストラクタの呼び出しによるアイテムコピー操作の数を測定するために使用される計測されたベクトルクラス:
template< class Item >
class Copy_tracking_vector
{
private:
static auto n_copy_ops()
-> int&
{
static int value;
return value;
}
vector<Item> items_;
public:
static auto n() -> int { return n_copy_ops(); }
void push_back( Item const& o ) { items_.push_back( o ); }
auto begin() const { return items_.begin(); }
auto end() const { return items_.end(); }
Copy_tracking_vector(){}
Copy_tracking_vector( Copy_tracking_vector const& other )
: items_( other.items_ )
{ n_copy_ops() += items_.size(); }
Copy_tracking_vector( Copy_tracking_vector&& other )
: items_( move( other.items_ ) )
{}
};
コンテナでの移動セマンティクスの使用
コンテナをコピーする代わりに移動することができます。
void print(const std::vector<int>& vec) {
for (auto&& val : vec) {
std::cout << val << ", ";
}
std::cout << std::endl;
}
int main() {
// initialize vec1 with 1, 2, 3, 4 and vec2 as an empty vector
std::vector<int> vec1{1, 2, 3, 4};
std::vector<int> vec2;
// The following line will print 1, 2, 3, 4
print(vec1);
// The following line will print a new line
print(vec2);
// The vector vec2 is assigned with move assingment.
// This will "steal" the value of vec1 without copying it.
vec2 = std::move(vec1);
// Here the vec1 object is in an indeterminate state, but still valid.
// The object vec1 is not destroyed,
// but there's is no guarantees about what it contains.
// The following line will print 1, 2, 3, 4
print(vec2);
}
移動したオブジェクトを再利用する
移動したオブジェクトを再利用することができます:
void consumingFunction(std::vector<int> vec) {
// Some operations
}
int main() {
// initialize vec with 1, 2, 3, 4
std::vector<int> vec{1, 2, 3, 4};
// Send the vector by move
consumingFunction(std::move(vec));
// Here the vec object is in an indeterminate state.
// Since the object is not destroyed, we can assign it a new content.
// We will, in this case, assign an empty value to the vector,
// making it effectively empty
vec = {};
// Since the vector as gained a determinate value, we can use it normally.
vec.push_back(42);
// Send the vector by move again.
consumingFunction(std::move(vec));
}