T.TAO
ブログへ戻る
/16 min read/Programming

C++ #9 C++11 の新機能

#C++#Programming#ComputerSystems

このノートでは C++11 の新機能を扱います。

前の規格から 8 年が空いた C++11 は、「別の言語のようだ」と言われるほど変化が大きいものでした。導入されたのは糖衣構文だけではありません。ムーブセマンティクスは値渡しのコストモデルを変え、スマートポインタは資源管理の既定のやり方を変え、ラムダは標準ライブラリのアルゴリズムを実際に使えるものにしました。以下はアルファベット順ではなく、おおむね重要度の順に見ていきます。

型推論 —— auto

auto は初期化子から変数の型を推論させ、コードを簡潔にし、冗長な型宣言を減らします。

C++auto x = 10;                          // int
auto it = vec.begin();                // std::vector<int>::iterator を書かずに済む
auto p = std::make_unique<Widget>();  // std::unique_ptr<Widget>

推論規則には注意が必要です。auto参照とトップレベル const を落とします

C++const std::string s = "hello";
auto  a = s;         // std::string(コピーされ、const も消える)
auto& b = s;         // const std::string&(保たれる)
const auto& c = s;   // const std::string&。読み取り専用走査の定番

コンテナの走査ではこれが決定的です。for (auto x : vec) は各要素をコピーします。中身が std::string なら実際のコストになります。既定は for (const auto& x : vec) とすべきです。

とはいえ auto は多ければよいものでもありません。返る型が自明でないとき(auto result = compute();)、読み手は定義まで飛ばないと何を握っているのか分かりません。経験則は、型が自明、あるいは書くのが煩雑なときは auto、それ以外は明示する、です。

範囲 for ループ

コンテナを走査する簡潔な構文です。

C++std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto& elem : vec) {
    std::cout << elem << std::endl;
}

これは begin() / end() を使う通常のループに展開されます。この二つのメンバ(あるいは ADL で見つかる自由関数)を提供する型なら、自作の型を含めて何でも使えます。

落とし穴がひとつ。範囲 for の中でコンテナのサイズを変えてはいけません。 vec.push_back() は再確保を起こしうるため内部のイテレータが無効になり、動作は未定義になります。

スマートポインタ

C++11 は動的メモリを自動管理する std::unique_ptrstd::shared_ptr を導入し、手動解放に伴う誤りを避けられるようにしました。

  • std::unique_ptr — 排他的な所有権。ムーブはできてもコピーはできません。オーバーヘッドはゼロで、大きさは生ポインタと同じ、デストラクタは delete 一回です。これを既定の選択にすべきです。
  • std::shared_ptr — 共有所有権。参照カウントが寿命を管理します。無償ではありません。オブジェクトとは別に制御ブロックがあり、カウントの増減はアトミック操作です。
  • std::weak_ptr — 所有しない観測者。shared_ptr の循環参照を断ち切るために使います。
C++auto p = std::make_unique<Widget>(args...);   // make_unique は C++14 から
auto q = std::make_shared<Widget>(args...);

void consume(std::unique_ptr<Widget> w);
consume(std::move(p));    // 所有権が移り、p は空になる

循環参照について。二つのオブジェクトが互いに shared_ptr を持つと、カウントがゼロになることはなく、メモリは漏れます。片方向を weak_ptr にすれば解決します。親子構造では、親から子へは shared_ptr、子から親へは weak_ptr が慣例です。

ラムダ式

ラムダ式は無名関数を簡潔に定義でき、コールバックや小さな計算に特に向いています。

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

完全な形は [キャプチャ](引数) 修飾子 -> 戻り値型 { 本体 } で、キャプチャリストが外側のどの変数を見られるかを決めます。

C++int factor = 3;
auto f1 = [factor](int x)  { return x * factor; };   // 値キャプチャ:スナップショット
auto f2 = [&factor](int x) { return x * factor; };   // 参照キャプチャ:現在の値
auto f3 = [=](int x)       { return x * factor; };   // すべて値で
auto f4 = [&](int x)       { return x * factor; };   // すべて参照で

参照キャプチャは寿命に注意が要ります。 ラムダをメンバや非同期タスクに保存した一方で、キャプチャしたローカル変数が既にスコープを抜けていれば、それはダングリング参照です。既定は値キャプチャとし、外側の状態を変更する必要があるときだけ参照に切り替えてください。

ラムダの実体は、コンパイラが生成する無名クラスです。キャプチャはそのメンバになり、本体は operator() になります。したがって各ラムダはそれぞれ固有の型を持ちます。見た目が同じ二つのラムダでも型は異なり、だからこそ auto で受けます。統一した型が必要なときは std::function を使いますが、型消去のコストが伴います。

ムーブセマンティクスと右辺値参照

C++11 は && で表す右辺値参照とムーブセマンティクスを導入しました。ムーブセマンティクスにより、不要なコピーを避けて性能を上げられます。

右辺値参照は一時オブジェクト(右辺値)に束縛でき、ムーブコンストラクタやムーブ代入演算子を通じて資源を移します。

C++std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1);
// vec1 の資源が vec2 へ移り、コピーは発生しない

左辺値と右辺値の見分け方には簡単な目安があります。アドレスを取れるものが左辺値です。 名前のついた変数は常に左辺値で、宣言された型が右辺値参照であっても変わりません。

C++void f(std::string&& s) {
    g(s);              // s は名前付きなので、渡るのは左辺値!
    g(std::move(s));   // これで g のムーブ版に届く
}

この一点が、ムーブセマンティクスのバグのほぼすべての出どころです。ムーブコンストラクタの正しい書き方、noexcept が決定的である理由、Rule of Five については《C++ #7 コピー制御と演算子オーバーロード》を参照してください。

完全転送

テンプレート中の T&& は右辺値参照ではなく転送参照です。左辺値を渡せば T&、右辺値を渡せば T と推論されます。std::forward と組み合わせれば、引数の値カテゴリをそのまま通せます。

C++template <typename T, typename... Args>
std::unique_ptr<T> make(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

std::move と同じく std::forward もキャストにすぎません。違いは、move が無条件に右辺値へ変換するのに対し、forward は元が右辺値だったときにだけ変換する点です。

nullptr キーワード

C++11 は空ポインタ定数として nullptr を導入し、NULL を置き換えてより強い型安全性を与えました。

問題は NULL がたいてい単なる 0 であることです。

C++void f(int);
void f(char*);

f(NULL);      // f(int) が呼ばれる! ほぼ確実に意図と違う
f(nullptr);   // f(char*) が呼ばれる

nullptr の型は std::nullptr_t で、任意のポインタ型へ暗黙変換できますが、整数へは変換されません。新しいコードに NULL は登場すべきではありません。

using キーワードと型エイリアス

using は型エイリアスを作り、従来の typedef を置き換えます。テンプレートの文脈では特に読みやすくなります。

C++using int_ptr = int*;
typedef int* int_ptr_old;             // 等価

真の利点は、typedef では表現できないエイリアステンプレートです。

C++template <typename T>
using Dict = std::map<std::string, T>;

Dict<int> counts;    // std::map<std::string, int>

また using は左から右へ読めるため、関数ポインタのような複雑な宣言がはるかに明快になります。

C++typedef void (*Callback)(int, const char*);    // 中央から外へ読む必要がある
using   Callback = void (*)(int, const char*); // 名前が左、型が右

constexpr キーワード

constexpr はコンパイル時定数を定義します。const と違い、constexpr は式がコンパイル時に評価されることを保証します。

C++constexpr int square(int x) {
    return x * x;
}
constexpr int result = square(5);   // コンパイル中に 25 が求まる

この区別ははっきり述べる価値があります。const は「この変数はあとで変更されない」という読み取り専用性を、constexpr は「この値はコンパイル時に決まる」というコンパイル時評価可能性を意味します。const int n = readFromFile(); は合法ですが、constexpr int n = readFromFile(); は違います。

C++11 の constexpr 関数は制約が厳しく、実質的に return 文ひとつだけで、ループを含むものは再帰にする必要がありました。C++14 でこれは緩和されます。《C++ #10 C++14 の新機能》を参照してください。

可変長テンプレート

可変長テンプレートは、任意の個数・任意の型の引数を受け取れるテンプレートです。printf のような型安全でない可変引数に、ついに代替が与えられました。

C++template <typename... Args>
void log(const Args&... args);

typename... Argsテンプレートパラメータパックargs...関数パラメータパックです。C++11 でパックを展開するには、再帰と終端のオーバーロードが必要でした。

C++void print() {}                        // 終端条件

template <typename T, typename... Rest>
void print(const T& first, const Rest&... rest) {
    std::cout << first << ' ';
    print(rest...);                    // ひとつ剥がして残りを再帰処理
}

print(1, "hello", 3.14);               // 出力:1 hello 3.14

sizeof...(Args) はパックの要素数を返します。C++17 ではこの再帰が畳み込み式の一行に縮みます。(std::cout &lt;&lt; ... &lt;&lt; args);

可変長テンプレートのもっとも重要な用途は、実は標準ライブラリの内部です。std::make_uniquestd::tupleemplace_back はいずれもこれに依存し、完全転送と組み合わせて任意のコンストラクタ引数を対象の型へそのまま届けます。

明示的な default と delete

C++11 では、コンパイラが生成する特別メンバ関数を明示的に要求したり禁止したりできます。

C++class Widget {
public:
    Widget() = default;                          // デフォルトコンストラクタが欲しい
    Widget(const Widget&) = delete;              // コピーを禁止する
    Widget& operator=(const Widget&) = delete;
    Widget(Widget&&) noexcept = default;         // 生成されるムーブ版で構わない
};

以前のコピー禁止は、コピーコンストラクタを private に宣言して定義しないという手法でした。エラーはリンク時に、しかも分かりにくいメッセージで出ます。= delete はそれを明快なコンパイルエラーに変えます。

= delete は特別メンバに限らず、不要なオーバーロードの抑制にも使えます。

C++void process(int);
void process(char) = delete;    // char が int へ昇格するのを防ぐ

そのほか知っておきたいもの

overridefinal オーバーライドする仮想関数を明示するため、シグネチャを書き間違えたときに、新しい関数が黙って作られるのではなくコンパイルエラーになります。《C++ #8 ポリモーフィズム》で述べた中でもっとも実用的な防御策です。

統一初期化。 {} 構文はあらゆる型で使え、縮小変換を禁止します。int x{3.14}; はエラーですが、int x = 3.14; は警告にとどまります。std::initializer_list のオーバーロードとの相互作用には注意してください。std::vector&lt;int> v{3, 1} は要素 2 つであって、1 が 3 つではありません。

enum class 強い型付けの列挙で、整数へ暗黙変換されず、列挙子が外側のスコープへ漏れません。素の enum の名前衝突問題はこれで解消します。

標準ライブラリのスレッド対応。 std::threadstd::mutexstd::atomicstd::future がメモリモデルとともに加わり、C++ は初めてプラットフォーム非依存の並行性モデルを手にしました。

std::arraystd::unordered_map 固定長配列のオーバーヘッドのないラッパーと、ハッシュに基づく連想コンテナ。

まとめ

C++11 のうち、コードの書き方を実際に変えたのは三つです。スマートポインタは生の new/delete をコードの匂いに変え、ムーブセマンティクスは値で返すことのコストを考えずに済むようにし、ラムダは標準ライブラリのアルゴリズムを「理屈の上では使える」から「実際に快適」へ引き上げました。残りの機能の多くは、この三つを支えるために存在しています。

このシリーズの記事

C++ プログラミング
  1. 01C++ #1 データとメモリ
  2. 02C++ #2 Struct と Union
  3. 03C++ #3 ポインタと配列
  4. 04C++ #4 関数
  5. 05C++ #5 オブジェクトとクラス
  6. 06C++ #6 継承
  7. 07C++ #7 コピー制御と演算子オーバーロード
  8. 08C++ #8 ポリモーフィズム
  9. 09C++ #9 C++11 の新機能
  10. 10C++ #10 C++14 の新機能
  11. 11C++ #11 C++17 の新機能