move semantics

34
MOVE SEMANTICS MITSUTAKA TAKEDA [email protected]

Upload: mitsutakatakeda

Post on 12-Apr-2017

218 views

Category:

Software


1 download

TRANSCRIPT

MOVE SEMANTICSMITSUTAKA TAKEDA

[email protected]

TABLE OF CONTENTS1. C++ for Java/Scala

1.1. Copy Semantics(Value Semantics)1.2. Copy Semantics(Value Semantics)1.3. 寄り道(Reference/Pointer Semantics)1.4. Where is My Copy?1.5. Copyの現実

2. Move Semantics2.1. Move!2.2. 寄り道(リソースの所有権)

3. L/R Value4. L/R Value Reference

4.1. 注意点(L/R Value と L/R Value Reference)5. Non Copyiable Object(Movable Object)6. Move Semantics導入前後

6.1. Move Semantics導入前6.2. Move Semantics導入後6.3. Move Semanticsの注意点

1 C++ FOR JAVA/SCALA

1.1 COPY SEMANTICS(VALUE SEMANTICS)C++ではオブジェクトは基本的に組み込み型のように振舞う(Value

Semantics)。 Javaで言うところのValue Object(int, String, etc)。int x = 0; int y = x; y = 10; // xの値は 0 か 10のどちらでしょうか?

1.2 COPY SEMANTICS(VALUE SEMANTICS)コピーとは等価の別オブジェクト(コピー先への変更はコピー元に

影響しない)を作ること。int x = 0; int y = x; // xをyへコピー assert(x == y); // コピー直後はxとyは等価。 y = 10; // yを変更。 assert(x == 0); // y(コピー先)への変更はx(コピー元)へ影響しない。 assert(y == 10);

1.3 寄り道(REFERENCE/POINTER SEMANTICS)Reference/Pointerセマンティックスも利用するこができます。int main() { int x = 0; int& y = x; // yはxへの参照。 y = 10; assert(x == y); assert(x == 10); assert(y == 10); int* z = &x; // zはxへのポインタ。 *z = 20; // ポインタの指す先(x)を変更。 assert(x == y); // yはxへの参照なのでxが変更されればyも変わる。 assert(x == 20); assert(y == 20); assert(*z == 20); return 0; }

1.4 WHERE IS MY COPY?何回コピーするでしょう?

class MyClass { public: MyClass() = default; MyClass(const MyClass&) { std::cout << "I'm Copied!\n"; } }; MyClass f(MyClass p0, const MyClass& p1) { return p0; } int main() { MyClass m; MyClass n = f(m, m); return 0; }

1.5 COPYの現実compilerが最適化してくれる場合もあり。

Return Value Optimization(Named Return Value Optimization)MyClass f() { return MyClass(); } void f_optimized(MyClass* out){ return *out;// Return Valueが出力用パラメータに置き換えられる。 } MyClass ret = f();// この呼出しは以下の呼出しに置き換えられてコピーが発生しない。 MyClass ret; f_optimized(&ret);// 関数内では、関数の呼出元が用意したメモリ領域(ret)を直接扱う。

2 MOVE SEMANTICS特定の状況ではオブジェクトのコピーは不要では?例えば、2度使

わないオブジェクト(関数の戻り値等)X func(){ X x; // xに対して何かする。 return x; } // funcの呼び出し側。 X ret = func();// retにfuncの戻り値をコピーする。コピー必要か?

2.1 MOVE!コピー不要・不可能なケースではオブジェクトの内部リソース(メモ

リ等)の所有権を移譲(move)すれば良い!struct X{ BigData* p; // Excelで処理しなければいけない程のビックデータ(リソース)へのポインタ。 }; // コピー(Javaで言うところのclone)のケースではBigDataをコピーする必要がある。 X original = ...; X copy = original; // originalのBigDataをコピー。時間が掛る。 predictFuture(copy);// コピーへの操作はoriginalへ影響しない。 // retを操作して影響を受けるoriginalはいない!funcの戻り値をretへコピーする必要なし! X ret = func(); // BigDataをコピーするのではなく、ポインタ(32/64bit)を戻り値からretへコピーするだけで良い。predictFuture(ret);

2.2 寄り道(リソースの所有権)C++のようにGCがない言語ではリソースの所有権という概念が非

常に重要。

どのオブジェクトがどのリソースを所有しているか(リソースの後片付けを誰が行うのか)を厳密に表現する必要。 Java/Scalaではリソースへの参照型Tのメンバ変数。C++では以下の5種類に分類。

C++ 型 所有権とコメントstd::unique_ptr<T> 1つのオブジェクトがリソースを専有std::shared_ptr<T> 複数のオブジェクトがリソースを共有raw pointer (T*) 所有はせずただのオプショナルなリソースへ

の参照reference (T&) 所有はせず必須な参照value (T) 1つのオブジェクトがリソースを専有。リソース

は動的多態不要面倒(ダングリング参照&循環参照でのメモリ・リーク)だが、モデルの意味を厳密に表現 & プログラムの性能を精確に予測可能。

Scala -> C++の移植はツライ。。。主語・目的語が省略された日本語を英語に翻訳するみたいな感じ。

3 L/R VALUEl(L)value(左辺値)&r(R)value(右辺値)

lvalueとはメモリ上に実体がありアドレスを取得できるオブジェクト。rvalueはアドレス取得不可能なもの。 名前の由来は代入演算子(=)の左辺になれるものがlvalue。右辺にしかなれないものが

rvalue。void f(int a){ std::cout << &a << std::endl; // aはlvalue。 } int g(int a) { return a; } int main() { int b = 0; std::cout << &b << std::endl; // bはlvalue。 f(b); std::cout << &0 << std::endl; // リテラルはrvalue。コンパイル・エラー。 std::cout << &(g(b)) << std::endl; // 関数の戻り値はrvalue。コンパイル・エラー。 return 0; }

4 L/R VALUE REFERENCEL/R Valueへの参照。

int main() { int a = 0; // lvalue 'a' int& la = a; // lvalue reference to lvalue 'a' int&& ra = a; // rvalue reference to lvalueは不可能。コンパイル・エラー。 int& lx = 0; // lvalue reference to rvalueは不可能。コンパイル・エラー。 int&& rx = 0; // rvalue reference to rvalue。 const int& cla = a; // const lvalue reference to lvalue。 const int&& cra = a; // const rvalue reference to lvalue。コンパイル・エラー。 const int& clx = 0; // const lvalue reference to rvalue。 const int&& crx = 0; // const rvalue reference to rvalue。 return 0; }

4.1 注意点(L/R VALUE と L/R VALUE REFERENCE)L/R Valueは値のカテゴリ(型のことじゃないよ!)、L/R Value

Referenceは型。 L/R Valueと L/R Value Referenceは直交した概念。

int f(){ return 0; } int main() { int a = 0; // aはint型でlvalueカテゴリ。 int& l = a; // lはintへのlvalue reference型でlvalueカテゴリ。 int&& r = f(); // rはintへのrvalue reference型でlvalueカテゴリ。 int&& rr = r; // コンパイル・エラー。rはlvalueなのでrvalue referenceへは束縛できない。 return 0; }

5 NON COPYIABLE OBJECT(MOVABLE OBJECT)本質的にコピーできないオブジェクトがある。例えばthreadオブジェクトを"コピーする"とはどういう意味? コピーした時点から2つのス

レッドが実行される?void task(){} int main() { std::thread t(task); std::thread copy = t;// スレッドをコピーするとはどういう意味? return 0; }

6 MOVE SEMANTICS導入前後コピーが高価なリソースを保持するオブジェクト & Non Copyiable

Objectの扱いは非常に煩雑。

6.1 MOVE SEMANTICS導入前不定な一時変数やら、手続型カッコ悪い。

// Move Semantics導入前 struct BigData { void* peta_byte_data; // ガチで大きなリソース。 }; // DBからビッグ・データを複数読み込む。 void readFromDB(std::vector<BigData>& ret){ } void predictFuture(std::vector<BigData>& d) {} int main() { std::vector<BigData> data; // コピーを防ぐため、一時変数を定義。 readFromDB(data); // 一時変数の領域へDBから読み込んだデータを書き込む。 predictFuture(data); return 0; }

6.2 MOVE SEMANTICS導入後性能を犠牲にせずより関数型的に記述できる。

struct BigData { void* peta_byte_data; // ガチで大きなリソース。 }; std::vector<BigData> readFromDB(){ }// 手続から関数へ。 void predictFuture(std::vector<BigData> d) {} int main() { predictFuture(readFromDB());// 一時変数不要に。 return 0; }

6.3 MOVE SEMANTICSの注意点moveの恩恵を受けることができるのは、moveがcopyと比較して

安価なときだけ。

例えばBigDataのようなデータ構造では、copyはPeta Byteコピーしなければいけないのに対して、 moveはポインタのコピー(32/64bit)だけなのでmoveはcopyと比較して非常に安価。

一方、標準のarrayはスタック上にメモリが確保されることが義務付けられているため、 データのコピーは不可避。

struct BigData { void* peta_byte_data; // ガチで大きなリソース。 }; template<typename base_t, typename expo_t> constexpr base_t POW(base_t base, expo_t expo) { return (expo != 0 )? base * POW(base, expo -1) : 1; } constexpr auto Peta = POW(10l,15l);// Peta = 10^15 BigData f() { BigData x; // fのスタック上に配置されるのはポインタ(peta_byte_data)のみ。 return x; // 関数fの終了後ポインタは破棄されるがポインタが指す先の領域は生き残る。 } std::array<int, Peta> g() { std::array<int, Peta> y; // Peta Byteのデータがgのスタック上に確保される。 return y; // yのデータは関数gの終了とともに破棄される。 } int main() { BigData good = f(); // moveの恩恵を受けることができる。 std::array<int, Peta> bad = g(); // moveの恩恵は受けることができない。(コピーされる) return 0; }

6.4 MOVE FOR USER-DEFINED TYPEgcc/clangでは、ユーザ定義型がmove semanticsをサポートできるとき、 ユーザ定義型は自動的にMovableな型(Move Constructor

& Move Assignment Operatorが定義された型)にな る。しかしVisual Studioでは自分でユーザ定義型をMovableにする必要があり。VSのおかげでmoveが実装できるようになりました(感謝!)

void acquire(void*) {} // リソースの取得。 void release(void*) {} // リソースの開放。 struct BigData { void* peta_byte_data; // Resource // RAII (Resource Accquisition Is Initialization) BigData(){ acquire(peta_byte_data); } ~BigData(){ release(peta_byte_data); } // Move Semantics BigData(BigData&& o){ // Move Constructor peta_byte_data = o.peta_byte_data; // ポインタのコピー。 o.peta_byte_data = nullptr; // move元のポインタをnullに設定してリソースの2重開放を防止。 } BigData& operator=(BigData&& o){ // Move Assignment Operator if(this != &o) { // 自己代入禁止。 release(peta_byte_data); // 自分のリソースを開放。 peta_byte_data = o.peta_byte_data; // ポインタのコピー。 o.peta_byte_data = nullptr; // move元のポインタをnullに設定してリソースの2重開放を防止。 } return *this; } };

6.5 COPY VS. MOVEオブジェクトはいつcopy(Copy Constructor/Assignemt Operatorが呼ばれる)されて、 いつmove(Move Constructor/Assignemt

Operatorが呼ばれる)されるの?

通常の関数のoverload解決ルールと一緒。bindの可否と優先順位。結論、rvalueはmoveされる。

  Value      R V L V(non const型) L V (const 型)R V Reference できる(優先) できない できないL V Reference できない できる(優先) できないconst L V Reference できる できる できる

struct MyClass { MyClass() {} // Copy Semantics MyClass(const MyClass& o){ std::cout << "Copy Constructor" << std::endl; } MyClass& operator=(const MyClass& o) { std::cout << "Copy Assignment Operator" << std::endl; return *this // Move Semantics MyClass(MyClass&& o){ std::cout << "Move Constructor" << std::endl; } MyClass& operator=(MyClass&& o) { std::cout << "Move Assignment Operator" << std::endl; return *this; } }; void f( MyClass&& x) { std::cout << "R V Reference" << std::endl; } void f( MyClass& x) { std::cout << "L V Reference" << std::endl;} void f(const MyClass& x) { std::cout << "const L V Reference" << std::endl;} int main() { f(MyClass()); // R V Reference MyClass x; // xはnon const型のL V。 f(x); // L V Reference const MyClass y; // yはconst型のL V。 f(y); // const L V Reference MyClass a; MyClass b = a; // Copy Constructor MyClass c; c = a; // Copy Assignment Operator MyClass d = std::move(a); // Move Constructor。std::moveは引数へのをrvalue referenceを取得する。後述。 MyClass e; e = std::move(b); // Move Assignment Operator return 0; }

6.6 COPY VS. MOVE (その2)void f(MyClass&& x){ // xはL Value? R Value? MyClass y = x; // Copy or Moved? }

6.7 MOVE SEMANTICSの実装について例外安全のために実際にMove Semanticsを実装するときは

noexceptにできるか熟考しましょう。

例外安全についてここに記すには余白が小さすぎるので、また別の機会に。

7 STD::MOVE & STD::FORWARDstd::moveはlvalueをrvalueに変換する。

std::forwardは条件付でlvalueをrvalueに変換する。struct MyClass{}; void f(MyClass&& x){ MyClass y = std::move(x); // xはrvalue referenceなのでxにはR Valueが束縛されているためmoveは安全。 } void f(const MyClass& x) { MyClass y = std::move(x); // xはlvalue referenceなのでxにはL Valueが束縛されているためmoveは危険。 }

7.1 TYPE DEDUCTION関数テンプレートは実引数からその引数の型を推論することがで

きる。template <typename T> void f(T x) { std::cout << typeid(x).name() << std::endl; assert(typeid(x) == typeid(int)); } int main() { int a = 0; f(a); // Tはintと推論される。 const int b = 0; f(b); // Tはconst intと推論される。 int& l = a; f(l); // Tはintと推論される。 int&& r = 0; f(r); // Tはintと推論される。 return 0; }

7.2 FORWARDING(UNIVERSAL) REFERENCEtemplate parameterにreferenceやconstが付属すると、推論され

た型Tとオブジェクトxの型は違う。

Forwarding Referenceを使用すると、R Valueが関数テンプレートに渡されたときTはそのR Valueの型に、 L Valueが渡されたときL

Value Referenceに推論される。

template <typename T> struct TD; // コンパイラの型推論の結果を表示するテクニック。 template <typename T> void f(T&& x){ // Forwarding(Universal) Reference。 TD<T> a; // メモ: コンパイル・エラーで型が表示される。 // error: implicit instantiation of undefined template 'TD<int &>' // TD<decltype(x)> a; } int main() { int a = 0; f(a); // Tはint&と推論される。xの型はint&。(LVR) const int b = 0; f(b); // Tはconst int&と推論される。xの型はconst int&。(const LVR) int& l = a; f(l); // Tはint&と推論される。xの型はint&。(LVR) int&& r = 0; f(r); // Tはint&と推論される。xの型はint&。(LVR) f(int(0)); // Tはintと推論される。xの型はint&&。(RVR) return 0; }

7.3 FORWARDING REFERENCE & STD::FORAWRD型推論の結果を考慮して右辺値が渡されたときはmoveで、左辺値が渡されたときはcopyするためにstd::forwardテンプレートが 使

用できる。template <typename T> void f(T&& x){ // std::forwardの宣言。 // template< class T > // T&& forward( typename std::remove_reference<T>::type& t ); // Tがnon reference(例えばint)のときは、std::forrwardはR Value Referenceに。 // TがL value reference(例えばint&)のときは、std::forrwardはL Value Referenceに。 T tmp = std::forward<T>(x); }

8 キーワードlvalue & rvaluelvalue reference & rvalue referenceforwarding referencemove semantics (Movable)copy semantics (Copyable)RAIItype deduction

9 参考情報"C++ Rvalue References Explained"

"Back to the Basics! Essentials of Modern C++Style@CPPCON2014," Herb Sutter,

"std::[email protected]"

"Effective Modern C++," Scott Meyers

http://thbecker.net/articles/rvalue_references/section_01.html

https://www.youtube.com/watch?v=xnqTKD8uD64

http://en.cppreference.com/w/cpp/utility/move