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
82
C++ はなぜあんなにも複雑なのか
WATANABE Yuki
February 02, 2022
Tweet
Share
More Decks by WATANABE Yuki
See All by WATANABE Yuki
速いクイックソート: Pattern-defeating quicksort
magicant
0
380
鉄道シミュレーターで自動運転を実装した話
magicant
0
300
ドキュメント、書けてますか?
magicant
0
120
Other Decks in Programming
See All in Programming
CSC509 Lecture 10
javiergs
PRO
0
160
テーブル定義書の構造化抽出して、生成AIでDWH分析を試してみた / devio2025tokyo
kasacchiful
0
370
One Enishi After Another
snoozer05
PRO
0
180
CSC509 Lecture 11
javiergs
PRO
0
290
Migration to Signals, Resource API, and NgRx Signal Store
manfredsteyer
PRO
0
140
pnpm に provenance のダウングレード を検出する PR を出してみた
ryo_manba
1
180
Temporal Knowledge Graphで作る! 時間変化するナレッジを扱うAI Agentの世界
po3rin
5
1.2k
釣り地図SNSにおける有料機能の実装
nokonoko1203
0
200
エンジニアに事業やプロダクトを理解してもらうためにやってること
murabayashi
0
120
自動テストのアーキテクチャとその理由ー大規模ゲーム開発の場合ー
segadevtech
0
340
ネストしたdata classの面倒な更新にさようなら!Lensを作って理解するArrowのOpticsの世界
shiita0903
1
250
なんでRustの環境構築してないのにRust製のツールが動くの? / Why Do Rust-Based Tools Run Without a Rust Environment?
ssssota
14
47k
Featured
See All Featured
Code Review Best Practice
trishagee
72
19k
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
140
34k
Writing Fast Ruby
sferik
630
62k
How GitHub (no longer) Works
holman
315
140k
Intergalactic Javascript Robots from Outer Space
tanoku
273
27k
Building a Scalable Design System with Sketch
lauravandoore
463
33k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
658
61k
Build The Right Thing And Hit Your Dates
maggiecrowley
38
2.9k
Site-Speed That Sticks
csswizardry
13
940
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
127
54k
Building Flexible Design Systems
yeseniaperezcruz
329
39k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
359
30k
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 やテンプレートを駆使する