第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指定子を使っているか
    

    ---

    まとめ

    本章で学んだこと:

  • Orthodox Canonical Form: 4つの特殊メンバ関数
  • コーディング規約: 命名、ファイル構成、const正しさ
  • 設計原則: SOLID、DRY、YAGNI
  • デバッグ技法: アサーション、Valgrind、GDB
  • テスト: ユニットテスト、TDD
  • コードレビュー: チェックリスト
  • これらのベストプラクティスを習慣化することで、高品質なC++コードを書けるようになります。

    ---

    練習問題

  • 以下のクラスをOrthodox Canonical Formに従って完成させてください:
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);