Featured image of post C++ Way

C++ Way

目次

背景

  • 高速なアプリケーションを動かすには C/C++ が有用
  • Python だけでは重くなる処理があり、C++ を使う必要が出てきた
  • この記事では、C++ が C と比べて何を追加しているのか、C++ らしい考え方を整理する

C++とは

C++ は、C をベースにしつつ、より大規模なプログラムを書きやすくするための機能を追加した言語。

C に対して、主に以下のような機能が追加されている。

  • クラス
  • コンストラクタ / デストラクタ
  • 参照
  • 関数オーバーロード
  • 名前空間
  • テンプレート
  • STL
  • 例外
  • virtual によるポリモーフィズム
  • RAII
  • スマートポインタ
  • move semantics
  • constexpr

C++ は単に「Cにクラスを足した言語」ではなく、
リソース管理、抽象化、型安全性、汎用プログラミングを強く意識した言語。


CとC++の大きな違い

C は、比較的シンプルな言語。

主に、

  • 関数
  • 構造体
  • ポインタ
  • 手動メモリ管理

を使ってプログラムを書く。

一方 C++ は、C の機能に加えて、

  • オブジェクト指向
  • ジェネリックプログラミング
  • RAII による自動的なリソース管理
  • 標準ライブラリによる便利なデータ構造
  • コンパイル時処理

などを使える。

つまり C++ は、C よりも高い抽象度で書ける。

ただし、抽象度が高い一方で、機能が多く、言語仕様もかなり複雑。


C++の基本思想

C++ でよく言われる考え方に、zero-overhead abstraction がある。

これはざっくり言うと、

使わない機能のコストは払わない。
使う機能も、手書きと同じくらい効率的であるべき。

という考え方。

つまり C++ は、抽象化しながらも、できるだけ実行時の無駄を増やさないことを目指している。

たとえば std::vector やテンプレートは高級な仕組みに見えるが、うまく使えばかなり効率よく動く。


クラス

C++ の大きな特徴の1つがクラス。

クラスは、データと処理をまとめる仕組み。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Counter {
private:
    int value;

public:
    Counter() : value(0) {}

    void increment() {
        value++;
    }

    int get() const {
        return value;
    }
};

使う側はこう。

1
2
3
4
Counter c;
c.increment();

int v = c.get();

C の struct は基本的にデータをまとめるものだったが、C++ の class はデータと関数を一緒に持てる。


structclass の違い

C++ では struct もメンバ関数を持てる。

1
2
3
4
5
6
7
8
9
struct Point {
    int x;
    int y;

    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
};

C++ の structclass の主な違いは、デフォルトのアクセス指定。

1
2
struct = デフォルト public
class  = デフォルト private

つまり、以下は public。

1
2
3
struct A {
    int x;
};

以下は private。

1
2
3
class B {
    int x;
};

コンストラクタ

コンストラクタは、オブジェクトが作られるときに自動で呼ばれる関数。

1
2
3
4
5
6
7
class User {
private:
    std::string name;

public:
    User(const std::string& n) : name(n) {}
};

使う側。

1
User user("Alice");

C では初期化処理を自分で関数として呼ぶことが多い。

1
user_init(&user);

C++ では、オブジェクト生成時に初期化処理を自動で走らせることができる。


デストラクタ

デストラクタは、オブジェクトが破棄されるときに自動で呼ばれる関数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class File {
public:
    File() {
        // ファイルを開く
    }

    ~File() {
        // ファイルを閉じる
    }
};

C++ では、スコープを抜けたときに自動でデストラクタが呼ばれる。

1
2
3
{
    File f;
} // ここで f のデストラクタが呼ばれる

この仕組みが、C++ の RAII につながる。


RAII

RAII は Resource Acquisition Is Initialization の略。

日本語にすると少し分かりにくいが、ざっくり言うと、

リソースの確保と解放を、オブジェクトの寿命に結びつける

という考え方。

たとえば、ファイルを開いたら必ず閉じる必要がある。

C ではこう書く。

1
2
3
4
5
6
7
8
9
FILE* fp = fopen("data.txt", "r");

if (fp == NULL) {
    return;
}

/* 何か処理 */

fclose(fp);

でも途中で return したり、エラーが起きたりすると、fclose を忘れる可能性がある。

C++ では、オブジェクトのデストラクタに後始末を書ける。

1
2
3
4
5
{
    std::ifstream file("data.txt");

    // 何か処理
} // スコープを抜けると自動で close される

このように、リソース解放を自動化できるのが RAII。

C++ ではかなり重要な考え方。


参照

C++ には参照がある。

1
2
3
void increment(int& x) {
    x++;
}

使う側。

1
2
int a = 10;
increment(a);

結果として a11 になる。

C ならポインタを使うことが多い。

1
2
3
void increment(int* x) {
    (*x)++;
}

C++ の参照は、ポインタよりも「別名」に近い。

1
2
3
4
5
int a = 10;
int& ref = a;

ref = 20;
// a も 20 になる

参照を使うと、ポインタより読みやすく書ける場面がある。


const

C++ では const がよく使われる。

1
2
3
void print(const std::string& s) {
    std::cout << s << std::endl;
}

これは、

1
2
s をコピーせずに参照で受け取る
ただし中身は変更しない

という意味。

C++ では、大きなオブジェクトを関数に渡すときに、

1
const T&

で受け取ることがよくある。


関数オーバーロード

C++ では、同じ名前の関数を引数違いで複数定義できる。

1
2
3
4
5
6
7
int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

使う側。

1
2
add(1, 2);       // int版
add(1.5, 2.5);   // double版

C では同じ名前の関数を複数定義できない。

C++ では、引数の型や数によって呼び分けられる。


名前空間

C++ には名前空間がある。

名前の衝突を防ぐための仕組み。

1
2
3
4
5
namespace math {
    int add(int a, int b) {
        return a + b;
    }
}

使う側。

1
int result = math::add(1, 2);

標準ライブラリの std::vectorstd::stringstd も名前空間。

1
2
std::vector<int> v;
std::string s;

テンプレート

テンプレートは、型をパラメータにできる仕組み。

1
2
3
4
template <typename T>
T add(T a, T b) {
    return a + b;
}

使う側。

1
2
add<int>(1, 2);
add<double>(1.5, 2.5);

型を省略できることもある。

1
2
add(1, 2);
add(1.5, 2.5);

テンプレートを使うと、型に依存しない汎用的なコードを書ける。

C++ の STL もテンプレートを多用している。


STL

STLとは

STL は Standard Template Library の略。

C++ 標準ライブラリのうち、特に汎用データ構造とアルゴリズムの仕組みを指す言葉。

現在はやや広めに、「C++ 標準ライブラリの便利な部品群」くらいの意味で使われることも多い。

STL の中心になるのは以下。

  • コンテナ
  • アルゴリズム
  • イテレータ

コンテナ

コンテナは、データを入れる箱。

代表例。

  • std::vector
    • 可変長配列
  • std::string
    • 文字列
  • std::map
    • キーと値の対応表
  • std::unordered_map
    • ハッシュマップ
  • std::set
    • 重複なし集合
  • std::deque
    • 両端キュー
  • std::list
    • 双方向リスト

例。

1
2
3
#include <vector>

std::vector<int> v = {1, 2, 3};

アルゴリズム

アルゴリズムは、データを処理する関数群。

代表例。

  • std::sort
  • std::find
  • std::copy
  • std::remove
  • std::transform

例。

1
2
3
4
5
6
#include <vector>
#include <algorithm>

std::vector<int> v = {3, 1, 2};

std::sort(v.begin(), v.end());

この場合、

  • std::vector がコンテナ
  • std::sort がアルゴリズム

イテレータ

イテレータは、コンテナの中身をたどる仕組み。

代表的には以下を使う。

1
2
begin()
end()

例。

1
2
3
4
5
6
7
8
#include <vector>
#include <iostream>

std::vector<int> v = {1, 2, 3};

for (auto it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << std::endl;
}

vector でも set でも、似た形で処理できるのはイテレータのおかげ。


例外

C++ には例外処理がある。

1
2
3
4
5
6
7
8
9
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("division by zero");
    }

    return a / b;
}

受け取る側。

1
2
3
4
5
try {
    int x = divide(10, 0);
} catch (const std::exception& e) {
    std::cout << e.what() << std::endl;
}

C ではエラーコードで失敗を返すことが多い。

1
2
3
4
5
int result = do_something();

if (result != 0) {
    // エラー処理
}

C++ では、エラーを例外として伝えることができる。

ただし、プロジェクトによっては例外を禁止することもある。


継承

C++ には継承がある。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Animal {
public:
    void eat() {
        std::cout << "eat" << std::endl;
    }
};

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

使う側。

1
2
3
Dog d;
d.eat();
d.bark();

DogAnimal の機能を引き継ぐ。

ただし、C++ では継承を使いすぎると設計が複雑になりやすい。

近年は、継承よりも合成を好む設計も多い。


virtual とポリモーフィズム

C++ では virtual 関数を使うことで、実行時に呼び出す関数を切り替えられる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Animal {
public:
    virtual void speak() {
        std::cout << "..." << std::endl;
    }
};

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

使う側。

1
2
3
Animal* a = new Dog();
a->speak(); // Dog::speak が呼ばれる
delete a;

このように、基底クラスのポインタ経由で派生クラスの関数を呼べる。

これをポリモーフィズムという。


vtable

C++ の virtual 関数は、実装上 vtable のような仕組みで実現されることが多い。

vtable は、仮想関数の呼び出し先を管理するための表。

ただし、vtable は C++ の言語仕様上「必ずこの形で実装しなければならない」と決まっているものではない。

あくまで、多くのコンパイラで使われる実装上の仕組み。

C には virtual やクラスがないため、言語機能としての vtable はない。

ただし C でも、関数ポインタを使えば vtable のような仕組みを手動で作れる。


スマートポインタ

C++ では、手動で new / delete を直接使うより、スマートポインタを使うことが多い。

代表例。

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

std::unique_ptr

所有者が1つだけのポインタ。

1
2
3
#include <memory>

std::unique_ptr<int> p = std::make_unique<int>(10);

スコープを抜けると、自動で解放される。

1
2
3
{
    auto p = std::make_unique<int>(10);
} // ここで自動解放

std::shared_ptr

複数箇所で所有を共有するポインタ。

1
2
3
4
#include <memory>

std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::shared_ptr<int> p2 = p1;

最後の所有者が消えたタイミングで解放される。

ただし、循環参照には注意が必要。


move semantics

C++11 以降では、move semantics が重要。

ざっくり言うと、

1
重いコピーを避けて、中身を移動する仕組み

たとえば std::vectorstd::string のような大きなデータを扱うとき、コピーすると重い。

move を使うと、内部データの所有権を移して効率化できる。

1
2
std::string a = "hello";
std::string b = std::move(a);

このあと、b が元の文字列データを受け取る。

a は有効なオブジェクトではあるが、中身は使わない方がよい。


auto

C++ では auto を使うと、型推論できる。

1
2
3
auto x = 10;        // int
auto y = 1.5;       // double
auto s = "hello";   // const char*

STL と組み合わせると便利。

1
2
3
4
5
std::vector<int> v = {1, 2, 3};

for (auto x : v) {
    std::cout << x << std::endl;
}

イテレータの型は長くなりがちなので、auto がよく使われる。


範囲for文

C++ では範囲for文が使える。

1
2
3
4
5
std::vector<int> v = {1, 2, 3};

for (int x : v) {
    std::cout << x << std::endl;
}

参照で受け取ることもできる。

1
2
3
for (int& x : v) {
    x *= 2;
}

変更しないなら const 参照がよく使われる。

1
2
3
for (const auto& x : v) {
    std::cout << x << std::endl;
}

constexpr

constexpr は、コンパイル時に計算できることを表す。

1
2
3
4
5
constexpr int square(int x) {
    return x * x;
}

constexpr int value = square(5);

この場合、value はコンパイル時に計算できる。

C++ では、実行時だけでなくコンパイル時に処理を行う仕組みが増えている。


C++ の便利さ

C++ の便利なところは、主に以下。

  • C に近い低レベル制御ができる
  • クラスでデータと処理をまとめられる
  • RAII によってリソース管理を自動化できる
  • STL によって便利なデータ構造を使える
  • テンプレートで汎用的なコードを書ける
  • const や型システムで安全性を高められる
  • 必要ならかなり高速に書ける

C++ の難しさ

一方で、C++ はかなり難しい言語でもある。

理由は以下。

  • 機能が多い
  • 古い書き方と新しい書き方が混在している
  • ポインタ、参照、所有権の理解が必要
  • コンパイルエラーが長くなりがち
  • テンプレート周りが複雑
  • ABI やビルドシステムの知識も必要になる
  • new / delete を雑に使うと危険
  • 未定義動作がある

そのため、現代的な C++ では、なるべく以下を使うことが多い。

  • std::vector
  • std::string
  • std::unique_ptr
  • std::shared_ptr
  • auto
  • RAII
  • STL アルゴリズム

逆に、できるだけ避けたいもの。

  • 生の new / delete
  • 不要な生ポインタ所有
  • 手動のメモリ管理
  • 巨大な継承階層
  • グローバル変数の多用

まとめ

C++ は、C をベースにしながら、より大規模で安全なプログラムを書きやすくするための機能を追加した言語。

一言でまとめると、

C++ は、Cの低レベル制御に、抽象化・型安全・リソース管理・汎用プログラミングを足した言語。

さらにもう少しかみ砕くと、

  • クラスでデータと処理をまとめる
  • コンストラクタで初期化する
  • デストラクタで後始末する
  • RAII でリソース管理を自動化する
  • 参照でポインタより自然に値を渡せる
  • テンプレートで汎用的なコードを書ける
  • STL で便利なデータ構造とアルゴリズムを使える
  • virtual で実行時ポリモーフィズムを使える
  • スマートポインタでメモリ管理を安全にしやすい
  • move semantics で重いコピーを避けられる

C++ は覚えることが多いが、
「RAII」「STL」「所有権」「型を活かす」 を意識すると、かなり C++ らしい書き方に近づく。

Built with Hugo
テーマ StackJimmy によって設計されています。