Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
C++ はなぜあんなにも複雑なのか
Search
WATANABE Yuki
February 02, 2022
Programming
0
72
C++ はなぜあんなにも複雑なのか
WATANABE Yuki
February 02, 2022
Tweet
Share
More Decks by WATANABE Yuki
See All by WATANABE Yuki
速いクイックソート: Pattern-defeating quicksort
magicant
0
240
鉄道シミュレーターで自動運転を実装した話
magicant
0
230
ドキュメント、書けてますか?
magicant
0
110
Other Decks in Programming
See All in Programming
iOS開発におけるCopilot For XcodeとCode Completion / copilot for xcode
fuyan777
1
1.3k
テストコード書いてみませんか?
onopon
2
300
アクターシステムに頼らずEvent Sourcingする方法について
j5ik2o
6
680
週次リリースを実現するための グローバルアプリ開発
tera_ny
1
910
BEエンジニアがFEの業務をできるようになるまでにやったこと
yoshida_ryushin
0
120
PHPで学ぶプログラミングの教訓 / Lessons in Programming Learned through PHP
nrslib
4
1k
PHPで作るWebSocketサーバー ~リアクティブなアプリケーションを知るために~ / WebSocket Server in PHP - To know reactive applications
seike460
PRO
2
740
fs2-io を試してたらバグを見つけて直した話
chencmd
0
290
今年のアップデートで振り返るCDKセキュリティのシフトレフト/2024-cdk-security-shift-left
tomoki10
0
340
rails newと同時に型を書く
aki19035vc
5
680
「とりあえず動く」コードはよい、「読みやすい」コードはもっとよい / Code that 'just works' is good, but code that is 'readable' is even better.
mkmk884
6
1.3k
サーバーゆる勉強会 DBMS の仕組み編
kj455
1
250
Featured
See All Featured
Building Better People: How to give real-time feedback that sticks.
wjessup
366
19k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
160
15k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
3
340
We Have a Design System, Now What?
morganepeng
51
7.3k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
33
2.7k
Reflections from 52 weeks, 52 projects
jeffersonlam
348
20k
Music & Morning Musume
bryan
46
6.3k
BBQ
matthewcrist
85
9.4k
A designer walks into a library…
pauljervisheath
205
24k
Measuring & Analyzing Core Web Vitals
bluesmoon
5
200
Practical Orchestrator
shlominoach
186
10k
Being A Developer After 40
akosma
89
590k
Transcript
C++ はなぜあんなにも複雑なのか 渡邊裕貴 / 2022-02-02 Rust との比較で考える
近況 最近、趣味で Rust を書いてます Rust に慣れるともう C++ は書きたくなくなるね
Rust と C++ の違い • 開発環境のエコシステム • 所有権と借用の管理 • ムーブセマンティクス
• 型安全な並行計算 • 例外とエラー処理 • …… 今日のお題 気が向いたら次回話す 多分一番有名
Rust のオブジェクトは動かせる struct S; impl Drop for S { fn
drop(&mut self) { println!("{:p}", self); } } fn main() { let a = S; let b = a; // ここで b のデストラクターだけが実行される }
C++ のオブジェクトは動かない struct S { ~S() { std::cout << this
<< std::endl; } }; int main(void) { auto a = S(); auto b = a; // b と a のデストラクターが実行される }
Rust はメンバーを移動してオブジェクトを生成する struct Person { name: String, age: i32, }
fn main() { let name = String::from("わたなべ"); let age = 36; let developer = Person { name, age }; let manager = developer; }
C++ はコンストラクターでオブジェクトを生成する struct Person { std::string name; int age; Person(const
char *name, int age) : name(name), age(age) {} Person(const Person &p) : name(p.name), age(p.age) {} }; int main(void) { Person developer("わたなべ", 36); Person manager(developer); }
オブジェクトの生成と移動 Rust 1. オブジェクトの移動が定義されている ◦ 自動的・暗黙的に 2. 代入によって オブジェクトを移動する 3.
移動によって オブジェクトを生成する コンストラクターの概念はない C++ 1. コンストラクターが定義される ◦ 暗黙的に または明示的に 2. コンストラクターを呼ぶことで オブジェクトを生成する 3. 新しいオブジェクトを生成することで 実質的にオブジェクトを移動する ここまでのまとめ
コピー & ムーブ
Rust では移動した後の変数は使えない let developer = Person { name, age };
let manager = developer; println!("{}", developer.name); // エラー!
Rust でオブジェクトを複製したいときは明示的に let developer = Person { name, age };
let manager = developer.clone(); println!("{}", developer.name); // OK
いちいち clone 関数を実装するのはめんどい マクロを使えば自動的に実装される #[derive(Clone)] // ←これを付けるだけで clone() が呼べるようになる struct
Person { name: String, age: i32, }
いちいち clone を呼ぶのもめんどい • #[derive(Copy)] を付けると clone() が自動で呼ばれるようになる • 整数型などの「軽い」型で有効
let x = 0; let y = x; // x.clone() とは書かなくて良い let z = x + y; // もう一度 x が使える
C++ ではデフォルトでオブジェクトがコピーされる Person manager = developer; とか書くとオブジェクトがコピーされる (C++ では、というか、もともと C
では構造体の代入は memcpy と等価) これをコンストラクターという概念と両立させるには?
コピーコンストラクター • 自分と同じ型の const 参照だけを受け取るコンストラクター • (条件が揃えば) 暗黙的に定義される • 自分で定義することもできる
Person manager = developer; を Person manager(developer); に等しいコンストラクター呼び出しの一種として扱うようにした
コピーコンストラクターって非効率じゃね? • Const 参照を受け取るので、元のオブジェクトを変化させることはできない • 実質的には移動ではなく複製が行われる • std::string や std::vector
は複製時にヒープを確保する • 移動先オブジェクトのコンストラクターでヒープ確保した直後に 移動元オブジェクトのデストラクターでヒープ解放するの? 😒 Person(const Person &p) : name(p.name), age(p.age) {} std::string のコンストラクター内で ヒープが確保される
ムーブコンストラクター • 自分と同じ型の右辺値参照のみを受け取るコンストラクター • (条件が揃えば) 暗黙的に定義される • Const でないので引数を書き換えることができる ◦
元のオブジェクトが管理するヒープ領域を奪って自分のものにできる ◦ 元のオブジェクト自体がなくなるわけではない ▪ 元のオブジェクトのデストラクターは後で普通に呼ばれる Person(Person &&p) : … { … } // ムーブコンストラクター Person(const Person &p) : … { … } // コピーコンストラクター
右辺値参照 ……ナニソレ?😅 • コピーとムーブ、二つのコンストラクターを区別するために追加された概念 • 普通の参照 (左辺値参照) と右辺値参照は別の型という扱い // ↓引数の型が異なるのでオーバーロードが許される
Person(Person &&p) : … { … } // 引数は右辺値参照 Person(const Person &p) : … { … } // 引数は左辺値参照
右辺値参照はキャストして作る // デフォルトは左辺値参照なのでコピーされる Person manager = developer; // 引数を右辺値参照にキャストして渡すとムーブされる Person
manager = static_cast<Person&&>(developer); // いちいちキャストするのがめんどいのでキャストするためだけのライブラリー関数がある Person manager = std::move(developer);
コピー & ムーブ Rust • 代入によって オブジェクトを生成・移動する • clone 関数を呼んで
オブジェクトを複製する • clone の実装は derive できる • 一部の型のオブジェクトは暗黙的に clone される ムーブがデフォルトで コピーしたいとき明示する C++ • コンストラクターを呼ぶことで オブジェクトを生成・移動・複製する • コピー/ムーブコンストラクターは 暗黙的に定義される場合がある • 引数の型で コピーとムーブを使い分ける ◦ 左辺値参照 vs 右辺値参照 コピーがデフォルトで ムーブしたいとき明示する ここまでのまとめ
すでに存在する変数への代入
Rust の代入は元のオブジェクトを解体する struct S(i32); impl Drop for S { fn
drop(&mut self) { println!("{}", self.0); } } fn main() { let mut a = S(0); a = S(1); // 代入時に S(0) のデストラクターが実行される // 関数の最後で S(1) のデストラクターが実行される }
C++ の代入は関数呼び出しの糖衣構文 std::string a("foo"); // 生成 std::string b = "bar";
// 生成 a = b; // 代入 a.operator=(b); // 代入の正体
コピー代入・ムーブ代入 • 代入はデフォルトではコピーを行う ◦ もともと C では構造体の代入は memcpy と等価 •
代入時もコンストラクターと同様にコピーとムーブを区別したい • (条件が揃えば) コピー代入演算子が暗黙的に定義される • (条件が揃えば) ムーブ代入演算子が暗黙的に定義される • 自分で定義することもできる
すでに存在する変数への代入 Rust • 古いオブジェクトは解体される • 他は変数の定義と再代入は同じ C++ • 再代入は関数呼び出しの糖衣構文 ◦
コンストラクターと同じく コピーとムーブの区別がある • 再代入自体は オブジェクトの生成も解体もしない ここまでのまとめ
関数呼び出しでの値のやりとり
C++ は生成によって移動する • 引数を値渡しするとき、オブジェクトの “移動” が発生 • 戻り値を返すとき、オブジェクトの “移動” が発生
• 毎回、移動先を生成し移動元を削除するのか? 😟 struct S {}; S f() { S s = S(); return s; } int main(void) { S s = S(f()); }
Copy elision • オブジェクトを移動せずに済ませる最適化 ◦ 移動先が予め分かっている場合 ◦ 移動元オブジェクトを最初から移動先のアドレスに生成する ◦ コピー
or ムーブコンストラクターの呼び出しが不要になる ▪ ただしコンストラクター内に printf とかがあると動作が変わる • 実際に最適化されるかどうかは種々の条件により異なる ◦ 関数呼び出しや return の式の書き方 ◦ C++ のバージョン ◦ コンパイラーに指定した最適化オプション ◦ コンパイラーの賢さ
関数の引数・戻り値の受け渡し Rust • 代入としてコンパイラーが最適化する C++ • Copy elision で最適化されたり されなかったりする
ここまでのまとめ
コンテナーの要素の出し入れ
コンテナーに効率よくオブジェクトを挿入したい std::vector<std::pair<int, int>> v; v.push_back({1, 2}); // 先に pair が生成されてから
vector 内へと移動される // 最初から vector 内に pair を生成するには?
コンストラクターを vector の中で呼ぶしかない std::vector<std::pair<int, int>> v; v.emplace_back(1, 2); // emplace_back
の中で直接 pair のコンストラクターが呼ばれる // オブジェクトを移動する必要がなくなった!
あらゆる引数をコンストラクターに渡せるためには ようこそテンプレートの世界へ template<typename T> class vector { template<typename... Args> T&
emplace_back(Args&&... args) { /*略*/ } }; テンプレの型に && が付いてるが 左辺値参照を渡すこともできる 可変長引数
コンテナーの要素の出し入れ Rust • オブジェクトを先に生成してから コンテナー内に移動する C++ • コンテナー内に直接オブジェクトを 生成できる •
コンストラクターに引数を渡すために テンプレートを駆使する ここまでのまとめ
まとめ • Rust のオブジェクトは代入により移動できる ◦ 細かいことはコンパイラーにおまかせ • C++ のオブジェクトは直接移動できない ◦
移動するためにコンストラクターや代入演算子を使う ◦ 移動を軽くするためにムーブコンストラクターやムーブ代入演算子を使う ◦ 移動を省くために copy elision やテンプレートを駆使する