C++
目次
主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。
- 概要
- 1. C++とは何か
- 2. Cとの違いとモダンC++
- 3. 型システムとリテラル
- 4. 変数・スコープ・auto
- 5. 制御フローと関数
- 6. 参照とポインタ
- 7. クラスとオブジェクト指向
- 8. コンストラクタとRAII
- 9. Move semanticsと右辺値参照
- 10. Smart pointers(unique_ptr / shared_ptr)
- 11. テンプレート
- 12. STL: コンテナ・イテレータ・アルゴリズム
- 13. Lambda式と関数オブジェクト
- 14. 例外処理
- 15. モジュール(C++20)
- 16. ConceptsとConstraints(C++20)
- 17. Ranges(C++20)
- 18. Coroutines(C++20)
- 19. 並行処理(thread / atomic / future)
- 20. ビルドシステム(CMake / package managers)
- 21. テストとデバッグ
- 22. よくある落とし穴FAQ
- 23. 学習ロードマップ(30日)
- 24. 用語集
- 発展: モダンC++の核心
- 25. コンストラクタとデストラクタ詳細
- 26. テンプレート深掘り
- 27. Conceptsと制約(C++20)深掘り
- 28. RangesとViews(C++20)深掘り
- 29. Coroutines(C++20)深掘り
- 30. メモリモデルと並行処理深掘り
- 実践: モダンC++の適用
- 応用: 実装モデル
- 上級: テンプレートと並行性
- C++ と標準化プロセス
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。
C++ は、低レイヤの制御力と高水準の抽象化を同時に扱う、表現力の広い言語です。
このページでは、値カテゴリ、RAII、所有権、テンプレート、標準ライブラリ、モダンC++ の設計判断を、実務で迷いやすい論点に沿って整理します。
1. C++とは何か
C++ は、「CをベースにOOP・ジェネリクス・例外を加えた多パラダイム言語」。1979年からBjarne Stroustrupが開発し、現在は C++23 が最新規格。ゼロコスト抽象化を理念とし、性能を犠牲にせず高度な抽象化を提供します。
主な用途:
- ゲームエンジン: Unreal Engine、cocos2d-x、Cryengine
- 金融・取引システム: 高頻度取引(HFT)、リスク計算
- OS / ブラウザ: Windows、macOS、Chrome、Firefoxの中核部分
- データベース・分散: MySQL、MongoDB、Redis(一部)
- 科学計算・HPC: CERN、宇宙開発、シミュレーション
- 組み込み・自動車: Autoware、車載ECU
1-1. C++ の歴史
Bjarne Stroustrupと「C with Classes」(1979)
1979年、Bjarne StroustrupがAT&Tベル研究所で、Simulaのクラス概念をCに移植する研究を始めました。当初の名前は 「C with Classes」。1983年に 「C++」 に改名(Cの ++ インクリメント由来)。
1985 C++ 第1版書籍
1989 C++ 2.0
1998 C++98(最初のISO標準化)
2003 C++03(バグ修正)
2011 C++11(モダンC++ 革命:auto、lambda、move、smart pointers)
2014 C++14
2017 C++17(structured binding、optional、variant、filesystem)
2020 C++20(concepts、ranges、coroutines、modules)
2023 C++23(std::expected、print、deducing this)
C++11の革命(2011)
C++11は 「現代C++ 革命」と呼ばれます。auto、lambda、move semantics、smart pointers、nullptr、range-based for、std::thread ─ 言語の使い方が一変しました。
それ以前は 「C with Classes」的な使い方が主流でしたが、C++11以降は 「モダンC++」と呼ばれる別の言語のような体験になりました。
C++20の四大機能
- Concepts: テンプレート制約の言語化
- Ranges: 関数型風コレクション操作
- Coroutines: ゼロコスト非同期
- Modules: #includeの置換
「第二の革命」と呼べる規模の追加です。
1-2. C++ の設計哲学
“C++ is designed to allow you to express ideas, but if you don’t have ideas about a particular topic, you don’t have to use those features.” ─ Bjarne Stroustrup
Zero-Overhead Abstraction(ゼロコスト抽象化)
「使わない機能のコストを払わなくていい」、「手で書くより遅くならない抽象化を提供する」が理念。
std::vector<int> v = {1, 2, 3};
for (int x : v) sum += x;
これは手書きのCループとほぼ同じ機械語にコンパイルされます。抽象化が無料であることがC++ の最大の魅力。
1-3. このセクションのまとめ
- 1979年StroustrupがC with Classesとして開始
- C++98 / 03 / 11 / 14 / 17 / 20 / 23と進化
- C++11で「モダンC++」革命
- C++20でconcepts/ranges/coroutines/modules
- 哲学: ゼロコスト抽象化、複数パラダイム、表現力
2. Cとの違いとモダンC++
C++ は Cのスーパーセットに近いが、現代の使い方は大きく違います。
2-1. Cとの主な違い
C++ にあってCにないもの:
クラス、継承、多態性
例外処理(try/catch)
テンプレート(ジェネリクス)
参照(&)型
関数オーバーロード
名前空間
STL(コンテナ・アルゴリズム)
RAII
ラムダ式(C++11+)
移動セマンティクス(C++11+)
Concepts(C++20+)
互換性:
ほとんどのCコードはC++ でもコンパイル可能
ただし違う書き方が推奨される(malloc → new → make_unique)
2-2. 「現代C++」のスタイル
C++11以降の現代的な書き方:
// 古いC++
int* arr = new int[100];
// ... 使う ...
delete[] arr;
// モダンC++
auto arr = std::make_unique<int[]>(100);
// 自動解放、例外安全
// 古い
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it)
std::cout << *it;
// モダン
for (auto x : v) std::cout << x;
「手動メモリ管理 → smart pointers」「raw pointer → 参照とsmart pointer」「マクロ → constexpr / concepts」が大原則。
2-3. このセクションのまとめ
- Cと高い互換性、ただし「現代C++ 」では別言語のような体験
- new/deleteよりmake_unique / make_shared
- raw pointerは所有しない、参照やsmart pointerを使う
- 範囲for、auto、lambdaが標準
3. 型システムとリテラル
C++ は 静的型付け + 強力な型推論 + テンプレート。
3-1. プリミティブ型
Cと同じ整数型・浮動小数点に加え、bool、char、新しい固定幅整数(<cstdint>)を持ちます。
int8_t / int16_t / int32_t / int64_t
uint8_t / ... / uint64_t
size_t / ptrdiff_t
char / wchar_t / char16_t / char32_t / char8_t (C++20)
3-2. リテラル
42 // int
42L // long
42LL // long long
42U // unsigned
3.14 // double
3.14f // float
"hello" // const char[]
"hello"s // std::string(C++14)
0b1010 // 2進
0x1f // 16進
1'000'000 // 桁区切り(C++14)
3-3. 文字列
const char* c_str = "hello"; // C文字列
std::string s = "hello"; // C++ 文字列
std::string_view sv = "hello"; // 参照のみ(C++17)
s + " world"; // 連結
s.length();
s.substr(0, 3);
std::string_view は 「コピーを作らない参照型文字列」。読み取り専用APIの引数で推奨。
3-4. このセクションのまとめ
- 整数: int / size_t / int32_tなど
- 文字列: std::string / std::string_view / const char*
- リテラル: 数値の桁区切り '、suffix(s, svなど)
4. 変数・スコープ・auto
4-1. autoとdecltype
auto x = 42; // int
auto s = "hello"s; // std::string
auto v = std::vector<int>{1, 2, 3};
decltype(x) y = x; // xと同じ型
auto f() -> int { return 42; } // 戻り値の型を後ろに(trailing return)
C++14以降、auto は関数の戻り値型推論にも使えます。
auto compute() {
return 42; // 推論でint
}
4-2. constとconstexpr
const int x = 10; // ランタイム不変
constexpr int y = 10; // コンパイル時定数
constexpr int factorial(int n) { // コンパイル時計算可能
return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int f5 = factorial(5); // コンパイル時に計算
constexpr は 「コンパイル時にも実行時にも使える」。テンプレートと組み合わせて強力。
4-3. このセクションのまとめ
- auto: 型推論
- decltype: 式の型を取得
- const: ランタイム不変
- constexpr: コンパイル時定数
- consteval: コンパイル時のみ(C++20)
5. 制御フローと関数
Cと基本的に同じですが、モダン機能が加わっています。
5-1. if文の進化
if (auto p = find(); p != end) { ... } // 初期化付きif(C++17)
if constexpr (sizeof(T) == 4) { ... } // コンパイル時if(C++17)
if constexpr はテンプレートで条件分岐を コンパイル時に解決するための機能。
5-2. range-based for(C++11)
for (auto x : container) { ... }
for (const auto& x : container) { ... } // 変更しないなら参照で
for (auto& x : container) { x = ...; } // 変更するなら参照
5-3. switch / structured binding
auto [first, second] = std::make_pair(1, "a"); // C++17構造化束縛
std::map<std::string, int> m;
for (const auto& [key, value] : m) { ... }
5-4. 関数
int add(int a, int b) { return a + b; }
// デフォルト引数
void greet(std::string name = "world") { ... }
// 関数オーバーロード
int add(int a, int b);
double add(double a, double b);
// 戻り値のtrailing return
auto divide(int a, int b) -> double { return double(a) / b; }
5-5. このセクションのまとめ
- if constexpr / 初期化付きif(C++17)
- range-based for
- 構造化束縛auto [a, b] = ...(C++17)
- 関数オーバーロード、デフォルト引数
6. 参照とポインタ
C++ にはCのポインタに加えて 参照(reference) があります。
6-1. 参照
int x = 10;
int& ref = x; // xの別名
ref = 20;
std::cout << x; // 20
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
参照は 「初期化必須」「再束縛不可」「null不可」。ポインタの問題(null、再代入)がない。
6-2. const参照
void print(const std::string& s) { ... } // コピーなしで読み取り
「大きいオブジェクトを関数に渡すときはconst&」が定石。コピーコストを避けつつ、関数内での変更を防ぐ。
6-3. ポインタvs参照の使い分け
参照(&):
- 必ず有効な値(null不可)
- 関数引数で「コピー回避 + 変更可」
- 再束縛しない
- 「単純な別名」が必要なら参照
ポインタ(*):
- nullになりうる
- 再束縛可能
- 配列・動的メモリと相性
- Cとの相互運用
- 所有権を持たないことを明示するならraw pointer
「まず参照、必要ならポインタ」が現代の指針。所有権を表現するなら smart pointer(次々章)。
6-4. このセクションのまとめ
- 参照(&): null不可、再束縛不可、安全
- const参照: 大きいオブジェクトの引数渡し
- raw pointer: null/再束縛可能、所有しない
- 所有権はsmart pointerで表現
7. クラスとオブジェクト指向
C++ のOOPは 「Java/C# より強力で複雑」。多重継承、多態性、private/public、virtualの組み合わせ。
7-1. クラス
class Person {
private:
std::string name_;
int age_;
public:
Person(std::string name, int age)
: name_(std::move(name)), age_(age) {}
std::string greet() const {
return "Hi, I'm " + name_;
}
// ゲッタ
const std::string& name() const { return name_; }
int age() const { return age_; }
};
constメンバ関数
std::string greet() const { ... } // constオブジェクトでも呼べる
「この関数はオブジェクトを変更しない」と表明。const安全性に重要。
7-2. 継承
class Animal {
public:
virtual ~Animal() = default; // 仮想デストラクタ(重要)
virtual void speak() = 0; // 純粋仮想関数(abstract)
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof!"; }
};
std::unique_ptr<Animal> a = std::make_unique<Dog>();
a->speak(); // 多態性、Dog::speakが呼ばれる
override キーワードでオーバーライドを明示(タイポを防げる、推奨)。virtual ~ で仮想デストラクタ。基底クラスのデストラクタが仮想でないとサブクラスのデストラクタが呼ばれず資源リーク。
7-3. 多重継承
C++ は 多重継承可能。Javaと違う点。
class A { public: void a() {} };
class B { public: void b() {} };
class C : public A, public B { };
C c;
c.a();
c.b();
ただし「ダイヤモンド継承」問題が起こり得て、virtual継承で解決。多重継承は 慎重に。
7-4. アクセス指定子
public: どこからでも
protected: 派生クラスから
private: クラス内のみ(デフォルト)
継承時の指定:
class B : public A AのpublicはBのpublic
class B : protected A AのpublicはBのprotected
class B : private A AのpublicはBのprivate
「ほぼ常にpublic継承」が現代的。
7-5. このセクションのまとめ
- class / struct(structはデフォルトpublic)
- constメンバ関数
- virtual / override / 純粋仮想
- 仮想デストラクタは継承使うなら必須
- 多重継承可能だが慎重に
8. コンストラクタとRAII
C++ で最も重要な概念のひとつ RAII(Resource Acquisition Is Initialization)。
RAIIは「解放処理を忘れないための作法」ではなく、リソースの寿命をオブジェクトの寿命へ結びつける設計です。ファイル、ロック、メモリ、ソケットのような外部資源を、制御フローではなく型で管理します。
8-1. コンストラクタとデストラクタ
class File {
FILE* f_;
public:
File(const char* path) : f_(fopen(path, "r")) {
if (!f_) throw std::runtime_error("can't open");
}
~File() {
if (f_) fclose(f_);
}
// コピー禁止、ムーブ可
File(const File&) = delete;
File& operator=(const File&) = delete;
File(File&& other) noexcept : f_(other.f_) { other.f_ = nullptr; }
File& operator=(File&&) noexcept;
};
void f() {
File file("data.txt"); // ファイルを開く
// ... 例外が起きても ...
} // ここで自動的にファイルが閉じられる!
これが RAII。コンストラクタでリソース取得、デストラクタで解放。例外があっても確実に解放される。
8-2. The Rule of Zero / Three / Five
Rule of Zero:
普通は何も書かない(コンパイラが正しく生成する)
Rule of Three(C++03):
コピーコンストラクタ・コピー代入・デストラクタの3つを書くなら
3つすべてを書く
Rule of Five(C++11):
ムーブコンストラクタ・ムーブ代入も加えて5つ
「まずRule of Zero、必要ならFive」が現代の指針。RAIIを活用すれば多くの場合Zeroで済む。
8-3. = default / = delete
class A {
public:
A() = default; // デフォルト
A(const A&) = delete; // コピー禁止
A(A&&) = default; // ムーブはデフォルト
};
8-4. このセクションのまとめ
- RAII: コンストラクタで取得、デストラクタで解放
- 例外安全性の基礎
- Rule of Zero(推奨)/ Three / Five
- = default / = deleteで明示的制御
9. Move semanticsと右辺値参照
C++11の 「ムーブセマンティクス」は革命的な機能。コピーを避けて所有権を移す仕組み。
9-1. lvalueとrvalue
lvalue: 名前のある式(変数)
rvalue: 一時的な値(リテラル、関数の戻り値)
int x = 10; // xはlvalue、10はrvalue
auto y = x + 1; // x + 1はrvalue
9-2. && とstd::move
void f(std::string& s); // lvalue参照
void f(std::string&& s); // rvalue参照(ムーブ用)
std::string a = "hello";
f(a); // lvalue版
f(std::move(a)); // rvalue版(aは使えない状態に)
f("hello"); // rvalue(リテラル)
std::move は 「rvalueにキャストする」関数で、ムーブを発動します。
9-3. ムーブコンストラクタ
class Buffer {
char* data_;
size_t size_;
public:
// コピーコンストラクタ(重い)
Buffer(const Buffer& other)
: data_(new char[other.size_]), size_(other.size_) {
std::memcpy(data_, other.data_, size_);
}
// ムーブコンストラクタ(軽い、所有権を移すだけ)
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
~Buffer() { delete[] data_; }
};
Buffer make() { return Buffer(...); }
Buffer b = make(); // ムーブ(コピーされない、高速)
これにより 「巨大オブジェクトを返り値で返しても遅くない」C++ になりました。
9-4. RVO / NRVO
コンパイラの最適化で、戻り値のコピーが省略される(Return Value Optimization)。C++17+ では一部のケースで義務化。
std::vector<int> create() {
std::vector<int> v;
return v; // ムーブ、もしくはRVOで完全に省略
}
9-5. このセクションのまとめ
- lvalue / rvalue / xvalue(ムーブ可)
- T&& はrvalue参照(ムーブ用)
- std::moveでlvalue → rvalue
- ムーブコンストラクタ・ムーブ代入で高速所有権移動
- RVOで多くの場合不要に
10. Smart pointers(unique_ptr / shared_ptr)
raw pointerを直接使わず、所有権を表現するsmart pointer を使うのが現代C++ の鉄則。
10-1. unique_ptr(単一所有)
auto p = std::make_unique<int>(42); // ヒープに42、pが所有
*p = 100;
// 自動解放、コピー不可、ムーブのみ可能
std::unique_ptr<int> q = std::move(p); // pの所有権をqに
// pはnullptrに
unique_ptr はゼロオーバーヘッド(raw pointerと同じ)。「単一所有ならunique_ptr」が原則。
10-2. shared_ptr(共有所有)
auto p = std::make_shared<int>(42);
auto q = p; // 参照カウント+1
std::cout << p.use_count(); // 2
// 両方が解放されると元のメモリが解放される
参照カウント方式。コストが高い(カウンタ用のメモリ + アトミック更新)ので、本当に共有所有が必要な場合のみ。
10-3. weak_ptr(循環参照対策)
class Node {
std::shared_ptr<Node> child; // OK
std::weak_ptr<Node> parent; // 循環防止
};
shared_ptrの循環参照を解消するため。
10-4. このセクションのまとめ
- 所有権はsmart pointerで
- unique_ptr: 単一所有、ゼロオーバーヘッド
- shared_ptr: 共有所有、参照カウント
- weak_ptr: 循環参照を防ぐ
- raw pointerは「所有しない参照」を表す(nullableな &)
11. テンプレート
C++ の 超強力な抽象化機構。Java/C# のジェネリクスより遥かに自由。
11-1. 関数テンプレート
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
max(1, 2); // T = int
max(1.5, 2.5); // T = double
max<double>(1, 2); // 明示
11-2. クラステンプレート
template <typename T>
class Stack {
std::vector<T> items_;
public:
void push(T x) { items_.push_back(std::move(x)); }
T pop() {
T x = std::move(items_.back());
items_.pop_back();
return x;
}
};
Stack<int> s;
s.push(1);
11-3. テンプレートの実体化
C++ のテンプレートは コンパイル時に型ごとに別の関数/クラスを生成。型消去(Java)でもreified(C#)でもなく、完全な特殊化。
Stack<int> とStack<double> はバイナリ上完全に別のクラス
→ コードサイズが膨らむ可能性
→ コンパイル時間が長くなる
11-4. SFINAEとConcepts
C++17までは SFINAE(Substitution Failure Is Not An Error)でテンプレート制約を表現していました。これが極めて読みにくく、C++20で Concepts が導入されました(第16章)。
11-5. このセクションのまとめ
- template<typename T> で関数・クラステンプレート
- 型ごとに実体化される(C# のreifiedに似て、より自由)
- SFINAEは古く読みにくい
- C++20 Conceptsでモダンに
12. STL: コンテナ・イテレータ・アルゴリズム
C++ の Standard Template Library。コレクション・アルゴリズムの宝庫。
12-1. 主要コンテナ
シーケンス:
vector動的配列(推奨)
array固定長配列
deque両端キュー
list双方向リンクリスト
forward_list単方向リンクリスト
連想:
mapキー順 (赤黒木)
set
multimap / multiset
unordered_mapハッシュ
unordered_set
その他:
stack / queue / priority_queue (アダプタ)
span (C++20、参照ビュー)
std::vector<int> v = {1, 2, 3};
v.push_back(4);
v.size();
v[0];
v.at(0); // 範囲チェック付き
std::map<std::string, int> m;
m["a"] = 1;
m.find("a");
12-2. イテレータ
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it;
}
// range-based for(C++11)
for (auto x : v) std::cout << x;
イテレータは 5種類のカテゴリ(input、output、forward、bidirectional、random access)。アルゴリズムが要求する性質に応じて。
12-3. アルゴリズム
#include <algorithm>
std::sort(v.begin(), v.end());
std::find(v.begin(), v.end(), 42);
std::count_if(v.begin(), v.end(), [](int x) { return x > 0; });
std::transform(v.begin(), v.end(), result.begin(), [](int x) { return x * 2; });
std::accumulate(v.begin(), v.end(), 0); // <numeric>
// C++20 Ranges
std::ranges::sort(v);
auto evens = v | std::views::filter([](int x) { return x % 2 == 0; });
12-4. このセクションのまとめ
- vector / map / unordered_mapが主役
- イテレータで統一的なアクセス
- <algorithm> の豊富な関数
- C++20 rangesで .begin/.endが省略可能
13. Lambda式と関数オブジェクト
auto add = [](int a, int b) { return a + b; };
add(1, 2); // 3
// キャプチャ
int x = 10;
auto plus_x = [x](int n) { return n + x; }; // 値キャプチャ
auto modify = [&x]() { x = 100; }; // 参照キャプチャ
auto all_by_value = [=]() { ... }; // すべて値
auto all_by_ref = [&]() { ... }; // すべて参照
// 戻り値型を明示
auto f = [](int x) -> double { return x * 1.5; };
// generic lambda(C++14+)
auto print = [](auto x) { std::cout << x; };
std::function<R(Args...)> で関数オブジェクトを汎用的に保持できますが、ラムダを直接autoで受けるほうが速い。
14. 例外処理
try {
risky();
} catch (const std::runtime_error& e) {
std::cerr << e.what();
} catch (const std::exception& e) {
std::cerr << "exception: " << e.what();
} catch (...) {
std::cerr << "unknown";
}
throw std::runtime_error("oops");
noexcept
void safe() noexcept { ... } // 例外を投げないと宣言
ムーブコンストラクタを noexcept にすると、std::vector のリサイズ時に ムーブが選ばれる(コピーではなく)。性能に重要。
例外を使うか
ゲームエンジンや組み込みでは 例外を使わないコードベースが多い(性能・予測可能性のため)。代わりに std::expected(C++23)や戻り値で対応。
15. モジュール(C++20)
#include の置き換え。コンパイル時間を劇的に短縮できる。
// math.cppm(モジュールインタフェース)
export module math;
export int add(int a, int b) { return a + b; }
// main.cpp
import math;
int main() {
return add(1, 2);
}
#include のテキスト展開ではなく、コンパイル済みのインタフェースをimportするため高速。ただし普及はまだ過渡期。
16. ConceptsとConstraints(C++20)
テンプレート制約を 言語レベルで明示。
#include <concepts>
template <std::integral T>
T add(T a, T b) { return a + b; }
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <Numeric T>
T sum(std::vector<T> v) { ... }
add(1, 2); // OK
add(1.5, 2.5); // 制約違反(integralでない)
これにより エラーメッセージが劇的に分かりやすくなりました。SFINAE時代の数百行のテンプレートエラーから解放されます。
17. Ranges(C++20)
関数型風のコレクション操作。
#include <ranges>
std::vector<int> v = {1, 2, 3, 4, 5};
auto result = v
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : result) std::cout << x; // 4, 16
| で パイプラインを組める。Java Stream / C# LINQに相当。遅延評価でゼロコスト。
Rangesの強みは、ループの意図を名前で表せることにある。フィルタ、変換、切り出し、結合を組み合わせると、インデックス操作よりもデータの流れが読みやすくなる。一方で、ビューは遅延評価なので、参照先のコンテナ寿命には注意する。ローカルな一時オブジェクトにぶら下がるビューを返すと、読みやすい見た目のまま危険なコードになる。
18. Coroutines(C++20)
ステートフルな関数を書く仕組み。ゼロコスト非同期を実現。
#include <coroutine>
task<int> compute() {
int x = co_await fetch();
int y = co_await process(x);
co_return x + y;
}
co_await co_yield co_return の3つで状態機械を表現。ただし C++ 標準にはランタイム(Task型など)が含まれていないので、cppcoro、folly、asio などのライブラリが必要。
Coroutinesは構文機能であって、JavaScriptのPromiseやC#のTaskのような標準実行環境そのものではない。どのイベントループで動くか、キャンセルをどう伝えるか、例外をどう扱うか、戻り値型をどう設計するかはライブラリ側の責務になる。採用時は、言語機能だけでなく利用するランタイムの設計を読む必要がある。
19. 並行処理(thread / atomic / future)
#include <thread>
#include <future>
#include <atomic>
#include <mutex>
// thread
std::thread t([] { std::cout << "hi"; });
t.join();
// jthread(C++20、自動join)
std::jthread t2([] { ... });
// mutex
std::mutex mtx;
{
std::lock_guard<std::mutex> lock(mtx);
// クリティカル
}
// atomic
std::atomic<int> counter = 0;
counter.fetch_add(1);
// future
auto fut = std::async(std::launch::async, []{ return 42; });
int result = fut.get();
20. ビルドシステム(CMake / package managers)
CMake
cmake_minimum_required(VERSION 3.20)
project(myapp CXX)
set(CMAKE_CXX_STANDARD 20)
add_executable(myapp main.cpp util.cpp)
target_compile_options(myapp PRIVATE -Wall -Wextra -O2)
Conan / vcpkg
C++ には長らく標準のパッケージマネージャがなく、Conan と vcpkg が二大選択肢。
vcpkg install fmt boost
C++のビルドでは、再現性が最重要である。コンパイラ、標準ライブラリ、ビルドタイプ、依存ライブラリのバージョン、オプションが少し変わるだけで挙動やABIが変わる。CMake Presets、lockfile、CI上のクリーンビルドを整えると、個人環境では動くが本番では壊れる問題を減らせる。
21. テストとデバッグ
Catch2 / Google Test
TEST_CASE("Add") {
REQUIRE(1 + 2 == 3);
CHECK("hello"s == "hello"s);
}
Sanitizers
g++ -fsanitize=address -fsanitize=undefined main.cpp
Valgrind / gdb / lldb
伝統的ツール。
C++のテストでは、単体テストだけでなく実行時検査を組み合わせる。ASanは範囲外アクセスやUse-After-Free、UBSanは未定義動作、TSanはデータ競合を見つける。通常のCIでは高速な単体テストを回し、夜間やリリース前にSanitizer付きの重いテストを走らせる構成が現実的である。
22. よくある落とし穴FAQ
Q1. raw pointerをいつ使う?
「所有しない、null可能な参照」が必要なとき。所有権はsmart pointerで。
Q2. std::moveしたオブジェクトはどう使える?
「有効だが未指定」状態。代入はOK、それ以外は実装次第。
Q3. 仮想デストラクタは何で必要?
基底クラスのポインタでdeleteされたとき、サブクラスのデストラクタを呼ぶため。継承する基底は仮想必須。
Q4. const参照とconstポインタ
const T* p; // Tがconst、p自体は変更可
T* const p; // pがconst、Tは変更可
const T* const p; // 両方const
const T& r; // Tはconst、参照は再束縛不可(参照は元々)
Q5. 例外を使うべきか
ライブラリは投げる、アプリは捕まえる。性能が極端にクリティカルなら使わない。
Q6. テンプレートのコンパイルエラーが読めない
C++17まではSFINAEで苦行。C++20 Conceptsで改善。エラーメッセージが分かりやすくなる。
Q7. std::endl vs “\n”
std::endl はフラッシュもする(遅い)。改行だけなら "\n"。
Q8. ヘッダオンリーvs分割
テンプレート関数・inline関数はヘッダに。それ以外はcppに。
Q9. C++ でCコードを呼ぶ
extern "C" { #include "header.h" } でCリンケージ。
Q10. 複数の規格で書きたい
__cplusplus マクロでチェック。C++17は201703L。
23. 学習ロードマップ(30日)
Week 1: モダンC++ 基礎
- 環境(GCC/Clang、CMake)
- auto、range for、lambda
- vector / string
- 参照とconst
Week 2: OOPとRAII
Week 3: テンプレートとSTL
- 関数・クラステンプレート
- イテレータと
- C++20 Concepts / Ranges
Week 4: 並行・実践
24. 用語集
- RAII: Resource Acquisition Is Initialization
- SFINAE: Substitution Failure Is Not An Error
- CRTP: Curiously Recurring Template Pattern
- PIMPL: Pointer to IMPLementation
- lvalue / rvalue: 名前あり / 一時値
- move: 所有権の移動
- STL: Standard Template Library
- ABI: Application Binary Interface
- ODR: One Definition Rule
- noexcept: 例外を投げない宣言
- constexpr / consteval: コンパイル時計算
発展: モダンC++の核心
ここからは、上記の各章で触れた主要トピックの 更なる深掘りです。日々のコードに直結するパターン、内部メカニズム、性能チューニング、モダンC++ のイディオムを網羅します。
25. コンストラクタとデストラクタ詳細
C++ のオブジェクトライフサイクルは表面的にはシンプルですが、コピー・ムーブ・暗黙生成・呼び出し順序を正確に理解しないとバグの元になります。
25-1. 6つの特殊メンバ関数
C++ クラスにはコンパイラが暗黙的に生成しうる 6つの特殊メンバ関数があります。
class Foo {
public:
Foo(); // デフォルトコンストラクタ
Foo(const Foo&); // コピーコンストラクタ
Foo(Foo&&) noexcept; // ムーブコンストラクタ
Foo& operator=(const Foo&); // コピー代入演算子
Foo& operator=(Foo&&) noexcept; // ムーブ代入演算子
~Foo(); // デストラクタ
};
暗黙生成のルール
- ユーザがコンストラクタを書かない: デフォルトコンストラクタが生成
- ユーザがコピー系を書かない: コピーが生成
- ユーザがムーブ系を書かない、かつコピー系も書かない: ムーブが生成
- デストラクタを書くとムーブの暗黙生成が抑制される(注意!)
Rule of Fiveを厳密に適用するタイミング
「生のリソース(ポインタ・ファイルハンドル)を直接所有する」場合は、5つすべてを書きます(または = default / = delete で明示)。
class FileHolder {
FILE* f_;
public:
explicit FileHolder(const char* path) : f_(std::fopen(path, "r")) {}
~FileHolder() { if (f_) std::fclose(f_); }
// コピー禁止(FILE* を共有してしまうとdouble closeする)
FileHolder(const FileHolder&) = delete;
FileHolder& operator=(const FileHolder&) = delete;
// ムーブは可能
FileHolder(FileHolder&& o) noexcept : f_(std::exchange(o.f_, nullptr)) {}
FileHolder& operator=(FileHolder&& o) noexcept {
if (this != &o) {
if (f_) std::fclose(f_);
f_ = std::exchange(o.f_, nullptr);
}
return *this;
}
};
std::exchange(target, new_value) は 「targetにnew_valueをセットして、元の値を返す」。ムーブで頻出するイディオム。
25-2. 初期化リスト
メンバ変数は コンストラクタ本体ではなく初期化リストで初期化するのが鉄則。
class Person {
std::string name_;
int age_;
public:
// Bad: 一度デフォルト初期化されてから代入される
Person(std::string n, int a) {
name_ = n;
age_ = a;
}
// Good: 初期化リストで直接構築
Person(std::string n, int a) : name_(std::move(n)), age_(a) {}
};
const メンバや参照メンバは 初期化リストでしか初期化できないので、習慣として使う。
初期化順序の罠
class A {
int x_;
int y_;
public:
// 危険: y_ が先に初期化されようとするが、宣言順序でx_ が先
A(int v) : y_(v), x_(y_) {} // x_ は未初期化のy_ で計算される!
};
メンバ変数は クラス内の宣言順で初期化されます(初期化リストの順序ではありません)。-Wreorder 警告で検出可能。
25-3. delegating constructor(委譲コンストラクタ)
C++11+ で、コンストラクタから別のコンストラクタを呼べる。
class Rect {
int w_, h_;
public:
Rect(int w, int h) : w_(w), h_(h) {}
Rect() : Rect(0, 0) {} // 委譲
Rect(int side) : Rect(side, side) {} // 正方形
};
これでコンストラクタごとの重複を減らせる。
25-4. inheriting constructor(継承コンストラクタ)
class Base {
public:
Base(int x) {}
Base(const std::string& s) {}
};
class Derived : public Base {
public:
using Base::Base; // BaseのコンストラクタすべてをDerivedに
};
Derived d(42); // OK
Derived d2("hi"); // OK
25-5. explicitと暗黙変換
class String {
public:
String(const char* s) {} // 暗黙変換が許される
explicit String(int capacity) {} // 暗黙変換禁止
};
void f(String s);
f("hello"); // OK: const char* → String
f(100); // エラー!explicitで禁止
f(String(100)); // OK: 明示的
「1引数のコンストラクタは原則 explicit」が現代の指針。意図しない暗黙変換を防ぎます。
25-6. デストラクタの呼び出し順序
class Member {
public:
~Member() { std::cout << "Member destroyed\n"; }
};
class Base {
public:
virtual ~Base() { std::cout << "Base destroyed\n"; }
};
class Derived : public Base {
Member m1_;
Member m2_;
public:
~Derived() override { std::cout << "Derived destroyed\n"; }
};
// 出力順:
// Derived destroyed
// Member destroyed (m2_、宣言の逆順)
// Member destroyed (m1_)
// Base destroyed
「派生クラスのデストラクタ → メンバの逆順 → 基底クラスのデストラクタ」。コンストラクタは逆。
25-7. 仮想デストラクタの必要性(再強調)
class Animal {
public:
~Animal() { ... } // 仮想ではない!
};
class Dog : public Animal {
std::string* name_;
public:
Dog() : name_(new std::string("Rex")) {}
~Dog() { delete name_; }
};
Animal* p = new Dog();
delete p; // Dogのデストラクタが呼ばれない!メモリリーク
継承を意図する基底クラスのデストラクタは必ず virtual にする。または final で継承禁止。
25-8. このセクションのまとめ
- 6つの特殊メンバ関数を意識
- Rule of Zero(リソースをsmart pointer / RAIIクラスに任せる)
- 必要ならRule of Five
- 初期化リストで初期化、宣言順序が初期化順序
- explicitで暗黙変換を防ぐ
- 継承するクラスのデストラクタはvirtual
- 委譲コンストラクタで重複削減
26. テンプレート深掘り
C++ のテンプレートは コンパイル時計算が可能なほど強力。基本構文だけでなく、特殊化・SFINAE・可変引数・CRTPまで踏み込みます。
26-1. 関数テンプレートvsクラステンプレート
// 関数テンプレート
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// クラステンプレート
template <typename T, std::size_t N>
class FixedArray {
T data_[N];
public:
T& operator[](std::size_t i) { return data_[i]; }
constexpr std::size_t size() const noexcept { return N; }
};
FixedArray<int, 10> arr;
std::size_t N のように、型ではなく値もテンプレートパラメータにできます(non-type template parameter)。
26-2. 部分特殊化と全特殊化
template <typename T>
struct TypeName {
static constexpr const char* value = "unknown";
};
// 全特殊化
template <>
struct TypeName<int> {
static constexpr const char* value = "int";
};
template <>
struct TypeName<std::string> {
static constexpr const char* value = "std::string";
};
// クラステンプレートは部分特殊化が可能
template <typename T>
struct TypeName<std::vector<T>> {
static constexpr const char* value = "std::vector<...>";
};
注意: 関数テンプレートは 全特殊化しかできません(部分特殊化はできない)。代わりにオーバーロードを使います。
26-3. 可変引数テンプレート(variadic templates)
C++11+ で 任意の数のテンプレートパラメータを扱える。
// 基底(再帰の終端)
void print() {}
// 1引数以上
template <typename T, typename... Rest>
void print(const T& first, const Rest&... rest) {
std::cout << first;
if constexpr (sizeof...(Rest) > 0) {
std::cout << ", ";
print(rest...);
}
}
print(1, "hello", 3.14, true);
Rest... がパラメータパック、sizeof...(Rest) でパックの要素数。std::tuple、std::variant、std::function など標準ライブラリの中核機能はこれで実装されています。
fold expression(C++17+)
template <typename... Args>
auto sum(Args... args) {
return (args + ...); // 全部足す
}
template <typename... Args>
void print(Args... args) {
((std::cout << args << " "), ...); // 全部coutに
}
sum(1, 2, 3, 4); // 10
print("a", "b", "c");
「演算子をパラメータパックに畳み込む」のC++17構文。可変引数テンプレートが圧倒的に書きやすくなりました。
26-4. SFINAE(Substitution Failure Is Not An Error)
「テンプレートの置換に失敗してもエラーではなく、その候補を除外する」というルール。条件付きテンプレートを実装する古典的なテクニック。
// std::enable_ifを使った例
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
T abs_int(T x) {
return x < 0 ? -x : x;
}
abs_int(5); // OK
abs_int(5.0); // エラー(候補から除外)
std::enable_if_t<bool, T> は 「boolがtrueならT、falseなら型として無効」というメタプログラミング。条件がfalseなら置換失敗となり、SFINAEで除外される。
if constexpr で代替(C++17+)
C++17以降は if constexpr を使って同じことが書ける場面が増えました。
template <typename T>
T process(T x) {
if constexpr (std::is_integral_v<T>) {
return x * 2;
} else if constexpr (std::is_floating_point_v<T>) {
return x * 2.5;
} else {
return x;
}
}
C++20以降は Concepts がSFINAEの置き換えとして圧倒的に推奨されます(27章)。
26-5. CRTP(Curiously Recurring Template Pattern)
「自分自身を派生させたクラスを基底のテンプレート引数にする」というモダンC++ の重要パターン。静的多態性(vtableなし)を実現します。
template <typename Derived>
class Comparable {
public:
bool operator!=(const Derived& other) const {
return !(static_cast<const Derived&>(*this) == other);
}
bool operator>(const Derived& other) const {
return other < static_cast<const Derived&>(*this);
}
// ...
};
class Money : public Comparable<Money> {
int amount_;
public:
Money(int a) : amount_(a) {}
bool operator==(const Money& o) const { return amount_ == o.amount_; }
bool operator<(const Money& o) const { return amount_ < o.amount_; }
};
Money a(100), b(200);
a < b; // OK(Moneyで実装)
a != b; // OK(CRTPでderived)
a > b; // OK
virtual を使わない多態性。インライン化されるので ゼロコスト。Boost、Eigenなどのライブラリで多用。
C++20ではConceptsやSpaceship Operatorで代替できる場面も
class Money {
int amount_;
public:
auto operator<=>(const Money&) const = default; // 比較演算子を全自動生成
};
26-6. テンプレートメタプログラミング(TMP)
コンパイル時に計算するプログラミング。型を操作する技法。
// コンパイル時の階乗
template <unsigned N>
struct Factorial {
static constexpr unsigned value = N * Factorial<N-1>::value;
};
template <>
struct Factorial<0> {
static constexpr unsigned value = 1;
};
constexpr auto f5 = Factorial<5>::value; // 120、コンパイル時計算
C++14以降は constexpr 関数で同じことがもっと素直に書けるので、TMPは大幅に減りました。
constexpr unsigned factorial(unsigned n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr auto f5 = factorial(5); // 120
26-7. テンプレートエラーメッセージとの戦い
C++17までのテンプレートエラーは 数百行に及ぶ呪文で有名です:
error: no matching function for call to 'foo(...)'
note: candidate template ignored: substitution failure
[T = std::vector<...>]: no type named 'iterator' in
'std::vector<std::map<std::string, ...
... 200行続く
C++20 Concepts でこれが大幅に改善されました(27章)。それまでは static_assert でコメント化したエラーメッセージを出すのが対策でした。
26-8. このセクションのまとめ
- 関数テンプレート / クラステンプレート / non-typeパラメータ
- 全特殊化(関数OK、クラスOK)/ 部分特殊化(クラスのみ)
- 可変引数テンプレート(Args...) + fold expression(C++17+)
- SFINAEは古いテクニック → if constexpr / Conceptsへ
- CRTPで静的多態性
- TMPはconstexpr関数で代替可能に
- C++20 Conceptsでエラーメッセージが劇的改善
27. Conceptsと制約(C++20)深掘り
C++20の Concepts は「SFINAEの地獄から解放する」言語機能。型に対する 明示的な制約を書けます。
27-1. 標準コンセプト
<concepts> ヘッダで多数の標準コンセプトが提供されます。
#include <concepts>
std::integral<T> // 整数型
std::floating_point<T> // 浮動小数点
std::signed_integral<T>
std::unsigned_integral<T>
std::same_as<T, U> // 同じ型
std::convertible_to<T, U> // TをUに変換可能
std::derived_from<T, U> // Uから派生
std::default_initializable<T>
std::copy_constructible<T>
std::move_constructible<T>
std::equality_comparable<T>
std::totally_ordered<T>
std::regular<T> // semiregular + 比較可能
std::invocable<F, Args...> // FがArgsで呼び出し可能
std::predicate<F, Args...> // Fがboolを返す
27-2. コンセプトの書き方
// 1. テンプレートパラメータ制約
template <std::integral T>
T add(T a, T b) { return a + b; }
// 2. requires節
template <typename T>
requires std::integral<T>
T add(T a, T b) { return a + b; }
// 3. requires + 関数引数
template <typename T>
T add(T a, T b) requires std::integral<T> { return a + b; }
// 4. 短縮構文(C++20+)
auto add(std::integral auto a, std::integral auto b) {
return a + b;
}
選択肢が4通りもあります。最初の形(テンプレートパラメータ制約)が最も一般的。
27-3. 自作コンセプト
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <typename T>
concept Stringifiable = requires(T x) {
{ std::to_string(x) } -> std::convertible_to<std::string>;
};
template <typename T>
concept Container = requires(T t) {
typename T::value_type;
typename T::iterator;
{ t.begin() } -> std::input_iterator;
{ t.end() } -> std::input_iterator;
{ t.size() } -> std::convertible_to<std::size_t>;
};
requires 式で、「この型で動かしたい操作のリスト」を書ける。これがコンパイラに 「この操作が可能ならOK」 という制約として読まれます。
27-4. requires式の構造
template <typename T>
concept Hashable = requires(T x) {
// 1. 単純要件: その式がvalidであること
std::hash<T>{};
// 2. 型要件: その型がvalidであること
typename T::value_type;
// 3. 複合要件: 式がvalid + 型/制約を満たすこと
{ std::hash<T>{}(x) } -> std::convertible_to<std::size_t>;
// 4. ネストしたrequires
requires std::copyable<T>;
};
27-5. conceptsのエラーメッセージ
template <std::integral T>
T add(T a, T b) { return a + b; }
add(1.0, 2.0);
C++20のエラー:
error: no matching function for call to 'add'
note: constraints not satisfied
note: 'std::integral<double>' evaluated to false
数百行のテンプレート展開エラーが、1行で要点が分かるようになりました。
27-6. Conceptsによるオーバーロード
template <std::integral T>
T process(T x) { return x * 2; }
template <std::floating_point T>
T process(T x) { return x * 2.5; }
process(5); // 10(整数版)
process(5.0); // 12.5(浮動版)
「型に応じて違う実装を選ぶ」が直感的に書けます。
27-7. このセクションのまとめ
- C++20 Conceptsでテンプレート制約を明示
- <concepts> に標準コンセプト多数
- 自作conceptはrequires { ... } で
- 4種類の要件: 単純 / 型 / 複合 / ネスト
- エラーメッセージが劇的に改善
- SFINAE / std::enable_ifは基本的に不要に
28. RangesとViews(C++20)深掘り
std::ranges は 「コレクション操作の関数型化」。Java Stream / C# LINQ / Rust Iteratorに相当する標準機能です。
28-1. rangeとは
// range = begin() とend() を持つもの
std::vector<int> v = {1, 2, 3, 4, 5};
auto first = std::ranges::begin(v);
auto last = std::ranges::end(v);
// Conceptsとして:
// std::ranges::range<T> begin/endを持つ
// std::ranges::input_range読める
// std::ranges::forward_range何度でも反復できる
// std::ranges::bidirectional_range
// std::ranges::random_access_range
// std::ranges::contiguous_range
28-2. rangeアルゴリズム
#include <ranges>
#include <algorithm>
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// 従来
std::sort(v.begin(), v.end());
// C++20 ranges
std::ranges::sort(v); // begin/endを渡さなくて良い
std::ranges::find(v, 5);
std::ranges::count_if(v, [](int x) { return x > 3; });
イテレータ対を渡す煩わしさが消えました。
28-3. views(遅延評価)
#include <ranges>
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = v
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; })
| std::views::take(3);
for (int x : result) {
std::cout << x << " ";
}
// 出力: 4 16 36
| 演算子で パイプラインを構築。遅延評価で、for ループで初めて要素ごとに計算されます。
主要なviews
std::views::filter(pred) // 条件で絞る
std::views::transform(fn) // 変換
std::views::take(n) // 先頭n個
std::views::drop(n) // 先頭n個を捨てる
std::views::take_while(pred)
std::views::drop_while(pred)
std::views::reverse // 逆順
std::views::iota(start, end) // 整数列を生成
std::views::iota(start) // 無限整数列
std::views::join // 入れ子を平坦化
std::views::split(delim) // 区切り文字で分割
std::views::elements<I> // tuple/pairのI番目
std::views::keys // mapのキー
std::views::values // mapの値
std::views::zip(...) // C++23
std::views::enumerate // C++23
std::views::chunk(n) // C++23
std::views::adjacent<N> // C++23
28-4. C++23での拡張
// std::ranges::toでVecなどに集約
auto result = std::views::iota(1, 11)
| std::views::filter([](int x) { return x % 2 == 0; })
| std::ranges::to<std::vector>();
// zip
auto v1 = std::vector{1, 2, 3};
auto v2 = std::vector{"a", "b", "c"};
for (auto [n, s] : std::views::zip(v1, v2)) { ... }
// enumerate
for (auto [i, x] : std::views::enumerate(v1)) {
std::cout << i << ": " << x << "\n";
}
これでPythonのzip / enumerateに近い使い勝手に。
28-5. 性能特性
rangesは ゼロコスト抽象化を目指して設計されており、最適化後のコードは 手書きのforループとほぼ同じ機械語になります。
ただし注意点:
- デバッグビルドでは遅くなることがある
- 一部のviewは 未確定の状態を持つので、コピーが思わぬコスト
- C++20 rangesは iterator/sentinelペアの設計で従来と少し違う
28-6. このセクションのまとめ
- range = begin/endを持つもの
- std::ranges::* でsort/findなどイテレータ対なしに呼べる
- views::* で遅延評価のパイプライン
- C++23でzip / enumerate / chunk / toが追加
- ゼロコスト、ただしデバッグビルドは遅め
29. Coroutines(C++20)深掘り
C++20のcoroutinesは 言語機能ですが、std::generator(C++23)以前は 標準ライブラリのTaskが含まれていない特殊な状況でした。基本概念と実用例を整理します。
29-1. キーワード
co_await何かのAwaitableを待つ(中断点)
co_yield値を返して中断
co_return値を返して終了
これらが 関数内に1つでもあると、その関数はコルーチンになります。
29-2. 標準的な使い方
// C++23のstd::generator
#include <generator>
std::generator<int> naturals() {
int n = 1;
while (true) {
co_yield n++;
}
}
for (int n : naturals() | std::views::take(10)) {
std::cout << n << " ";
}
// 1 2 3 4 5 6 7 8 9 10
29-3. asyncタスク(cppcoro例)
C++20自体はTask型を提供しないため、外部ライブラリ(cppcoro、folly::coro、asio)を使うのが実用的です。
#include <cppcoro/task.hpp>
cppcoro::task<int> fetch(std::string url) {
auto response = co_await http_get(url);
co_return response.length();
}
cppcoro::task<int> main_task() {
int len = co_await fetch("https://example.com");
co_return len;
}
書き味はC# / TypeScriptのasync/awaitに似ています。
29-4. Coroutinesは低レベル機構
C++ のcoroutinesは 「低レベルな状態機械生成機能」。実際にユーザが使うには、promise_type を持つTaskクラスが必要です。
struct Task {
struct promise_type {
Task get_return_object() { return Task{}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task simple() {
co_return;
}
このボイラープレートを意識せずに使うために、ライブラリがTask / Generatorを提供します。
29-5. このセクションのまとめ
- co_await / co_yield / co_returnがキーワード
- std::generator(C++23)でジェネレータが標準入り
- asyncタスクはcppcoro / folly::coro / asioで
- promise_typeでカスタマイズ可能(高度)
- ゼロコスト:状態機械にコンパイルされる
30. メモリモデルと並行処理深掘り
C++11で 言語にメモリモデルが定義され、ロックフリープログラミングが可能になりました。
30-1. std::threadとjoin/detach
#include <thread>
void worker(int id) {
std::cout << "thread " << id << "\n";
}
std::thread t(worker, 1);
t.join(); // 終了を待つ
// またはdetach(バックグラウンド化)
std::thread t2(worker, 2);
t2.detach(); // メインを抜けても続行(注意:寿命管理が困難)
std::thread は joinかdetachを呼ばずに破棄すると std::terminate。RAIIの精神に反するので、std::jthread(C++20) が推奨される。
std::jthread jt(worker, 1);
// スコープを抜けるとき自動的にjoin
jthread は stop_token も持つ。協調的キャンセルが可能。
30-2. mutexとlock guard
#include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx); // RAIIでロック
++counter;
} // スコープを抜けるとアンロック
C++17の std::scoped_lock は複数のmutexをデッドロック回避でロックできる。
std::mutex m1, m2;
std::scoped_lock lock(m1, m2); // 両方をデッドロックなしでロック
30-3. condition variable
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// 待機側
void wait_for_ready() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // readyがtrueになるまで待つ
}
// 通知側
void make_ready() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_all();
}
cv.wait(lock, predicate) 形式が spurious wakeup(偽の起床)対応込みなので推奨。
30-4. atomicとmemory_order
<atomic> ヘッダで lock-freeな並行操作を提供。
#include <atomic>
std::atomic<int> counter{0};
counter.fetch_add(1); // アトミックに +1
counter.fetch_sub(1);
counter.compare_exchange_strong(expected, desired);
int v = counter.load();
counter.store(100);
memory_order
counter.fetch_add(1, std::memory_order_relaxed); // 順序保証なし、最速
counter.fetch_add(1, std::memory_order_acquire); // load系で順序保証
counter.fetch_add(1, std::memory_order_release); // store系
counter.fetch_add(1, std::memory_order_acq_rel); // 両方
counter.fetch_add(1, std::memory_order_seq_cst); // 順次一貫性、デフォルト、最強
memory_order_seq_cst がデフォルトで最も理解しやすい。ロックフリーアルゴリズムを書くときだけ relaxed/acquire/releaseを使う。
30-5. futureとpromise
#include <future>
std::future<int> fut = std::async(std::launch::async, []{
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
});
int result = fut.get(); // 終わるまでブロック
std::async は手軽だが、起動ポリシーがプラットフォーム依存で実用上はthread poolを自前で書くか、外部ライブラリ(folly、Intel TBB)を使うことが多い。
30-6. このセクションのまとめ
- std::thread / std::jthread(C++20、RAIIでjoin)
- mutex + lock_guard / unique_lock / scoped_lock
- condition_variable + while述語パターン
- atomic + memory_orderでロックフリー
- future / promise / async(実用は限定的)
- 並行は難しい:まずstd::mutexで、ロックフリーは最後の手段
31. モダンC++ パターン
実務で頻出するC++ デザインパターンを集約。
31-1. PIMPL(Pointer to IMPLementation)
ヘッダから実装の詳細を隠す。コンパイル時間も短縮。
// my_class.h
class MyClass {
public:
MyClass();
~MyClass();
void do_something();
private:
class Impl;
std::unique_ptr<Impl> pimpl_;
};
// my_class.cpp
class MyClass::Impl {
public:
int internal_state;
void really_do() { ... }
};
MyClass::MyClass() : pimpl_(std::make_unique<Impl>()) {}
MyClass::~MyClass() = default; // ここでImplのサイズが分かる
void MyClass::do_something() { pimpl_->really_do(); }
PIMPLの利点
- ABI安定性: ヘッダを変えなくても実装を差し替えられる
- コンパイル時間短縮: 内部の依存ヘッダがクライアントに漏れない
- カプセル化強化
31-2. Type Erasure
std::function のように 「具体的な型を消して同じインターフェースで扱う」パターン。
class Shape {
struct Concept {
virtual ~Concept() = default;
virtual double area() const = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
};
template <typename T>
struct Model : Concept {
T data;
Model(T d) : data(std::move(d)) {}
double area() const override { return data.area(); }
std::unique_ptr<Concept> clone() const override {
return std::make_unique<Model>(*this);
}
};
std::unique_ptr<Concept> impl_;
public:
template <typename T>
Shape(T x) : impl_(std::make_unique<Model<T>>(std::move(x))) {}
double area() const { return impl_->area(); }
};
Shape は 特定の基底クラスを継承していなくても、area() を持っていれば受け入れる。Goのinterfaceに近い動作をC++ で実現。
31-3. Tag dispatch
「タグ型で関数オーバーロードを切り替える」テクニック。
struct fast_tag {};
struct slow_tag {};
void impl(int x, fast_tag) { /* 最適化版 */ }
void impl(int x, slow_tag) { /* 汎用版 */ }
template <typename Tag>
void process(int x, Tag tag) {
impl(x, tag);
}
process(5, fast_tag{});
if constexprで書ける場面も増えましたが、ライブラリ実装で今も使われます。
31-4. Builder Pattern
class Pizza {
std::vector<std::string> toppings_;
int size_;
public:
class Builder {
Pizza pizza_;
public:
Builder& size(int s) { pizza_.size_ = s; return *this; }
Builder& topping(std::string t) {
pizza_.toppings_.push_back(std::move(t));
return *this;
}
Pizza build() && { return std::move(pizza_); }
};
};
auto pizza = Pizza::Builder{}
.size(12)
.topping("cheese")
.topping("tomato")
.build();
複雑な構築を流暢に表現。JavaのBuilderパターンと同じ思想。
31-5. Singleton(Meyers Singleton)
C++11以降の 正しいシングルトン:
class Logger {
public:
static Logger& instance() {
static Logger logger; // C++11+ でthread-safe
return logger;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() = default;
};
Logger::instance().log("hello");
C++11で 「関数local staticの初期化はスレッドセーフ」が保証されたので、これだけでOK。double-checked lockingなどは不要に。
ただし シングルトンの是非自体が議論的。テストしにくい・グローバル状態を生むなど。
31-6. 範囲ベースのScopeGuard
template <typename F>
class ScopeGuard {
F fn_;
bool active_ = true;
public:
ScopeGuard(F f) : fn_(std::move(f)) {}
~ScopeGuard() { if (active_) fn_(); }
void dismiss() { active_ = false; }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
};
template <typename F>
ScopeGuard<F> make_scope_guard(F f) { return ScopeGuard<F>(std::move(f)); }
void f() {
auto guard = make_scope_guard([]{ std::cout << "cleanup\n"; });
// 何らかの処理
if (rare_condition) guard.dismiss(); // 解除も可能
} // 自動的に "cleanup"
例外安全な後処理を構造化する。Goの defer 相当。
31-7. このセクションのまとめ
- PIMPLでヘッダから実装隠蔽 + コンパイル時間短縮
- Type ErasureでGo-like interface
- Tag dispatch / if constexpr
- Builder patternで流暢なAPI
- Meyers Singleton(thread-safe by C++11)
- ScopeGuardで例外安全な後処理
32. パフォーマンス深掘り
「ゼロコスト抽象化」を活かすための具体的テクニック。
32-1. コピー省略(copy elision / RVO)
std::vector<int> create() {
std::vector<int> v;
return v; // RVOでコピーされない
}
std::vector<int> v = create();
C++17で mandatory copy elision が導入され、特定パターンでは コンパイラがコピーを省略する義務があります。これにより auto v = make_huge_object() がゼロコストに。
32-2. 無駄なコピーを避ける
// Bad: コピーが多い
std::string greet(std::string name) { // 引数でコピー
return "Hello, " + name; // 連結でコピー
}
// Good
std::string greet(const std::string& name) { // 借用
return "Hello, " + name; // 戻り値はRVO
}
// Moveを活かす
std::string make_greeting(std::string name) {
return "Hello, " + std::move(name); // ムーブで連結
}
「シンクパラメータ(最終的に保持する引数)はムーブで受ける」のがイディオム。
32-3. emplaceとpush_back
std::vector<std::pair<int, std::string>> v;
v.push_back({1, "hi"}); // pairが一時オブジェクトとして作られる
v.push_back(std::make_pair(1, "hi")); // 同上
v.emplace_back(1, "hi"); // 直接構築(コピー/ムーブなし)
emplace_back は vectorの中で直接構築。コピー/ムーブのコストが消える。map.emplace、unordered_map.emplace も同様。
32-4. small string optimization(SSO)
std::string は 短い文字列をヒープに置かず、object内に直接持つ最適化を実装系が行います(SSO)。
std::string s = "hi"; // ヒープ確保なし(SSO)
std::string s = "this is a long string ..."; // ヒープ確保
短い文字列なら std::string が予想以上に速い。
32-5. constexpr / consteval / constinit
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr auto f5 = factorial(5); // コンパイル時に計算
auto f6 = factorial(6); // 実行時にも呼べる
consteval int compile_only(int n) { return n * 2; } // コンパイル時のみ(C++20)
constinit int global = factorial(5); // static初期化のみ(C++20)
constexpr は 「コンパイル時にも実行時にも使える」、consteval は 「コンパイル時のみ」、constinit は 「static initializationの保証」(static initialization order fiascoの対策)。
32-6. inlineとlink-time optimization(LTO)
// inlineヒント(コンパイラへの提案)
inline int add(int a, int b) { return a + b; }
ただしmodernコンパイラは inline キーワードに依存せず、自分で判断します。inline の主な用途は ODRを満たすため(ヘッダで関数定義を書くため)。
LTO(Link Time Optimization)を有効にすると、翻訳単位を超えてインライン化ができます。
g++ -O3 -flto main.cpp other.cpp -o app
32-7. キャッシュ局所性
// Bad: random accessの構造体配列
struct Particle {
Vec3 position;
Vec3 velocity;
Vec3 acceleration;
float mass;
Color color;
int health;
};
std::vector<Particle> particles;
for (auto& p : particles) p.position += p.velocity; // 全フィールドをロード
// Good: SoA (Struct of Arrays)
struct Particles {
std::vector<Vec3> positions;
std::vector<Vec3> velocities;
// ...
};
for (size_t i = 0; i < n; ++i) {
positions[i] += velocities[i]; // 必要なフィールドだけロード
}
ゲーム・科学計算で重要なテクニック。
32-8. このセクションのまとめ
- RVO / copy elisionで戻り値ゼロコスト
- emplace_* で直接構築
- ムーブを活かすシンクパラメータパターン
- constexpr / consteval / constinit
- LTOでリンク時インライン
- キャッシュ局所性 / SoA
- まず計測(perf / Tracy / Valgrind cachegrind)
33. デバッグとサニタイザ
C++ の生産性は 「ツールの活用度」に大きく依存します。
33-1. AddressSanitizer(ASan)
g++ -fsanitize=address -fno-omit-frame-pointer -g main.cpp
検出できるバグ:
- ヒープ・スタック・グローバルのバッファオーバーフロー
- use-after-free / use-after-return
- 二重free / 不正なfree
- メモリリーク
実行時間オーバーヘッドは2倍程度。CIで常時有効化が現代の標準。
33-2. UndefinedBehaviorSanitizer(UBSan)
g++ -fsanitize=undefined -g main.cpp
未定義動作の動的検出:
- 符号付き整数オーバーフロー
- 0除算
- NULL参照外し
- アライメント違反
- ビットシフト範囲外
33-3. ThreadSanitizer(TSan)
g++ -fsanitize=thread -g main.cpp
データレースを検出。並行処理を書く場合は必須。ASan/UBSan/TSanは同時に使えないので、CIで別ジョブとして走らせる。
33-4. Valgrind
valgrind --leak-check=full --show-leak-kinds=all ./app
valgrind --tool=cachegrind ./app # キャッシュ解析
valgrind --tool=callgrind ./app # 呼び出しグラフ
ASanより遅い(10倍以上)が、検出能力は最強クラス。
33-5. gdb / lldb
g++ -g main.cpp
gdb ./a.out
(gdb) break main
(gdb) run
(gdb) print x
(gdb) backtrace
(gdb) frame 2
(gdb) print *p
(gdb) watch counter
(gdb) catch throw
コアダンプ解析:
ulimit -c unlimited
./crashing_app
gdb ./crashing_app core
33-6. clang-tidy / cppcheck
静的解析ツール。
clang-tidy main.cpp -- -std=c++20
cppcheck --enable=all main.cpp
潜在的バグ・スタイル違反を検出。CIで必ず実行。
33-7. このセクションのまとめ
- ASan: メモリバグ検出(CI常時)
- UBSan: 未定義動作検出
- TSan: データレース検出
- Valgrind: より厳密、遅い
- gdb/lldb: 対話的デバッグ
- clang-tidy / cppcheck: 静的解析
- 組み合わせて使う
34. 拡張FAQ
Q1. std::move をした後の値はどう扱う?
「有効だが未指定の状態(valid but unspecified)」。代入・破棄は安全だが、内容は読み取らないのが原則。
Q2. std::vector<bool> が他のvectorと違う?
std::vector<bool> は bit packed(ビット単位で詰められた)特殊化で、vector<int> などと挙動が違う。bool* を取れない、参照が bool& でない、など。std::vector<char> か std::deque<bool> を使うのが推奨される場面も。
Q3. noexcept を付けるべきか
- ムーブコンストラクタ・ムーブ代入: 強く推奨(vectorのreallocationで重要)
- swap: 推奨
- destructor: 暗黙的にnoexcept
- ほかは「投げないと確信できる」場合のみ
Q4. enum class と enum の違い
enum Color { RED, GREEN, BLUE }; // 古い、暗黙変換可
enum class Color { RED, GREEN, BLUE }; // 新しい、強い型付け(C++11+)
新規コードは 常に enum class。
Q5. const と constexpr の違い
const は 「変更しない」、constexpr は 「コンパイル時に計算可能」。constexpr の方が強い保証。
Q6. forward declaration(前方宣言)はいつ使う?
ヘッダで他のクラスを参照だけしたい(ポインタ・参照・関数宣言)場合に、#include の代わりに class Foo; で十分なら使う。コンパイル時間が劇的に短縮。
Q7. std::endl vs '\n'
std::endl は 改行 + フラッシュ。フラッシュコストがある。通常は '\n' を使う。
Q8. オーバーロードとoverrideの違い
オーバーロード: 同じ関数名で違う引数の関数を複数定義。 override: 基底クラスの仮想関数を派生クラスで再定義。
Q9. using と typedef の違い
typedef std::vector<int> IntVec; // 古い
using IntVec = std::vector<int>; // 新しい(C++11+、推奨)
template <typename T>
using MyVec = std::vector<T>; // テンプレートエイリアス(typedefでは書けない)
Q10. auto の戻り値型推論はいつ使う?
C++14+ で関数戻り値型推論可能。短い関数・テンプレートでヘルプ。明らかでない場合は明示的に。
Q11. decltype(auto) と auto
auto は 値、decltype(auto) は 参照を保ったまま推論。
int x = 0;
int& r = x;
auto a = r; // int(参照が剥がれる)
decltype(auto) b = r; // int&(参照が保たれる)
Q12. std::shared_ptr のサイクル
class Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // サイクル!
};
std::weak_ptr で対処。
Q13. テンプレートをヘッダに書かないといけないのはなぜ?
実体化が 使用箇所で必要だから。.cpp に書くと他の翻訳単位で実体化できない(明示的実体化を除く)。
Q14. 例外を投げるか戻り値で返すか
ライブラリ層は例外、API境界は戻り値が一つの指針。C++23の std::expected<T, E> はRustの Result 相当。
Q15. なぜ delete[] と delete を区別する?
new[] で確保したものは delete[]、new で確保したものは delete。ミックスはUB。unique_ptr<T[]> を使えば自動的に正しく動く。
Q16. friendは使うべき?
カプセル化を破る。最小限にし、operatorのために使う場合が主流。
Q17. multiple inheritanceのダイヤモンド
class A { int x; };
class B : public A {};
class C : public A {};
class D : public B, public C {}; // Aが2つ含まれる!
class B : virtual public A {}; // virtual継承で1つにまとめる
class C : virtual public A {};
class D : public B, public C {};
複雑なので、できる限り避ける。
Q18. operator== と operator!= を両方書く必要がある?
C++20から operator== だけ書けば != が自動生成。さらに <=>(spaceship operator)で比較全部生成。
Q19. std::optional とnullable pointer
std::optional<int> find(); // 値の有無
std::shared_ptr<int> find(); // ポインタの所有
int* find(); // raw pointer(所有しない)
意味が違う。optional は 「値の有無を表現」、smart pointerは 所有権、raw pointerは borrow。
Q20. static_cast / dynamic_cast / reinterpret_cast / const_cast
static_cast<int>(3.14) // 通常のキャスト
dynamic_cast<Derived*>(base_ptr) // 実行時の安全なdown cast
reinterpret_cast<char*>(&x) // 危険、ビット解釈
const_cast<int*>(const_ptr) // constを外す(ほぼ使わない)
Cスタイルキャスト (int)x は避ける。意図が不明確。
Q21. ABIとは
Application Binary Interface。バイナリレベルの互換性。クラス定義を変えるとABIが壊れ、別の翻訳単位とリンクできなくなる。PIMPLで軽減。
Q22. ヘッダオンリーライブラリ
すべての実装をヘッダに書くライブラリ。リンクが不要で配布が楽(fmt、Boost.Header-onlyなど)。inline や template で実現。
Q23. compile timeが遅い
- 不要な #includeを減らす
- 前方宣言を活用
- PIMPLで実装隠蔽
- precompiled header
- modules(C++20)
- ccache
Q24. RTTIを無効化したい
-fno-rtti で dynamic_cast / typeid が無効に。エンベデッド・ゲームエンジンで一般的。
Q25. exceptionを無効化したい
-fno-exceptions で例外を無効化。ゲームエンジン・エンベデッドで多い。代わりに戻り値で。
Q26. Cとの互換性
extern "C" { ... } でCリンケージ。Cのヘッダをincludeする際の標準パターン。
Q27. 値を返すか出力引数で返すか
C++17以降は 値を返すのが推奨(RVOで速い)。古いコードはout parameter(void f(int& out))が多いが、std::optional やstruct戻り値で代替できる。
Q28. std::span(C++20)とは
「配列のビュー」。std::vector や生配列を統一的に扱える。
void process(std::span<const int> data) {
for (int x : data) ...
}
std::vector<int> v = ...;
int arr[10];
process(v); // OK
process(arr); // OK
Q29. string_view と string の使い分け
void print(std::string_view s); // 借用OK、コピーしたくない
std::string make_string(); // 所有を返す
Q30. なぜModern C++ は強力か
- ゼロコスト抽象化: 抽象化が無料
- RAII + smart pointer: メモリ安全(に近い)
- 移動セマンティクス: 高速な所有権移動
- テンプレート + Concepts: 型安全な汎用化
constexpr: コンパイル時計算- 多パラダイム: OOP・関数型・genericを選べる
35. 図解
35-1. 値型のメモリ配置
class Foo {
int a; // 4 byte
char b; // 1 byte
double c; // 8 byte
};
sizeof(Foo) = ?
レイアウト:
+------+------+------+------+
| a (4 byte) |
+------+------+------+------+
| b (1) | padding (7 byte) | ← 8 byte alignedのため
+------+------+------+------+
| c (8 byte) |
+------+------+------+------+
合計24 byte
メンバ順序を変えると詰められます。
35-2. 仮想関数テーブル
Animal* p = new Dog();
p->speak();
メモリ:
+-----------+ +---------------+
| p (8 byte)| ──────→ | vptr ────┐ |
+-----------+ | members | | Dog object
+----------+ |
| |
↓ |
+---------------+
| vtable for Dog|
+---------------+
| Dog::speak |
| Dog::~Dog |
+---------------+
p->speak() = (*p->vptr->speak)(p)
35-3. unique_ptrの構造
std::unique_ptr<Foo>
+----+
| p | ──→ Foo object (heap)
+----+
サイズ = ポインタ1つぶん(8 byte on 64bit)
オーバーヘッドゼロ
35-4. shared_ptrの構造
std::shared_ptr<Foo>
+----+----+
| p | cb | ──→ control block
+----+----+ +------------------+
| strong count |
| weak count |
| deleter |
+------------------+
↓
Foo object
サイズ = ポインタ2つぶん(16 byte)
カウンタ更新がアトミック
35-5. ムーブセマンティクス
Buffer create() {
Buffer b;
return b; // RVO: コピーもムーブもなし
}
Buffer src = ...;
Buffer dst = std::move(src);
Before: After:
src ─→ [data][size] src ─→ [nullptr][0]
dst ─→ [data][size]
所有権が移動、データはコピーされない
36. 拡張学習ロードマップ(60日)
Phase 1: モダンC++ 基礎(Day 1-15)
- Day 1-2: 環境構築(GCC/Clang、CMake)、Hello World
- Day 3-4: 基本型、auto、const、constexpr
- Day 5-6: 関数、参照、ポインタの基礎
- Day 7-8: STLコンテナ(vector、string、map)
- Day 9-10: range-based for、lambda
- Day 11-12: クラスとOOPの基礎
- Day 13-14: コピー・ムーブ・RAIIの概念
- Day 15: ASan / UBSanを使ってデバッグ
Phase 2: 中級(Day 16-30)
- Day 16-17: 継承・virtual・多態性
- Day 18-19: smart pointer(unique/shared/weak)
- Day 20-21: ムーブセマンティクスと右辺値参照
- Day 22-23: 例外と例外安全性
- Day 24-25: テンプレート基礎
- Day 26-27: STLアルゴリズム + イテレータ
- Day 28-29: 関数オブジェクトとstd::function
- Day 30: 中規模アプリ(CLIツール、簡易JSONパーサ)
Phase 3: モダン機能(Day 31-45)
- Day 31-32: C++17機能(structured binding、optional、variant)
- Day 33-34: C++20 Concepts
- Day 35-36: C++20 Ranges + views
- Day 37-38: C++20 Coroutines(基本)
- Day 39-40: C++20 Modules
- Day 41-42: テンプレート深掘り(CRTP、SFINAE、TMP)
- Day 43-44: メモリモデル + atomic
- Day 45: 中規模並行処理
Phase 4: 実践(Day 46-60)
- Day 46-47: CMake詳細、vcpkg/Conan
- Day 48-49: Catch2 / Google Test
- Day 50-51: ベンチマーク(Google Benchmark)
- Day 52-53: 性能チューニング(perf、Tracy)
- Day 54-55: ライブラリ実装(自作Vector、自作unique_ptr)
- Day 56-58: 大規模OSSのソース読解(fmt、json、Boost)
- Day 59-60: 自作プロジェクトをGitHub公開
37. 拡張用語集
あ行
- アライメント: 型サイズの倍数アドレスへの配置
- アロケータ: STLコンテナのメモリ確保戦略を担う型
- 値カテゴリ: lvalue / xvalue / prvalue
- 一時オブジェクト: prvalue、
auto&&で延命できる - イニシャライザリスト: コンストラクタの
: name(value)構文 - インライン化: 関数呼び出しを展開する最適化
- エイリアステンプレート:
template<typename T> using Vec = std::vector<T>;
か行
- 共用体: 複数のメンバが同じメモリを共有
- キャスト: 型変換、
static_cast/dynamic_cast/reinterpret_cast/const_cast - コピー省略: RVO / NRVO / copy elision
- コルーチン:
co_awaitを持つ関数(C++20) - コンセプト: テンプレート制約(C++20)
- コンパイル時計算:
constexpr/consteval
さ行
- 参照:
T&別名、null不可、再束縛不可 - 参照折りたたみ:
T&& &→T&などのテンプレート時の規則 - シンクパラメータ: 引数を最終的に保持するパラメータ(ムーブで受ける)
- スコープガード: 例外安全な後処理
- スマートポインタ:
unique_ptr/shared_ptr/weak_ptr - 静的多態: テンプレート / CRTP(vtableなし)
- 設計時定数:
constexpr値
た行
- 多重継承: 複数の基底クラス
- ダミー型: タグディスパッチで使う空struct
- デフォルト引数: 関数引数のデフォルト値
- テンプレート: 型・値をパラメータ化する仕組み
- 動的多態: 仮想関数
な行
- 名前空間:
namespace foo { ... }で名前を区切る - ネイティブAOT: 事前ネイティブコンパイル
は行
- 半順序: ぴったりではない大小関係(C++20
partial_ordering) - 比較演算子の自動生成:
auto operator<=>(...) = default; - 不透明型: ヘッダで詳細を隠した型
- プレースメントnew: 既存メモリ上に構築(
new (ptr) T(...)) - ペアノ算術: TMPの古典例
- ヘッダ・オンリー: 実装をヘッダに書くライブラリ
- ポリシークラス: 振る舞いを型で注入するパターン
ま行
や〜わ行
- ユニオン:
union { ... } - 要素アクセス:
[]/at()/operator[] - ラムダ式:
[capture](args) { ... } - 連鎖メソッド: Builderパターン
A〜Z
- ABI: Application Binary Interface
- AOT: Ahead-Of-Time
- API: Application Programming Interface
- ASan: AddressSanitizer
- AST: Abstract Syntax Tree
- CRTP: Curiously Recurring Template Pattern
- CTAD: Class Template Argument Deduction(C++17)
- DLL: Dynamic Link Library
- EBO: Empty Base Optimization
- EBCO: Empty Base Class Optimization
- GCC: GNU Compiler Collection
- IDE: Integrated Development Environment
- JIT: Just-In-Time
- LTO: Link-Time Optimization
- MSVC: Microsoft Visual C++
- NRVO: Named Return Value Optimization
- ODR: One Definition Rule
- OOP: Object-Oriented Programming
- PGO: Profile-Guided Optimization
- PIMPL: Pointer to IMPLementation
- POD: Plain Old Data
- POSIX: Portable Operating System Interface
- RAII: Resource Acquisition Is Initialization
- RTTI: Run-Time Type Information
- RVO: Return Value Optimization
- SFINAE: Substitution Failure Is Not An Error
- SIMD: Single Instruction Multiple Data
- SLO: Static Linker Optimization
- SOA: Struct of Arrays
- SSO: Small String Optimization
- STL: Standard Template Library
- TLS: Thread-Local Storage
- TMP: Template Metaprogramming
- TSan: ThreadSanitizer
- UB: Undefined Behavior
- UBSan: UndefinedBehaviorSanitizer
- UDL: User-Defined Literal
- VLA: Variable Length Array
- WG21: ISO/IEC JTC 1/SC 22/WG 21(C++ 標準化委員会)
実践: モダンC++の適用
ここからは 実コードに近い形のレシピ集。日常で頻出する場面で「モダンC++ ならどう書くか」を集めました。
39. 標準ライブラリリファレンス(厳選)
39-1. と入出力
#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
// 標準入出力
std::cin >> x;
std::cout << x << "\n";
std::cerr << "error\n";
// ファイル
std::ifstream f("data.txt");
std::string line;
while (std::getline(f, line)) {
std::cout << line << "\n";
}
// 文字列ストリーム
std::ostringstream oss;
oss << "x = " << 42 << ", y = " << 3.14;
std::string s = oss.str();
// フォーマット
std::cout << std::setw(10) << std::setfill('0') << 42 << "\n"; // 0000000042
std::cout << std::fixed << std::setprecision(3) << 3.14159 << "\n"; // 3.142
std::cout << std::hex << 255 << "\n"; // ff
std::format(C++20)
#include <format>
auto s = std::format("Hello, {}! Age: {}", name, age);
auto s2 = std::format("{:>10}", 42); // 右寄せ10桁
auto s3 = std::format("{:.3f}", 3.14159); // "3.142"
auto s4 = std::format("{:#x}", 255); // "0xff"
printf の安全な代替。Rustの format! / Pythonのf-stringに相当。C++23で std::print も追加。
39-2. 主要関数
#include <algorithm>
#include <numeric>
// 探索
std::find(v.begin(), v.end(), 5);
std::find_if(v.begin(), v.end(), [](int x) { return x > 0; });
std::binary_search(sorted.begin(), sorted.end(), 5);
std::lower_bound(sorted.begin(), sorted.end(), 5);
std::upper_bound(sorted.begin(), sorted.end(), 5);
// 変形
std::sort(v.begin(), v.end());
std::sort(v.begin(), v.end(), std::greater<>{}); // 降順
std::reverse(v.begin(), v.end());
std::rotate(v.begin(), v.begin() + n, v.end());
std::shuffle(v.begin(), v.end(), gen);
// 集約
std::accumulate(v.begin(), v.end(), 0); // 合計
std::accumulate(v.begin(), v.end(), 1, std::multiplies<>{}); // 積
std::count(v.begin(), v.end(), 5);
std::count_if(v.begin(), v.end(), pred);
// コピー・変換
std::copy(src.begin(), src.end(), dst.begin());
std::copy_if(src.begin(), src.end(), back_inserter(dst), pred);
std::transform(src.begin(), src.end(), dst.begin(), fn);
// 集合演算
std::set_union(a.begin(), a.end(), b.begin(), b.end(), back_inserter(out));
std::set_intersection(...);
std::set_difference(...);
// その他
std::min_element(v.begin(), v.end());
std::max_element(v.begin(), v.end());
std::minmax_element(v.begin(), v.end());
std::all_of(v.begin(), v.end(), pred);
std::any_of(v.begin(), v.end(), pred);
std::none_of(v.begin(), v.end(), pred);
これらすべてはC++20 ranges版が std::ranges::sort(v) のように書けます。
39-3. (時刻と期間)
#include <chrono>
using namespace std::chrono;
// 期間
auto d1 = 1s;
auto d2 = 500ms;
auto d3 = std::chrono::seconds(10);
// 時刻
auto now = system_clock::now();
auto epoch = now.time_since_epoch();
auto secs = duration_cast<seconds>(epoch).count();
// 計測
auto start = steady_clock::now();
do_work();
auto end = steady_clock::now();
auto elapsed = duration_cast<milliseconds>(end - start);
std::cout << elapsed.count() << "ms\n";
// C++20で日付
year_month_day today{floor<days>(system_clock::now())};
C++20で timezone、calendar が標準化され、Java/Pythonのdate-time APIに追いついた。
39-4. (C++17+)
#include <filesystem>
namespace fs = std::filesystem;
fs::path p = "/usr/local/bin/myapp";
p.filename(); // "myapp"
p.extension(); // ""
p.parent_path(); // "/usr/local/bin"
fs::exists(p);
fs::is_directory(p);
fs::is_regular_file(p);
fs::file_size(p);
// イテレート
for (const auto& entry : fs::directory_iterator("/some/path")) {
std::cout << entry.path() << "\n";
}
// 再帰
for (const auto& entry : fs::recursive_directory_iterator(".")) {
std::cout << entry.path() << "\n";
}
// 操作
fs::create_directory("new_dir");
fs::copy_file("a.txt", "b.txt");
fs::remove("old.txt");
fs::rename("from.txt", "to.txt");
それまでPOSIXやBoostに頼っていたファイルシステム操作が、C++ 標準で書けるように。
39-5.
#include <regex>
std::regex re(R"(\d{3}-\d{4}-\d{4})");
std::string s = "電話: 090-1234-5678";
if (std::regex_search(s, re)) {
std::cout << "match\n";
}
std::smatch match;
if (std::regex_search(s, match, re)) {
std::cout << match[0] << "\n"; // "090-1234-5678"
}
// 全マッチ
auto begin = std::sregex_iterator(s.begin(), s.end(), re);
auto end = std::sregex_iterator{};
for (auto it = begin; it != end; ++it) {
std::cout << (*it)[0] << "\n";
}
// 置換
std::string result = std::regex_replace(s, re, "***");
R"(...)" は 生文字列リテラル(raw string)。エスケープが要らない。
39-6.
#include <random>
std::random_device rd;
std::mt19937 gen(rd()); // メルセンヌ・ツイスタ
std::uniform_int_distribution<> dist(1, 100);
int x = dist(gen);
std::normal_distribution<> norm(0.0, 1.0);
double y = norm(gen);
std::shuffle(v.begin(), v.end(), gen);
rand() と違い、質の高い乱数生成を提供。エンジン(生成器)と分布(変換)を組み合わせる設計。
39-7. と (C++17+)
#include <optional>
#include <variant>
std::optional<int> find(const std::string& key) {
if (...) return 42;
return std::nullopt;
}
auto result = find("key");
if (result.has_value()) {
std::cout << *result << "\n";
}
int v = result.value_or(-1);
// variant: 複数の型のうち1つ
std::variant<int, std::string, double> v;
v = 42;
v = "hello";
v = 3.14;
std::visit([](const auto& x) {
std::cout << x << "\n";
}, v);
// 取得
if (std::holds_alternative<int>(v)) {
int n = std::get<int>(v);
}
optional は Rustの Option、variant は 判別共用体に相当。
39-8. このセクションのまとめ
- iostream / fstream / stringstream
- C++20 std::format / std::print(C++23)
- algorithm: 大量の汎用関数
- chrono: 時刻・期間・C++20でcalendar
- filesystem: ファイル操作(C++17+)
- regex: 正規表現
- random: 高品質乱数
- optional / variant: 値の不在 / 判別共用体
40. 実用パターン集
40-1. RAIIラッパの定型
template <typename T, typename Deleter>
class UniqueResource {
T resource_;
Deleter deleter_;
bool active_ = true;
public:
UniqueResource(T r, Deleter d) : resource_(std::move(r)), deleter_(std::move(d)) {}
~UniqueResource() { if (active_) deleter_(resource_); }
UniqueResource(const UniqueResource&) = delete;
UniqueResource& operator=(const UniqueResource&) = delete;
UniqueResource(UniqueResource&& other) noexcept
: resource_(std::move(other.resource_)),
deleter_(std::move(other.deleter_)),
active_(std::exchange(other.active_, false)) {}
};
// 使用例
auto file = UniqueResource(std::fopen("data.txt", "r"),
[](FILE* f) { if (f) std::fclose(f); });
C++23で std::unique_resource が提案されています。
40-2. 観察者パターン
class EventEmitter {
std::vector<std::function<void(const std::string&)>> listeners_;
public:
void on(std::function<void(const std::string&)> listener) {
listeners_.push_back(std::move(listener));
}
void emit(const std::string& msg) {
for (auto& l : listeners_) l(msg);
}
};
EventEmitter e;
e.on([](const auto& m) { std::cout << m << "\n"; });
e.emit("hello");
std::function で 任意の関数オブジェクトを保持。
40-3. 関数コンビネータ
template <typename F, typename G>
auto compose(F f, G g) {
return [=](auto x) { return f(g(x)); };
}
auto double_then_square = compose(
[](int x) { return x * x; },
[](int x) { return x * 2; }
);
double_then_square(3); // (3*2)^2 = 36
40-4. ファクトリ関数
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double r_;
public:
explicit Circle(double r) : r_(r) {}
double area() const override { return M_PI * r_ * r_; }
};
std::unique_ptr<Shape> create_shape(const std::string& kind) {
if (kind == "circle") return std::make_unique<Circle>(1.0);
return nullptr;
}
40-5. Strong typedef(型エイリアス強化)
template <typename T, typename Tag>
class StrongType {
T value_;
public:
explicit StrongType(T v) : value_(std::move(v)) {}
T& get() { return value_; }
const T& get() const { return value_; }
};
struct UserIdTag {};
struct OrderIdTag {};
using UserId = StrongType<int, UserIdTag>;
using OrderId = StrongType<int, OrderIdTag>;
void process(UserId uid);
process(UserId(42)); // OK
process(OrderId(42)); // コンパイルエラー!
int の意味の取り違えを防ぐ。
40-6. このセクションのまとめ
- RAIIラッパで任意リソースを管理
- std::functionでコールバック保持
- 関数合成
- ファクトリ + smart pointer
- Strong typedefで意味的型安全
41. C++ コンパイルとリンクの内部
41-1. 翻訳単位(Translation Unit)
foo.cpp + #includeされるすべてのヘッダ
↓
1つの翻訳単位として独立コンパイル
↓
foo.o(オブジェクトファイル)
各 .cpp は 独立コンパイル。リンカが最後に結合。
41-2. ODR(One Definition Rule)
「プログラム全体で各シンボルの定義は1つだけ」というルール。違反するとリンクエラーor UB。
// header.h
int x = 0; // BAD: 複数の .cppでincludeされるとODR違反
// 正しい:
inline int x = 0; // C++17+ inline変数
extern int x; // 宣言、定義は別の .cppで
constexpr int x = 0; // 暗黙的inline
template<typename T> T x{}; // テンプレートはODR例外
41-3. inlineの真の意味
inline int square(int x) { return x * x; }
inline は インライン化のヒントではなく(コンパイラはほぼ無視)、「複数の翻訳単位で同じ定義を持つことを許す」という意味。
ヘッダで関数定義を書くなら inline 必須。
41-4. リンケージ
// 内部リンケージ(このファイル限定)
static int x = 0; // C流
namespace { int y = 0; } // C++ 流(推奨)
// 外部リンケージ(他のファイルから見える)
int z = 0;
extern int z; // 宣言
// no linkage(関数内ローカル)
void f() { int local = 0; }
namespace { ... }(無名namespace)は static のC++ 流で、より柔軟(クラス・テンプレート・型エイリアスにも適用可)。
41-5. forward declaration(前方宣言)
// header.h
class Foo; // 前方宣言(ポインタ・参照・関数戻り値でOK)
class Bar {
Foo* foo_ptr_; // OK
Foo& foo_ref_; // OK
Foo getValue(); // OK(関数の戻り値)
// Foo foo_; // NG(完全な型が必要)
};
ヘッダの依存を最小化し、コンパイル時間を短縮。
41-6. precompiled header(PCH)
# pch.hを一度コンパイルして使い回す
g++ -x c++-header pch.h -o pch.h.gch
g++ -include pch.h main.cpp
巨大プロジェクトでビルド時間を劇的に短縮。Visual Studio・CMakeが標準サポート。
41-7. modules(C++20、コンパイル時間の救世主)
#include を置き換える新メカニズム。
// math.cppm
export module math;
export int add(int a, int b) { return a + b; }
// main.cpp
import math;
int main() { return add(1, 2); }
#include のテキスト展開と違い、コンパイル済みのinterface をimportするため桁違いに高速。普及途上ですが、将来の標準。
41-8. このセクションのまとめ
- 翻訳単位ごとに独立コンパイル → リンク
- ODR: 1つの定義のみ
- inlineは「複数定義を許す」、コンパイラへのヒントではない
- リンケージ: static / 無名namespace / extern
- 前方宣言で依存縮小
- precompiled header / modulesでビルド時間短縮
42. Cとの混在とFFI
C++ からCライブラリを呼ぶ・C++ ライブラリをCから呼ぶ場合の作法。
42-1. CヘッダをC++ からinclude
extern "C" {
#include "c_library.h"
}
extern "C" で 「Cリンケージとして扱う」と宣言。C++ のname manglingを回避。
多くのCヘッダは内部で:
// c_library.h
#ifdef __cplusplus
extern "C" {
#endif
int c_function(int);
#ifdef __cplusplus
}
#endif
このように C++ から #includeされても自動的にextern “C” が適用されるように書かれています。
42-2. C++ からCへの関数公開
extern "C" {
int my_function(int x) {
return x * 2;
}
}
この関数は name manglingされず、Cから呼び出せる。
42-3. 型の互換性
C++ からCへ渡せるもの:
プリミティブ型
POD struct(Cのレイアウトと同じ)
関数ポインタ
避けるべき:
C++ クラス(vtable、private等で互換性なし)
templates
例外
C++ ランタイム機能
42-4. このセクションのまとめ
- extern "C" でCリンケージ
- Cヘッダは __cplusplus判定で対応
- POD struct + プリミティブ型は安全に渡せる
- C++ クラス・例外はC側で扱えない
43. 性能チューニングの実例
43-1. プロファイル取得
# Linux perf
perf record ./app
perf report
# Tracy(モダンプロファイラ)
# ソースにTracy::ZoneScopedを埋め込む
# Valgrind callgrind
valgrind --tool=callgrind ./app
kcachegrind callgrind.out.*
43-2. ホットループの最適化
// Bad: イテレータ範囲を毎回計算
for (size_t i = 0; i < v.size(); ++i) { ... }
// Good: endをキャッシュ
for (auto it = v.begin(), end = v.end(); it != end; ++it) { ... }
// Best: range-for(コンパイラが最適化)
for (auto& x : v) { ... }
43-3. アロケーション削減
// Bad: 毎回stringオブジェクトを作る
for (auto& item : items) {
std::string formatted = "Item: " + std::to_string(item.id);
log(formatted);
}
// Better: バッファを使い回す
std::string buf;
buf.reserve(64);
for (auto& item : items) {
buf.clear();
buf += "Item: ";
buf += std::to_string(item.id);
log(buf);
}
// Best: std::format_toで直接書き込み
std::string buf;
for (auto& item : items) {
buf.clear();
std::format_to(std::back_inserter(buf), "Item: {}", item.id);
log(buf);
}
43-4. キャッシュフレンドリ
// Bad: ポインタの羅列(毎回キャッシュミス)
std::vector<std::unique_ptr<Particle>> particles;
for (auto& p : particles) p->update();
// Good: 値の羅列(キャッシュ局所性)
std::vector<Particle> particles;
for (auto& p : particles) p.update();
巨大データの場合、100倍以上速くなることもあります。
43-5. SIMDを活用
#include <immintrin.h> // x86 AVX
void add_avx(const float* a, const float* b, float* c, size_t n) {
for (size_t i = 0; i + 8 <= n; i += 8) {
__m256 va = _mm256_loadu_ps(a + i);
__m256 vb = _mm256_loadu_ps(b + i);
__m256 vc = _mm256_add_ps(va, vb);
_mm256_storeu_ps(c + i, vc);
}
// 残り
}
コンパイラの自動ベクトル化(-O3 -march=native)でも効くことが多いので、まず -O3 でビルドして測ってから手書きSIMDを検討。
43-6. このセクションのまとめ
- まず計測(perf / Tracy / Valgrind)
- ホットループはイテレータをキャッシュ、またはrange-for
- アロケーションを再利用
- キャッシュ局所性を高める(SoA、配列)
- SIMD(intrinsics)は最終手段
- -O3 -march=native -fltoを有効に
44. 大規模コードベースでのC++
44-1. ヘッダの規律
// my_class.h
#pragma once // またはinclude guard
// 必要最小限のincludeのみ
#include <string>
#include <vector>
// 前方宣言で済むなら #includeしない
class OtherClass;
namespace myapp {
class MyClass {
// 実装の詳細は隠す(PIMPL)
class Impl;
std::unique_ptr<Impl> pimpl_;
public:
MyClass();
~MyClass();
void do_something();
};
} // namespace myapp
#include ガイドライン
1. 自分のヘッダを最初にinclude(自己完結性確認)
2. Cシステムヘッダ
3. C++ 標準ライブラリ
4. サードパーティライブラリ
5. 自プロジェクトの他のヘッダ
44-2. namespace階層
namespace mycompany::myapp::core {
class Engine {};
} // namespace mycompany::myapp::core
C++17で 入れ子namespaceの短縮構文が使えるように。深い階層も読みやすい。
44-3. ビルド時間管理
- 不要な #includeを減らす(include-what-you-use)
- 前方宣言を活用
- PIMPLでヘッダを小さく
- ccacheで再ビルドを高速化
- ninjaでビルドシステムを高速化(CMakeからninja生成)
- modules(C++20)に移行
- distccd / iceccで分散ビルド
44-4. テスト戦略
// Catch2
#include <catch2/catch_test_macros.hpp>
TEST_CASE("Vector operations", "[vector]") {
std::vector<int> v = {1, 2, 3};
SECTION("size") {
REQUIRE(v.size() == 3);
}
SECTION("push_back") {
v.push_back(4);
REQUIRE(v.size() == 4);
REQUIRE(v.back() == 4);
}
}
// Google Test
#include <gtest/gtest.h>
TEST(VectorTest, Size) {
std::vector<int> v = {1, 2, 3};
EXPECT_EQ(v.size(), 3);
}
TEST(VectorTest, PushBack) {
std::vector<int> v;
v.push_back(1);
EXPECT_EQ(v.size(), 1);
}
モック(Google Mock)
class MockDatabase : public Database {
public:
MOCK_METHOD(int, count, (), (override));
MOCK_METHOD(void, save, (const std::string&), (override));
};
TEST(ServiceTest, SavesData) {
MockDatabase db;
EXPECT_CALL(db, save("hello")).Times(1);
Service svc(&db);
svc.process("hello");
}
44-5. CIと静的解析
# GitHub Actionsの例
- name: Build
run: cmake -B build && cmake --build build
- name: Test
run: ctest --test-dir build
- name: ASan
run: cmake -B build-asan -DCMAKE_CXX_FLAGS=-fsanitize=address &&
cmake --build build-asan && ctest --test-dir build-asan
- name: clang-tidy
run: clang-tidy src/*.cpp
- name: cppcheck
run: cppcheck --enable=all src/
44-6. このセクションのまとめ
- ヘッダは最小化、PIMPLで隠蔽
- 入れ子namespace短縮構文
- ビルド時間: include最小化、ccache、ninja、modules
- テスト: Catch2 / Google Test + Google Mock
- CI: ASan/UBSan/TSanを別ジョブで
- 静的解析: clang-tidy / cppcheck
45. C++23と将来
45-1. C++23の主要追加
// std::expected(RustのResult相当)
std::expected<int, std::string> parse(const std::string& s) {
if (...) return 42;
return std::unexpected("invalid");
}
// std::print
std::print("Hello, {}!\n", name);
// std::generator
std::generator<int> naturals() {
int n = 1;
while (true) co_yield n++;
}
// "deducing this" - explicit object parameter
struct Foo {
template <typename Self>
auto& method(this Self&& self) { return self.x_; }
};
// std::flat_map / std::flat_set(メモリ効率の良いマップ)
// std::mdspan(多次元配列ビュー)
std::mdspan<int, std::extents<size_t, 3, 4>> matrix(data);
matrix[1, 2]; // C++23で多次元 [] が使える
45-2. C++26の方向性
WG21で議論中の主要トピック:
- Reflection(コンパイル時リフレクション)
- Pattern matching(強力なパターンマッチング)
- Contracts(事前/事後条件)
- Networking library
- Linear algebra
C++ は 進化を続けている言語で、毎3年に大型改訂が来ます。
45-3. このセクションのまとめ
- C++23: std::expected / std::print / std::generator
- deducing thisでtemplateの自然な書き方
- std::mdspanで多次元配列
- C++26: Reflection / Pattern matching / Contracts
- WG21が言語進化を主導
応用: 実装モデル
C++ の挙動は「そういうものだ」で済ませず、なぜそうなっているかを理解すると応用が効きます。ここでは内部実装に迫ります。
47. メモリモデル詳解
47-1. オブジェクトのメモリレイアウト
class Empty {};
sizeof(Empty); // 1(0ではない)
C++ では「サイズ0のオブジェクトは作れない」というルールがあるため、空クラスでも1 byteのサイズを持ちます。
Empty Base Optimization (EBO)
class Empty {};
class A : Empty {
int x;
};
sizeof(A); // 4(Emptyが消える)
空の基底クラスは 追加サイズなしで配置される最適化。std::tuple や std::function で活用。
47-2. 多重継承のレイアウト
class A { int a; }; // 4 byte
class B { int b; }; // 4 byte
class C : public A, public B { int c; };
C obj;
&obj // C全体の先頭
static_cast<A*>(&obj) // Aの部分の先頭(同じ)
static_cast<B*>(&obj) // Bの部分の先頭(Aの後ろ)
reinterpret_cast ではなく static_cast を使うことが重要。多重継承では アドレスが調整される。
47-3. 仮想関数テーブル
class Animal {
public:
virtual void speak() {}
virtual ~Animal() = default;
};
sizeof(Animal); // 8(vptrのぶん、64bitシステムで)
virtual 関数を1つでも持つと、vptr(仮想関数テーブルへのポインタ)が追加されます。
Animalインスタンスのレイアウト:
+------+
| vptr | ──→ Animalのvtable
+------+ ┌──────────────┐
| ... | │ &Animal::speak│
+------+ │ &Animal::~Animal│
└──────────────┘
Dog : public Animalの場合:
+------+
| vptr | ──→ Dogのvtable
+------+ ┌──────────────┐
| ... | │ &Dog::speak │
+------+ │ &Dog::~Dog │
└──────────────┘
p->speak() は (*p->vptr->speak)(p) と等価。間接呼び出しのコストがあるが、CPUの分岐予測でかなり最適化される。
47-4. アライメント最適化
struct Bad {
char a; // 1
int b; // 4
char c; // 1
};
sizeof(Bad); // 12(パディング多い)
struct Good {
int b; // 4
char a; // 1
char c; // 1
};
sizeof(Good); // 8
「サイズの大きいフィールドから順に並べる」とパディングが減る。組み込み・ハイパフォーマンス領域で重要。
alignas で明示
struct alignas(64) CacheLine {
int data[16];
};
alignas(16) char buffer[1024]; // 16バイト境界
SIMDで必要なアライメントを満たすために使う。
47-5. このセクションのまとめ
- 空クラスでもsizeofは1
- EBOで空基底クラスは消える
- 多重継承ではcastでアドレス調整
- virtual関数でvptrが追加される(8 byteぶん)
- フィールド順でパディング量が変わる
- alignasで明示的アライメント
48. ストリーム再考
48-1. iostreamの落とし穴
std::cin >> x;
std::cin は std::coutとsync されているので、デフォルトで遅い。競技プログラミングでは:
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
これだけで何倍も速くなることがあります。
48-2. ファイル入出力
std::ifstream f("data.txt");
if (!f) {
// エラー処理
}
std::string line;
while (std::getline(f, line)) {
process(line);
}
バイナリモード
std::ifstream f("data.bin", std::ios::binary);
std::vector<char> buf((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
これでファイル全体をvectorに読み込み。
48-3. formatで安全なprintf
#include <format>
// printf
printf("%s is %d\n", name.c_str(), age); // c_str必要、型違いでUB
// format(C++20)
std::cout << std::format("{} is {}\n", name, age); // 型安全
C++23では std::print(...) がさらに便利。
49. 例外安全性
「例外が起きてもデータが矛盾せず・リソースリークしない」コードを書く規律。
49-1. 4つの保証レベル
no-throw guarantee: 例外を一切投げない
strong guarantee: 例外時に呼び出し前の状態に戻る(コミットorロールバック)
basic guarantee: 例外時にオブジェクトは有効な状態(中身は変わるかも)
no guarantee: 例外時に未定義状態(バグ)
49-2. スワップによる強い保証
class MyVector {
int* data_;
size_t size_;
public:
void push_back(int x) {
// 新しい配列を作って成功してから入れ替え
int* new_data = new int[size_ + 1]; // ここで例外が起きてもoldは無事
std::copy(data_, data_ + size_, new_data);
new_data[size_] = x;
// ここからは例外を投げないで完了させる
delete[] data_;
data_ = new_data;
++size_;
}
};
「新しいリソースをまず確保 → 完成してから古いとswap → 古いを破棄」というパターン。
49-3. RAIIで例外安全に
class Db {
std::unique_ptr<Connection> conn_;
public:
Db() : conn_(std::make_unique<Connection>()) {}
// デストラクタは暗黙的にconn_.reset() を呼ぶ
};
void f() {
Db db; // コンストラクタで例外が起きてもconn_ は構築途中なので大丈夫
risky(); // 例外が起きてもdbのデストラクタが呼ばれてconn_ が解放される
}
RAIIを徹底すれば、ほとんどのケースで自動的に例外安全になる。
49-4. noexceptとmove
class T {
public:
T(T&&) noexcept; // ムーブをnoexceptに
};
std::vector<T> v;
v.push_back(T()); // 拡張時にnoexceptムーブが使われる(速い)
// noexceptでないとコピーが使われる(遅い)
「ムーブコンストラクタはnoexceptにする」のが性能・正しさの両面で重要。
49-5. このセクションのまとめ
- 4つの保証: no-throw / strong / basic / no
- スワップで強い保証
- RAIIで自動的に例外安全
- ムーブをnoexceptに(vectorで重要)
- 例外を使わない選択肢(戻り値・std::expected)
50. 実践プロジェクト構成
50-1. 推奨ディレクトリ構成
myproject/
├── CMakeLists.txt
├── README.md
├── LICENSE
├── .gitignore
├── include/
│ └── myproject/
│ ├── core.h
│ └── util.h
├── src/
│ ├── core.cpp
│ └── util.cpp
├── test/
│ ├── core_test.cpp
│ └── util_test.cpp
├── benchmark/
│ └── core_bench.cpp
├── examples/
│ └── hello.cpp
├── docs/
│ └── doxyfile
├── third_party/
└── cmake/
└── modules/
ヘッダは include/<project>/ 配下、#include "myproject/core.h" で参照する慣習。
50-2. CMakeLists.txtのテンプレート
cmake_minimum_required(VERSION 3.20)
project(myproject VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# ライブラリ
add_library(myproject
src/core.cpp
src/util.cpp
)
target_include_directories(myproject PUBLIC include)
target_compile_options(myproject PRIVATE -Wall -Wextra -Werror)
# 実行ファイル
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE myproject)
# テスト
enable_testing()
add_subdirectory(test)
# パッケージング
include(GNUInstallDirs)
install(TARGETS myproject myapp
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
50-3. Sanitizerをオプション化
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
if(ENABLE_ASAN)
target_compile_options(myproject PUBLIC -fsanitize=address -fno-omit-frame-pointer)
target_link_options(myproject PUBLIC -fsanitize=address)
endif()
cmake -B build -DENABLE_ASAN=ON
50-4. このセクションのまとめ
- include/<project>/ とsrc/ で分離
- CMakeは近代的に(target_xxxでターゲット単位)
- Sanitizerをオプション化
- enable_testing() + add_subdirectory(test)
- インストール対応で配布可能に
上級: テンプレートと並行性
ここからはC++ の 上級者だけが踏み込む領域。テンプレートメタプログラミング、コンセプトの設計、ロックフリー、コンパイラ最適化を理解する世界です。
52. テンプレートメタプログラミング詳解
52-1. type traits
<type_traits> で 「型に関する問い合わせ・操作」ができる。
#include <type_traits>
std::is_integral<int>::value // true
std::is_integral_v<int> // true(C++17 short form)
std::is_pointer_v<int*> // true
std::is_const_v<const int> // true
std::is_same_v<int, int> // true
// 型操作
std::remove_const_t<const int> // int
std::remove_pointer_t<int*> // int
std::remove_reference_t<int&> // int
std::add_pointer_t<int> // int*
std::decay_t<const int&> // int
// 条件分岐
std::conditional_t<true, int, double> // int
// 共通型
std::common_type_t<int, double> // double
C++ のテンプレートメタプログラミングの 基本ツール群。Conceptsと組み合わせて使う。
52-2. constexpr ifとSFINAEの比較
// C++17: if constexpr(推奨)
template <typename T>
auto process(T x) {
if constexpr (std::is_integral_v<T>) {
return x * 2;
} else if constexpr (std::is_floating_point_v<T>) {
return x * 2.5;
} else {
static_assert(sizeof(T) == 0, "unsupported type");
}
}
// C++14以前: SFINAE
template <typename T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
auto process(T x) { return x * 2; }
template <typename T,
std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
auto process(T x) { return x * 2.5; }
if constexpr は 同じ関数内で型ごとの分岐を書ける。SFINAEは オーバーロードで分岐するため可読性が低い。新規コードは if constexpr を使う。
52-3. conceptsによる制約
template <typename T>
concept Hashable = requires(T x) {
{ std::hash<T>{}(x) } -> std::convertible_to<std::size_t>;
};
template <Hashable T>
class Cache {
std::unordered_map<T, int> map_;
};
「std::hash でhash化できる型」という制約を表現。SFINAEと違って 意図が明示的。
52-4. variadic templatesの応用
// 全部をstd::coutに流す
template <typename... Args>
void print(Args&&... args) {
((std::cout << std::forward<Args>(args) << " "), ...);
}
print(1, "hello", 3.14); // "1 hello 3.14 "
// 全部足す
template <typename... Args>
auto sum(Args... args) { return (args + ...); }
sum(1, 2, 3, 4, 5); // 15
// すべての要素が条件を満たすか
template <typename Pred, typename... Args>
bool all(Pred p, Args&&... args) {
return (p(std::forward<Args>(args)) && ...);
}
all([](int x) { return x > 0; }, 1, 2, 3); // true
C++17の fold expressionで書きやすさが激変しました。
52-5. std::tupleの活用
auto t = std::make_tuple(1, "hello", 3.14);
std::get<0>(t); // 1
std::get<1>(t); // "hello"
std::get<int>(t); // 1(C++14、型でアクセス)
// structured binding(C++17)
auto [a, b, c] = t;
// std::applyで関数に展開
auto f = [](int x, const char* s, double d) { ... };
std::apply(f, t); // f(1, "hello", 3.14) と等価
複数値を扱うのにテンプレート + tupleは強力。
52-6. 完全転送(perfect forwarding)
template <typename... Args>
auto make_unique_my(Args&&... args) {
return std::unique_ptr<Foo>(new Foo(std::forward<Args>(args)...));
}
Args&& は 「forwarding reference」(またはuniversal reference)。lvalue / rvalueを そのまま転送する。
std::forward<T>(arg) で 「lvalueはlvalueとして、rvalueはrvalueとして渡す」を実現。
52-7. このセクションのまとめ
- type_traitsで型を問い合わせ・操作
- if constexprがSFINAEの代替(C++17+)
- conceptsで意図的な制約(C++20+)
- variadic templates + fold expression
- std::tuple + structured binding
- 完全転送Args&& + std::forward
53. ロックフリーと並行プリミティブ
「ロックなしで安全に並行アクセス」を実現する技法。難易度は最高クラス。
53-1. CAS(Compare-And-Swap)
std::atomic<int> counter{0};
int expected = counter.load();
while (!counter.compare_exchange_weak(expected, expected + 1)) {
// expectedは最新値で更新される
}
「期待値と一致したら更新、しなかったら取得した最新値で再試行」というアルゴリズムの中核。
53-2. メモリオーダ
// 最も緩い: 順序保証なし、最速
counter.fetch_add(1, std::memory_order_relaxed);
// release: storeの前に他のストアを完了させる
flag.store(true, std::memory_order_release);
// acquire: loadの後に他のロードが見える
if (flag.load(std::memory_order_acquire)) {
// ここで読む値はstore(true) より前のストアを反映している
}
// seq_cst: 全スレッドで順序が一致(デフォルト、最強だが遅い)
counter.fetch_add(1); // デフォルトでseq_cst
「まずseq_cst、ボトルネックなら緩める」が安全な指針。relaxed/acquire/releaseは 誤用するとバグの温床。
53-3. ロックフリーキュー(参考実装の概念)
// 単純化したSPSC(Single-Producer Single-Consumer)キュー
template <typename T, size_t N>
class SpscQueue {
std::array<T, N> buffer_;
std::atomic<size_t> head_{0};
std::atomic<size_t> tail_{0};
public:
bool push(T x) {
auto t = tail_.load(std::memory_order_relaxed);
auto next = (t + 1) % N;
if (next == head_.load(std::memory_order_acquire)) return false;
buffer_[t] = std::move(x);
tail_.store(next, std::memory_order_release);
return true;
}
bool pop(T& out) {
auto h = head_.load(std::memory_order_relaxed);
if (h == tail_.load(std::memory_order_acquire)) return false;
out = std::move(buffer_[h]);
head_.store((h + 1) % N, std::memory_order_release);
return true;
}
};
「プロデューサ1人・コンシューマ1人」という限定で、ロックなしで動くキュー。実用にはLockfree2、Boost.Lockfree、moodycamelなどのライブラリを推奨。
53-4. このセクションのまとめ
- CAS (compare_exchange) がロックフリーの核
- メモリオーダ: seq_cst/acquire/release/relaxed
- まずseq_cst、緩めるのは慎重に
- 自前実装は危険、ライブラリ推奨
- TSanで検証
54. アロケータ
STLコンテナのメモリ確保戦略を カスタマイズできる仕組み。ゲーム・組み込みで重要。
54-1. デフォルトアロケータ
std::vector<int> v; // デフォルトはstd::allocator<int>
std::vector<int, std::allocator<int>> v2; // 明示
std::allocator は内部で ::operator new / ::operator delete を呼びます。
54-2. PMR(Polymorphic Memory Resources、C++17+)
ランタイムでアロケータを切り替えられる。
#include <memory_resource>
std::pmr::monotonic_buffer_resource pool(10000);
std::pmr::vector<int> v{&pool};
for (int i = 0; i < 1000; ++i) v.push_back(i);
// メモリはpoolから確保、個別newなし
monotonic_buffer_resource は 「解放しない」高速アロケータ。短期間の処理に有効。
std::pmr::synchronized_pool_resource pool; // スレッド安全
std::pmr::unsynchronized_pool_resource pool; // 単一スレッド、高速
ゲームループの1フレーム分など、「使い捨てメモリプール」として使う。
54-3. このセクションのまとめ
- std::allocatorがデフォルト
- PMR (C++17+) で動的にアロケータを差し替え
- monotonic_buffer_resourceで高速使い捨て
- ゲーム・組み込み・HPCで活用
55. C++ におけるエラーハンドリング戦略
C++ には複数のエラー伝達手段があり、プロジェクトで統一するのが重要。
55-1. 選択肢
1. 例外(throw / try-catch)
- 標準的、スタックを巻き戻して伝播
- パフォーマンスへの影響は通常小さい
- ゲーム/組み込みで嫌われることがある(停止時間)
2. 戻り値(int / bool)
- C流、シンプル
- 失敗時のメタデータが乏しい
3. errno / GetLastError
- Cスタイル、グローバル状態
- 推奨されない
4. std::optional
- 「失敗 = 値なし」のシンプルな場合
- エラー情報が伝わらない
5. std::expected (C++23)
- RustのResult相当
- 推奨される現代的な選択肢
6. std::error_code
- <system_error> ベース
- エラーカテゴリと番号
55-2. std::expectedの使い方
#include <expected>
std::expected<int, std::string> parse(const std::string& s) {
try {
return std::stoi(s);
} catch (const std::exception& e) {
return std::unexpected(e.what());
}
}
auto result = parse("42");
if (result.has_value()) {
std::cout << *result << "\n";
} else {
std::cerr << "error: " << result.error() << "\n";
}
// チェイン
auto chained = parse("42")
.transform([](int x) { return x * 2; }) // 成功時の変換
.transform_error([](const std::string& e) { return "wrapped: " + e; });
C++23で標準化。Rustの Result<T, E> に相当する近代的なAPI。
55-3. このセクションのまとめ
- 例外 / 戻り値 / optional / expected / error_codeから選択
- C++23 std::expectedが現代的選択
- ライブラリでは例外、パフォーマンス重視ならexpected
- プロジェクト全体で統一
56. C++実務の補足知識
56-1. 命名規則
ISO標準スタイルはありませんが、よく見る規則:
PascalCase: クラス名 (MyClass)
camelCase: 変数・関数 (myFunction)
snake_case: STL標準ライブラリ流 (std::unordered_map)
UPPER_CASE: 定数・マクロ (MAX_SIZE)
Google C++ Style Guide、LLVM、Boost、各社内規約で違うので、プロジェクト内で一貫させるのが第一。
56-2. ヘッダ拡張子
.h Cヘッダ(Cとの互換性意識)
.hpp C++ ヘッダ(最も普及)
.hh一部のスタイル(Google)
.h++ まれ
.ixx / .cppm C++20 modules
.hpp が最も一般的。
56-3. includeの順序
#include "myproject/foo.h" // 1. このソースに対応するヘッダを最初に
// (自己完結性確認)
#include <stdio.h> // 2. Cシステムヘッダ
#include <sys/socket.h>
#include <vector> // 3. C++ 標準ライブラリ
#include <string>
#include "boost/asio.hpp" // 4. サードパーティライブラリ
#include "myproject/bar.h" // 5. 自プロジェクトの他のヘッダ
56-4. クラス定義の中身の順序
class MyClass {
public:
// 型定義(using、enum、struct)
// 静的メンバ
// コンストラクタ・デストラクタ
// 操作(set、modify)
// 取得(get)
// 演算子オーバーロード
private:
// ヘルパー関数
// データメンバ
};
これも好みですが、「publicが上」「コンストラクタ → 操作 → 取得」が通例。
56-5. 文書化(Doxygen)
/**
* @brief円の面積を計算する
*
* @param radius円の半径(負の値を渡すとUB)
* @return double面積(π * radius²)
*
* @throws std::invalid_argument radiusがNaNなら
*
* @code
* auto area = circle_area(5.0); // 78.54
* @endcode
*/
double circle_area(double radius);
Doxygenで HTMLドキュメントを自動生成できる。プロジェクトのAPI文書として広く使われる。
56-6. このセクションのまとめ
- 命名規則はプロジェクト内で一貫
- .hppが一般的
- include順序: 自分のヘッダ → C → C++ → 3rd → 自プロジェクト
- クラス内: public → private、論理順
- DoxygenでAPI文書
58. 補遺:頻出ケーススタディ
58-1. 巨大データ処理
// メモリマップトファイル(POSIX)
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/stat.h>
int fd = open("huge.dat", O_RDONLY);
struct stat st;
fstat(fd, &st);
auto* data = static_cast<const char*>(
mmap(nullptr, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0));
// 巨大ファイルも仮想メモリ越しにアクセスできる
munmap(const_cast<char*>(data), st.st_size);
close(fd);
58-2. シリアライゼーション
// バイナリ書き込み(PODのみ)
struct Pod { int x; double y; };
Pod p{42, 3.14};
std::ofstream f("data.bin", std::ios::binary);
f.write(reinterpret_cast<const char*>(&p), sizeof(p));
// 構造的なシリアライゼーションには:
// - Protocol Buffers
// - flatbuffers
// - msgpack
// - JSON (nlohmann::json)
// - cereal
58-3. 並行データ構造
// std::shared_mutex(C++17+)でリーダーズライターロック
std::shared_mutex rwlock;
void read() {
std::shared_lock lock(rwlock); // 複数のreader並行可
// 読み取り
}
void write() {
std::unique_lock lock(rwlock); // writerは単独
// 書き込み
}
58-4. 高速hash関数
// std::hashの品質はライブラリ依存
// 高品質なhashが必要なら:
// - xxhash
// - cityhash
// - SipHash
// - absl::Hash
58-5. プロセス起動・標準入出力
// C++ 標準にはない、popen / fork / Boost.Process / Reprocを使う
#include <cstdio>
FILE* p = popen("ls -la", "r");
char buf[256];
while (fgets(buf, sizeof(buf), p)) {
std::cout << buf;
}
pclose(p);
C++26で std::process が議論中。
58-6. UIフレームワーク
Qt: 最も成熟、商用・GPL
wxWidgets: ネイティブ風
Dear ImGui: 即時モードGUI、ゲーム・ツール向け
GTKmm: GTKのC++ バインディング
GUI標準ライブラリはC++ 標準にはない。
58-7. ベンチマーク(Google Benchmark)
#include <benchmark/benchmark.h>
static void BM_StringConcat(benchmark::State& state) {
for (auto _ : state) {
std::string s = "a" + std::string("b");
benchmark::DoNotOptimize(s);
}
}
BENCHMARK(BM_StringConcat);
BENCHMARK_MAIN();
ウォームアップ・統計込みで正確な計測。
58-8. プロジェクト依存管理
vcpkg: Microsoft製、CMakeと統合が良い
Conan: Python製、より柔軟
Hunter: CMakeオンリー
manually: third_party/ にコピー
「vcpkgが最初の選択」が現代的。
59. C++ 学習リソース推奨
書籍
入門:
『A Tour of C++』Stroustrup
『C++ Primer』Lippman
『Effective Modern C++』Meyers
中級:
『Effective C++』Meyers
『Effective STL』Meyers
『C++ Concurrency in Action』Williams
上級:
『Modern C++ Design』Alexandrescu
『C++ Templates: The Complete Guide』Vandevoorde
『The C++ Standard Library』Josuttis
Web
- cppreference.com(公式リファレンス、最初に開く)
- isocpp.org(FAQ、ガイドライン、提案動向)
- C++ Core Guidelines (Stroustrup, Sutter)
- CppCon、Meeting C++ 等のカンファレンス動画
- compiler explorer (godbolt.org) でアセンブリを見る
- quick-bench.comで速度比較
コミュニティ
- Stack Overflow [c++] タグ
- /r/cpp(Reddit)
- C++ Slack
- 各種Meetup
60. C++学習の次のステップ
C++ は 学習者を試す言語ですが、その先に 最大の自由と最大の力が待っています。
- Cのような 直接性
- Java/C# のような 抽象化能力
- Pythonのような 生産性(Modern C++ なら)
- Rustに近い 安全性(規律次第)
- アセンブリに近い 性能
これらを一つの言語で持てるのは、世界でC++ だけです。
このガイドを読み終えたあなたは、もう「C++ は古い」というステレオタイプには騙されません。Modern C++ は今もっとも進化が活発なシステム言語の一つで、Rustと健全な競争関係にあります。
C++ と標準化プロセス
C++ の標準化は ISO/IEC 14882 により管理されます。仕様書ドラフトは公開アクセス可能で、最新のC++26の進捗状況をリアルタイムで確認できます。
Bjarne Stroustrup が述べるように、「C++ は委員会でも設計言語でもなく、ユーザーコミュニティの実験的な風土により進化する」ことが特徴です。
C++ 標準ライブラリの進化
C++98/03: vector, string, map, algorithm, iterator
C++11: auto, lambda, nullptr, smart pointers, range-based for, move
C++14: generic lambda, std::make_unique, auto型推論の拡張
C++17: std::optional, std::variant, std::any, std::string_view, filesystem
C++20: concepts, ranges, coroutines, modules, <numbers>, spaceship operator
C++23: std::expected, std::print, deducing this, flat_map/flat_set
Core Guidelinesの役割
C++ Core Guidelines は、Bjarne Stroustrup と Herb Sutter が主導する公式ガイドライン集です。規格ドラフト自体のような詳細さではなく、「何をすべきか」を判断のための原則と実例で示します。
主要な原則:
- 所有権を明示する: raw pointerは非所有、smart pointerで所有を表現
- 例外安全性: basic guarantee, strong guarantee, nothrow guarantee
- RAII: リソース取得は初期化、スコープ終了で自動解放
- 型安全性: narrow cast を避ける、variant/optional で値の有無を明示
- 並行性: data race がない、atomicと mutex の適切な選択
C++ リファレンスの標準的リソース
cppreference.com は、最も信頼できるC++ リファレンスサイトです。標準ライブラリの全クラス・関数・テンプレートがカバーされます。
例えば std::vector の完全なリファレンス:
- 全メンバ関数とその複雑度
- C++ 標準でのサポート履歴(C++98, 11, 17, 20 での追加)
- 実装例とよくある落とし穴
- 異なるコンパイラでの動作差
C++規格ドラフトの読み方
C++ Standard Draft (eel.is) は最新の標準草案です。
セクション構成:
- § 1-5: 導入と定義
- § 6: 基本概念(宣言、オブジェクト、スコープ)
- § 7: 宣言子
- § 8: クラス
- § 14: テンプレート
- § 15-18: 例外、前処理、ライブラリ
各セクションは 引用と 図で相互参照されており、実装者や言語デザイナーに必須の仕様書です。
まとめ
C++は、低レベル制御、ゼロコスト抽象化、テンプレート、RAII、標準ライブラリを組み合わせて、性能と表現力を両立させる言語です。一方で機能が広く、所有権、ライフタイム、例外安全、ビルド、ABIを雑に扱うと複雑さが急に増えます。小さな規約を決め、現代的な書き方へ寄せることが実務では重要です。
参考文献
公式・標準
- C++ Standard Draft - 最新のC++規格ドラフト
- ISO C++ - 標準化委員会の公式サイト
解説・補助
- C++ Core Guidelines - Stroustrup・Sutter主導の公式ガイドライン
- cppreference - 最も信頼できるC++リファレンス