C++ へようこそ - Modern C++

C++ は、作成以来、世界で最も広く使用されているプログラミング言語の 1 つになりました。 適切に記述された C++ プログラムは、高速で効率的です。 言語は他の言語よりも柔軟性が高く、最高レベルの抽象化で動作し、シリコンのレベルで動作します。 C++ によって、高度に最適化された標準ライブラリが提供されます。 これにより、低レベルのハードウェア機能にアクセスして、速度を最大化し、メモリ要件を最小限に抑えることができます。 C++ では、ゲーム、デバイス ドライバー、HPC、クラウド、デスクトップ、埋め込み、モバイル アプリなど、ほぼすべての種類のプログラムを作成できます。 他のプログラミング言語のライブラリやコンパイラさえも C++ で作成されます。

C++ の最初の要件の 1 つは、C 言語との下位互換性でした。 その結果、C++ を使用すると常に、生ポインター、配列、null で終了する文字列、その他の機能を使用して、C スタイルでプログラミングすることができます。 これらを使用すると、優れたパフォーマンスを実現できますが、バグや複雑さを引き起こす可能性もあります。 C++ は、その進化に伴って機能が強化され、C スタイルのイディオムを使用する必要性が大幅に低減されています。 従来の C プログラミング機能は、必要な時に引き続き使用することができます。 ただし、最新の C++ コードでは、それらを必要とすることは少なくなってきているはずです。 Modern C++ コードは、よりシンプル化され、安全性が向上し、より洗練され、しかもこれまでにないほど高速です。

以下のセクションでは、Modern C++ の主な機能の概要について説明します。 特に明記されていない限り、ここに記載される機能は、C++ 11 以降で使用できます。 Microsoft C++ コンパイラでは、/std コンパイラ オプションを設定して、プロジェクトに使用する標準のバージョンを指定できます。

リソースとスマート ポインター

C スタイルのプログラミングでバグの主なクラスの 1 つは、"メモリ リーク" です。 多くの場合、リークは、new で割り当てられたメモリに対する delete の呼び出しに失敗したことが原因で発生します。 Modern C++ では、"リソース取得は初期化である" (RAII) の原則が重視されています。 考え方はシンプルです。 リソース (ヒープ メモリ、ファイル ハンドル、ソケットなど) は、オブジェクトによって "所有される" 必要があります。 そのオブジェクトによって、そのコンストラクターで新たに割り当てられたリソースが作成または受け取られるか、デストラクターで削除されます。 RAII の原則により、所有しているオブジェクトが範囲外になったときにすべてのリソースがオペレーティング システムに返されることが保証されます。

RAII 原則を簡単に導入できるように、C++ 標準ライブラリには、3 つの種類のスマート ポインター (std::unique_ptrstd::shared_ptrstd::weak_ptr) が用意されています。 スマート ポインターは、所有しているメモリの割り当てと削除を処理します。 次の例は、make_unique() の呼び出しでヒープに割り当てられる配列メンバーを持つクラスを示しています。 newdelete の呼び出しは、unique_ptr クラスによってカプセル化されます。 widget オブジェクトが範囲外になると、unique_ptr デストラクターが呼び出され、配列に割り当てられたメモリが解放されます。

#include <memory>
class widget
{
private:
    std::unique_ptr<int[]> data;
public:
    widget(const int size) { data = std::make_unique<int[]>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);  // lifetime automatically tied to enclosing scope
                        // constructs w, including the w.data gadget member
    // ...
    w.do_something();
    // ...
} // automatic destruction and deallocation for w and w.data

可能な限り、スマート ポインターを使用してヒープ メモリを管理します。 new および delete 演算子を明示的に使用する必要がある場合は、RAII の原則に従ってください。 詳細については、「オブジェクトの有効期間とリソースの管理 (RAII)」を参照してください。

std::string および std::string_view

C スタイルの文字列は、バグのもう 1 つの主な原因です。 std::string および std::wstring を使用すると、C スタイルの文字列に関連するほとんどすべてのエラーを取り除くことができます。 また、検索、追加、先頭への追加などのメンバー関数の利点も得られます。 どちらも速度について高度に最適化されています。 読み取り専用アクセスのみが必要とされる関数に文字列を渡す場合、C++ 17 では、std::string_view を使用して、パフォーマンスをさらに向上させることができます。

std::vector およびその他の標準ライブラリのコンテナー

標準ライブラリのコンテナーはすべて、RAII の原則に従います。 これらは、要素を安全に走査するための反復子を提供します。 さらに、これらは、パフォーマンスについて高度に最適化されており、正確性について徹底的にテストされています。 これらのコンテナーを使用すると、カスタム データ構造で発生する可能性のあるバグや非効率性が排除されます。 C++ では、生の配列の代わりに vector をシーケンシャル コンテナーとして使用します。

vector<string> apples;
apples.push_back("Granny Smith");

既定の連想コンテナーとしては、(unordered_map ではなく) map を使用します。 縮退および複数ケースには、setmultimapmultiset を使用します。

map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";

パフォーマンスの最適化が必要な場合は、次の使用を検討します。

  • 順序指定されていない連想コンテナー (例: unordered_map)。 これらの、要素ごとのオーバーヘッドが軽減され、一定時間ルックアップを使用できますが、正確かつ効率的に使用するのは困難な場合があります。
  • 並べ替えられた vector。 詳細については、「アルゴリズム」を参照してください。

C スタイルの配列は使用しないでください。 データへの直接アクセスを必要とする古い API については、代わりに、f(vec.data(), vec.size()); などのアクセサー メソッドを使用します。 コンテナーの詳細については、「C++ 標準ライブラリのコンテナー」を参照してください。

標準ライブラリのアルゴリズム

プログラムのカスタム アルゴリズムを記述する必要があると思う前に、まず、C++ 標準ライブラリのアルゴリズムを確認してください。 標準ライブラリには、検索、並べ替え、フィルター処理、ランダム化などの多くの一般的な操作のためのさまざまなアルゴリズムが含まれており、その数は増え続けています。 数値演算ライブラリは広範囲にわたります。 C++ 17 以降では、多くのアルゴリズムの並列バージョンが提供されています。

次に重要な例を示します。

  • for_each: 既定の走査アルゴリズム (範囲ベースの for ループと共に)。
  • transform: コンテナー要素の非インプレース変更用
  • find_if: 既定の検索アルゴリズム。
  • sortlower_bound、その他の既定の並べ替えおよび検索アルゴリズム。

比較子を記述するには、厳格な < を使用し、可能な場合は、"名前付きラムダ式" を使用します。

auto comp = [](const widget& w1, const widget& w2)
     { return w1.weight() < w2.weight(); }

sort( v.begin(), v.end(), comp );

auto i = lower_bound( v.begin(), v.end(), widget{0}, comp );

明示的な型名の代わりとなる auto

C++ 11 に、変数、関数、テンプレート宣言で使用するための auto キーワードが導入されました。 auto を使用して、オブジェクトの型を推測するようにコンパイラに指示します。これにより、型を明示的に入力する必要がなくなります。 auto は、推測された型が入れ子になったテンプレートである場合に特に役立ちます。

map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++

範囲ベースの for ループ

C スタイルによる配列およびコンテナーの反復処理では、インデックス処理エラーが発生しやすく、入力も面倒です。 このようなエラーを排除し、コードを読みやすくするには、標準ライブラリのコンテナーと生の配列の両方で範囲ベースの for ループを使用します。 詳細については、「範囲ベースの for ステートメント」を参照してください。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {1,2,3};

    // C-style
    for(int i = 0; i < v.size(); ++i)
    {
        std::cout << v[i];
    }

    // Modern C++:
    for(auto& num : v)
    {
        std::cout << num;
    }
}

マクロの代わりとなる constexpr

C および C++ のマクロは、コンパイルの前にプリプロセッサによって処理されるトークンです。 マクロ トークンの各インスタンスは、ファイルがコンパイルされる前に、定義された値または式に置き換えられます。 C スタイルのプログラミングによって、マクロは、コンパイル時定数値を定義するために一般的に使用されます。 しかし、マクロはエラーが発生しやすく、デバッグが困難です。 Modern C++ では、コンパイル時定数に constexpr 変数を使用することをお勧めします。

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

一様初期化

Modern C++ では、どの型に対しても中かっこ初期化を使用できます。 この形式の初期化は、配列、ベクター、またはその他のコンテナーを初期化する場合に特に便利です。 次の例では、v2S の 3 つのインスタンスで初期化されます。 v3 は、中かっこを使用して自身が初期化される S の 3 つのインスタンスで初期化されます。 コンパイラは、v3 の宣言された型に基づいて各要素の型を推測します。

#include <vector>

struct S
{
    std::string name;
    float num;
    S(std::string s, float f) : name(s), num(f) {}
};

int main()
{
    // C-style initialization
    std::vector<S> v;
    S s1("Norah", 2.7);
    S s2("Frank", 3.5);
    S s3("Jeri", 85.9);

    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);

    // Modern C++:
    std::vector<S> v2 {s1, s2, s3};

    // or...
    std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

}

詳細については、「かっこ初期化」を参照してください。

移動セマンティクス

Modern C++ によって、"移動セマンティクス" が提供されるため、不要なメモリのコピーを排除することができます。 この言語の以前のバージョンでは、特定の状況下で、コピーを避けることができませんでした。 "移動" 操作により、コピーを作成することなく、リソースの所有権はあるオブジェクトから次のオブジェクトに転送されます。 一部のクラスは、ヒープ メモリ、ファイル ハンドルなどのリソースを所有します。 リソースを所有するクラスを実装すると、その "移動コンストラクター" と "移動代入演算子" を定義できます。 コピーが不要な場合、コンパイラによって、オーバーロード解決時に、これらの特殊なメンバーが選択されます。 標準ライブラリのコンテナー型では、移動コンストラクターが定義されている場合、オブジェクトのそれが呼び出されます。 詳細については、「移動コンストラクターと移動代入演算子 (C++)」を参照してください。

ラムダ式

C スタイルのプログラミングでは、"関数ポインター" を使用して関数を別の関数に渡すことができます。 関数ポインターは、保守や解釈には不便です。 それらが参照する関数は、呼び出される位置から遠く離れた、ソース コードの他の場所で定義されている可能性があります。 また、それらは、タイプセーフではありません。 Modern C++ によって、"関数オブジェクト" が提供されます。これは、operator() 演算子をオーバーライドするクラスであり、関数と同様に呼び出すことができます。 関数オブジェクトを作成する最も便利な方法は、インライン ラムダ式を使用することです。 次の例は、ラムダ式を使用して関数オブジェクトを渡して、ベクター内の各要素で find_if 関数を呼び出す方法を示しています。

    std::vector<int> v {1,2,3,4,5};
    int x = 2;
    int y = 4;
    auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });

ラムダ式 [=](int i) { return i > x && i < y; } は、"型 int の単一の引数を受け取り、その引数が x より大きく、y より小さいかどうかを示すブール値を返す関数" として読み取ることができます。ラムダ式では、周囲のコンテキストから変数 xy を使用できることに注意してください。 [=] は、それらの変数が値によって "キャプチャされる" ことを指定します。つまり、ラムダ式には、それらの値の独自のコピーが含まれます。

例外

Modern C++ では、エラー状況を報告および処理する最善の方法として、エラー コードよりも例外を重視しています。 詳細については、「例外とエラー処理に関する最新の C++ のベストプラクティス」を参照してください。

std::atomic

スレッド間の通信メカニズムには、C++ 標準ライブラリのstd::atomic 構造体とそれに関連する型を使用します。

std::variant (C++ 17)

C スタイルのプログラミングでは、一般的に、共用体を使用して、さまざまな型のメンバーが同じメモリ位置を専有できるようにして、メモリを節約します。 しかし、共用体はタイプセーフではなく、プログラミング エラーも発生しやすくなります。 C++ 17 では、共用体に代わる、より堅牢で安全な方法として、std::variant クラスが導入されています。 std::visit 関数を使用すると、タイプセーフな方法で、variant 型のメンバーにアクセスできます。

関連項目

C++ 言語リファレンス
ラムダ式
C++ 標準ライブラリ
Microsoft C/C++ 言語の準拠