本ツールで使用しているオープンソースライブラリ

本ツールのコードには 1 個のオープンソースライブラリが含まれています。

C++ チートシート — クイックリファレンス

C++17 / C++20 の構文、標準ライブラリ、クラス、テンプレート、そして最もよく使われるイディオムを網羅した簡潔なリファレンス。日常の約 80% のシナリオをカバーします。

C++

C++ C++17 / C++20

ISO C++ (GCC / Clang / MSVC) · マルチパラダイム(OOP・ジェネリック・関数型) · 静的・強い型付け・名目型

おすすめの学習パス

まず「Hello World とビルド環境」でコンパイル/実行の流れを掴み、次に変数・型・制御フロー・関数に慣れ、標準ライブラリコンテナ(vector / map)と文字列に進み、RAII とスマートポインタ(メモリセクション)を理解し、その後に OOP とテンプレートを学習します。スレッド、ネットワーク、正規表現、ビルドツールは必要に応じて参照してください。FAQ セクションは落とし穴を避けるための振り返りに向いています。

1.Hello World とビルド環境

C++ プログラムをゼロからコンパイル・実行・構成する方法を学びます。ツールチェーン、コマンドライン引数、複数ファイル、C++20 モジュールを含みます。

最小プログラム

すべての C++ プログラムは main() から実行を開始し、戻り値 0 が成功を表します。std::cout は標準出力ストリームで、<< 演算子で値を送ります。

1
2
3
4
5
6
#include <iostream>
int main() {
std::cout << "Hello, world!\n";
return 0;
}

コンパイルと実行

g++ または clang++ でコンパイルします。-std で C++ 標準バージョンを指定し、-Wall -Wextra で警告を有効化し、-o で出力ファイル名を指定します。実行は ./app で行います。

1
2
3
4
5
6
7
8
// $ g++ -std=c++17 -Wall -Wextra main.cpp -o app
// $ ./app
// clang++ も同じフラグ;MSVC は cl /EHsc main.cpp を使用
#include <iostream>
int main() {
std::cout << "Built with C++17\n";
return 0;
}

CMake ビルド

CMake はクロスプラットフォーム対応の主流ビルドシステムです。CMakeLists.txt でプロジェクトとターゲットを宣言し、-B でビルドディレクトリを指定、--build でコンパイルします。生成物は build/ 配下に出力されます。

1
2
3
4
5
6
7
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(app LANGUAGES CXX)
add_executable(app main.cpp)
target_compile_features(app PRIVATE cxx_std_17)
// $ cmake -B build && cmake --build build

コマンドライン引数

main の argc は引数の個数、argv は引数文字列の配列です。argv[0] は常にプログラム名となり、ユーザー引数は argv[1] から読み取ります。

1
2
3
4
5
6
7
#include <iostream>
int main(int argc, char* argv[]) {
std::cout << "argc=" << argc << '\n';
for (int i = 0; i < argc; ++i)
std::cout << "argv[" << i << "]=" << argv[i] << '\n';
return 0;
}

終了コード

main の戻り値はプロセスの終了コードとして OS に渡されます。0 が成功、非 0 がエラーの種別を表します。EXIT_SUCCESS / EXIT_FAILURE を使うとより読みやすくなります。

1
2
3
4
5
#include <cstdlib>
int main() {
if (!work()) return 1; // 非 0 = 失敗
return EXIT_SUCCESS; // return 0 と同等
}

複数ファイルのビルド

宣言はヘッダファイルに置き(#pragma once で多重インクルードを防ぎ)、定義は .cpp に置いて、使用側で #include します。コンパイル時は各 .cpp をまとめてコンパイラに渡しリンクします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// math.h —— 宣言
#pragma once
int add(int a, int b);
// math.cpp —— 定義
#include "math.h"
int add(int a, int b) { return a + b; }
// main.cpp —— 使用
#include <iostream>
#include "math.h"
int main() {
std::cout << add(2, 3) << '\n';
}
// $ g++ -std=c++17 math.cpp main.cpp -o app

C++20 モジュール

モジュールはヘッダファイルの代替です。export module でモジュールを宣言し、export でシンボルを公開し、import で取り込みます。コンパイルが速くマクロの汚染がない反面、まだ普及段階です。

1
2
3
4
5
6
7
8
9
10
11
// math.cppm —— モジュール
module;
#include <cmath>
export module math;
export int sq(int x) { return x * x; }
// main.cpp —— インポート
import math;
#include <iostream>
int main() { std::cout << sq(5) << '\n'; }
// $ g++ -std=c++20 -fmodules-ts math.cppm main.cpp -o app

名前空間

名前空間はシンボルを整理して衝突を防ぎます。geo::area のように修飾してアクセスします。using namespace で全シンボルを一度に取り込むと曖昧になりやすいので、using std::cout のように個別に導入する方が望ましいです。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
namespace geo {
constexpr double pi = 3.14159;
double area(double r) { return pi * r * r; }
}
int main() {
std::cout << geo::area(1.0) << '\n';
using std::cout; // 明示的な using 宣言
cout << geo::pi << '\n';
}

2.変数と定数

宣言と型推論、const/constexpr、参照、構造化束縛、スコープ、C++ スタイルのキャストについて。

auto 型推論

auto は初期化子から型を推論するため、必ず同時に初期化する必要があります。推論では参照とトップレベルの const は外れるため、必要なら明示的に const auto& と書きます。

1
2
3
4
auto x = 42; // int
auto d = 3.14; // double
auto s = std::string("hi");
const auto& r = x; // const と参照を保持

const と constexpr

const は値が不変であることを示し、constexpr はコンパイル時に評価されることを保証します。配列サイズやテンプレート引数にも使えます。C++20 以降は constexpr 関数により多くのロジックを含められます。

1
2
3
4
const int n = 42; // 実行時またはコンパイル時定数
constexpr int k = 42; // コンパイル時定数でなければならない
constexpr int sq(int x) { return x * x; }
static_assert(sq(6) == 36); // コンパイル時チェック

参照

参照は既存オブジェクトの別名です。宣言時に必ず初期化が必要で、常に同じオブジェクトを指します。引数として参照を渡せばコピーを避けつつ元の値を変更できます。

1
2
3
4
5
int x = 1;
int& r = x; // 参照は別名。null 不可・再束縛不可
r = 2; // x を書き換える
const int& c = x; // 読み取り専用ビュー
void bump(int& v) { ++v; } // 参照渡しで元の値を変更

構造化束縛

C++17 では pair / tuple / 構造体 / 配列を分解して個別の変数に束縛できます。map の反復で key と value を分解するのが最も一般的な使い方です。

1
2
3
4
5
6
std::pair<int, double> p{1, 2.5};
auto [a, b] = p; // 名前付き変数に展開
std::map<std::string, int> m;
for (const auto& [key, val] : m) {
// key / val を直接使用
}

インライン変数

C++17 以前、ヘッダ内の非 constexpr グローバル変数は多重定義エラーを引き起こしました。inline 変数を指定すれば、複数の翻訳単位で同じオブジェクトを共有できます。

1
2
3
4
5
// header.h
struct Config {
static inline int threads = 4; // ヘッダー内で定義
};
inline constexpr int kMax = 100; // ヘッダー定数としてよく使う

mutable メンバ

const メンバ関数内でも mutable メンバは変更できます。キャッシュ、呼び出し回数、ミューテックスなど、論理的にオブジェクトの状態を変えない用途で使われます。

1
2
3
4
struct Counter {
int get() const { ++n_; return n_; }
mutable int n_ = 0; // const 関数からでも変更可
};

decltype

decltype は式の宣言型を取得します。decltype((x)) は括弧が余計につくことで参照として推論される典型的な落とし穴です。後置戻り値型はテンプレート関数でよく使われます。

1
2
3
4
5
int x = 1;
decltype(x) y = 2; // int
decltype((x)) r = x; // int&、括弧が 1 つ多い = 参照
// 後置戻り値型:戻り値型が引数に依存
template <typename T> auto twice(T v) -> decltype(v + v) { return v + v; }

スコープとシャドウィング

C++ では内側スコープが外側と同じ名前を覆い隠す(シャドウィング)ことができます。グローバル名のシャドウィングは避けましょう。::n と書けば明示的にグローバルにアクセスできますが、可読性は下がります。

1
2
3
4
5
6
7
8
#include <iostream>
int n = 1; // グローバル
int main() {
int n = 2; // グローバルを隠す
{ int n = 3; } // ブロックスコープ、抜けたら破棄
std::cout << n << '\n'; // 2
std::cout << ::n << '\n'; // 1、明示的にグローバルへ
}

リテラルサフィックス

サフィックスでリテラルの型を指定します。LL は long long、f は浮動小数点数、s は std::string です。0b の二進数プレフィックスと ' の桁区切り記号も可読性を高めます。

1
2
3
4
5
int a = 0b1010; // 2 進数 (C++14)
int b = 1'000'000; // 数字区切り
long long c = 5LL;
double d = 3.14f; // float
auto s = "hi"s; // std::string(using std::string_literals)

C++ スタイルキャスト

static_cast はコンパイル時の意味的変換、dynamic_cast は実行時の多態的変換(ポインタ失敗時は nullptr、参照は bad_cast を送出)、const_cast は const を外しますが、元のオブジェクトが非 const である場合に限ります。

1
2
3
4
5
6
double d = 3.7;
int a = static_cast<int>(d); // 意味論的な変換
const double cd = 3.7;
double& ref = const_cast<double&>(cd); // const を外す(注意して使う)
// dynamic_cast:ダウンキャスト(多態型用)
Derived* dp = dynamic_cast<Derived*>(base_ptr);

3.データ型

組み込み型、列挙型、構造体、テンプレート型、そして std::optional / std::variant / std::any などのモダンなユーティリティ型について。

組み込み型

組み込み型はサイズと符号の有無で区別されます。<cstdint> は int32_t / uint64_t などの固定幅型を提供し、プラットフォーム間で動作が一貫します。

1
2
3
4
5
6
7
#include <cstdint>
int32_t i = -1; // 固定幅の符号付き整数
uint64_t u = 0; // 固定幅の符号なし整数
float f = 1.5f; // 32 ビット浮動小数
double d = 2.5; // 64 ビット浮動小数
bool b = true;
char c = 'A';

enum class

enum class は素の enum より安全です。値はスコープ修飾してアクセスする必要があり、暗黙的に int に変換されないため、名前漏れや整数の誤用を防げます。

1
2
3
4
enum class Color { Red, Green, Blue };
Color c = Color::Red; // スコープ指定が必要
int n = static_cast<int>(c); // 0、明示的変換
enum class Flag : uint8_t { A = 1, B = 2 }; // 基底型を指定

構造体

struct はデフォルトでメンバが公開され、振る舞いのないデータをまとめるのに適しています。デフォルト値を持つメンバがあれば、波括弧による集約初期化で一行でオブジェクトを生成できます。

1
2
3
4
5
struct Point {
double x = 0.0; // 既定メンバー初期化
double y = 0.0;
};
Point p{1.0, 2.0}; // 集成初期化

共用体

union は複数のメンバで同じメモリを共有し、同時に 1 つだけを保持します。標準 C++ では std::variant の方が安全なため、union は基本的に低レベルのメモリ再利用のために使います。

1
2
3
4
5
6
union Value {
int i;
double d; // 同じメモリ、同時に 1 つのメンバー
};
Value v;
v.d = 3.14; // d に書くと i の古い値は無効

テンプレート型

テンプレートは型をパラメータ化し、使用された型ごとにコンパイラがインスタンスを生成します。ジェネリックプログラミングと標準ライブラリコンテナの基礎です。

1
2
3
4
5
template <typename T> struct Box {
T value;
};
Box<int> b1{42};
Box<double> b2{3.14}; // 同じテンプレートの異なる実体化

型エイリアス

using による型エイリアスは typedef より読みやすく、テンプレートエイリアスも定義できます。エイリアスは新しい型を作るのではなく、同じ型の別名に過ぎません。

1
2
3
4
5
using Id = unsigned long; // 型エイリアス
using PairInt = std::pair<int, int>;
template <typename T>
using Ptr = std::shared_ptr<T>; // テンプレートエイリアス
Ptr<int> p = std::make_shared<int>(1);

optional

std::optional は「値があるかもしれないし、ないかもしれない」戻り値を表現し、番兵値や out 引数の代替となります。bool 文脈で存在を確認し、* で値を取り出します。

1
2
3
4
5
6
7
8
#include <optional>
std::optional<int> div(int a, int b) {
if (b == 0) return std::nullopt; // 値なし
return a / b;
}
if (auto v = div(6, 2)) {
int n = *v; // アンラップ
}

variant

std::variant は型安全な共用体で、常に指定された型リストのいずれかを保持します。std::get は指定型を取得し(型不一致なら bad_variant_access)、std::get_if は例外を投げずポインタを返します。

1
2
3
4
5
#include <variant>
std::variant<int, double, std::string> v;
v = 3.14; // 現在は double を保持
double d = std::get<double>(v);
if (auto* s = std::get_if<std::string>(&v)) { } // 安全なアクセス

std::any

std::any は任意の型を格納して実行時に取り出せます。異種コンテナやプラグイン引数に適していますが、型消去のコストがあるため、可能なら variant を使う方が望ましいです。

1
2
3
#include <any>
std::any a = 42;
int n = std::any_cast<int>(a); // 型が違うと bad_any_cast を投げる

pair と tuple

pair は 2 値、tuple は任意の個数の値を保持します。構造化束縛で要素を名前付き変数に展開すると、std::get のインデックス指定より読みやすくなります。

1
2
3
4
5
std::pair<int, int> p{1, 2};
auto [a, b] = p; // アンラップ
std::tuple<int, double, char> t{1, 2.5, 'x'};
auto [i, d, c] = t;
std::get<1>(t); // インデックスで 2.5 を取得

4.ポインタと配列

生ポインタ、スマートポインタ、配列、参照 —— メモリの所有者とその使い分けについて。

生ポインタ

生ポインタはオブジェクトのアドレスを保持します。* でデリファレンスし、& でアドレスを取得します。生ポインタはメモリを所有しないため、new / delete と組み合わせるかスマートポインタに任せます。

1
2
3
4
5
int x = 10;
int* p = &x; // x のアドレスを指す
*p = 20; // ポインタ経由で x を変更
if (p == nullptr) { } // null チェック
int* q = nullptr; // ヌルポインタ

ポインタと配列

式の中では配列名は先頭要素へのポインタに減衰します。p+i はアドレスのオフセット、*(p+i) で値を取得します。これは C から受け継いだ低レベルの挙動です。

1
2
3
4
int arr[4] = {1, 2, 3, 4};
int* p = arr; // 配列名は先頭要素へのポインタに減衰
std::cout << *(p + 2); // 3、ポインタ演算
// arr[i] は *(arr + i) と同等

nullptr

nullptr は型安全な空ポインタ定数で、NULL や 0 の代替です。null ポインタのデリファレンスは未定義動作なので、必ず使用前に null チェックをしましょう。

1
2
3
4
5
int* p = nullptr;
if (p == nullptr) { /* 安全な分岐 */ }
void f(int* p) {
if (!p) return; // null チェック後に使用
}

new と delete

new はヒープメモリを確保してポインタを返し、delete で解放します。new[] は必ず delete[] と対にします。手動管理はリークしやすいので、RAII コンテナやスマートポインタを優先しましょう。

1
2
3
4
int* p = new int(5); // 単体を動的確保
delete p; // 解放
int* arr = new int[10]; // 動的配列
delete[] arr; // [] と対で使うこと

unique_ptr

unique_ptr は所有権を独占し、スコープを抜ける際に自動で delete します。make_unique は例外安全。所有権の移譲は std::move で行い、コピーは禁止されています。

1
2
3
4
5
#include <memory>
std::unique_ptr<int> p = std::make_unique<int>(42);
*p = 43; // 生ポインタと同じように使う
// p はスコープを抜けると自動解放、delete 不要
std::unique_ptr<int> q = std::move(p); // 所有権を移動

shared_ptr

shared_ptr は参照カウントで所有権を共有し、カウントが 0 になった時点で解放されます。循環参照によるリークを避けるため、weak_ptr で輪を断ち切りましょう。

1
2
3
4
std::shared_ptr<int> a = std::make_shared<int>(10);
std::shared_ptr<int> b = a; // 参照カウント +1
// 最後の参照がスコープを抜けると自動解放
long use = a.use_count(); // 2

weak_ptr

weak_ptr はオブジェクトを所有せずカウントにも影響しません。lock() で一時的に shared_ptr に昇格し(オブジェクトが既に解放されていれば空を返す)、循環参照を断ち切るために使います。

1
2
3
4
5
std::shared_ptr<int> sp = std::make_shared<int>(1);
std::weak_ptr<int> wp = sp; // カウントを増やさない
if (auto lk = wp.lock()) { // shared_ptr に昇格
// lk を使用;sp 解放後は lock() が null を返す
}

参照 vs ポインタ

参照は構文がシンプルで必ず非 null なので、引数では参照を優先します。「オブジェクトが存在しないかもしれない」「別のオブジェクトを指し直したい」場合にはポインタを使います。

1
2
3
4
5
void inc(int& v) { ++v; } // 参照:非 null・直接
void inc2(int* v) { if (v) ++*v; } // ポインタ:null 可
int x = 1;
inc(x); // x が 2 になる
inc2(&x); // x が 3 になる

const ポインタ

読み方は右から左へ。int* const はポインタ自体が const、const int* は指し先が const です。混同はよくある混乱ポイントです。

1
2
3
4
int x = 1, y = 2;
int* const p = &x; // ポインタが const、指し先を変えられない
const int* q = &x; // 指す先が const、値を変えられない
const int* const r = &x; // 両方 const

void* ポインタ

void* は「未知の型のメモリ」へのポインタで、間接参照するには明示的に具体的な型へキャストする必要があります。C スタイルの API が任意データを受け渡すために使いますが、C++ ではなるべく避けましょう。

1
2
3
void* raw = malloc(64); // 不透明なメモリ
int* p = static_cast<int*>(raw); // 明示的変換が必要
free(raw);

5.制御フロー

if / for / while / switch、そして C++17 で導入された if / switch の初期化ステートメントについて。

if / else

if / else は条件に応じて実行パスを選択します。C++17 以降は if に初期化ステートメントを付与できます(初期化付き if を参照)。

1
2
3
4
5
6
7
if (n > 0) {
// 正の数の分岐
} else if (n == 0) {
// ゼロの分岐
} else {
// 負の数の分岐
}

for ループ

古典的な for は初期化・条件・ステップの 3 段構成です。カウントループでは ++i を優先すると、意味的にも一時オブジェクトの生成も減らせます。

1
2
3
4
for (int i = 0; i < 5; ++i) {
// 0..4
}
for (int i = 4; i >= 0; --i) { } // 降順

範囲 for

範囲 for(C++11)はコンテナの要素を走査し、手作業のインデックスや境界ミスを防ぎます。要素を変更するなら auto&、読み取り専用なら const auto& を使います。

1
2
3
4
std::vector<int> v{1, 2, 3};
for (const auto& x : v) { } // 読み取り専用
for (auto& x : v) { ++x; } // 要素を変更
for (const auto& [k, val] : m) { } // map を展開

while / do-while

while は条件を先に判定してからループ本体に入り、do-while は最低 1 回は実行されます。回数が不明で実行時の条件に依存するループに適しています。

1
2
3
int i = 0;
while (i < 3) { ++i; } // 判定してから実行
do { ++i; } while (i < 3); // 実行してから判定、最低 1 回

switch / case

switch は整数 / 列挙値によって分岐し、if の連なりより読みやすくなります。各 case には必ず break(または return)を付けないと、後続の case へフォールスルーします。

1
2
3
4
5
switch (cmd) {
case 1: run(); break;
case 2: run2(); break;
default: help(); break; // フォールバック
}

break と continue

continue は現在の反復の残りをスキップして次へ進み、break はループ全体を即座に抜けます。多重ループではラベル付き goto で外側まで脱出できます。

1
2
3
4
5
for (int i = 0; i < 10; ++i) {
if (i % 2) continue; // 奇数をスキップ
if (i > 6) break; // ループを終了
// 0,2,4,6 のみ処理
}

初期化付き if

C++17 以降は if / switch で初期化変数を宣言でき、その変数のスコープは分岐内に限定されます。外側へ漏れないため、find-then-check の慣用表現として使えます。

1
2
3
4
5
6
if (auto it = m.find("key"); it != m.end()) {
// it のスコープは if 内に限定
}
switch (int v = parse(); v) {
case 0: break;
}

三項演算子

三項演算子は if / else の代入を 1 行で書けます。両分岐の型は互換性が必要で、複雑なロジックでは素直に if の方が読みやすくなります。

1
2
int max = (a > b) ? a : b; // 条件 ? 真の値 : 偽の値
std::string s = ok ? "yes" : "no";

goto ラベル

goto はラベル行に直接ジャンプし、構造化された制御フローを壊します。ほぼ常にフラグや return で置き換え可能で、合理的な使い道は多重ループからの脱出だけです。

1
2
3
4
5
6
7
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (bad(i, j)) goto done; // 二重ループから脱出
}
}
done:
// 多重ネストを抜けるためだけに使う。濫用しないこと

6.関数とラムダ

宣言と定義、オーバーロード、引数の渡し方、ラムダと関数テンプレート、可変長テンプレート、再帰について。

宣言と定義

宣言はコンパイラにシグネチャを伝え、定義は実装を提供します。宣言はヘッダに何度含まれても問題ありませんが、定義はプログラムにつき 1 つしか許されません。

1
2
3
4
5
6
// 宣言(プロトタイプ):引数型がインターフェース
int add(int a, int b);
// 定義:実装
int add(int a, int b) {
return a + b;
}

関数オーバーロード

関数オーバーロードにより、同じ名前の関数を実引数の型でディスパッチできます。コンパイラが最も一致する版を選びます。戻り型だけではオーバーロードを区別できません。

1
2
3
4
void print(int v) { }
void print(const std::string& s) { }
print(1); // int 版が選ばれる
print("hi"); // string 版が選ばれる

デフォルト引数

デフォルト引数により、呼び出し側で末尾の実引数を省略できます。デフォルト値は右端から連続して指定する必要があり、宣言と定義のどちらか一方でしか指定できません。

1
2
3
void greet(const std::string& name, int times = 1) { }
greet("hi"); // times=1
greet("hi", 3); // times=3

引数の渡し方

値渡しはオブジェクト全体をコピーします。const& はコピーを避けて変更も禁止、& は元の値を変更可能、&&(右辺値参照)は一時オブジェクトを受け取りリソースを移譲できます。

1
2
3
4
void read(const std::string& s); // 読み取り専用:const& でコピー回避
void write(std::string& s); // 元の値を変更
void copy(std::string s); // コピーが必要
void own(std::string&& s); // 所有権を移動

戻り値

現代の C++ では RVO / NRVO により一時オブジェクトの戻りがゼロコピーになります。ローカルオブジェクトのアドレスや参照を返してはいけません(ダングリング参照になります)。

1
2
3
4
5
std::string make() { return "abc"; } // NRVO がコピーを省略
auto [a, b] = std::pair<int, int>{1, 2}; // 返却された pair を展開
int& at(std::vector<int>& v, size_t i) {
return v[i]; // 参照を返す
}

ラムダ式

ラムダは匿名の関数オブジェクトです。[] のキャプチャリスト + 引数 + 本体で構成され、<algorithm> と組み合わせてコールバックや比較関数を書く主流の方法です。

1
2
3
4
auto add = [](int a, int b) { return a + b; };
int r = add(1, 2);
std::sort(v.begin(), v.end(),
[](int a, int b) { return a > b; }); // 降順比較子

ラムダキャプチャ

[=] は全てを値でキャプチャ、[&] は全てを参照でキャプチャ、[x] は指定した変数のみをキャプチャします。参照キャプチャでは元の変数がラムダの生存期間より長く生きる必要があります。

1
2
3
4
5
int base = 10;
auto f = [base](int x) { return base + x; }; // 値でキャプチャ
auto g = [&base](int x) { return base + x; }; // 参照でキャプチャ
auto h = [=]() {}; // すべて値で
auto k = [&]() {}; // すべて参照で

関数テンプレート

関数テンプレートは呼び出し時の実引数から型を推論してインスタンス化されます。1 つの実装で全型に対応できますが、使われる演算子はその型でサポートされている必要があります。

1
2
3
4
template <typename T>
T max_of(T a, T b) { return (a > b) ? a : b; }
int m = max_of(3, 5); // int に推論される
double d = max_of(2.5, 1.5); // double に推論される

可変長テンプレート

パラメータパック ...Args は任意の個数の実引数を受け取り、畳み込み式 (args + ...) で二項演算子をパック全体に展開できます。printf 形式の自作関数に代わるモダンな手法です。

1
2
3
4
5
template <typename... Args>
int sum(Args... args) {
return (args + ...); // C++17 フォールド式
}
int total = sum(1, 2, 3, 4); // 10

再帰

再帰は関数が自分自身を呼び出すことで、基底条件で終了します。深い再帰はコールスタックを消費するため、スタックオーバーフローや部分問題の重複計算に注意しましょう。

1
2
3
4
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}

7.文字列

std::string の基本、連結、検索・置換、フォーマット、文字列ストリーム、数値との相互変換について。

std::string の基本

std::string は可変で伸長可能な文字コンテナで、メモリは自動管理されます。[] の範囲外アクセスは未定義動作なので、例外を投げる at() を使いましょう。

1
2
3
4
5
6
#include <string>
std::string s = "hello";
s += " world"; // 連結
size_t len = s.size(); // 長さ
char c = s[0]; // 添字アクセス
bool empty = s.empty();

文字列リテラル

"..." リテラルはデフォルトで const char* です。s サフィックスで std::string に、string_view は非所有の読み取り専用ビューで、引数に渡すとコピーを避けられます。

1
2
3
4
auto a = "plain"; // const char*
auto b = "u8"s; // std::string(using std::string_literals)
std::string_view v = "view"; // 読み取り専用ビュー
std::string_view::size_type n = v.size();

連結と reserve

大量の連結では reserve で事前に容量を確保し、再アロケーションを減らしましょう。+ を繰り返すと一時オブジェクトが生成されるため、ホットパスではバッファの再利用を検討してください。

1
2
3
4
5
6
std::string s = "a";
s.append("b").append("c"); // チェーンで追加
s.push_back('d');
s += 'e';
std::string out;
out.reserve(a.size() + b.size()); // 事前確保

部分文字列と検索

substr(pos, len) は部分文字列を取り出し、find / rfind は最初にマッチした位置を探します。見つからない場合は std::string::npos を返します。

1
2
3
4
5
6
std::string s = "hello world";
s.substr(6, 5); // "world"
s.find("world"); // 6
s.find('o'); // 最初の o
s.rfind('o'); // 右から探す
if (s.find("x") == std::string::npos) { }

置換

メンバ関数 replace(pos, len, str) は位置指定で置換し、<algorithm> の std::replace は値指定で全マッチ文字を一括置換します。

1
2
3
std::string s = "a-b-c";
s.replace(1, 1, "+"); // 位置 1 から 1 文字置換
std::replace(s.begin(), s.end(), '-', '_'); // 一括置換

フォーマット出力

std::format(C++20)はプレースホルダで整形し、引数の個数をコンパイル時にチェックします。sprintf や cout の連結に代わる手段です。:04d はゼロ詰め、:.2f は小数 2 桁固定です。

1
2
3
4
#include <format> // C++20
std::string s = std::format("{}-{:04d}", 42, 7);
double d = std::format("{:.2f}", 3.14159);
// s = "42-0007",d = "3.14"

stringstream

stringstream はメモリ上の文字列をストリームとして扱います。ostringstream で組み立て、istringstream で >> により解析でき、整形と逆パースに便利なツールです。

1
2
3
4
5
6
#include <sstream>
std::ostringstream oss;
oss << "x=" << 42 << " y=" << 3.5;
std::string s = oss.str();
std::istringstream iss("10 20");
int a, b; iss >> a >> b;

文字列と数値の変換

stoi / stod / stoll は文字列を数値にパースし、std::to_string は逆方向に変換します。パース失敗やオーバーフローでは std::invalid_argument / out_of_range が送出されます。

1
2
3
4
int i = std::stoi("42"); // int に変換
int j = std::stoi("ff", nullptr, 16); // 16 進数
double d = std::stod("3.14");
std::string s = std::to_string(42); // "42"

生文字列リテラル

R"(...)" 生文字列リテラル内ではバックスラッシュがそのまま扱われるため、正規表現やパス、複数行テキストでエスケープに悩まず済みます。内容に )" を含む場合は R"tag(...)tag" のようにカスタムデリミタを使います。

1
2
3
std::string re = R"(\w+@\w+\.\w+)"; // エスケープ不要な正規表現
std::string path = R"(C:\tmp\file)"; // バックスラッシュをそのまま保持
std::string tag = R"tag(<b>bold</b>)tag"; // 独自デリミタ

文字の走査

範囲 for による文字単位の走査が最も簡潔です。変更したいときは char& を使います。begin / end のイテレータ形式はレガシーコードやアルゴリズムライブラリとの連携に向きます。

1
2
3
for (char c : s) { } // 文字ごと(値)
for (char& c : s) { c = 'x'; } // 文字ごと(変更可)
for (auto it = s.begin(); it != s.end(); ++it) { }

8.コンテナとアルゴリズム

vector / array / map / set / 優先度付きキューと、ソート・検索・ranges アルゴリズムについて。

vector

vector は動的配列で、末尾の追加・削除が O(1)、ランダムアクセスも O(1) です。push_back を多用するなら reserve で容量を確保し、先頭挿入は避けましょう(O(n))。

1
2
3
4
5
6
std::vector<int> v;
v.push_back(1); // 末尾に追加
v.emplace_back(2); // その場で構築
v.pop_back(); // 末尾を削除
int x = v[0]; // 範囲外は未定義;at() は例外
v.reserve(100); // 事前確保

std::array

std::array はスタック上の固定長配列をモダンにラップしたもので、size() / begin() / end() を持ち、アルゴリズムライブラリに渡せます。固定長は array、可変長は vector を使います。

1
2
3
std::array<int, 4> a{1, 2, 3, 4}; // スタック上の固定長
int n = a.size(); // コンパイル時に既知
std::sort(a.begin(), a.end());

map

std::map は順序付きキー/値ストア(赤黒木)で、キーでソートされ検索は O(log n) です。順序が不要なら unordered_map(平均 O(1))を使います。

1
2
3
4
5
std::map<std::string, int> m;
m["a"] = 1; // キーがあれば上書き
m.insert_or_assign("b", 2); // C++17
auto it = m.find("a");
if (it != m.end()) { } // 存在チェック

unordered_map

unordered_map はハッシュテーブルで、平均 O(1) で検索でき、順序はありません。operator[] は存在しないキーに対してデフォルト値を挿入してしまうため、検索のみなら find または at() を使います。

1
2
3
4
5
#include <unordered_map>
std::unordered_map<int, int> m;
m[1] = 10; // ハッシュバケット探索
int v = m[1]; // キーが無いとデフォルト値を挿入
if (m.find(1) != m.end()) { }

set

set は順序付きで重複のない集合を保持し、insert / erase / find はいずれも O(log n) です。unordered_set はハッシュ版(順序なし、O(1))。C++20 の contains() で存在判定ができます。

1
2
3
4
5
std::set<int> s{3, 1, 2};
s.insert(4); // 自動で重複除去 + ソート
s.erase(1);
if (s.contains(2)) { } // C++20 存在確認
std::unordered_set<int> us; // ハッシュ版

deque と list

deque は両端の追加・削除が O(1)、list は中間挿入が O(1) ですがランダムアクセスは O(n) です。アクセスパターンに合わせてコンテナを選びましょう。

1
2
3
4
5
6
std::deque<int> dq; // 両端の挿入削除 O(1)
dq.push_front(1);
dq.push_back(2);
std::list<int> lst; // 双方向リンクリスト
lst.push_back(3);
lst.insert(lst.begin(), 0); // 途中挿入 O(1)

priority_queue(ヒープ)

priority_queue はヒープで、push が O(log n)、top で最大値/最小値が O(1) で取得できます。デフォルトは最大ヒープで、最小ヒープにするには greater を指定します。

1
2
3
4
5
6
#include <queue>
std::priority_queue<int> pq; // 既定は最大ヒープ
pq.push(3); pq.push(1); pq.push(2);
int top = pq.top(); // 3
pq.pop();
// 最小ヒープ:priority_queue<int, vector<int>, greater<int>>

ソート

std::sort はランダムアクセスコンテナを O(n log n) でインプレースにソートします。カスタム比較器は「a が b の前に来るか」を返します。list ではメンバ関数 sort() を使います。

1
2
3
4
std::sort(v.begin(), v.end()); // 昇順
std::sort(v.begin(), v.end(), std::greater<int>()); // 降順
std::sort(v.begin(), v.end(),
[](const Item& a, const Item& b) { return a.price < b.price; });

検索

順序なしの集合では std::find で線形検索、ソート済みコンテナでは binary_search / lower_bound で二分探索(O(log n))を行います。find はイテレータを返し、end() と比較して命中を判定します。

1
2
3
4
5
auto it = std::find(v.begin(), v.end(), 42);
if (it != v.end()) { }
std::sort(v.begin(), v.end()); // 二分探索前にソート
auto lo = std::lower_bound(v.begin(), v.end(), 50);
bool ok = std::binary_search(v.begin(), v.end(), 42);

ranges パイプライン

C++20 の ranges は | で遅延評価されるビューを連結します。filter で抽出、transform で変換でき、コピーもアロケーションも発生せず、シーケンス処理を式として書けます。

1
2
3
4
5
#include <ranges>
std::vector<int> v{1, 2, 3, 4, 5};
auto even = v | std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : even) { } // 4, 16

9.動的メモリと所有権

RAII、ムーブセマンティクス、スマートポインタの使い分け、例外安全性 —— C++ のリソース管理の中核です。

RAII

RAII はリソースの取得をコンストラクタ、解放をデストラクタに結びつけ、オブジェクト破棄時に自動で解放します。例外安全で明示的なクリーンアップは不要です。

1
2
3
4
5
6
7
8
// RAII:リソースの寿命をオブジェクトの寿命に紐付ける
class File {
FILE* f_;
public:
File(const char* path) { f_ = fopen(path, "r"); }
~File() { if (f_) fclose(f_); } // デストラクタで自動解放
};
{ File f("a.txt"); } // スコープを抜けると自動クローズ

make_unique

make_unique は unique_ptr を生成します。new と構築を 1 ステップで行えるため、構築中に例外が出ても生ポインタはリークしません。配列版は make_unique<T[]>(n) です。

1
2
3
auto p = std::make_unique<Widget>(args...); // 例外安全
auto q = std::make_unique<int[]>(10); // 動的配列
// new Widget(args...) と同等だがより安全

make_shared

make_shared はオブジェクトとコントロールブロックを 1 回のアロケーションで確保します。new の後に shared_ptr で包むより 1 回分少ないです。C++20 以降はコントロールブロックがサイズ調整で無駄を出しません。

1
2
3
std::shared_ptr<Big> sp = std::make_shared<Big>();
// 1 回の確保でオブジェクトと制御ブロックを同時に確保
std::weak_ptr<Big> wp = sp; // 弱参照と組み合わせる

ムーブセマンティクス

std::move は左辺値を右辺値参照へキャストし、コピーではなくムーブを発動させます。内部ポインタが移譲され、ソースは空になるため、大規模配列のディープコピーを避けられます。

1
2
3
std::vector<int> big(1000000);
auto moved = std::move(big); // 所有権を移動、big は空になる
// std::move はデータを動かさず、big を「奪取可能」とマークするだけ

ムーブコンストラクタ

ムーブコンストラクタはソースのリソースを奪い取りソースを空にするため、コピーより高速です。vector の再アロケーションでコピーではなくムーブを選んでもらえるよう noexcept を付けます。

1
2
3
4
5
struct Buffer {
int* data;
Buffer(Buffer&& other) noexcept
: data(other.data) { other.data = nullptr; }
};

戻り値最適化

コンパイラは余分なコピー / ムーブ構築を省略し、戻り値を直接呼び出し側に構築します。C++17 以降は値返しされる一時オブジェクトのゼロコピーが保証されています。

1
2
3
4
5
std::vector<int> make() {
std::vector<int> v(1000);
return v; // NRVO:直接ターゲットに構築
}
auto v = make(); // コピーゼロ

循環参照を断ち切る

親 / 子の木構造では、親が shared_ptr を持ち、子が親を指す戻りは weak_ptr にします。さもないと互いに参照し合い、永久に解放されません。lock() で一時的に shared_ptr に昇格し生存を確認します。

1
2
3
4
5
6
7
8
9
10
struct Node;
struct Parent {
std::shared_ptr<Node> child;
};
struct Node {
std::weak_ptr<Parent> parent; // 逆参照用に weak
void refresh() {
if (auto p = parent.lock()) { p->update(); }
}
};

noexcept

noexcept は関数が例外を投げないと宣言し、vector の再アロケーションがムーブを選ぶ根拠となります。万一例外を投げると std::terminate で即座に終了するため、確信がある場合のみ付けます。

1
2
3
4
5
void parse() noexcept { } // 投げないことを約束
std::vector<int> v(1000);
void run() noexcept {
v.push_back(1); // 危険:push_back は bad_alloc を投げ得る
}

手動管理のメモリリーク

手動の new / delete は途中で例外が出たり早期 return したりするとリークします。RAII(スマートポインタやコンテナ)ならクリーンアップをデストラクタに任せられ、例外安全です。

1
2
3
4
5
6
7
// BAD:途中の return や例外でリーク
int* p = new int(5);
use(p);
delete p;
// GOOD:RAII が自動クリーンアップ
auto sp = std::make_unique<int>(5);
use(sp.get());

10.オブジェクト指向

クラス、アクセス制御、コンストラクタとデストラクタ、継承と多態性、純粋仮想インタフェース、friend、演算子オーバーロードについて。

クラス定義

class はデータと操作をまとめてカプセル化します。メンバはデフォルトで非公開(struct と逆)で、private なデータには public メソッドでアクセスします。

1
2
3
4
5
6
7
8
9
class Account {
public:
void deposit(double n) { balance_ += n; }
double balance() const { return balance_; }
private:
double balance_ = 0;
};
Account a;
a.deposit(100);

アクセス指定子

public は誰でもアクセス可、protected は派生クラスもアクセス可、private はクラス自身のみ。class はデフォルトで private、struct はデフォルトで public です。

1
2
3
4
5
6
7
8
class Widget {
public: // 誰でもアクセス可
int visible = 0;
protected: // 自身と派生クラス
int semi = 0;
private: // 自身のみ
int hidden = 0;
};

コンストラクタ

コンストラクタはクラスと同名で、メンバ初期化リスト : x(x_) を使うと関数本体での代入より効率的に初期化できます。引数なしのものはデフォルトコンストラクタと呼ばれます。

1
2
3
4
5
6
7
struct Vec2 {
double x, y;
Vec2() : x(0), y(0) {} // デフォルトコンストラクタ
Vec2(double x_, double y_) : x(x_), y(y_) {} // 引数付きコンストラクタ
};
Vec2 a; // デフォルト
Vec2 b{1.0, 2.0}; // 引数付き

委譲コンストラクタ

委譲コンストラクタは、同じクラスの別コンストラクタを呼び出して初期化処理の重複を避けられます。委譲先はすでに宣言されている必要があります。

1
2
3
4
5
6
class Point {
int x_, y_;
public:
Point() : Point(0, 0) {} // 下のコンストラクタに委譲
Point(int x, int y) : x_(x), y_(y) {} // ターゲットコンストラクタ
};

デストラクタ

デストラクタはオブジェクト破棄時に呼ばれ、RAII でリソースを解放する場所です。基底クラスのデストラクタは virtual 必須で、そうでないと基底ポインタ経由で派生オブジェクトを delete した際に派生部分が解体されません。

1
2
3
4
5
6
class ScopedLock {
std::mutex& m_;
public:
explicit ScopedLock(std::mutex& m) : m_(m) { m_.lock(); }
~ScopedLock() { m_.unlock(); } // 自動ロック解除
};

継承

継承により派生クラスは基底クラスのインタフェースと実装を再利用できます。基底クラスのポインタ/参照経由で virtual 関数を呼ぶと、実行時に多態的にディスパッチされます。

1
2
3
4
5
6
struct Animal { virtual void speak() const { } };
struct Dog : Animal {
void speak() const override { /* bark */ }
};
Animal* a = new Dog();
a->speak(); // 多態:Dog 版を呼ぶ

仮想関数と抽象クラス

virtual はオーバーライド可能な関数を宣言し、純粋仮想(=0)にするとそのクラスは抽象クラスとなりインスタンス化できません。virtual を持つクラスではデストラクタも virtual にすべきです。

1
2
3
4
5
6
7
8
struct Shape {
virtual double area() const = 0; // 純粋仮想 → 抽象クラス
virtual ~Shape() = default; // 基底クラスのデストラクタは virtual
};
struct Circle : Shape {
double r_;
double area() const override { return 3.14 * r_ * r_; }
};

override と final

override は「基底クラスの virtual を確実にオーバーライドしている」と宣言し、シグネチャが違えば新しい関数を黙って作るのではなくコンパイルエラーにしてくれます。final はそれ以上のオーバーライドを禁止します。

1
2
3
4
5
struct Base { virtual void f(int) { } };
struct Derived : Base {
void f(int) override; // オーバーライドを明示
void g() final; // 派生クラスでの再オーバーライドを禁止
};

純粋仮想インタフェース

純粋仮想関数 = 0 は実装を持たずインタフェースだけを定義し、それを含むクラスは抽象クラスになります。派生クラスはインスタンス化のために実装しなければなりません。Java のインタフェースに似ています。

1
2
3
4
5
6
7
8
struct Logger {
virtual void log(const std::string&) = 0;
virtual ~Logger() = default;
};
struct ConsoleLogger : Logger {
void log(const std::string& msg) override { }
};
std::unique_ptr<Logger> l = std::make_unique<ConsoleLogger>();

friend

friend は指定したクラスや関数に private メンバへのアクセスを許し、カプセル化を破ります。密に結合した場面(演算子オーバーロード、内部イテレータなど)で慎重に使いましょう。

1
2
3
4
5
class Secret {
int code_ = 42;
friend class Reveal; // Reveal はプライベートにアクセス可
friend int read(const Secret&);
};

演算子オーバーロード

演算子オーバーロードにより、ユーザー定義型に +、-、<< などの構文を許せます。直感的な意味(+ はオペランドを変更しない、<< は出力など)を保ち、乱用は読みやすさを損ないます。

1
2
3
4
5
6
struct Money {
double v;
Money operator+(const Money& o) const { return {v + o.v}; }
};
Money a{1.5}, b{2.0};
Money c = a + b; // operator+ を呼ぶ

11.エラー処理

例外の捕捉、標準例外階層、独自例外、noexcept、そして optional / expected による例外なしのエラー表現について。

try / catch

try ブロック内で投げられた例外は catch で捕捉されます。std::exception& の基底クラスで受け、必要なら具体的な型ごとに分岐します。捕捉されなかった例外は呼び出しスタックを遡ります。

1
2
3
4
5
try {
risky();
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << '\n';
}

参照による捕捉

const& で捕捉すれば多態性を保ったままゼロコピーで受け取れます。値で捕捉するとスライス(派生部分の情報が落ちる)が起こります。catch(...) は何でも捕捉しますが型情報は失われます。

1
2
3
4
5
// GOOD:const& で捕捉してスライシングを回避
catch (const std::runtime_error& e) { }
// BAD:値で捕捉するとスライシング
catch (std::runtime_error e) { }
// catch (...) は全て捕捉するが型情報なし

標準例外階層

標準例外は std::exception から派生します。具体的な型の catch を先に、std::exception& のフォールバックを最後に置き、最も具体的なマッチが選ばれます。

1
2
3
4
catch (const std::bad_alloc& e) { } // 割り当て失敗
catch (const std::out_of_range& e) { } // 範囲外
catch (const std::invalid_argument& e) { } // 不正な引数
catch (const std::exception& e) { } // フォールバック

独自例外

独自例外は std::runtime_error(やその兄弟)から派生させ、using でコンストラクタを継承します。what() がメッセージを保持し、呼び出し側では基底クラスで捕捉すれば十分です。

1
2
3
4
5
class MyError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
throw MyError("config missing");

noexcept と例外

noexcept は例外を投げないと約束し、vector の再アロケーションやムーブはこれに依存します。万一例外を投げると std::terminate で即座に終了するため、確信が持てない場合は付けないでください。

1
2
3
4
5
void parse() noexcept { } // 投げないことを約束
std::vector<int> v(1000); // vector の操作は投げ得る
void run() noexcept {
v.push_back(1); // 危険:push_back は投げ得る
}

optional で失敗を表す

「失敗が想定される」操作では std::optional を返す方が例外より軽量で、呼び出し側は空の分岐を明示的に扱えます。例外は本当に予期しないエラーに取っておきましょう。

1
2
3
4
5
std::optional<double> sqrt_opt(double x) {
if (x < 0) return std::nullopt;
return std::sqrt(x);
}
if (auto r = sqrt_opt(-1.0)) { } else { /* 値なし分岐 */ }

std::expected

std::expected(C++23)は値とエラー記述のどちらかを保持します。optional より情報量が多く、例外より挙動が予測可能で、パースやバリデーションのコードに適しています。

1
2
3
4
5
6
#include <expected> // C++23
std::expected<int, std::string> parse(const std::string& s) {
try { return std::stoi(s); }
catch (...) { return std::unexpected("bad number"); }
}
if (auto v = parse("42")) { int n = *v; }

例外安全性

例外安全性は RAII で担保します。クリーンアップをローカルオブジェクトのデストラクタに入れることで、正常 return でも例外送出でも必ず実行され、リークや状態の不整合を防ぎます。

1
2
3
4
5
6
7
struct Guard {
~Guard() { /* ロールバックまたはクリーンアップ */ }
};
void op() {
Guard g; // どの退出経路でもデストラクタが実行
do_something_that_may_throw();
}

errno と C の連携

errno は C 時代のグローバルなエラーコードで、次の失敗呼び出しで上書きされ、スレッドセーフではありません。C++ では例外を優先し、errno は C 関数と連携するとき直ちに読むだけにしましょう。

1
2
3
if (fopen("a.txt", "r") == nullptr) {
std::cerr << "errno=" << errno << '\n'; // すぐに読む
}

12.ファイルとストリーム I/O

ファイルの読み書き、行ごとの読み込み、フォーマット出力、filesystem のパス、 binary ファイルについて。

ファイル書き込み

ofstream でファイルを書き込み用に開き、<< でフォーマット出力します。デストラクタで自動的に閉じますが、明示的に close() を呼べば書き込み失敗を早く検出できます。デフォルトでは既存ファイルを上書きします。

1
2
3
4
#include <fstream>
std::ofstream out("out.txt");
out << "hello " << 42 << '\n'; // フォーマット書き込み
out.close(); // 明示的に閉じる

ファイル読み込み

ifstream でファイルを読み込み用に開き、getline で 1 行ずつ読み込みます。ループ条件は getline の戻り値で、EOF か失敗に当たると終了します。

1
2
3
4
5
std::ifstream in("data.txt");
std::string line;
while (std::getline(in, line)) { // 1 行ずつ読む
// line を処理
}

行読みと単語読み

getline は 1 行全体を読み、>> は空白をスキップして 1 単語を読みます。混在させる場合、>> の後に残る改行文字を ignore() で捨てないと、次の getline が空文字列を返します。

1
2
3
4
5
std::string line;
std::getline(std::cin, line); // 改行まで
int n;
std::cin >> n; // 空白を飛ばして単語を読む
std::cin.ignore(); // 残りの改行を破棄

フォーマット制御

<iomanip> の setw / setprecision / fixed で出力フォーマットを制御します。表形式で桁を揃えたり小数点以下の桁数を抑えたりするときに便利です。

1
2
3
4
#include <iomanip>
std::cout << std::setw(8) << std::left << "name";
std::cout << std::fixed << std::setprecision(2) << 3.14159;
// 3.14

メモリ上のストリームをファイルへ

ostringstream でメモリ上にまとめてから一度だけファイルに書き出すと、ディスク I/O を減らせます。入力のパースでは istringstream で先に検証すると安全です。

1
2
3
std::ostringstream oss;
oss << "id=" << 7;
std::ofstream("log.txt") << oss.str();

filesystem ディレクトリ

<filesystem>(C++17)はパスとディレクトリを扱います。exists / create_directories / 反復処理など、文字列でパスを組み立てるより堅牢で、区切り文字はプラットフォームに合わせて自動処理されます。

1
2
3
4
5
#include <filesystem>
namespace fs = std::filesystem;
fs::exists("a.txt");
fs::create_directories("out/sub"); // 再帰的にディレクトリを作成
for (auto& e : fs::directory_iterator(".")) { }

パスの分解

fs::path はパスの分解と結合を扱い、プラットフォームごとに正しい区切り文字を使います。filename / extension / parent_path で構成要素を取り出せます。

1
2
3
4
5
fs::path p = "dir/sub/file.txt";
p.filename(); // "file.txt"
p.extension(); // ".txt"
p.parent_path(); // "dir/sub"
p.replace_extension(".md");

バイナリファイル

バイナリモード std::ios::binary では改行の変換は行われず、write / read はバイト列単位で読み書きします。構造体を書き出すときはメモリのアラインメントとエンディアンに注意しましょう。

1
2
3
4
5
std::ofstream out("data.bin", std::ios::binary);
int v = 12345;
out.write(reinterpret_cast<const char*>(&v), sizeof(v));
std::ifstream in("data.bin", std::ios::binary);
in.read(reinterpret_cast<char*>(&v), sizeof(v));

標準入力の検証

cin >> が失敗するとストリームが fail 状態になり、その後の読み込みは全て失敗します。clear() で状態をリセットし、ignore() で不正入力を破棄してから続行します。読み込み後は必ず cin の状態を確認しましょう。

1
2
3
4
5
6
int n;
std::cin >> n;
if (!std::cin) {
std::cin.clear(); // fail 状態をクリア
std::cin.ignore(); // 汚れたデータを破棄
}

13.よくある落とし穴

C++ の代表的な 10 個の落とし穴 —— 正しい書き方は GOOD、間違った書き方は BAD で色分けしています。

符号付きと符号なしの混在

符号付きと符号なしを混在させて比較すると、符号付きが暗黙的に符号なしへ変換され、-1 は巨大な正の数になります。比較前に明示的にキャストするか符号の有無を揃えましょう。

1
2
3
4
5
// BAD:-1 が巨大な無符号数に変換される
int x = -1;
if (x < 0u) { /* 常に偽 */ }
// GOOD:明示的に符号付きへ変換してから比較
if (static_cast<int>(0u) > x) { }

ダングリング参照

ローカル変数の参照やポインタを返すとダングリング参照になります。関数から戻った時点でメモリは無効になり、使用すると未定義動作です。値で返すかオブジェクトの寿命を延ばしましょう。

1
2
3
4
// BAD:ローカルオブジェクトの参照を返す
int& bad() { int x = 1; return x; }
// GOOD:値を返す(RVO でコピーゼロ)
int good() { int x = 1; return x; }

基底クラスのデストラクタが非 virtual

基底クラスのポインタ経由で派生オブジェクトを delete する場合、基底クラスのデストラクタは virtual でなければなりません。そうでないと派生部分が解体されず、リソースがリークします。

1
2
3
4
5
// BAD:派生クラスのデストラクタが実行されない
struct Base { ~Base() { } };
// GOOD:virtual で完全な破棄を保証
struct Base2 { virtual ~Base2() = default; };
delete base_ptr; // Base2 版のみ安全

整数除算

整数同士の除算は整数になり(切り捨て)、double に代入しても少数部は戻りません。少なくとも片方を浮動小数点にキャストしてから割りましょう。

1
2
3
4
5
6
// BAD:2/3 は 0 になる
int a = 2, b = 3;
double r = a / b; // 0.0
// GOOD:片方を浮動小数に変換
int n2 = static_cast<int>(a);
double r2 = static_cast<double>(a) / b; // 0.666...

ループ内の文字列連結

s = s + x は毎回既存の文字列全体をコピーして一時オブジェクトを作るため O(n²) になります。reserve + += でその場で追加すれば O(n)。大規模連結では桁違いの差になります。

1
2
3
4
5
6
7
// BAD:ループ内で文字列全体を繰り返しコピー
std::string s;
for (int i = 0; i < 10000; ++i) s = s + "x";
// GOOD:事前確保 + その場で追加
std::string t;
t.reserve(10000);
for (int i = 0; i < 10000; ++i) t += "x";

using namespace std

ヘッダ内で using namespace std を書くと、インクルードする全ファイルに std のシンボルが漏れて名前衝突の原因になります。using 宣言を個別にするか std:: で修飾しましょう。

1
2
3
4
5
// BAD:全てのインクルード元を汚染
using namespace std;
// GOOD:必要なものだけ、実装ファイルに
using std::string;
using std::cout;

i++ か ++i か

i++ は古い値を返すためコピー / 一時オブジェクトが必要で、++i は直接インクリメントします。int では差が出ませんが、ユーザー定義型やイテレータでは ++i の方がコピーを 1 回減らせます。

1
2
3
4
// BAD:古い値が不要ならコピーの無駄
for (int i = 0; i < n; i++) { }
// GOOD:イテレータでは差が大きい
for (int i = 0; i < n; ++i) { }

const の正しさ

読み取り専用の引数は const& を使うと、意味が自己説明的になりコピーも避けられます。const を正しく付けるとコンパイラが意図しない変更を検出し、呼び出し側にも引数を変更しないことが伝わります。

1
2
3
4
// BAD:読み取り専用なのに const を書き忘れ
void dump(std::string s);
// GOOD:const& で読み取り専用を明示
void dump(const std::string& s);

マクロの濫用

マクロは型チェックを素通りし、スコープも無視し、引数を複数回評価する副作用が出ます。const / constexpr 関数やテンプレートで済むならそちらを優先しましょう。

1
2
3
4
// BAD:マクロは型チェックなし
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// GOOD:constexpr 関数は型チェックあり
constexpr int max_of(int a, int b) { return a > b ? a : b; }

ループでの値コピー

範囲 for で値を使うと各要素をコピーします。大きなオブジェクトは const auto& で読み取り、auto& で変更します。サイズが小さくコピーが欲しいときだけ値にしましょう。

1
2
3
4
// BAD:要素ごとにコピー
for (auto item : items) { use(item); }
// GOOD:読み取り専用参照
for (const auto& item : items) { use(item); }

14.スレッドと並行処理

std::thread、ミューテックス、アトミック操作、条件変数、非同期タスクについて。

スレッドの作成

std::thread は新しいスレッドを生成して呼び出し可能オブジェクト(関数 / ラムダ)を実行します。スレッドオブジェクトは破棄前に必ず join か detach する必要があり、そうでないと std::terminate が呼ばれます。

1
2
3
4
5
6
#include <thread>
std::thread t([] {
// 新スレッドの処理
std::cout << "hello from thread\n";
});
// join() か detach() が必須

join と detach

join は現在のスレッドをブロックして子スレッドの終了を待ち、detach は子スレッドを独立に走らせます。detach 後は join できず、アクセスするオブジェクトは生存し続ける必要があります。

1
2
3
4
5
std::thread t(worker);
t.join(); // スレッド終了までブロック
// または
// t.detach(); // 分離、バックグラウンドで実行
if (t.joinable()) t.join(); // joinable を確認してから join

mutex

mutex は共有データを保護し、同時に 1 つのスレッドだけが保持できるようにします。手動の lock / unlock は漏れやすいので、lock_guard や unique_lock を優先しましょう。

1
2
3
4
5
6
#include <mutex>
std::mutex m;
int shared = 0;
m.lock();
++shared;
m.unlock(); // 例外時は実行されずデッドロック

lock_guard

lock_guard は RAII でミューテックスを管理します。構築時にロックし、破棄時にアンロックするため、例外を含むどの抜け道でも必ずアンロックされます。標準的なロック取得手段です。

1
2
3
4
5
std::mutex m;
{
std::lock_guard<std::mutex> g(m); // 構築時にロック
++shared; // クリティカルセクション
} // 破棄時にロック解除

アトミック操作

std::atomic は基本型にロックフリーのアトミック操作を提供します。fetch_add / load / store はすべてアトミックで、ミューテックスよりオーバーヘッドが小さいです。

1
2
3
4
5
#include <atomic>
std::atomic<int> counter{0};
counter.fetch_add(1); // 原子的にインクリメント
int cur = counter.load(); // 原子的読み込み
counter.store(42); // 原子的書き込み

condition_variable

condition_variable は条件待ちに使います。wait でロックを解放しつつブロックし、notify で起こします。誤った起床に備えて wait には必ず述語を渡ししょう。

1
2
3
4
5
6
7
8
9
std::mutex m;
std::condition_variable cv;
bool ready = false;
// プロデューサー
{ std::lock_guard<std::mutex> g(m); ready = true; }
cv.notify_one();
// コンシューマー
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [] { return ready; });

async / future

std::async は非同期タスクを起動して future を返し、get() で結果を待ちます。手書きの thread + 共有変数よりシンプルで、std::launch::async を指定すれば新しいスレッドが必須になります。

1
2
3
4
#include <future>
auto fut = std::async(std::launch::async,
[] { return compute(); });
int result = fut.get(); // 結果をブロック待機

thread_local

thread_local 変数はスレッドごとに独立したコピーを持ち、ロック不要のキャッシュ・カウンタ・スクラッチ状態として自然に働きます。スレッド終了時に破棄され、スレッドプライベートデータに適します。

1
2
3
4
5
thread_local int cache = 0; // スレッドごとの独立コピー
int get() {
if (!cache) cache = compute(); // スレッドごとに 1 回計算
return cache;
}

データ競合

2 つのスレッドが同じ非アトミック変数を同時に読み書きするとデータ競合になり、結果は未定義で予測不能です。atomic か mutex で同期しましょう。

1
2
3
4
5
6
7
// BAD:非アトミック変数への並行読み書き
int shared = 0;
// スレッド A: ++shared スレッド B: ++shared
// → 結果が 2 ではなく 1 になる可能性
// GOOD:アトミックかミューテックスで保護
std::atomic<int> safe{0};
safe.fetch_add(1);

並列アルゴリズム

C++17 の <execution> はアルゴリズムに実行ポリシーを追加します。par は並列、unseq はベクトル化。大量のデータを自動的にマルチコア活用しますが、要素が共有可变状態を持たないことが前提です。

1
2
3
4
5
#include <execution>
std::vector<int> v(1000);
std::sort(std::execution::par, v.begin(), v.end());
std::transform(std::execution::par_unseq,
v.begin(), v.end(), v.begin(), [](int x) { return x * 2; });

15.ネットワーク(ソケット)

POSIX ソケットの作成、listen、connect、send / recv、タイムアウト制御について(クロスプラットフォームなネットワークには Boost.Asio や libcurl を推奨)。

ソケットの作成

POSIX ソケットはネットワーク I/O の基盤です。socket() で作成し、AF_INET は IPv4、SOCK_STREAM は TCP を意味します。返り値はファイルディスクリプタで、-1 は失敗です。

1
2
3
4
#include <sys/socket.h>
#include <netinet/in.h>
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { /* errno が原因を示す */ }

bind と listen

bind でソケットをポートに紐付け、listen で接続待ちに入ります。htons はホストバイト順からネットワークバイト順へ変換します。sockaddr_in は IPv4 アドレスを表す構造体です。

1
2
3
4
5
6
struct sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY; // 全ネットワークインターフェースにバインド
bind(fd, (sockaddr*)&addr, sizeof(addr));
listen(fd, 16); // リスニングキュー

accept

accept はキューからクライアント接続を取り出し、新しいソケットを返します。接続ごとに fd が 1 つ割り当てられ、元の待ち受け fd は次の accept に使われ続けます。

1
2
3
4
5
6
while (true) {
int client = accept(fd, nullptr, nullptr);
if (client < 0) continue;
handle(client); // 新しい接続
close(client);
}

クライアント connect

クライアントは socket() + connect() でサーバアドレスへ接続します。inet_pton はドット表記の IP をバイナリへ変換します。connect 失敗は -1 を返します。

1
2
3
4
5
6
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
connect(fd, (sockaddr*)&addr, sizeof(addr));

send と recv

send / recv は TCP ソケット上でバイトを送受信します。recv が 0 を返すのは相手が切断、-1 はエラーです。TCP はバイトストリームのため、メッセージ境界は自分で定義します。

1
2
3
4
5
std::string msg = "hello";
send(fd, msg.data(), msg.size(), 0);
char buf[1024];
int n = recv(fd, buf, sizeof(buf), 0);
// n == 0 は相手がクローズ;n < 0 はエラー

ホスト名解決

getaddrinfo はホスト名とサービスを connect 可能なアドレスのリストに変換し、IPv4 / IPv6 を自動で扱います。手書きの inet_pton + ポート指定に代わる推奨手段です。

1
2
3
4
5
#include <netdb.h>
struct addrinfo* res;
getaddrinfo("example.com", "80", nullptr, &res);
// res のリンクリストを順に connect を試す
freeaddrinfo(res);

タイムアウト制御

SO_RCVTIMEO で recv にタイムアウトを設定でき、満了時は -1 が返り errno=EWOULDBLOCK になります。ブロックする recv を制御可能にし、永遠のハングアップを防げます。

1
2
3
4
#include <sys/time.h>
struct timeval tv{3, 0}; // 3 秒
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
// recv が 3 秒超 → -1、errno = EWOULDBLOCK

最小限の HTTP リクエスト

HTTP リクエストはテキストプロトコルで、リクエスト行 + ヘッダ + 空行で構成されます。デモではソケットへ生で送信していますが、本番では libcurl(リダイレクト、TLS、圧縮を自動処理)を使いましょう。

1
2
3
4
5
6
std::string req =
"GET / HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n\r\n";
send(fd, req.data(), req.size(), 0);
// レスポンス読み:ステータス行 + ヘッダ + Content-Length で本体

16.時間と日付

chrono の duration / time_point、2 種類のクロック、フォーマット、スリープ、経過時間の計測について。

duration

duration は時間の長さを型付きの単位で表します。seconds / milliseconds / microseconds などがあり、リテラルサフィックス s / ms / us で直感的に書けます。duration_cast で単位をまたいで変換します。

1
2
3
4
5
#include <chrono>
using namespace std::chrono_literals;
auto d = 3s + 500ms; // 3.5 秒
std::chrono::seconds s = 90s;
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(s);

time_point

time_point は時間軸上の特定の瞬間で、クロックと duration のオフセットで構成されます。time_point に duration を加減算すれば別の瞬間へ移動でき、2 つの time_point を引くと duration が得られます。

1
2
3
4
using namespace std::chrono;
auto now = system_clock::now(); // 現在時刻
auto past = now - 1h; // 1 時間前
auto elapsed = now - past; // 時間の長さ

system_clock

system_clock はシステムの壁時計に対応し、to_time_t / from_time_t で time_t と相互変換できます。カレンダー時刻の表示やログのタイムスタンプに使いますが、システム時刻変更の影響を受けます。

1
2
3
auto tp = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(tp);
// t は strftime / put_time で整形可能

steady_clock

steady_clock は単調増加でシステム時刻変更の影響を受けないため、経過時間の計測やデッドラインの判定など、計時の基準として使います。カレンダー表示には system_clock を使いましょう。

1
2
3
4
5
auto t0 = std::chrono::steady_clock::now();
// 経過時間の計測には steady_clock
work();
auto t1 = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0);

時間のフォーマット

put_time は strftime 形式で time_t をローカル時刻文字列に整形します。%Y-%m-%d %H:%M:%S が最もよく使われます。localtime は静的バッファへのポインタを返すためスレッドセーフではありません。

1
2
3
4
#include <iomanip>
auto tp = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(tp);
std::cout << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S");

スレッドのスリープ

sleep_for は指定した長さだけ、sleep_until は指定した時刻まで現在のスレッドをスリープさせます。メインスレッドを止めると UI が固まるため、バックグラウンドやテスト用途に限定します。

1
2
3
4
5
#include <thread>
using namespace std::chrono_literals;
std::this_thread::sleep_for(500ms);
std::this_thread::sleep_until(
std::chrono::steady_clock::now() + 2s);

経過時間の計測

計測のイディオムは、開始時刻を記録 → 終了時刻との差で duration を取得 → duration_cast で単位変換 → count() で値を取り出す、という流れです。steady_clock を使えばシステム時刻変更の影響を受けません。

1
2
3
4
5
auto start = std::chrono::steady_clock::now();
heavy_work();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
std::cout << "elapsed " << ms << " ms\n";

C スタイルの時間変換

time_t は秒単位のタイムスタンプで、gmtime で UTC 構造体、localtime でローカル構造体に変換し、strftime で整形します。これらは静的バッファを使うためスレッドセーフではありません。

1
2
3
4
5
std::time_t t = std::time(nullptr);
std::tm* utc = std::gmtime(&t);
std::tm* local = std::localtime(&t);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d", local);

タイムゾーン

C++20 の zoned_time はタイムゾーン付きで時刻を出力し、夏時間(サマータイム)を自動で扱います。古い標準ライブラリにはタイムゾーンサポートがないため、システムの localtime やサードパーティライブラリで補いましょう。

1
2
3
4
5
#include <chrono>
using namespace std::chrono;
auto tp = system_clock::now();
auto z = current_zone();
std::cout << zoned_time(z, tp) << '\n'; // タイムゾーン付きで出力

17.プロセスとシグナル

system、fork / exec / wait による子プロセス、環境変数、シグナル処理、パイプ経由の出力取得(POSIX 概念)について。

system

system() はシェルに文字列コマンドを渡して実行します。手軽ですが、コマンドインジェクションやシェルの差異により安全ではありません。出力を取りたい / 引数を渡したい場合は fork + exec や popen を使います。

1
2
3
#include <cstdlib>
int code = std::system("ls -l"); // シェルコマンドを実行
// 非 0 は通常失敗を意味、詳細はプラットフォーム依存

fork

fork は現在のプロセスを複製して子プロセスを作ります。親プロセスには子の pid が、子プロセスには 0 が返り、-1 は失敗です。fork 後、両プロセスは fork の呼び出し位置から実行を続けます。

1
2
3
4
5
6
7
#include <unistd.h>
pid_t pid = fork();
if (pid == 0) {
// 子プロセス
} else if (pid > 0) {
// 親プロセス、pid は子プロセス ID
} else { /* fork 失敗 */ }

exec

exec ファミリは現在のプロセス内に新しいプログラムを読み込んで自身を置き換えます。fork + exec を組み合わせて外部コマンドを起動します。成功時は exec から戻らず、失敗時は -1 を返すため、子プロセス側で失敗パスを処理します。

1
2
3
4
5
pid_t pid = fork();
if (pid == 0) {
execl("/bin/echo", "echo", "hi", nullptr);
_exit(1); // exec が失敗したときだけ到達
}

wait

waitpid は指定した子プロセスの終了を待ち、ステータスを取得します。WIFEXITED は正常終了を判定し、WEXITSTATUS は終了コードを取り出します。wait しない子プロセスはゾンビになります。

1
2
3
4
5
pid_t pid = fork();
if (pid == 0) { /* 子プロセスの仕事 */ _exit(0); }
int status;
waitpid(pid, &status, 0);
bool ok = WIFEXITED(status) && WEXITSTATUS(status) == 0;

環境変数

getenv は環境変数を読み(無ければ nullptr)、setenv は設定、unsetenv は削除します。環境変数は親プロセスから子プロセスへ渡されるシンプルな設定チャネルです。

1
2
3
4
#include <cstdlib>
const char* home = getenv("HOME"); // なければ nullptr
setenv("APP_ENV", "prod", 1); // 1 = 既存を上書き
unsetenv("APP_ENV");

signal

signal でシグナルハンドラを登録し、SIGINT(Ctrl+C)や SIGTERM(kill のデフォルト)を受け取れます。ハンドラ内では非同期シグナル安全な操作だけを行い、メモリアロケーションや I/O は避けましょう。

1
2
3
#include <csignal>
void handler(int sig) { /* できるだけ短く、重い処理は避ける */ }
std::signal(SIGINT, handler); // Ctrl+C で発火

sigaction

sigaction は signal より信頼性が高く、シグナルマスクやフラグを設定できます。SA_RESTART を付ければシグナルで中断されたブロッキング呼び出しが自動で再開します。本番コードでは sigaction を優先しましょう。

1
2
3
4
5
struct sigaction sa{};
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGTERM, &sa, nullptr);

exit と _exit

exit はプロセスを終了し、atexit ハンドラを実行し、バッファをフラッシュします。_exit は何もせず即座に終了します。fork 後の子プロセスでは親リソースの二重解放を防ぐため _exit を使うべきです。

1
2
3
exit(0); // atexit フックを実行してから終了
_exit(0); // 即座に終了、クリーンアップなし
return 0; // main の return は exit(0) と同等

popen

popen はコマンドを実行し、その出力をパイプで読みます("r" で読み / "w" で書き)。system と異なり出力を取り込めるのが利点です。pclose でクローズしつつ終了ステータスを取得します。

1
2
3
4
FILE* p = popen("ls -l", "r");
char buf[256];
while (fgets(buf, sizeof(buf), p)) { /* 各行を処理 */ }
int code = pclose(p);

18.正規表現

std::regex のマッチ、検索、置換、キャプチャグループ、フラグ、生文字列リテラルの書き方について。

基本的なマッチ

std::regex はデフォルトで ECMAScript 文法を使います。regex_match は文字列全体の一致を要求し、regex_search は部分文字列を探します。R"()" 生文字列リテラルを使えばエスケープ不要です。

1
2
3
#include <regex>
std::regex re(R"(\d{3}-\d{4})"); // 123-4567 に一致
bool m = std::regex_match("123-4567", re); // true

全体マッチとグループ

regex_search は最初にマッチした部分文字列を見つけ、smatch に全体(m[0])とキャプチャグループ(m[1]…)を保存します。regex_match は文字列全体の一致を要求し、フォーマット検証に向きます。

1
2
3
4
5
6
7
std::regex re(R"((\d{4})-(\d{2})-(\d{2}))");
std::smatch m;
std::string s = "date: 2026-08-02";
if (std::regex_search(s, m, re)) {
std::cout << m[0] << '\n'; // 2026-08-02
std::cout << m[1] << '\n'; // 2026 第一グループ
}

全マッチの走査

regex_search を前のマッチの末尾から繰り返すことで全マッチを走査します。m.suffix().first はマッチ以降の残りの開始点、または regex_iterator を直接使っても良いでしょう。

1
2
3
4
5
6
7
8
std::regex re(R"(\d+)");
std::smatch m;
std::string s = "a1 b22 c333";
auto it = s.cbegin();
while (std::regex_search(it, s.cend(), m, re)) {
std::cout << m[0] << ' '; // 1 22 333
it = m.suffix().first;
}

置換

regex_replace は全マッチをまとめて置換します。置換テンプレートでは $& がマッチ全体、$1 がキャプチャグループを参照します。位置を指定して置換したい場合は regex_iterator を使いましょう。

1
2
3
4
std::regex re(R"(\d+)");
std::string s = "v1.2.3";
std::string out = std::regex_replace(s, re, "[$&]");
// v[1].[2].[3]

sregex_iterator

sregex_iterator は「全マッチを列挙する」をイテレータにカプセル化したもので、手書きの while regex_search ループより簡潔で、大量抽出に適しています。

1
2
3
4
5
6
std::regex re(R"(\d+)");
std::string s = "a1 b22 c333";
std::sregex_iterator it(s.begin(), s.end(), re);
for (; it != std::sregex_iterator{}; ++it) {
std::cout << it->str() << ' '; // 1 22 333
}

キャプチャグループ

キャプチャグループは丸括弧で定義し、smatch をインデックスで参照します。(?:...) は番号を消費しない非キャプチャグループ、名前付きグループ (?<name>...) は m["name"] でアクセスします。

1
2
3
4
5
6
std::regex re(R"((\w+)@(\w+)\.(\w+))");
std::smatch m;
if (std::regex_match("[email protected]", m, re)) {
std::cout << m[1] << ' ' << m[2] << '.' << m[3];
// a b.com
}

マッチフラグ

std::regex のコンストラクタの第 2 引数はフラグです。icase は大小文字無視、multiline は ^ / $ を行頭・行末に、ECMAScript / extended は文法方言を選びます。複数指定は | で組み合わせます。

1
2
3
std::regex re("hello", std::regex::icase); // 大文字小文字を無視
std::regex re2("^a.*b$", std::regex::multiline); // 行ごとの ^ $
std::regex re3("x+", std::regex::extended); // POSIX 構文

生文字列で正規表現を書く

R"(...)" 内ではバックスラッシュがそのまま扱われるため、正規表現やパスをエスケープなしで書けます。)" を含む場合は R"tag(...)tag" のようにカスタムデリミタを指定します。正規表現は常に生文字列リテラルで書きましょう。

1
2
3
4
// 生文字列でエスケープ不要:もう \\d は不要
std::regex re(R"(\w+@\w+\.\w+)");
// 従来の書き方だとバックスラッシュを数え間違いやすい
std::regex re2("\\w+@\\w+\\.\\w+");

正規表現の例外

不正な正規表現は構築時に std::regex_error を送出します。クラッシュを防ぐため捕捉しましょう。コンパイルコストの高い正規表現はオブジェクトを再利用して再コンパイルを避けます。

1
2
3
4
5
try {
std::regex r("([unclosed"); // 不正な正規表現
} catch (const std::regex_error& e) {
std::cerr << e.what() << '\n';
}

19.ビルドとデバッグ

コンパイラフラグ、Makefile / CMake、フォーマッタ、サニタイザ、デバッガ、プロファイラについて。

主なコンパイルフラグ

よく使うフラグ:-std で標準を選択、-O で最適化レベル、-g でデバッグ情報、-Wall -Wextra で警告、-Werror で警告をエラー化。複数ファイルは -c で個別コンパイルしてからリンクします。

1
2
3
// $ g++ -std=c++20 -O2 -Wall -Wextra -g main.cpp -o app
// -pthread はスレッドライブラリをリンク(std::thread を使うなら必須)
// $ g++ -c a.cpp -o a.o コンパイルのみ、リンクしない

Makefile

Makefile はターゲットと依存を宣言し、make が古いものだけ再ビルドします。$< は最初の依存、$@ はターゲット、$^ は全依存。レシピ行は必ずタブでインデントします。

1
2
3
4
5
6
7
8
9
# Makefile
CXX = g++
CXXFLAGS = -std=c++20 -Wall -Wextra
app: main.o math.o
\t$(CXX) $^ -o $@
main.o: main.cpp math.h
\t$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
\trm -f app *.o

CMake

CMake は宣言的なビルドシステムで、Makefile や VS プロジェクトをクロスプラットフォームに生成します。target_link_libraries でライブラリをリンクし、find_package で依存を見つけます。ビルドは独立した build/ ディレクトリで行います。

1
2
3
4
5
6
7
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(server LANGUAGES CXX)
add_executable(server main.cpp)
find_package(Threads REQUIRED)
target_link_libraries(server PRIVATE Threads::Threads)
# $ cmake -B build && cmake --build build -j

pkg-config

pkg-config はライブラリのコンパイル / リンク用フラグを照会し、$(pkg-config ...) でシェル置換によりコマンドラインへ注入します。手書きのパスを避けられ、サードパーティライブラリをリンクする標準的な手段です。

1
2
3
// $ pkg-config --cflags --libs openssl
// -I/usr/include/openssl -lssl -lcrypto
// $ g++ main.cpp $(pkg-config --cflags --libs openssl) -o app

clang-format

clang-format はコードを自動整形し、.clang-format ファイルでスタイルを統一します。ColumnLimit で行幅、SortIncludes でインクルード順を制御できます。コミット前に実行すると一貫性が保てます。

1
2
3
4
5
6
# .clang-format
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 100
SortIncludes: true
# $ clang-format -i src/*.cpp

サニタイザ

サニタイザはコンパイル時に有効化すると、実行時にメモリ / 並行性のバグを検出します。address は境界外 / リーク、undefined は未定義動作、thread はデータ競合。テストでは必ず有効にしましょう。

1
2
3
4
// $ g++ -fsanitize=address,undefined -g main.cpp -o app
// ASan:範囲外 / リーク / use-after-free
// $ g++ -fsanitize=thread -g main.cpp -o app
// TSan:データ競合

gdb

gdb はコマンドラインのデバッガで、-g を付けてコンパイルするとシンボル情報が埋め込まれます。break でブレークポイント、run で起動、bt でバックトレース、print で値を確認。VSCode / CLion の GUI デバッガも内部では gdb を使っています。

1
2
3
4
5
6
7
// $ g++ -g main.cpp -o app
// $ gdb ./app
// (gdb) break main
// (gdb) run
// (gdb) print x
// (gdb) bt
// (gdb) next / step

valgrind

valgrind は実行時のメモリ問題(未初期化読み、境界外、リーク、二重解放)を検出します。--leak-check=full を付ければ各リークの呼び出しスタックが表示されます。低速なのでテストの一部に対して実行すれば十分です。

1
2
3
// $ valgrind --leak-check=full ./app
// ==12345== 20 bytes in 1 blocks are definitely lost
// $ valgrind --tool=helgrind ./app // データ競合の検出

静的ライブラリ

ar は複数の .o を静的ライブラリ lib*.a にまとめます。-L でライブラリディレクトリを、-l でライブラリ名(lib プレフィックスと .a サフィックスは省略)を指定します。静的ライブラリは実行ファイルにリンクされるため、実行時の依存はありません。

1
2
3
// $ ar rcs libmath.a math1.o math2.o
// $ g++ main.cpp -L. -lmath -o app
// 実行時に libmath.a を別途配布する必要なし

プロファイリング

gprof / perf でホットな関数をプロファイリングします。g++ -pg で計装して実行し、gprof で関数ごとの呼び出し回数と時間配分を出力して最適化ポイントを探します。

1
2
3
// $ g++ -pg main.cpp -o app
// $ ./app && gprof app gmon.out
// 各関数の呼び出し回数と時間割合を出力

公式リンク

公式ドキュメントとリソースへの直接リンク。

このチートシートについて

本ページは C++17/C++20 の自己完結型チートシートであり、言語コア、標準テンプレートライブラリ(STL)、ビルドツールチェーンについて、実プロジェクトで多用される約 80% の一般的な用法をカバーしている。内容はモダンな慣用句を重視し、スマートポインタ(unique_ptr / shared_ptr)、ムーブセマンティクス、auto による型推論、範囲 for、構造化束縛、そして C++20 の concepts と ranges を扱う。C++ は Bjarne Stroustrup が 1985 年に開発した言語で、C の高いパフォーマンスと高水準の抽象化能力を兼ね備え、システムプログラミング、ゲームエンジン、高性能計算の基盤となっている。 19 の章はそれぞれ一つのテーマに特化しており、基礎文法、変数、型とポインタ、制御フロー、関数、文字列、コレクション、メモリ管理、オブジェクト指向、エラー処理、入出力、よくある落とし穴、並行処理、ネットワーク、時間、プロセス、正規表現、ビルドツールを扱っている。各セクションには「概念の紹介 + そのままコピーできるコードスニペット」が付属し、查阅や実験に使いやすい。 すべてのコードと文章はブラウザ上でローカルにレンダリングされ、データは一切端末から外部へ送信されない。信頼すべきリファレンスは cppreference および ISO C++ 標準草案を参照されたい。

バージョン 2.1.0