C++ コーディング規約 (C++ Core Guidelines)
C++ Core Guidelines に基づく、モダンC++(C++17/20/23)向けの包括的なコーディング規約。型安全性、リソース安全性、不変性、明確性を重視します。
使用するタイミング
- 新しいC++コード(クラス、関数、テンプレート)を書くとき
- 既存のC++コードをレビューまたはリファクタリングするとき
- C++プロジェクトでアーキテクチャ上の決定を行うとき
- C++コードベース全体で一貫したスタイルを適用するとき
- 言語機能間の選択をするとき(例:
enum vs enum class、生ポインタ vs スマートポインタ)
使用しないタイミング
- C++以外のプロジェクト
- モダンC++の機能を採用できないレガシーCコードベース
- 特定のガイドラインがハードウェア制約と矛盾する組み込み/ベアメタル環境(選択的に適用してください)
横断的な原則
これらのテーマはガイドライン全体に繰り返し登場し、基盤を形成します。
- RAIIを至る所で (P.8, R.1, E.6, CP.20): リソースの寿命をオブジェクトの寿命に結び付ける
- デフォルトで不変にする (P.10, Con.1-5, ES.25):
const/constexprから始める;可変性は例外
- 型安全性 (P.4, I.4, ES.46-49, Enum.3): 型システムを使用してコンパイル時にエラーを防ぐ
- 意図を表現する (P.3, F.1, NL.1-2, T.10): 名前、型、概念は目的を伝えるべき
- 複雑性を最小化する (F.2-3, ES.5, Per.4-5): シンプルなコードは正しいコード
- ポインタセマンティクスより値セマンティクスを優先する (C.10, R.3-5, F.20, CP.31): 値での返却とスコープ付きオブジェクトを優先する
哲学とインターフェース (P., I.)
主要ルール
| ルール | 概要 |
|---|
| P.1 | アイデアをコードで直接表現する |
| P.3 | 意図を表現する |
| P.4 | 理想的には、プログラムは静的に型安全であるべき |
| P.5 | 実行時チェックよりコンパイル時チェックを優先する |
| P.8 | リソースをリークさせてはならない |
| P.10 | 可変データより不変データを優先する |
| I.1 | インターフェースを明示的にする |
| I.2 | 非constグローバル変数を避ける |
| I.4 | インターフェースを正確かつ強い型付けにする |
| I.11 | 所有権を生ポインタや参照で転送してはならない |
| I.23 | 関数の引数は少なく保つ |
推奨例
struct Temperature {
double kelvin;
};
Temperature boil(const Temperature& water);
非推奨例
double boil(double* temp);
int g_counter = 0;
関数 (F.*)
主要ルール
| ルール | 概要 |
|---|
| F.1 | 意味のある操作を注意深く名前付けされた関数としてパッケージ化する |
| F.2 | 関数は単一の論理操作を実行すべき |
| F.3 | 関数は短くシンプルに保つ |
| F.4 | 関数がコンパイル時に評価される可能性がある場合、constexprと宣言する |
| F.6 | 関数が例外を投げてはならない場合、noexceptと宣言する |
| F.8 | 純粋関数を優先する |
| F.16 | 「入力」パラメータについては、コピーが安価な型は値渡し、それ以外はconst&で渡す |
| F.20 | 「出力」値については、出力パラメータより戻り値を優先する |
| F.21 | 複数の「出力」値を返すには、構造体を返すことを優先する |
| F.43 | ローカルオブジェクトへのポインタや参照を決して返してはならない |
パラメータ渡し
void print(int x);
void analyze(const std::string& data);
void transform(std::string s);
struct ParseResult {
std::string token;
int position;
};
ParseResult parse(std::string_view input);
void parse(std::string_view input,
std::string& token, int& pos);
純粋関数とconstexpr
constexpr int factorial(int n) noexcept {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
static_assert(factorial(5) == 120);
アンチパターン
- 関数から
T&&を返す (F.45)
va_arg / Cスタイル可変引数を使用する (F.55)
- 別のスレッドに渡されるラムダで参照キャプチャを使用する (F.53)
- ムーブセマンティクスを阻害する
const Tを返す (F.49)
クラスとクラス階層 (C.*)
主要ルール
| ルール | 概要 |
|---|
| C.2 | 不変条件が存在する場合はclassを、データメンバが独立して変化する場合はstructを使用する |
| C.9 | メンバの露出を最小限にする |
| C.20 | デフォルト操作の定義を避けられるなら、そうする(ゼロの法則) |
| C.21 | コピー/ムーブ/デストラクタを定義または=deleteする場合、それらすべてを扱う(5つの法則) |
| C.35 | 基底クラスのデストラクタ: public virtual または protected non-virtual |
| C.41 | コンストラクタは完全に初期化されたオブジェクトを作成すべき |
| C.46 | 単一引数のコンストラクタはexplicitと宣言する |
| C.67 | ポリモーフィックなクラスはpublicなコピー/ムーブを抑制すべき |
| C.128 | 仮想関数: virtual、override、finalのうち正確に1つだけを指定する |
ゼロの法則
struct Employee {
std::string name;
std::string department;
int id;
};
5つの法則
class Buffer {
public:
explicit Buffer(std::size_t size)
: data_(std::make_unique<char[]>(size)), size_(size) {}
~Buffer() = default;
Buffer(const Buffer& other)
: data_(std::make_unique<char[]>(other.size_)), size_(other.size_) {
std::copy_n(other.data_.get(), size_, data_.get());
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
auto new_data = std::make_unique<char[]>(other.size_);
std::copy_n(other.data_.get(), other.size_, new_data.get());
data_ = std::move(new_data);
size_ = other.size_;
}
return *this;
}
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
private:
std::unique_ptr<char[]> data_;
std::size_t size_;
};
クラス階層
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};
アンチパターン
- コンストラクタ/デストラクタ内で仮想関数を呼び出す (C.82)
- 非トリビアル型に対して
memset/memcpyを使用する (C.90)
- 仮想関数とオーバーライダーで異なるデフォルト引数を提供する (C.140)
- データメンバを
constや参照にすると、ムーブ/コピーが抑制される (C.12)
リソース管理 (R.*)
主要ルール
| ルール | 概要 |
|---|
| R.1 | RAIIを使用してリソースを自動的に管理する |
| R.3 | 生ポインタ(T*)は非所有とする |
| R.5 | スコープ付きオブジェクトを優先する;不必要にヒープ割り当てをしない |
| R.10 | malloc()/free()を避ける |
| R.11 | newとdeleteの明示的な呼び出しを避ける |
| R.20 | 所有権を表現するにはunique_ptrまたはshared_ptrを使用する |
| R.21 | 所有権を共有しない限り、shared_ptrよりunique_ptrを優先する |
| R.22 | shared_ptrを作成するにはmake_shared()を使用する |
スマートポインタの使用法
auto widget = std::make_unique<Widget>("config");
auto cache = std::make_shared<Cache>(1024);
void render(const Widget* w) {
if (w) w->draw();
}
render(widget.get());
RAIIパターン
class FileHandle {
public:
explicit FileHandle(const std::string& path)
: handle_(std::fopen(path.c_str(), "r")) {
if (!handle_) throw std::runtime_error("Failed to open: " + path);
}
~FileHandle() {
if (handle_) std::fclose(handle_);
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&& other) noexcept
: handle_(std::exchange(other.handle_, nullptr)) {}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (handle_) std::fclose(handle_);
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
private:
std::FILE* handle_;
};
アンチパターン
- 裸の
new/delete (R.11)
- C++コードでの
malloc()/free() (R.10)
- 単一の式での複数リソース割り当て (R.13 -- 例外安全の危険)
unique_ptrで十分な場合のshared_ptr (R.21)
式と文 (ES.*)
主要ルール
| ルール | 概要 |
|---|
| ES.5 | スコープを小さく保つ |
| ES.20 | 常にオブジェクトを初期化する |
| ES.23 | {}初期化子構文を優先する |
| ES.25 | 変更が意図されていない限り、オブジェクトをconstまたはconstexprと宣言する |
| ES.28 | const変数の複雑な初期化にはラムダを使用する |
| ES.45 | マジック定数を避ける;記号定数を使用する |
| ES.46 | 狭小化/精度損失を伴う算術変換を避ける |
| ES.47 | 0やNULLではなくnullptrを使用する |
| ES.48 | キャストを避ける |
| ES.50 | constをキャストして外してはならない |
初期化
const int max_retries{3};
const std::string name{"widget"};
const std::vector<int> primes{2, 3, 5, 7, 11};
const auto config = [&] {
Config c;
c.timeout = std::chrono::seconds{30};
c.retries = max_retries;
c.verbose = debug_mode;
return c;
}();
アンチパターン
- 初期化されていない変数 (ES.20)
- ポインタとして
0またはNULLを使用する (ES.47 -- nullptrを使用)
- Cスタイルキャスト (ES.48 --
static_cast、const_castなどを使用)
constをキャストして外す (ES.50)
- 名前付き定数がないマジックナンバー (ES.45)
- 符号ありと符号なしの算術演算を混在させる (ES.100)
- ネストされたスコープで名前を再利用する (ES.12)
エラー処理 (E.*)
主要ルール
| ルール | 概要 |
|---|
| E.1 | 設計の初期段階でエラー処理戦略を策定する |
| E.2 | 関数が割り当てられたタスクを実行できないことを示すために例外を投げる |
| E.6 | RAIIを使用してリークを防ぐ |
| E.12 | 例外を投げることが不可能または許容されない場合、noexceptを使用する |
| E.14 | 目的に合わせて設計されたユーザー定義型を例外として使用する |
| E.15 | 値で投げ、参照でキャッチする |
| E.16 | デストラクタ、解放、swapは決して失敗してはならない |
| E.17 | すべての関数ですべての例外をキャッチしようとしない |
例外階層
class AppError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
class NetworkError : public AppError {
public:
NetworkError(const std::string& msg, int code)
: AppError(msg), status_code(code) {}
int status_code;
};
void fetch_data(const std::string& url) {
throw NetworkError("connection refused", 503);
}
void run() {
try {
fetch_data("https://api.example.com");
} catch (const NetworkError& e) {
log_error(e.what(), e.status_code);
} catch (const AppError& e) {
log_error(e.what());
}
}
アンチパターン
intや文字列リテラルなどの組み込み型を投げる (E.14)
- 値でキャッチする(スライシングのリスク) (E.15)
- エラーを静かに飲み込む空のcatchブロック
- フロー制御に例外を使用する (E.3)
errnoのようなグローバル状態に基づくエラー処理 (E.28)
定数と不変性 (Con.*)
すべてのルール
| ルール | 概要 |
|---|
| Con.1 | デフォルトでは、オブジェクトを不変にする |
| Con.2 | デフォルトでは、メンバ関数をconstにする |
| Con.3 | デフォルトでは、ポインタと参照をconstにして渡す |
| Con.4 | 構築後に変更されない値にはconstを使用する |
| Con.5 | コンパイル時に計算可能な値にはconstexprを使用する |
class Sensor {
public:
explicit Sensor(std::string id) : id_(std::move(id)) {}
const std::string& id() const { return id_; }
double last_reading() const { return reading_; }
void record(double value) { reading_ = value; }
private:
const std::string id_;
double reading_{0.0};
};
void display(const Sensor& s) {
std::cout << s.id() << ": " << s.last_reading() << '\n';
}
constexpr double PI = 3.14159265358979;
constexpr int MAX_SENSORS = 256;
並行性と並列性 (CP.*)
主要ルール
| ルール | 概要 |
|---|
| CP.2 | データ競合を避ける |
| CP.3 | 書き込み可能なデータの明示的な共有を最小化する |
| CP.4 | スレッドではなくタスクの観点で考える |
| CP.8 | 同期にvolatileを使用しない |
| CP.20 | RAIIを使用し、生のlock()/unlock()は決して使わない |
| CP.21 | 複数のミューテックスを取得するにはstd::scoped_lockを使用する |
| CP.22 | ロックを保持している間は未知のコードを決して呼び出さない |
| CP.42 | 条件なしで待機しない |
| CP.44 | lock_guardやunique_lockに名前を付けることを忘れない |
| CP.100 | 絶対に必要でない限り、ロックフリープログラミングを使用しない |
安全なロック
class ThreadSafeQueue {
public:
void push(int value) {
std::scoped_lock lock(mutex_);
queue_.push(value);
}
bool pop(int& value) {
std::scoped_lock lock(mutex_);
if (queue_.empty()) return false;
value = queue_.front();
queue_.pop();
return true;
}
private:
std::queue<int> queue_;
mutable std::mutex mutex_;
};
auto future = std::async(std::launch::async, [&] {
return expensive_computation();
});
アンチパターン
- ロックされていない共有データ (CP.2)
- フラグに
volatileを使用する (CP.8)
- 生の
lock()/unlock()ペア (CP.20)
- デッドロックを引き起こす可能性のある複数ミューテックスの不適切な順序でのロック (CP.21)
- スリープループ(
std::this_thread::sleep_for)で条件をポーリングする (CP.42)
テンプレートとジェネリックプログラミング (T.*)
主要ルール
| ルール | 概要 |
|---|
| T.1 | テンプレートを使用してコードの重複を減らす |
| T.10 | すべてのテンプレート引数にコンセプトを指定する |
| T.20 | 「呼び出し可能オブジェクト」とやり取りするためのコンセプトを定義する際には、std::invocableのような「タグ」コンセプトを優先する |
| T.41 | テンプレート内ではオブジェクトのコピーとムーブが最小限のコストになるようにする |
| T.47 | 高い汎用性を持つコードでは名前の衝突を避けるために、名前のないラムダではなく名前付きの関数オブジェクトを使用する |
| T.65 | タグディスパッチを使用して、異なる関数の実装を選択する |
コンセプトの使用 (C++20)
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;
template<Numeric T>
T sum(const std::vector<T>& values) {
T total = 0;
for (const auto& v : values) {
total += v;
}
return total;
}
std::vector<int> ints = {1, 2, 3};
std::vector<std::string> strs = {"hello", "world"};
sum(ints);
タグディスパッチ
struct random_access_iterator_tag {};
struct bidirectional_iterator_tag : random_access_iterator_tag {};
template<typename Iter>
void advance_impl(Iter& iter, int n, bidirectional_iterator_tag) {
while (n-- > 0) ++iter;
}
template<typename Iter>
void advance_impl(Iter& iter, int n, random_access_iterator_tag) {
iter += n;
}
template<typename Iter>
void advance(Iter& iter, int n) {
advance_impl(iter, n, typename Iter::iterator_category{});
}
パフォーマンス (Per.*)
主要ルール
| ルール | 概要 |
|---|
| Per.1 | 時期尚早な最適化はしない |
| Per.2 | プロファイリングなしに最適化しない |
| Per.3 | 不要なコピーを避ける |
| Per.4 | コードの論理的明確性を犠牲にして最適化しない |
| Per.5 | 実行時とコンパイル時のリソースのバランスを取る |
| Per.10 | 依存関係のないサブ式をループから移動する |
| Per.11 | 計算を実行時からコンパイル時に移動する |
| Per.17 | 通常の状況ではreserve()を使用して不要な再割り当てを避ける |
コピー回避
std::vector<int> create_large_vector() {
std::vector<int> result(1'000'000);
return result;
}
void process(const std::vector<int>& data) {
}
std::vector<int> vec;
vec.reserve(10'000);
for (int i = 0; i < 10'000; ++i) {
vec.push_back(i);
}
その他推奨事項
命名規則 (NL.*)
- 型:
PascalCase (Customer, TemperatureSensor)
- 関数と変数:
snake_case (calculate_tax(), max_retries)
- マクロ:
UPPER_CASE (MAX_BUFFER_SIZE)
- 名前空間:
snake_case (my_company::core)
- プライベートメンバ変数:
trailing_underscore_ (id_, buffer_)
コメントとドキュメント
- コードの「なぜ」を説明する;「何を」は説明しない(コードがそれを示すべき)
- 複雑なロジックや非自明なトレードオフにはコメントを使用する
// TODO: と // FIXME: コメントを使用して既知の問題をマークする
- パブリックAPIにはDoxygenスタイルのコメントを使用する
ファイルとインクルード
- ガードの代わりに
#pragma onceを使用する(広くサポートされている)
- ヘッダーは自己完結型でなければならない(必要なものをすべてインクルードする)
- ヘッダー内では
using namespace std;を使用しない
- 前方宣言を優先する;必要な場合のみインクルードする
- 論理的なグループにインクルードを整理する: 自分自身、標準ライブラリ、サードパーティ、プロジェクト
モダンC++のイディオム
autoは、型が明らかであるか、正確な型を指定することが不可能または面倒な場合に使用する(ES.11)
- 所有権の転送には
std::move()を、一時オブジェクトの転送にはstd::forward()を使用する(F.18)
- 文字列ビューには
std::string_viewを、所有されていない配列ビューにはstd::spanを使用する
- オプショナルな値には
std::optionalを使用する
- 型消去された呼び出し可能オブジェクトには
std::functionを使用する(できれば避ける)
- 列挙型には
enum classを使用する(Enum.3)
- 継承のない型の区別には
std::variantを使用する
std::visitを使用してstd::variantに対してパターンマッチングする
- 範囲ベースのforループとアルゴリズムを優先する
push_backの代わりにemplace_backを使用して、一時オブジェクトの作成を避ける
クイックリファレンス
常に行うこと
絶対に避けること
このガイドはC++ Core Guidelinesに基づいています。詳細と根拠については、公式ドキュメントを参照してください。特定のプロジェクトのニーズや制約に合わせてルールを適用することを忘れないでください。