C++/オブジェクト指向プログラミング

出典: フリー教科書『ウィキブックス(Wikibooks)』
< C++

オブジェクト指向プログラミングの基本概念[編集]

オブジェクト指向プログラミングとは[編集]

オブジェクト指向プログラミング(OOP)は、ソフトウェア設計のパラダイムであり、データとその操作を一つの「オブジェクト」としてまとめて扱う手法です。オブジェクトは状態(データ)と振る舞い(メソッド)を持ち、これによりプログラムの構造を直感的に理解しやすくなります。

クラスとオブジェクト[編集]

クラスはオブジェクトの設計図であり、オブジェクトはクラスのインスタンスです。クラスにはメンバー変数(フィールド)とメンバー関数(メソッド)が含まれます。

class Dog {
 public:
    std::string name;
    void bark() {
        std::cout << name << " says Woof!" << std::endl;
    }
};

Dog myDog;
myDog.name = "Buddy";
myDog.bark();

インスタンスとインスタンシエーション[編集]

クラスからオブジェクトを生成するプロセスをインスタンシエーションと呼びます。上記の例では、myDogDogクラスのインスタンスです。

カプセル化、継承、多態性[編集]

カプセル化
データとそれを操作するメソッドを一つにまとめ、外部からの不正アクセスを防ぐ。
継承
既存のクラス(基底クラス)を基に新しいクラス(派生クラス)を作成し、コードの再利用を促進する。
多態性
同じ操作を異なるオブジェクトで実行したときに異なる動作をする能力。

クラスの定義とメンバー[編集]

クラスの基本構造[編集]

クラスはキーワードclassで定義し、メンバー変数とメンバー関数を持ちます。

class Car {
 public:
    std::string model;
    int year;
    void drive() {
        std::cout << "Driving " << model << std::endl;
    }
};

メンバー変数とメンバー関数[編集]

メンバー変数はオブジェクトの状態を表し、メンバー関数はその操作を定義します。

アクセス指定子(public, private, protected)[編集]

アクセス指定子により、クラスメンバーのアクセス範囲を制御できます。

public
全てのクラスからアクセス可能
private
同一クラスからのみアクセス可能
protected
同一クラスおよび派生クラスからアクセス可能

コンストラクタとデストラクタ[編集]

コンストラクタ
オブジェクトの初期化を行う特別なメンバー関数。
デストラクタ
オブジェクトが破棄されるときに呼び出される特別なメンバー関数。
class Book {
    std::string title;
  public:
    Book(std::string t) : title{t} {} // コンストラクタ
    ~Book() { // デストラクタ
        std::cout << "Destroying " << title << std::endl;
    }
};

オブジェクトの管理[編集]

スタック上のオブジェクトとヒープ上のオブジェクト[編集]

スタック上のオブジェクトは自動的に管理され、スコープを抜けると自動的に破棄されます。ヒープ上のオブジェクトはnewキーワードで動的に割り当てられ、deleteキーワードで明示的に破棄します。

Book myBook("C++ Primer"); // スタック上のオブジェクト
Book* ptrBook = new Book("Effective C++"); // ヒープ上のオブジェクト
delete ptrBook; // ヒープ上のオブジェクトの破棄

RAII(Resource Acquisition Is Initialization)とスマートポインタ[編集]

RAIIはリソースの獲得と解放をオブジェクトのライフサイクルに基づいて管理する手法です。C++のスマートポインタ(std::unique_ptr, std::shared_ptr)はRAIIを実現するためのツールです。

std::unique_ptr<Book> smartBook = std::make_unique<Book>("Modern C++"); // RAIIを利用したスマートポインタ

コピーコンストラクタとムーブコンストラクタ[編集]

コピーコンストラクタ
オブジェクトをコピーする際に呼び出されるコンストラクタ。
ムーブコンストラクタ
オブジェクトをムーブする際に呼び出されるコンストラクタ(C++11以降)。
Book b1("C++ Concurrency");
Book b2 = b1; // コピーコンストラクタが呼び出される
Book b3 = std::move(b1); // ムーブコンストラクタが呼び出される

コピー代入演算子とムーブ代入演算子[編集]

コピー代入演算子
オブジェクトに別のオブジェクトをコピー代入する際に呼び出される。
ムーブ代入演算子
オブジェクトに別のオブジェクトをムーブ代入する際に呼び出される(C++11以降)。
Book b4("Effective STL");
Book b5;
b5 = b4; // コピー代入演算子が呼び出される
Book b6;
b6 = std::move(b4); // ムーブ代入演算子が呼び出される

継承と派生クラス[編集]

継承の基本[編集]

継承は、既存のクラスを基に新しいクラスを作成し、コードの再利用を促進します。

class Animal {
 public:
    void eat() {
        std::cout << "Eating" << std::endl;
    }
};

class Dog : public Animal {
 public:
    void bark() {
        std::cout << "Barking" << std::endl;
    }
};

派生クラスの定義[編集]

派生クラスは基底クラスのメンバー変数とメンバー関数を継承します。

基底クラスと派生クラスの関係[編集]

派生クラスは基底クラスのメンバー変数とメンバー関数にアクセスできますが、アクセス指定子により制限されます。

オーバーライドと仮想関数[編集]

オーバーライド
基底クラスの仮想関数を派生クラスで再定義すること。
仮想関数
基底クラスで宣言され、派生クラスでオーバーライドされることを意図した関数。
class Animal {
 public:
    virtual void speak() {
        std::cout << "Animal sound" << std::endl;
    }
};

class Dog : public Animal {
 public:
    void speak() override {
        std::cout << "Woof" << std::endl;
    }
};

finalとoverrideキーワード[編集]

final
クラスや仮想関数に対して、これ以上の派生やオーバーライドを禁止する。
override
派生クラスで基底クラスの仮想関数をオーバーライドしていることを明示する。
class FinalAnimal final {
 public:
    virtual void sound() final {
        std::cout << "Final sound" << std::endl;
    }
};

多態性と動的バインディング[編集]

仮想関数と多態性[編集]

仮想関数は動的バインディングを利用して、多態性を実現します。基底クラスのポインタまたは参照を通じて派生クラスのメソッドを呼び出すことができます。

Animal* a = new Dog();
a->speak(); // Dogのspeak()が呼び出されます。

この場合、aAnimal型のポインタですが、実際に呼び出されるメソッドはDogクラスで定義されたspeak()メソッドです。これは動的バインディングによって、実行時に適切な関数が選択されるためです。多態性により、プログラムの柔軟性と拡張性が向上し、派生クラスの追加や変更が容易になります。

純粋仮想関数と抽象クラス[編集]

純粋仮想関数
実装がない仮想関数。純粋仮想関数を含むクラスは抽象クラスとなり、インスタンス化できません。
抽象クラス
純粋仮想関数を含むクラス。
class Shape {
  public:
    [[nodiscard]] virtual auto area() const -> double = 0;  // 純粋仮想関数
};

class Circle : public Shape {
  public:
    double radius;
    [[nodiscard]] auto area() const -> double override {
      return 3.14 * radius * radius;
    }
};

仮想デストラクタ[編集]

基底クラスのデストラクタを仮想デストラクタとして宣言することで、派生クラスのデストラクタが適切に呼び出されるようにします。

class Base {
  public:
    virtual ~Base() = default;  // 仮想デストラクタ
};

class Derived : public Base {
  public:
    ~Derived() override {
        // 派生クラスの特有のクリーンアップ処理
    }
};

インターフェースクラス[編集]

純粋仮想関数のみを含む抽象クラスをインターフェースクラスと呼びます。インターフェースクラスはクラス間の共通の振る舞いを定義するために使用されます。

class Printable {
 public:
    virtual void print() const = 0;
};

class Document : public Printable {
 public:
    void print() const override {
        // Documentの表示処理
    }
};

テンプレートとジェネリックプログラミング[編集]

クラステンプレートと関数テンプレート[編集]

テンプレートは、型や値をパラメータとして受け取ることができる汎用的なプログラム設計手法です。

template <typename T>
class Container {
 public:
    T element;
    void print() {
        std::cout << element << std::endl;
    }
};

template <typename T>
void swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

テンプレート特化と部分特化[編集]

テンプレート特化
特定の型や値に対して特別な実装を提供すること。
部分特化
テンプレートの一部のパラメータを特定の値や型に対して制限すること。
template <>
class Container<std::string> {
 public:
    std::string element;
    void print() {
        std::cout << "String: " << element << std::endl;
    }
};

コンセプト(C++20)[編集]

コンセプトは、テンプレートの型に要求される条件を表現するための機能です。C++20から導入され、より明確なテンプレートの制約を記述できるようになりました。

template <typename T>
concept Numeric = std::is_arithmetic_v<T>;

template <Numeric T>
T square(T x) {
    return x * x;
}

requiresキーワードと制約ベースプログラミング[編集]

requiresキーワードは、テンプレートの型に要求される条件を直接指定するために使用されます。

template <typename T>
void process(T value) requires std::is_integral_v<T> {
    // 整数型の値を処理する
}

モジュール(C++20)[編集]

モジュールの基本[編集]

モジュールは、コードを論理的にグループ化し、依存関係を明確化するための機能です。C++20で導入され、従来のヘッダーインクルード方式よりも効率的であり、コンパイル速度を向上させます。

// MyModule.cpp
module MyModule;
export module MyModule;

export void greet() {
    std::cout << "Hello, Module!" << std::endl;
}

モジュールの定義とインポート[編集]

モジュールはmoduleキーワードを使用して定義され、importキーワードを使用して他のモジュールをインポートします。

// main.cpp
import MyModule;

int main() {
    greet(); // MyModuleをインポートして使用
    return 0;
}

モジュールの利点と使用例[編集]

モジュールは、名前空間の汚染を防ぎ、依存関係を明確にし、ビルド時間を短縮するなどの利点があります。また、ライブラリの開発やプロジェクトの構造化に役立ちます。

コルーチン(C++20)[編集]

コルーチンの概念と基本構造[編集]

コルーチンは非同期プログラミングを支援するための新しいC++20の機能であり、非同期処理の記述をより直感的にします。

コルーチン(Coroutine)とは[編集]

コルーチンは、関数の実行を一時停止し、後で再開できる機能です。従来の関数呼び出しでは、呼び出し側のコードから制御が関数に移り、関数の実行が終了するまでブロックされます。しかし、コルーチンでは、関数の実行を途中で一時停止し、制御を呼び出し側に戻すことができます。後で再開すると、一時停止した場所から処理を再開することができます。

この機能は、非同期プログラミングにおいて非常に役立ちます。従来の手法では、非同期処理を実装するために状態機械を明示的に管理する必要がありましたが、コルーチンを使えばそのような複雑さを隠蔽できます。

C++20のCORUOTINEの概要[編集]

C++20では、言語レベルでコルーチンをサポートしています。これにより、ボイラープレートコードを減らし、よりシンプルな記述が可能になります。主な構成要素は以下の通りです。

  • co_awaitキーワード: コルーチンの一時停止と再開を制御します。
  • co_yieldキーワード: 値を生成するジェネレータコルーチンを実装するために使用します。
  • co_returnキーワード: コルーチンの終了を示します。
  • coroutine_handleクラス: コルーチンの制御を行うハンドルを提供します。
  • 戻り値オブジェクトとpromise_type: コルーチンの振る舞いを定義するために使用されます。

コルーチンの利点[編集]

コルーチンには以下のような利点があります。

  1. 非同期プログラミングの簡素化: 従来の状態機械ベースの実装と比べて、コードが簡潔で読みやすくなります。
  2. 効率性: コンテキスト切り替えのオーバーヘッドが小さく、スレッドよりも軽量です。
  3. 構造化プログラミング: 制御フローをフラットに記述できるため、深い入れ子になることがありません。
  4. 一般化されたイテレータ: コルーチンを使ってジェネレータイテレータを実装できます。

課題と制限事項[編集]

一方で、コルーチンには以下のような課題や制限事項もあります。

  1. コンパイラサポート: C++20のコルーチンは比較的新しい機能なので、すべてのコンパイラがサポートしているわけではありません。
  2. 例外安全性: コルーチン中での例外処理は複雑になる可能性があります。
  3. デバッグ: コルーチンのデバッグは従来の関数よりも難しい場合があります。
  4. パフォーマンス: コンテキスト切り替えにはある程度のオーバーヘッドがあります。

C++20のコルーチンは、非同期プログラミングを大幅に簡素化する強力な機能です。しかし、従来の手法とトレードオフがあるため、適切な使用場面を見極める必要があります。今後、コンパイラサポートが進み、ベストプラクティスが蓄積されることで、より広く利用されるようになるでしょう。

コード例
#include <coroutine>
#include <iostream>

struct Task {
    struct promise_type {
        auto get_return_object() -> Task {
            return Task{
                std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        static auto initial_suspend() -> std::suspend_always { return {}; }
        static auto final_suspend() noexcept -> std::suspend_always {
            return {};
        }
        void unhandled_exception() {}
        void return_void() {}
    };

    explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Task() {
        if (handle) {
            handle.destroy();
        }
    }

    static auto await_ready() -> bool { return false; }
    void await_suspend(std::coroutine_handle<promise_type> h) { handle = h; }
    void await_resume() {}

    std::coroutine_handle<promise_type> handle;
};

auto async_task() -> Task {
    std::cout << "Task started" << std::endl;
    co_await std::suspend_always{};
    std::cout << "Task resumed" << std::endl;
    co_return;
}

auto main() -> int {
    Task const task = async_task();
    std::cout << "Before resuming" << std::endl;
    task.handle.resume();
    std::cout << "After resuming" << std::endl;
    return 0;
}

co_await, co_yield, co_return[編集]

co_await
コルーチン内で非同期操作の完了を待つための式。
co_yield
現在の値を返し、一時停止して次の呼び出しまでコントロールを返す。
co_return
コルーチンを終了し、最終結果を返す。

コルーチンの実際の使用例[編集]

コルーチンは非同期プログラミングのほかに、ジェネレータやステートマシンなど、様々な用途に使用することができます。上記の例では、ジェネレータとして使用されています。

Rangesライブラリ(C++20)[編集]

Rangesライブラリの紹介[編集]

Rangesライブラリは、範囲やビューを扱うための新しい機能を提供します。コレクションの操作やフィルタリングを簡潔に記述できるようになります。

#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // フィルタリングと変換を組み合わせたパイプライン
    auto doubled_even = numbers
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x * 2; });

    for (int num : doubled_even) {
        std::cout << num << " ";
    }

    return 0;
}

ビューとアダプタ[編集]

ビュー
元のコンテナを変更せずに、そのコンテンツに対する抽象化された読み取り手段を提供します。
アダプタ
ビューを操作し、新しいビューを生成するための関数やオブジェクトです。

Rangeベースのアルゴリズム[編集]

Rangesライブラリは、範囲に対する操作をより直感的かつ効率的に実行するためのアルゴリズムも提供します。これにより、従来のループよりもシンプルで読みやすいコードを書くことができます。

まとめと演習問題[編集]

章の要約[編集]

本章では、オブジェクト指向プログラミングの基本概念から最新のC++20の機能までを学びました。クラスとオブジェクトの定義、継承と多態性、テンプレートとジェネリックプログラミング、モジュール、コルーチン、Rangesライブラリなどについて理解を深めました。

実践的な演習問題[編集]

  1. クラスPersonを定義し、名前と年齢をメンバーとして持つようにしてください。
  2. Personクラスを継承したStudentクラスを定義し、学生番号を追加してください。
  3. Personクラスを基にして、異なるデータ型(整数、文字列など)を扱えるジェネリックなContainerクラスを定義してください。
  4. コルーチンを使用して、フィボナッチ数列を生成するジェネレータを実装してください。

プロジェクト例[編集]

  • オブジェクト指向なタスク管理システムの開発
  • C++20の新機能を活用したテストフレームワークの構築
  • Rangesライブラリを使用したデータ処理ツールの開発