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
79
C++ はなぜあんなにも複雑なのか
WATANABE Yuki
February 02, 2022
Tweet
Share
More Decks by WATANABE Yuki
See All by WATANABE Yuki
速いクイックソート: Pattern-defeating quicksort
magicant
0
330
鉄道シミュレーターで自動運転を実装した話
magicant
0
260
ドキュメント、書けてますか?
magicant
0
120
Other Decks in Programming
See All in Programming
イベントストーミング図からコードへの変換手順 / Procedure for Converting Event Storming Diagrams to Code
nrslib
1
340
AWS CDKの推しポイント 〜CloudFormationと比較してみた〜
akihisaikeda
3
310
なぜ「共通化」を考え、失敗を繰り返すのか
rinchoku
1
500
LT 2025-06-30: プロダクトエンジニアの役割
yamamotok
0
310
Systèmes distribués, pour le meilleur et pour le pire - BreizhCamp 2025 - Conférence
slecache
0
100
データの民主化を支える、透明性のあるデータ利活用への挑戦 2025-06-25 Database Engineering Meetup#7
y_ken
0
320
datadog dash 2025 LLM observability for reliability and stability
ivry_presentationmaterials
0
110
git worktree × Claude Code × MCP ~生成AI時代の並列開発フロー~
hisuzuya
1
460
PHPでWebSocketサーバーを実装しよう2025
kubotak
0
110
生成AIで日々のエラー調査を進めたい
yuyaabo
0
640
Julia という言語について (FP in Julia « SIDE: F ») for 関数型まつり2025
antimon2
3
980
設計やレビューに悩んでいるPHPerに贈る、クリーンなオブジェクト設計の指針たち
panda_program
6
1.3k
Featured
See All Featured
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
107
19k
GraphQLとの向き合い方2022年版
quramy
47
14k
Building a Modern Day E-commerce SEO Strategy
aleyda
41
7.3k
Documentation Writing (for coders)
carmenintech
71
4.9k
The Language of Interfaces
destraynor
158
25k
Docker and Python
trallard
44
3.4k
Git: the NoSQL Database
bkeepers
PRO
430
65k
Site-Speed That Sticks
csswizardry
10
660
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
181
53k
KATA
mclloyd
29
14k
Bootstrapping a Software Product
garrettdimon
PRO
307
110k
It's Worth the Effort
3n
185
28k
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 やテンプレートを駆使する