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)
- signed と unsigned の算術演算を混在させる(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)
- エラーを静かに飲み込む空のキャッチブロック
- フロー制御に例外を使用する(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_);
data_.push(value);
}
bool pop(int& value) {
std::scoped_lock lock(mutex_);
if (data_.empty()) return false;
value = data_.front();
data_.pop();
return true;
}
private:
std::queue<int> data_;
mutable std::mutex mutex_;
};
アンチパターン
- 同期に
volatile を使用する(CP.8)
- 裸の
lock()/unlock() ペア(CP.20 -- 代わりに scoped_lock を使用)
- ロックを保持したまま未知のコードを呼び出す(CP.22 -- デッドロックのリスク)
- 条件変数なしでビジーループ(CP.42 --
condition_variable を使用)
- 適切なケースなしでのロックフリーデータ構造の使用(CP.100)
std::async からの未来を無視する(CP.25 -- これはブロッキング破棄を引き起こす)