第11章:モダンC++

はじめに

C++11は「モダンC++」の始まりとされ、言語に根本的な変革をもたらしました。Bjarne Stroustrupは次のように述べています:

> "C++11 feels like a new language." > > (C++11は新しい言語のように感じる)

本章では、C++11以降の主要な機能について学び、現代的なC++プログラミングスタイルを身につけます。

---

1. C++の進化

1.1 バージョン履歴

| バージョン | 年 | 主な追加機能 | |-----------|-----|-------------| | C++98/03 | 1998/2003 | 最初の標準、STL | | C++11 | 2011 | auto, ラムダ, ムーブ, スマートポインタ | | C++14 | 2014 | ジェネリックラムダ, 二進リテラル | | C++17 | 2017 | if constexpr, 構造化束縛, optional | | C++20 | 2020 | コンセプト, コルーチン, モジュール | | C++23 | 2023 | std::expected, std::print |

1.2 42課題との関係

42のCPP Moduleは主にC++98を対象としていますが、モダンC++の概念を理解することは重要です:

  • C++98で学ぶ「なぜ」が、C++11以降の「どのように簡単になったか」を理解する基盤
  • 実務ではC++11以降が標準
  • 概念の理解が深まる
  • ---

    2. 型推論

    2.1 auto

    変数の型をコンパイラが推論:

    // C++98
    std::vector<std::string>::iterator it = vec.begin();
    std::map<std::string, std::vector<int>>::const_iterator map_it = myMap.begin();
    
    // C++11
    auto it = vec.begin();
    auto map_it = myMap.cbegin();
    
    // 基本的な使用
    auto x = 42;          // int
    auto y = 3.14;        // double
    auto z = "Hello";     // const char*
    auto s = std::string("Hello");  // std::string
    
    // 参照とconst
    int value = 42;
    auto a = value;       // int (コピー)
    auto& b = value;      // int& (参照)
    const auto& c = value; // const int& (const参照)
    auto* p = &value;     // int*
    

    2.2 decltype

    式の型を取得:

    int x = 42;
    decltype(x) y = 100;  // int
    
    // 関数の戻り値型に使用
    template <typename T, typename U>
    auto add(T a, U b) -> decltype(a + b) {
        return a + b;
    }
    
    // decltype(auto) (C++14)
    int& getRef();
    decltype(auto) r = getRef();  // int& (参照を保持)
    auto r2 = getRef();           // int (コピー)
    

    2.3 型推論のガイドライン

    // 良い使用例
    auto iter = container.begin();  // イテレータ型は冗長
    auto callback = [](int x) { return x * 2; };  // ラムダの型は書けない
    auto result = someFunction();  // 戻り値型が明らか
    
    // 避けるべき使用例
    auto x = 42;  // 明示的にint x = 42;の方が読みやすい場合も
    auto data = getData();  // 戻り値型が不明確だと混乱する
    

    ---

    3. 範囲forループ

    3.1 基本構文

    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // C++98
    for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << std::endl;
    }
    
    // C++11
    for (int x : vec) {
        std::cout << x << std::endl;
    }
    
    // 参照を使用(変更可能)
    for (int& x : vec) {
        x *= 2;
    }
    
    // const参照(読み取り専用、コピー回避)
    for (const int& x : vec) {
        std::cout << x << std::endl;
    }
    
    // auto を使用
    for (auto& x : vec) {
        x *= 2;
    }
    
    for (const auto& x : vec) {
        std::cout << x << std::endl;
    }
    

    3.2 様々なコンテナで使用

    // 配列
    int arr[] = {1, 2, 3, 4, 5};
    for (int x : arr) {
        std::cout << x << std::endl;
    }
    
    // std::map
    std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
    for (const auto& [name, age] : ages) {  // C++17 構造化束縛
        std::cout << name << ": " << age << std::endl;
    }
    
    // C++11/14での書き方
    for (const auto& pair : ages) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    
    // 初期化リスト
    for (int x : {1, 2, 3, 4, 5}) {
        std::cout << x << std::endl;
    }
    

    ---

    4. ラムダ式

    4.1 基本構文

    // 構文: [キャプチャ](引数) -> 戻り値型 { 本体 }
    
    // 基本的なラムダ
    auto add = [](int a, int b) { return a + b; };
    std::cout << add(3, 5) << std::endl;  // 8
    
    // 戻り値型を明示
    auto divide = [](double a, double b) -> double {
        if (b == 0) return 0;
        return a / b;
    };
    
    // 引数なしラムダ
    auto greet = []() { std::cout << "Hello!" << std::endl; };
    greet();
    
    // 即時実行
    []() {
        std::cout << "Immediately executed!" << std::endl;
    }();
    

    4.2 キャプチャ

    int x = 10;
    int y = 20;
    
    // 値でキャプチャ(コピー)
    auto by_value = [x, y]() {
        std::cout << x << ", " << y << std::endl;
        // x = 100;  // エラー:デフォルトではconst
    };
    
    // 参照でキャプチャ
    auto by_ref = [&x, &y]() {
        x = 100;
        y = 200;
    };
    by_ref();
    std::cout << x << ", " << y << std::endl;  // 100, 200
    
    // 全てを値でキャプチャ
    auto all_by_value = [=]() {
        std::cout << x << ", " << y << std::endl;
    };
    
    // 全てを参照でキャプチャ
    auto all_by_ref = [&]() {
        x = 1000;
    };
    
    // ミックス
    auto mixed = [=, &x]() {  // yは値、xは参照
        x = 42;
        // y = 42;  // エラー
    };
    
    // mutableラムダ(値キャプチャした変数を変更可能)
    auto mutable_lambda = [x]() mutable {
        x = 100;  // ローカルコピーを変更
        return x;
    };
    

    4.3 ジェネリックラムダ(C++14)

    // autoを引数に使用
    auto generic = [](auto a, auto b) {
        return a + b;
    };
    
    std::cout << generic(1, 2) << std::endl;       // 3 (int)
    std::cout << generic(1.5, 2.5) << std::endl;   // 4.0 (double)
    std::cout << generic(std::string("Hello"), std::string(" World")) << std::endl;
    
    // 完全転送
    auto forward_lambda = [](auto&&... args) {
        return someFunction(std::forward<decltype(args)>(args)...);
    };
    

    4.4 アルゴリズムとの組み合わせ

    std::vector<int> vec = {5, 2, 8, 1, 9, 3};
    
    // ソート
    std::sort(vec.begin(), vec.end(), [](int a, int b) {
        return a > b;  // 降順
    });
    
    // フィルタリング
    auto it = std::remove_if(vec.begin(), vec.end(), [](int x) {
        return x < 5;
    });
    vec.erase(it, vec.end());
    
    // 変換
    std::transform(vec.begin(), vec.end(), vec.begin(), [](int x) {
        return x * 2;
    });
    
    // 集約
    int sum = std::accumulate(vec.begin(), vec.end(), 0, [](int acc, int x) {
        return acc + x;
    });
    

    ---

    5. ムーブセマンティクス

    5.1 問題:不要なコピー

    // C++98: vectorを返すと大量のコピーが発生
    std::vector<int> createLargeVector() {
        std::vector<int> vec(1000000);
        // ... データを設定
        return vec;  // コピーが発生(最適化がない場合)
    }
    
    std::vector<int> v = createLargeVector();  // さらにコピー
    

    5.2 右辺値参照

    // 左辺値(lvalue): 名前があり、アドレスが取れる
    int x = 42;
    int& lref = x;  // 左辺値参照
    
    // 右辺値(rvalue): 一時的な値
    // int& error = 42;  // エラー:左辺値参照は右辺値にバインドできない
    int&& rref = 42;  // 右辺値参照:OK
    
    // std::move: 左辺値を右辺値にキャスト
    std::string s = "Hello";
    std::string&& rvalue = std::move(s);
    // sはまだ有効だが、中身は不定
    

    5.3 ムーブコンストラクタとムーブ代入演算子

    class Buffer {
        char* data;
        size_t size;
    
    public:
        // コンストラクタ
        Buffer(size_t n) : data(new char[n]), size(n) {}
    
        // コピーコンストラクタ
        Buffer(const Buffer& other) : data(new char[other.size]), size(other.size) {
            std::copy(other.data, other.data + size, data);
            std::cout << "Copy constructor" << std::endl;
        }
    
        // ムーブコンストラクタ
        Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
            other.data = nullptr;
            other.size = 0;
            std::cout << "Move constructor" << std::endl;
        }
    
        // コピー代入演算子
        Buffer& operator=(const Buffer& other) {
            if (this != &other) {
                delete[] data;
                size = other.size;
                data = new char[size];
                std::copy(other.data, other.data + size, data);
            }
            std::cout << "Copy assignment" << std::endl;
            return *this;
        }
    
        // ムーブ代入演算子
        Buffer& operator=(Buffer&& other) noexcept {
            if (this != &other) {
                delete[] data;
                data = other.data;
                size = other.size;
                other.data = nullptr;
                other.size = 0;
            }
            std::cout << "Move assignment" << std::endl;
            return *this;
        }
    
        ~Buffer() { delete[] data; }
    };
    
    int main() {
        Buffer b1(1000);
    
        Buffer b2 = b1;              // コピーコンストラクタ
        Buffer b3 = std::move(b1);   // ムーブコンストラクタ
        // b1は使用不可(有効だが不定状態)
    
        Buffer b4(100);
        b4 = b2;                     // コピー代入
        b4 = std::move(b2);          // ムーブ代入
    
        return 0;
    }
    

    5.4 Rule of Five

    C++11では、Rule of Threeが拡張されてRule of Fiveになりました:

  • デストラクタ
  • コピーコンストラクタ
  • コピー代入演算子
  • ムーブコンストラクタ
  • ムーブ代入演算子
  • class ModernResource {
    public:
        ModernResource() = default;
        ~ModernResource() = default;
    
        // コピー
        ModernResource(const ModernResource&) = default;
        ModernResource& operator=(const ModernResource&) = default;
    
        // ムーブ
        ModernResource(ModernResource&&) noexcept = default;
        ModernResource& operator=(ModernResource&&) noexcept = default;
    };
    

    ---

    6. スマートポインタ

    6.1 unique_ptr

    所有権が唯一のスマートポインタ:

    #include <memory>
    
    // 作成
    std::unique_ptr<int> p1(new int(42));
    auto p2 = std::make_unique<int>(42);  // C++14、推奨
    
    // 使用
    std::cout << *p2 << std::endl;
    
    // コピー不可
    // std::unique_ptr<int> p3 = p2;  // エラー
    
    // ムーブは可能
    std::unique_ptr<int> p3 = std::move(p2);
    // p2はnullptrになる
    
    // カスタムデリータ
    auto fileDeleter = [](FILE* f) { fclose(f); };
    std::unique_ptr<FILE, decltype(fileDeleter)> file(
        fopen("test.txt", "r"),
        fileDeleter
    );
    
    // 配列
    auto arr = std::make_unique<int[]>(10);
    arr[0] = 42;
    

    6.2 shared_ptr

    参照カウント方式の共有スマートポインタ:

    #include <memory>
    
    // 作成
    auto p1 = std::make_shared<int>(42);  // 推奨
    
    std::cout << "Count: " << p1.use_count() << std::endl;  // 1
    
    {
        std::shared_ptr<int> p2 = p1;  // コピー可能
        std::cout << "Count: " << p1.use_count() << std::endl;  // 2
    }  // p2のデストラクタで参照カウント減少
    
    std::cout << "Count: " << p1.use_count() << std::endl;  // 1
    
    // カスタムデリータ
    auto custom_deleter = [](int* p) {
        std::cout << "Deleting " << *p << std::endl;
        delete p;
    };
    std::shared_ptr<int> p3(new int(100), custom_deleter);
    

    6.3 weak_ptr

    循環参照を防ぐための弱参照:

    #include <memory>
    
    class Node {
    public:
        std::shared_ptr<Node> next;
        std::weak_ptr<Node> prev;  // 循環参照を防ぐ
    
        ~Node() { std::cout << "Node destroyed" << std::endl; }
    };
    
    int main() {
        auto node1 = std::make_shared<Node>();
        auto node2 = std::make_shared<Node>();
    
        node1->next = node2;
        node2->prev = node1;  // weak_ptrなので循環参照にならない
    
        // weak_ptrの使用
        if (auto locked = node2->prev.lock()) {  // shared_ptrに変換
            // lockedを使用
        }
    
        return 0;
    }  // 両方のNodeが正しく破棄される
    

    6.4 スマートポインタのベストプラクティス

    // 良い例:make_sharedを使用
    auto p = std::make_shared<MyClass>(args);
    
    // 避けるべき例:newを直接使用
    std::shared_ptr<MyClass> p(new MyClass(args));
    
    // 関数に渡す場合
    void useResource(const std::shared_ptr<Resource>& r);  // 所有権を共有
    void takeOwnership(std::unique_ptr<Resource> r);        // 所有権を移動
    void justUse(Resource& r);                               // 生ポインタ/参照で十分な場合
    

    ---

    7. その他の重要な機能

    7.1 nullptr

    // C++98: NULLは0に展開される可能性
    void func(int x);
    void func(char* p);
    func(NULL);  // 曖昧になる可能性
    
    // C++11: nullptrは明確にポインタ型
    func(nullptr);  // 常にfunc(char*)が呼ばれる
    
    int* p = nullptr;
    if (p == nullptr) {
        // ...
    }
    

    7.2 constexpr

    コンパイル時定数/関数:

    // コンパイル時定数
    constexpr int MAX_SIZE = 100;
    
    // コンパイル時関数
    constexpr int factorial(int n) {
        return n <= 1 ? 1 : n * factorial(n - 1);
    }
    
    // コンパイル時に計算
    constexpr int fact5 = factorial(5);  // 120
    int arr[factorial(5)];  // 配列サイズとして使用可能
    
    // C++14以降はより複雑な関数も可能
    constexpr int fibonacci(int n) {
        if (n <= 1) return n;
        int a = 0, b = 1;
        for (int i = 2; i <= n; ++i) {
            int temp = a + b;
            a = b;
            b = temp;
        }
        return b;
    }
    

    7.3 static_assert

    コンパイル時アサート:

    static_assert(sizeof(int) == 4, "This code requires 32-bit integers");
    static_assert(std::is_integral<int>::value, "int must be integral");
    
    template <typename T>
    class Container {
        static_assert(std::is_default_constructible<T>::value,
                      "T must be default constructible");
        // ...
    };
    

    7.4 初期化リスト

    #include <initializer_list>
    
    class MyArray {
        std::vector<int> data;
    public:
        MyArray(std::initializer_list<int> list) : data(list) {}
    };
    
    MyArray arr = {1, 2, 3, 4, 5};
    
    // 統一初期化構文
    int x{42};
    std::vector<int> vec{1, 2, 3, 4, 5};
    std::map<std::string, int> ages{{"Alice", 30}, {"Bob", 25}};
    

    7.5 可変長テンプレート引数

    // 可変長引数
    template <typename... Args>
    void print(Args... args) {
        (std::cout << ... << args) << std::endl;  // C++17 fold式
    }
    
    print(1, 2, 3);              // 123
    print("Hello", " ", "World"); // Hello World
    
    // パラメータパックの展開
    template <typename T, typename... Rest>
    void printAll(T first, Rest... rest) {
        std::cout << first;
        if constexpr (sizeof...(rest) > 0) {
            std::cout << ", ";
            printAll(rest...);
        }
    }
    

    7.6 構造化束縛(C++17)

    // ペアの分解
    std::pair<int, std::string> getPair() {
        return {42, "Hello"};
    }
    
    auto [number, text] = getPair();
    
    // mapのイテレーション
    std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
    for (const auto& [name, age] : ages) {
        std::cout << name << ": " << age << std::endl;
    }
    
    // 配列の分解
    int arr[3] = {1, 2, 3};
    auto [a, b, c] = arr;
    

    7.7 std::optional(C++17)

    #include <optional>
    
    std::optional<int> findValue(const std::vector<int>& vec, int target) {
        auto it = std::find(vec.begin(), vec.end(), target);
        if (it != vec.end()) {
            return *it;
        }
        return std::nullopt;
    }
    
    int main() {
        std::vector<int> vec = {1, 2, 3, 4, 5};
    
        if (auto result = findValue(vec, 3)) {
            std::cout << "Found: " << *result << std::endl;
        } else {
            std::cout << "Not found" << std::endl;
        }
    
        // value_or: 値がなければデフォルト値を返す
        int value = findValue(vec, 10).value_or(-1);
    
        return 0;
    }
    

    ---

    まとめ

    本章で学んだこと:

  • 型推論: auto、decltype
  • 範囲forループ: コンテナの簡潔なイテレーション
  • ラムダ式: 無名関数、キャプチャ
  • ムーブセマンティクス: 右辺値参照、効率的なリソース転送
  • スマートポインタ: unique_ptr、shared_ptr、weak_ptr
  • その他: nullptr、constexpr、static_assert、構造化束縛
  • 次章では、C++のベストプラクティスについて学びます。

    ---

    練習問題

  • 以下のC++98コードをモダンC++に書き換えてください:
std::vector<int> v;
for (int i = 0; i < 10; ++i) {
    v.push_back(i * i);
}
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << std::endl;
}

  • unique_ptrを使って、ファイルを安全に開閉するクラスを作成してください。
  • 以下のクラスにムーブコンストラクタとムーブ代入演算子を追加してください:
class String {
    char* data;
    size_t length;
public:
    String(const char* s);
    String(const String& other);
    String& operator=(const String& other);
    ~String();
};

  • ラムダ式を使って、以下の操作を行うプログラムを作成してください:
- vectorから偶数のみを抽出 - 各要素を2倍 - 合計を計算