第12章:ベストプラクティス
はじめに
C++は強力な言語ですが、その自由度の高さゆえに、品質の高いコードを書くには規律が必要です。本章では、42のCPP Moduleで重視されるOrthodox Canonical Formをはじめ、実践的なコーディング規約、デバッグ手法、そして現代C++における設計原則を学びます。
---
1. Orthodox Canonical Form
1.1 定義と背景
42のCPP Moduleで重要視されるOrthodox Canonical Form(OCF)は、C++クラスが持つべき4つの特殊メンバ関数を明示的に定義することを要求します:
- デフォルトコンストラクタ
- コピーコンストラクタ
- コピー代入演算子
- デストラクタ
class Sample {
private:
int* data;
std::string name;
public:
// 1. デフォルトコンストラクタ
Sample() : data(new int(0)), name("default") {
std::cout << "Default constructor called" << std::endl;
}
// 2. コピーコンストラクタ
Sample(const Sample& other) : data(new int(*other.data)), name(other.name) {
std::cout << "Copy constructor called" << std::endl;
}
// 3. コピー代入演算子
Sample& operator=(const Sample& other) {
std::cout << "Copy assignment operator called" << std::endl;
if (this != &other) {
delete data;
data = new int(*other.data);
name = other.name;
}
return *this;
}
// 4. デストラクタ
~Sample() {
std::cout << "Destructor called" << std::endl;
delete data;
}
};
1.2 なぜOCFが必要か
リソース管理の一貫性:
class WithoutOCF {
int* data;
public:
WithoutOCF() : data(new int(42)) {}
~WithoutOCF() { delete data; }
// コピーコンストラクタと代入演算子がない → 二重解放の危険
};
void dangerous() {
WithoutOCF a;
WithoutOCF b = a; // シャローコピー
} // デストラクタで同じポインタを2回delete → 未定義動作!
1.3 OCFの完全な実装例
class CanonicalClass {
private:
int* data_;
size_t size_;
std::string name_;
public:
// ============================================================
// 1. デフォルトコンストラクタ
// ============================================================
CanonicalClass()
: data_(new int[10]())
, size_(10)
, name_("Untitled") {
std::cout << "Default constructor: " << name_ << std::endl;
}
// パラメータ付きコンストラクタ(オプション)
explicit CanonicalClass(size_t size, const std::string& name = "Untitled")
: data_(new int[size]())
, size_(size)
, name_(name) {
std::cout << "Parameterized constructor: " << name_ << std::endl;
}
// ============================================================
// 2. コピーコンストラクタ
// ============================================================
CanonicalClass(const CanonicalClass& other)
: data_(new int[other.size_])
, size_(other.size_)
, name_(other.name_ + "_copy") {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copy constructor: " << name_ << std::endl;
}
// ============================================================
// 3. コピー代入演算子
// ============================================================
CanonicalClass& operator=(const CanonicalClass& other) {
std::cout << "Copy assignment operator" << std::endl;
if (this != &other) {
// 古いリソースを解放
delete[] data_;
// 新しいリソースを確保してコピー
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
name_ = other.name_;
}
return *this;
}
// ============================================================
// 4. デストラクタ
// ============================================================
~CanonicalClass() {
std::cout << "Destructor: " << name_ << std::endl;
delete[] data_;
}
// ============================================================
// その他のメンバ関数
// ============================================================
int& operator[](size_t index) { return data_[index]; }
const int& operator[](size_t index) const { return data_[index]; }
size_t size() const { return size_; }
const std::string& name() const { return name_; }
};
1.4 Rule of Three / Five / Zero
Rule of Three(C++98): デストラクタ、コピーコンストラクタ、コピー代入演算子のいずれかを定義するなら、3つすべてを定義せよ。
Rule of Five(C++11): ムーブコンストラクタとムーブ代入演算子を加えた5つすべてを定義せよ。
Rule of Zero(モダンC++): スマートポインタを使い、特殊メンバ関数を一切定義しない。
// Rule of Zero: スマートポインタによる自動管理
class ModernClass {
private:
std::unique_ptr<int[]> data_;
size_t size_;
std::string name_;
public:
ModernClass(size_t size = 10, const std::string& name = "Modern")
: data_(std::make_unique<int[]>(size))
, size_(size)
, name_(name) {}
// コピー/ムーブ/デストラクタは自動生成に任せる
// または必要に応じてdefaultを使用
};
---
2. コーディング規約
2.1 命名規則
// クラス名: PascalCase
class MyClassName {};
class BankAccount {};
// 関数名: camelCase または snake_case
void processData();
void process_data();
// 変数名: camelCase または snake_case
int itemCount;
int item_count;
// メンバ変数: 接尾辞やプレフィックス
class Example {
private:
int value_; // 接尾辞アンダースコア
int m_value; // プレフィックス m_
std::string name_;
};
// 定数: UPPER_SNAKE_CASE または kPascalCase
const int MAX_SIZE = 100;
constexpr int kDefaultSize = 50;
// テンプレートパラメータ: PascalCase
template <typename ValueType>
class Container {};
2.2 ファイル構成
// header.hpp
#ifndef PROJECT_MODULE_HEADER_HPP
#define PROJECT_MODULE_HEADER_HPP
#include <string>
#include <vector>
namespace myproject {
class MyClass {
public:
MyClass();
~MyClass();
void doSomething();
private:
int value_;
};
} // namespace myproject
#endif // PROJECT_MODULE_HEADER_HPP
// source.cpp
#include "header.hpp"
#include <iostream>
namespace myproject {
MyClass::MyClass() : value_(0) {}
MyClass::~MyClass() {}
void MyClass::doSomething() {
std::cout << "Doing something" << std::endl;
}
} // namespace myproject
2.3 includeの順序
// 1. 対応するヘッダ(source.cppの場合)
#include "MyClass.hpp"
// 2. 同じプロジェクトのヘッダ
#include "Utils.hpp"
#include "Config.hpp"
// 3. サードパーティライブラリ
#include <boost/algorithm/string.hpp>
// 4. 標準ライブラリ
#include <iostream>
#include <string>
#include <vector>
// アルファベット順にソート(各グループ内)
2.4 const正しさ
class ConstCorrectness {
public:
// ゲッターはconstメンバ関数
int getValue() const { return value_; }
const std::string& getName() const { return name_; }
// セッターは非const
void setValue(int v) { value_ = v; }
void setName(const std::string& n) { name_ = n; }
// const参照で受け取る(コピーを避ける)
void processData(const std::vector<int>& data);
// 変更する場合は参照
void modifyData(std::vector<int>& data);
private:
int value_;
std::string name_;
};
// constポインタとポインタのconst
const int* ptr1; // 指す先がconst
int* const ptr2 = &x; // ポインタ自体がconst
const int* const ptr3 = &x; // 両方const
---
3. 設計原則
3.1 SOLID原則
S - 単一責任の原則(Single Responsibility Principle):
// 悪い例: 複数の責任を持つ
class Employee {
void calculatePay();
void saveToDatabase();
void generateReport();
};
// 良い例: 責任を分離
class Employee {
std::string name;
double salary;
};
class PayrollCalculator {
double calculatePay(const Employee& e);
};
class EmployeeRepository {
void save(const Employee& e);
};
class ReportGenerator {
std::string generate(const Employee& e);
};
O - 開放閉鎖の原則(Open/Closed Principle):
// 拡張に対して開いており、修正に対して閉じている
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
double area() const override { return 3.14159 * radius * radius; }
};
class Rectangle : public Shape {
double width, height;
public:
double area() const override { return width * height; }
};
// 新しい形状を追加しても既存コードを変更する必要がない
class Triangle : public Shape {
double base, height;
public:
double area() const override { return 0.5 * base * height; }
};
L - リスコフの置換原則(Liskov Substitution Principle):
// 派生クラスは基底クラスと置換可能であるべき
class Bird {
public:
virtual void move() = 0; // flyではなくmove
};
class Sparrow : public Bird {
public:
void move() override { fly(); }
private:
void fly() { /* 飛ぶ */ }
};
class Penguin : public Bird {
public:
void move() override { walk(); }
private:
void walk() { /* 歩く */ }
};
I - インターフェース分離の原則(Interface Segregation Principle):
// 悪い例: 大きすぎるインターフェース
class IWorker {
virtual void work() = 0;
virtual void eat() = 0;
virtual void sleep() = 0;
};
// 良い例: 分割されたインターフェース
class IWorkable {
virtual void work() = 0;
};
class IFeedable {
virtual void eat() = 0;
};
class Human : public IWorkable, public IFeedable {
void work() override { /* ... */ }
void eat() override { /* ... */ }
};
class Robot : public IWorkable {
void work() override { /* ... */ }
// eatは実装しない
};
D - 依存性逆転の原則(Dependency Inversion Principle):
// 悪い例: 具象クラスに依存
class MySQLDatabase {
void save(const std::string& data);
};
class UserService {
MySQLDatabase db; // 具象に依存
public:
void saveUser(const User& user) {
db.save(user.toString());
}
};
// 良い例: 抽象に依存
class IDatabase {
public:
virtual void save(const std::string& data) = 0;
virtual ~IDatabase() = default;
};
class UserService {
std::unique_ptr<IDatabase> db; // 抽象に依存
public:
UserService(std::unique_ptr<IDatabase> database)
: db(std::move(database)) {}
void saveUser(const User& user) {
db->save(user.toString());
}
};
3.2 その他の設計原則
DRY(Don't Repeat Yourself):
// 悪い例
void processUsers() {
for (auto& user : users) {
validate(user);
transform(user);
save(user);
}
}
void processOrders() {
for (auto& order : orders) {
validate(order);
transform(order);
save(order);
}
}
// 良い例
template <typename T>
void processItems(std::vector<T>& items) {
for (auto& item : items) {
validate(item);
transform(item);
save(item);
}
}
YAGNI(You Aren't Gonna Need It):
// 悪い例: 将来のために過度に設計
class UserManager {
void createUser();
void deleteUser();
void updateUser();
void archiveUser(); // 使わないかも
void restoreUser(); // 使わないかも
void exportUserToXML(); // 使わないかも
void importUserFromXML(); // 使わないかも
void syncWithCloud(); // 使わないかも
};
// 良い例: 必要なものだけ
class UserManager {
void createUser();
void deleteUser();
void updateUser();
};
---
4. デバッグ技法
4.1 アサーション
#include <cassert>
void divide(int a, int b) {
assert(b != 0 && "Division by zero!"); // デバッグビルドでのみ有効
return a / b;
}
// static_assert: コンパイル時チェック
template <typename T>
class Container {
static_assert(sizeof(T) <= 1024, "T is too large");
// ...
};
4.2 ログ出力
// シンプルなログマクロ
#ifdef DEBUG
#define LOG(msg) std::cerr << "[LOG] " << __FILE__ << ":" << __LINE__ << " " << msg << std::endl
#else
#define LOG(msg)
#endif
void processData() {
LOG("Starting data processing");
// ...
LOG("Data processing complete");
}
// より洗練されたログクラス
class Logger {
public:
enum Level { DEBUG, INFO, WARNING, ERROR };
static void log(Level level, const std::string& message) {
const char* levelStr[] = {"DEBUG", "INFO", "WARNING", "ERROR"};
std::cerr << "[" << levelStr[level] << "] " << message << std::endl;
}
static void debug(const std::string& msg) { log(DEBUG, msg); }
static void info(const std::string& msg) { log(INFO, msg); }
static void warning(const std::string& msg) { log(WARNING, msg); }
static void error(const std::string& msg) { log(ERROR, msg); }
};
4.3 Valgrind
メモリリーク検出:
# コンパイル(デバッグ情報付き)
g++ -g -o program program.cpp
# Valgrindで実行
valgrind --leak-check=full --show-leak-kinds=all ./program
出力の読み方:
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 5 allocs, 4 frees
==12345==
==12345== 40 bytes in 1 blocks are definitely lost
==12345== at 0x4C2E0EF: operator new[](unsigned long)
==12345== by 0x400A3B: main (program.cpp:15)
4.4 AddressSanitizer
# コンパイル時に有効化
g++ -fsanitize=address -g -o program program.cpp
# 実行
./program
検出できるエラー:
- メモリリーク
- Use-after-free
- ヒープバッファオーバーフロー
- スタックバッファオーバーフロー
- 二重解放
4.5 GDB/LLDB
# GDBでデバッグ
g++ -g -o program program.cpp
gdb ./program
# 基本コマンド
(gdb) break main # ブレークポイント設定
(gdb) run # 実行
(gdb) next # 次の行へ(ステップオーバー)
(gdb) step # 関数内へ(ステップイン)
(gdb) print variable # 変数の値を表示
(gdb) backtrace # コールスタック表示
(gdb) continue # 実行継続
(gdb) quit # 終了
---
5. テスト
5.1 ユニットテストの基本
// 手動テスト
void testAddition() {
Calculator calc;
int result = calc.add(2, 3);
if (result != 5) {
std::cerr << "FAIL: add(2, 3) expected 5, got " << result << std::endl;
} else {
std::cout << "PASS: add(2, 3)" << std::endl;
}
}
// マクロを使ったアサーション
#define ASSERT_EQ(expected, actual) \
do { \
if ((expected) != (actual)) { \
std::cerr << "FAIL: " << __FILE__ << ":" << __LINE__ \
<< " Expected " << (expected) << ", got " << (actual) << std::endl; \
return false; \
} \
} while (0)
bool testCalculator() {
Calculator calc;
ASSERT_EQ(5, calc.add(2, 3));
ASSERT_EQ(0, calc.add(-1, 1));
ASSERT_EQ(-5, calc.add(-2, -3));
return true;
}
5.2 テスト駆動開発(TDD)
// 1. 最初にテストを書く(レッド)
void testStack() {
Stack<int> stack;
ASSERT_TRUE(stack.empty());
stack.push(42);
ASSERT_FALSE(stack.empty());
ASSERT_EQ(42, stack.top());
stack.pop();
ASSERT_TRUE(stack.empty());
}
// 2. テストが通る最小限のコードを書く(グリーン)
template <typename T>
class Stack {
std::vector<T> data;
public:
bool empty() const { return data.empty(); }
void push(const T& value) { data.push_back(value); }
T top() const { return data.back(); }
void pop() { data.pop_back(); }
};
// 3. リファクタリング(リファクタ)
// コードを整理しながらテストが通ることを確認
5.3 境界値テスト
void testBoundaries() {
// 最小値
ASSERT_EQ(0, factorial(0));
ASSERT_EQ(1, factorial(1));
// 最大値
ASSERT_EQ(120, factorial(5));
// エッジケース
ASSERT_THROWS(factorial(-1), std::invalid_argument);
// オーバーフロー
// factorial(21) はintでオーバーフロー
}
---
6. コードレビューチェックリスト
6.1 一般的なチェック項目
□ コンパイル警告がないか
□ メモリリークがないか
□ 未初期化変数がないか
□ 境界チェックは適切か
□ エラーハンドリングは適切か
□ コードの重複はないか
□ 命名は明確か
□ コメントは適切か
□ テストはあるか
6.2 C++固有のチェック項目
□ Orthodox Canonical Formを満たしているか
□ const正しさは守られているか
□ 参照とポインタの使い分けは適切か
□ 例外安全性は確保されているか
□ RAIIパターンを使っているか
□ スマートポインタを適切に使っているか
□ 仮想デストラクタは必要か
□ explicit指定子は適切か
□ オーバーライドにはoverride指定子を使っているか
---
まとめ
本章で学んだこと:
これらのベストプラクティスを習慣化することで、高品質なC++コードを書けるようになります。
---
練習問題
class DynamicString {
char* data;
size_t length;
public:
explicit DynamicString(const char* str = "");
// 他のメンバ関数を追加
};
- 以下のコードの問題点を指摘し、修正してください:
class Widget {
public:
Widget() { resource = new int[100]; }
~Widget() { delete resource; } // 問題あり
private:
int* resource;
};
- SOLID原則のいずれかに違反している以下のクラスを改善してください:
class UserManager {
public:
void createUser(const std::string& name);
void deleteUser(int id);
void sendEmail(int userId, const std::string& subject, const std::string& body);
void generateMonthlyReport();
void backupDatabase();
};
- 以下の関数のテストケースを考案してください:
bool isPalindrome(const std::string& str);