このノートでは C++14 の新機能を扱います。C++11 の大改訂に比べると、C++14 はマイナーリリースです。新しいパラダイムは導入せず、C++11 がやりかけたことを仕上げています。だからこそ学習コストはほぼゼロで、使い心地は明確に良くなります。
戻り値型の推論
C++11 で戻り値型を推論させるには、後置戻り値型と decltype が必要でした。
C++// C++11
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { return t + u; }
C++14 では単にコンパイラに任せられます。
C++// C++14
template <typename T, typename U>
auto add(T t, U u) { return t + u; }
推論規則は auto 変数と同じで、参照とトップレベル const は落ちます。保持したい場合は decltype(auto) を使います。
C++std::vector<int> v{1, 2, 3};
auto get1(std::vector<int>& c) { return c[0]; } // int を返す(コピー)
decltype(auto) get2(std::vector<int>& c) { return c[0]; } // int& を返す
この区別は、ジェネリックな転送ラッパーを書くとたちまち重要になります。
注意点。戻り値型の推論は呼び出し側から関数定義が見えている必要があるため、ヘッダで宣言だけを公開するインターフェースには使えません。
ジェネリックラムダ
C++11 のラムダは引数型を明記する必要がありました。C++14 は auto を許し、これはテンプレート化された operator() を持つクロージャ型を生成するのと等価です。
C++auto print = [](const auto& x) { std::cout << x << '\n'; };
print(42);
print("hello");
print(3.14);
これによりラムダが汎用アルゴリズムの記述に実用的に使えるようになります。標準ライブラリと組み合わせるとその効果は明白です。
C++std::sort(v.begin(), v.end(),
[](const auto& a, const auto& b) { return a.score > b.score; });
以前はここに要素型を完全に書く必要があり、コンテナを変えるたびに書き直していました。
ラムダの初期化キャプチャ
C++11 のキャプチャリストは既存の変数を値か参照で捕まえることしかできませんでした。つまりムーブ専用型はそもそもキャプチャできない——std::unique_ptr はラムダに入れられませんでした。
C++14 はキャプチャリスト内で新しい変数を直接初期化できます。
C++auto ptr = std::make_unique<Widget>();
auto task = [p = std::move(ptr)]() { p->run(); }; // 所有権がクロージャへ移る
式の結果を捕まえてクロージャ内での再計算を避ける用途にも使えます。
C++auto f = [size = v.size() * 2]() { return size; };
std::make_unique
C++11 は make_shared を出荷しながら make_unique を忘れていました。C++14 がそれを補います。
C++auto p = std::make_unique<Widget>(arg1, arg2);
型名を二度書かずに済むだけでなく、実際の安全上の穴もふさぎます。次を考えてください。
C++process(std::unique_ptr<Widget>(new Widget), compute());
C++17 より前、関数引数の評価順は未規定でした。コンパイラは new Widget を実行し、次に compute() を呼び、最後に unique_ptr を構築してもよかったのです。compute() が例外を投げれば、生の Widget は永久に漏れます。make_unique は確保と所有権の引き受けを単一の呼び出しに閉じ込めるので、この窓が存在しません。
原則。生の new を書かない。 make_unique と make_shared があれば、書く理由はほぼありません。
変数テンプレート
C++11 には関数テンプレートとクラステンプレートがありました。C++14 は変数テンプレートを追加します。
C++template <typename T>
constexpr T pi = T(3.1415926535897932385);
float f = pi<float>;
double d = pi<double>;
標準ライブラリの _v 接尾辞の型特性——std::is_integral_v<T>、std::is_same_v<A, B>——は変数テンプレートです(C++17 で追加)。std::is_integral<T>::value よりずっと短く書けます。
緩和された constexpr
C++11 の constexpr 関数は実質的に単一の return 文に限られていたため、ループを含むものは再帰で書くしかありませんでした。
C++// C++11: 再帰のみ
constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }
C++14 は constexpr 関数内でローカル変数、ループ、分岐を許します。
C++// C++14
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
これによりコンパイル時計算は、芸を見せるためのものから実用的な道具になりました。ルックアップテーブル、ビットマスク、文字列ハッシュはすべてコンパイル中に計算できます。
細かい変更
2 進リテラルと桁区切り。
C++int mask = 0b1010'1010;
int large = 1'000'000;
区切り文字 ' はどこに置いてもよく、可読性にのみ影響し、値は変わりません。ビットマスクや大きな定数を書くときに便利です。
[[deprecated]] 属性。 廃止予定のインターフェースに印を付け、使用箇所でコンパイラ警告を出します。
C++[[deprecated("use renderV2() instead")]]
void render();
std::exchange。 新しい値を書き込み、古い値を返します。ムーブコンストラクタで読みやすく書けます。
C++Buffer(Buffer&& o) noexcept
: data_(std::exchange(o.data_, nullptr)),
size_(std::exchange(o.size_, 0)) {}
std::shared_timed_mutex。 読み書きロックが標準ライブラリに入り、複数の読み手か単一の書き手を許します。
まとめ
C++14 の機能はどれも立ち止まって学ぶほどのものではありませんが、合わせると C++11 のコードは目に見えて短くなります。ジェネリックラムダがアルゴリズムを汎用にし、初期化キャプチャが所有権をクロージャへ入れられるようにし、緩和された constexpr がコンパイル時計算を書けるものにしました。プロジェクトがまだ C++11 なら、C++14 への移行コストは実質ゼロで、もっとも費用対効果の高いアップグレードです。