コンテンツにスキップ

C++/ユースケース集

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

イントロダクション

[編集]

C++の概要と歴史

[編集]

C++は、1980年代初めにBjarne Stroustrupによって開発されたプログラミング言語です。C言語にオブジェクト指向の概念を追加し、より強力で柔軟なプログラムが書けるように進化しました。C++は、高速で効率的なプログラムを書くために多くの機能を提供し、特にシステムプログラミングやゲーム開発、リアルタイムシステム、アプリケーション開発において広く利用されています。

ユースケース集の目的と構成

[編集]

このユースケース集では、C++の主要な機能やその実際の使用方法を、さまざまなユースケースを通して学びます。初心者から上級者まで役立つ情報を提供し、C++を使った問題解決のアプローチを深く掘り下げます。

C++の特徴と他の言語との違い

[編集]

C++は、効率的なメモリ管理、低レベルのアクセス、そしてオブジェクト指向プログラミングをサポートする多くの機能を提供します。他の高級言語と比較して、パフォーマンスや制御を細かく管理できる点が特徴です。

基本的なユースケース

[編集]

基本的な入出力

[編集]

C++では、標準入出力を扱うために、iostreamライブラリを使用します。これにより、コンソールからの入力や、コンソールへの出力が簡単に行えます。

hello.cc
#include <iostream>

auto main() -> int {
    std::cout << "こんにちは、C++!" << std::endl;
    return 0;
}

このプログラムは、コンソールに「こんにちは、C++!」と表示します。

ファイル操作(読み書き)

[編集]

C++では、fstreamライブラリを使用してファイル操作を行います。ファイルを開く、読み書きする、閉じる操作が簡単に行えます。

fileio.cc
#include <iostream>
#include <fstream>

auto main() -> int {
    // 出力ファイルのオープン時にエラーチェック
    std::ofstream outfile("sample.txt");
    if (!outfile) {
        std::cerr << "エラー: ファイルを開けませんでした。" << std::endl;
        return 1; // 異常終了
    }

    // ファイルに書き込み
    outfile << "C++でファイル操作を学びましょう。" << std::endl;
    outfile.close(); // 明示的にファイルを閉じる

    // 入力ファイルのオープン時にエラーチェック
    std::ifstream infile("sample.txt");
    if (!infile) {
        std::cerr << "エラー: ファイルを開けませんでした。" << std::endl;
        return 1; // 異常終了
    }

    // ファイルから読み込み
    std::string line;
    while (std::getline(infile, line)) {
        std::cout << line << std::endl;
    }
    infile.close(); // 明示的にファイルを閉じる

    return 0; // 正常終了
}

データ構造の活用

[編集]

配列とベクター

[編集]

C++では、配列とstd::vectorを使ってデータを管理します。配列は固定長ですが、std::vectorは動的にサイズを変更できます。

固定長配列

[編集]
ary.cc
#include <iostream>

auto main() -> int {
    const int ary[] {2, 3, 5, 7, 11}; 
    for (size_t i = 0; const auto& el : ary) {
        std::cout << "ary[" << i << "] = " << el << std::endl;
        i++;
    }
    return 0;
}

動的配列(std::vector

[編集]
vector.cc
#include <iostream>
#include <vector>

auto main() -> int {
#include <iostream>
#include <vector>

auto main() -> int {
    auto vec = std::vector<int>{2, 3, 5, 7, 11};
    for (size_t i = 0; const auto& el : vec) {
        std::cout << "vec[" << i << "] = " << el << std::endl;
        i++;
    }
    return 0;
}

リストとマップ

[編集]

C++の標準ライブラリには、リンクリストを実装するstd::listや、キーと値のペアを管理するstd::mapstd::unordered_mapがあります。これらを利用することで、効率的なデータ管理が可能です。

list.cc
#include <iostream>
#include <list>

auto main() -> int {
    auto list = std::list<int>{2, 3, 5, 7, 11};
    list.push_back(6);  // 要素を追加
    for (size_t i = 0; const auto& el : list) {
        std::cout << "list[" << i << "] = " << el << std::endl;
        i++;
    }
    return 0;
}
map.cc
#include <iostream>
#include <map>

auto main() -> int {
    std::map<int, std::string> map{{1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}, {5, "five"}};

    for (const auto& pair : map) {
        std::cout << "key: " << pair.first << ", value: " << pair.second << std::endl;
    }

    return 0;
}

関数とラムダ式

[編集]

関数の定義と呼び出し

[編集]

C++では、関数を定義して呼び出すことができます。引数の型、戻り値の型を指定して、関数を作成します。

func.cc
#include <iostream>

auto add(int a, int b) -> int {
    return a + b;
}

auto main() -> int {
    int result = add(5, 3);
    std::cout << "5 + 3 = " << result << std::endl;
    return 0;
}

ラムダ式

[編集]

ラムダ式は、簡単に関数オブジェクトを作成できるC++の機能です。特に、無名関数を使用したり、引数として渡したりする際に便利です。

#include <iostream>

auto main() -> int {
    auto add = [](int a, int b) { return a + b; };
    std::cout << "5 + 3 = " << add(5, 3) << std::endl;
    return 0;
}

クラスとオブジェクト指向

[編集]

クラスとインスタンス

[編集]

C++では、クラスを使用してオブジェクト指向プログラミングを行います。クラスは、データとそれに関連する操作をひとつの単位としてまとめます。

#include <iostream>
#include <stdexcept>  // 例外処理を使うために追加

class Rectangle {
private:
    int width, height;

public:
    // explicit キーワードを使って暗黙の型変換を防止
    explicit Rectangle(int w, int h) {
        // 幅と高さが負でないか検証
        if (w <= 0 || h <= 0) {
            throw std::invalid_argument("幅と高さは正の整数でなければなりません");
        }
        width = w;
        height = h;
    }

    // auto を使って戻り値の型を推論する
    auto area() const -> int {
        return width * height;
    }

    // 幅と高さのゲッターを追加(必要に応じてセッターも追加可能)
    auto getWidth() const -> int { return width; }
    auto getHeight() const -> int { return height; }
};

auto main() -> int {
    try {
        // 幅と高さに正の値を入れて rectangle を作成
        Rectangle rect(5, 3);
        std::cout << "面積: " << rect.area() << std::endl;

        // 例外を発生させるケース
        Rectangle invalidRect(-1, 3); // ここで例外が発生します
    } catch (const std::invalid_argument& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }

    return 0;
}

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

[編集]

関数テンプレート

[編集]

C++では、テンプレートを使用することで、型に依存しない汎用的な関数を定義できます。これにより、同じ関数を異なる型で使用することができます。

func1.cc
#include <iostream>

template <typename T>
auto add(T a, T b) -> T {
    return a + b;
}

auto main() -> int {
    std::cout << add(5, 3) << std::endl;       // int
    std::cout << add(2.5, 3.5) << std::endl;   // double
    std::cout << add(1.1f, 2.2f) << std::endl; // float
    return 0;
}
;func2.cc
:テンプレートを用いないジェネリックプログラミング
#include <iostream>
#include <string>

auto add(auto a, auto b) -> auto {
    return a + b;
}

auto main() -> int {
    std::cout << "5 + 3 = " << add(5, 3) << std::endl;
    std::cout << "3.1 + 4.9 = " << add(3.2, 4.9) << std::endl;
    std::cout << "std::string{\"abc\"} +  std::string{\"xyz\"} = " << std::string{"abc"} +  std::string{"xyz"} << std::endl;
    return 0;
}

スマートポインタとメモリ管理

[編集]

スマートポインタとは

[編集]

C++11以降、メモリ管理に便利な機能としてスマートポインタが導入されました。std::unique_ptrstd::shared_ptrstd::weak_ptrの3種類があり、それぞれが異なる用途に応じたメモリ管理を提供します。これらは、手動でdeleteを呼び出すことなくメモリの解放を自動的に行います。

std::unique_ptr

[編集]

std::unique_ptrは、所有権が1つのポインタにしかないことを保証します。このため、所有権が移動すると自動的にオブジェクトが破棄されます。

#include <iostream>
#include <memory>

class MyClass {
public:
    auto greet() const -> void { std::cout << "Hello, Smart Pointer!" << std::endl; }
};

auto main() -> int {
    auto ptr = std::make_unique<MyClass>(); // 型推論を活用
    ptr->greet();  // 出力: Hello, Smart Pointer!

    // ptrはスコープを抜けると自動的にメモリを解放する
    return 0;
}

std::shared_ptr

[編集]

std::shared_ptrは、複数のポインタが同じオブジェクトを共有する場合に使います。オブジェクトは、最後のshared_ptrが破棄されるまで解放されません。

#include <iostream>
#include <memory>

class MyClass {
public:
    auto greet() const -> void { std::cout << "Shared Pointer!" << std::endl; }
};

auto main() -> int {
    auto ptr1 = std::make_shared<MyClass>();  // 型推論を活用
    auto ptr2 = ptr1;  // 共有

    ptr1->greet();  // 出力: Shared Pointer!
    ptr2->greet();  // 出力: Shared Pointer!

    // ptr1とptr2が両方スコープを抜けると、自動的にメモリが解放される
    return 0;
}


高度なC++のテクニック

[編集]

C++のムーブセマンティクス

[編集]

C++11以降、ムーブセマンティクスが導入され、オブジェクトの効率的な転送(コピーではなく)を実現できるようになりました。これにより、コピーのコストを回避し、パフォーマンスを向上させることができます。

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

[編集]

ムーブセマンティクスを実現するには、ムーブコンストラクタとムーブ代入演算子を定義する必要があります。

#include <iostream>
#include <vector>
#include <memory>

class MyClass {
private:
    std::unique_ptr<int> data;

public:
    explicit MyClass(int value) : data(std::make_unique<int>(value)) {}
    
    // ムーブコンストラクタ
    MyClass(MyClass&& other) noexcept = default;

    // コピーコンストラクタとコピー代入演算子は削除
    MyClass(const MyClass& other) = delete;
    MyClass& operator=(const MyClass& other) = delete;

    void show() const { std::cout << *data << std::endl; }
};

auto main() -> int {
    std::vector<MyClass> vec;
    vec.push_back(MyClass(10));  // ムーブセマンティクスが発生

    // ここでMyClass(10)がムーブされる
    vec[0].show();  // 出力: 10
    return 0;
}

C++の標準ライブラリを活用する

[編集]

std::algorithmを使った処理

[編集]

C++の標準ライブラリには、多くのアルゴリズムが用意されており、これらを活用することで、効率的なプログラムを構築できます。例えば、ソートや検索など、よく使う処理はstd::algorithmに標準で提供されています。

ソート

[編集]
#include <iostream>
#include <algorithm>
#include <vector>

auto main() -> int {
    auto vec = std::vector{4, 1, 3, 9, 7};
    
    std::sort(vec.begin(), vec.end());
    
    // 出力の簡潔化
    std::for_each(vec.begin(), vec.end(), [](const auto& n) { std::cout << n << " "; });
    std::cout << std::endl;  // 出力: 1 3 4 7 9
    return 0;
}

検索

[編集]
#include <iostream>
#include <algorithm>
#include <vector>

auto main() -> int {
    auto vec = std::vector{4, 1, 3, 9, 7};
    
    if (auto it = std::find(vec.begin(), vec.end(), 3); it != vec.end()) {
        std::cout << "Found " << *it <<"!" << std::endl;
    } else {
        std::cout << "Not Found!" << std::endl;
    }
    return 0;
}

C++の例外処理

[編集]

例外の基本

[編集]

C++の例外処理は、trycatchthrowを使用して実現します。これにより、予期しないエラーが発生した場合に、プログラムの実行を適切に制御できます。

基本的な例外処理

[編集]
#include <iostream>
#include <stdexcept>

auto riskyFunction() -> void {
    throw std::runtime_error("Something went wrong!");
}

auto main() -> int {
    try {
        riskyFunction();
    } catch (const std::runtime_error& e) {
        std::cout << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

カスタム例外クラス

[編集]

C++では、ユーザー定義の例外クラスを作成して、より詳細なエラー処理を行うことができます。

#include <iostream>
#include <stdexcept>

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "My custom exception occurred!";
    }
};

auto main() -> int {
    try {
        throw MyException();
    } catch (const MyException& e) {
        std::cout << e.what() << std::endl;
    }
    return 0;
}

C++の性能と最適化

[編集]

プロファイリングと最適化

[編集]

C++のプログラムを最適化するためには、まずボトルネックを特定し、最適化すべき部分を把握することが非常に重要です。ここでは、**プロファイリングツール**を使ってプログラムのパフォーマンスを測定し、**最適化**を行う方法について説明します。

以下は、C++のプログラムをプロファイリングし、ボトルネックを見つけて最適化する実例です。

1. プログラムの例

[編集]

まず、最適化前のプログラムを紹介します。このプログラムは、ベクトル内の数値の平方を計算し、その合計を求めるものです。

#include <iostream>
#include <vector>
#include <chrono>

auto main() -> int {
    std::vector<int> vec(1'000'000, 5); // 100万個の5で埋めたベクトル

    auto start = std::chrono::high_resolution_clock::now();
    
    long long sum = 0;
    for (auto val : vec) {
        sum += val * val; // それぞれの値の平方を計算して合計
    }

    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> duration = end - start;

    std::cout << "Sum: " << sum << "\n";
    std::cout << "Time taken: " << duration.count() << " seconds\n";
    return 0;
}

このコードは、ベクトル内の各要素の平方を計算し、その合計を求めるシンプルなプログラムです。このコードに対して、プロファイリングを行い、最適化していきます。

2. プロファイリングツールの使用

[編集]

まず、このプログラムを実行し、プロファイリングツールを使って性能を測定します。ここでは、gprofを使用することを例に説明します。gprofはGNUのプロファイリングツールで、プログラムのどの部分がどれだけ実行されているかを計測できます。

gprofの使い方
[編集]
  1. コンパイル時にプロファイリングオプションを有効にする
    プログラムを-pgオプションを使ってコンパイルします。
    c++ -pg -o profiling_example profiling_example.cpp
    
  2. プログラムを実行する
    プログラムを実行してプロファイリングデータを生成します。
    ./profiling_example
    
  3. プロファイル結果を確認する
    プログラム実行後に生成されるgmon.outというファイルを元にプロファイリング結果を表示します。
    gprof profiling_example gmon.out > analysis.txt
    
  4. プロファイリング結果の分析
    analysis.txtにプロファイリング結果が保存されます。ボトルネックとなっている関数や処理が分かるため、最適化が必要な部分が明確になります。

3. 最適化

[編集]

プロファイリングの結果、次のような最適化を行うことができます:

アルゴリズムの改善
このプログラムでは、val * valの計算を2回行っていませんか?計算を一度だけ行って変数に格納することで無駄な計算を削減できます。
データアクセスの最適化
ベクトルのデータアクセスは連続したメモリ領域に格納されているため、キャッシュ効率が良くなりますが、他にも並列処理を導入することも検討できます。

4. 最適化後のコード例

[編集]

プロファイリング結果に基づいて、最適化を施したコードは以下の通りです。

#include <iostream>
#include <vector>
#include <chrono>

auto main() -> int {
    std::vector<int> vec(1'000'000, 5); // 100万個の5で埋めたベクトル

    auto start = std::chrono::high_resolution_clock::now();
    
    long long sum = 0;
    for (auto val : vec) {
        int square = val * val; // 一度だけ計算
        sum += square;
    }

    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> duration = end - start;

    std::cout << "Sum: " << sum << "\n";
    std::cout << "Time taken: " << duration.count() << " seconds\n";
    return 0;
}

5. 並列化による最適化

[編集]

更に、並列処理を使って性能を向上させる方法もあります。例えば、C++17以降ではstd::for_eachを使った並列化が可能です。以下のように並列処理を適用することができます。

#include <iostream>
#include <vector>
#include <chrono>
#include <execution>  // C++17以降で使用可能

auto main() -> int {
    std::vector<int> vec(1'000'000, 5);

    auto start = std::chrono::high_resolution_clock::now();

    long long sum = 0;
    std::for_each(std::execution::par, vec.begin(), vec.end(), [&sum](int val) {
        sum += val * val;
    });

    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> duration = end - start;

    std::cout << "Sum: " << sum << "\n";
    std::cout << "Time taken: " << duration.count() << " seconds\n";
    return 0;
}

ここでは、std::for_eachstd::execution::parを指定して並列実行を行っています。これにより、複数のスレッドを利用して並列に計算が行われ、パフォーマンスが向上する可能性があります。

6. コンパイラ最適化オプション

[編集]

最後に、コンパイラの最適化オプションを使って、コードを最適化することも可能です。例えば、c++を使って以下のようにコンパイルすることで、コードの最適化を行います。

c++ -O3 -o optimized_example optimized_example.cpp

-O3オプションは最適化のレベルを最大に設定します。これにより、コンパイラがコードを自動的に最適化して、実行速度が向上することが期待できます。

結論

[編集]
  • プロファイリングを行ってボトルネックを特定し、最適化することが重要です。
  • アルゴリズムの改善や**データアクセスの効率化**を行うと、プログラムのパフォーマンスが向上します。
  • 並列化を使って複数のスレッドで計算を並行して行うことでも、パフォーマンスの向上が期待できます。
  • 最後に、コンパイラの**最適化オプション**を利用して、コードの最適化を自動的に行うことも可能です。

このように、プロファイリングと最適化を適切に活用することで、C++プログラムのパフォーマンスを大幅に改善できます。

コンパイラの最適化

[編集]
c++ -O3 my_program.cpp -o my_program

-O3オプションは最適化を最大限に行い、より高速な実行を目指します。

結論

[編集]

C++の今後と未来

[編集]

C++は、依然として非常に強力で効率的なプログラミング言語です。C++11以降、言語機能は大きく進化し、今後も新しい機能や改善が期待されます。C++20、C++23など、最新バージョンでの機能拡張により、開発者はより効率的に、そして安全にコードを書くことができるようになります。

C++の学習は、非常に深い知識を要求しますが、その分、得られるスキルは大きく、システムプログラミングやアプリケーション開発において非常に有用です。