What’s New In C++17 言語仕様¶
このノートでは C++17 で注目すべき言語仕様を学習する。すでに cpprefjp がそのへんをきれいに整理している。それを利用して、読みながら急所を記していくことにする。
タイピングの都合で訳語は cpprefjp のものと一部変更して記す。
変数・データ構造関係¶
16 進浮動小数点数リテラルのサポート¶
16 進浮動小数点数リテラルのサポート。これにより値を正確に表現できる。IEEE754 的なアレだ。
書式は prefix 仮数部 指数部 suffix という構成だ。ここで
prefix は
0xまたは0Xのいずれか必須仮数部は整数部と小数部を
.でつなげて書く。ただし文字は 16 進数文字が使える。指数部(掛ける 2 の何乗であるかを示す部位)は prefix 符号 整数という構成
prefix は
pまたはP符号は
+または-整数は十進数で記す
suffix は次のとおり
指定がなければ
double型fまたはFならばfloat型lまたはLならばlong double型
インライン変数¶
インライン関数の変数版が追加された。これにともない inline の意味も若干変化するようだ。さらに constexpr との絡みもある。
以下、すべての式はあるヘッダーファイルでなされるものとする。
inline type name = value;と宣言かつ定義することが許される。この場合、これを#includeする翻訳単位すべてにおいてnameで参照される実体は同一となる。static inline type name = value;と宣言かつ定義することが許される。この場合には翻訳単位ごとに実体は別物になる。静的メンバー変数であって
constexprが付くものは暗黙的にinlineとなる。注意として
constexprが付く関数は暗黙的にinline関数だが、constexpr変数に関してはそのようなことはない。
構造化束縛¶
Python における次のようなコードと同等の代入構文が追加。
values = (100, 200, 300,)
first, second, third = values
C++17 ではこのように書く:
auto [first, second, third] = value;
autoの部分にはconst/volatile,&を付けてもよい。この記法を適用できるのは右辺が組み込み配列、構造体(の非静的メンバー変数)、
std::pair,std::tuple, etc.要素の順序はユーザー定義型ならばメンバー変数の宣言順序と、配列ならば位置の順序とそれぞれ等しい。
この機能の弱点
未使用変数を指定する手段がない。その結果、不要なコピーが生じるかもしれない。
入れ子の一括束縛はサポートされていない。
その他にも細かい規則がある。使うときに確認。
厳密な式の評価順¶
operator.(),operator->(),operator.*(),operator->*()の引数の評価順が引数、オブジェクトの順になる。operator()(), 関数、コンストラクターの引数の評価順が引数リストの左から右に順番に評価される。operator[]().operator>>(),operator<<()も左から右とする。operator=()は右から左とする。operator+=()などの代入を伴う二項演算子は右から左とする。メンバー関数の形で提供される
operator?()は、それが準じる組み込み演算子の評価順に合わせるものとする。operator new()系はメモリー確保を先に行い、それから初期化子の評価を行うものとする。
その他¶
braced-init-list による 直接 初期化における型推論。
auto変数の型を決定する規則が細かくなった。braced-init-list が単一要素からなるときには、その要素の型を推論するものとする。
braced-init-list が同一の型を持たない要素複数からなるときは、コンパイルエラーとなる。例えば
intとdoubleのような(昇格可能な)ケースでも不適合とする。
属性
[[maybe_unused]],[[nodiscard]]が追加。後述。値のコピー省略を保証
〈右辺値を変数の初期化のために使用する場合、コピーもムーブも省略する〉保証のことだが、C++ という言語の性質を考えると、生半可な理解でここに何かを書くことを避けたい。
今まで別の新機能を試すのに書いたコードにおいて、この手のコンストラクターが呼び出されなかったことがあった。それがこの C++17 新機能によるものだとすれば、理解を誤っていることになる。これは怖い。
参照メンバーや
constメンバーをもつクラスに対する placement new の適用。これについては背景がわからない。
enum class変数の初期化のときに整数を用いることが許される。ただし次の条件をすべて満たすときに限る:enum classに基底型が指定されている初期化リストが単一の要素からなる
直接初期化である
精度を失わない変換である
cpprefjp のデモコードでは列挙子のない
byteという scoped enum を定義している。このコードは上の条件をすべて説明してくれている。operator new()のオーバーロードvoid* operator new(std::size_t, std::align_val_t)が追加。これによりalinas()を用いた自前の型を定義しなくても、記憶領域を動的に割り当てる際に直接 alignment を指定することができるかもしれない。集成体初期化の拡張において、その基底クラスに対しても初期化可能になる。
基底クラスに対する braced-init-list を派生クラスに対するそれの中に入れ子にして書けばいいようだ。最初に現れる braced-init-list が(最初の)基底クラスに対する braced-init-list と解釈されるのだろう。
制御構文¶
if 文と switch 文の括弧の中で変数の初期化が許される¶
Python で言うところの := のような役割を果たすのだろうか?セミコロンを使うことになるので、書く手間は Python と同じ程度?
if(size_t n = v.size(); n < 10){
// ...
}
// 初期化コードは optional
switch(; size_t n = v.size()){
case 0:
// ...
}
属性 [[fallthrough]]¶
switch 文で case ラベルの処理が何かあり、その処理を break せずに次の
case ラベルの処理を敢えてさせたいとする。このときまともなコンパイラーは警告を出す。それを抑止するために、コンパイラーが break を期待している行に
[[fallthrough]] と書くことが許される。
これは使わないから覚えなくていい。
if constexpr 文¶
コンパイル時に if 文を評価させる構文だ。構文は if と括弧の間にキーワード
constexpr を挟むだけの単純なものだ。使い方はふつうの constexpr と同程度に難しい。
if constexpr (condition){
statement;
}
範囲 for ループにおける仕様変更¶
対象となる範囲の begin() と end() の型が異なっていても OK となる。
//auto first = range.begin(), last = range.end();
auto first = range.begin();
auto last = range.end();
for(; first != last; ++first){
statement;
}
ラムダ式¶
ラムダ式において、捕獲リストに
*thisを指定すると copy capture することになる。オブジェクトをコピーした上で
constになる。非constメンバー関数を呼び出せない。それを避けるにはラムダ式をmutableにする。cpprefjp の例ではメンバー関数void F::onFinish(int)が非constであることに注意。
constexprラムダ式。もしラムダ式が定数式であるような場合にコンパイル時に評価させる。ラムダ式関連の機能の学習は後回しにして一気にやる。
テンプレート¶
畳み込みをサポート。〈可変引数テンプレートのパラメータパックに対して二項演算を累積的に行う〉機能。
cpprefjp のデモコードにおける
sum(),sum0(),all(),print_all()のコードをよく見ておくこと。畳み込みを分類すると次のようになるようだ:
単項演算子を用いるか二項演算子を用いるか
左畳み込みか右畳み込みか(演算子のオペランドの評価順によって使い分けるのだろう)。
畳み込み式は丸括弧で囲まれることで表すものとする。シンプルだ。
template <typename... T> auto fold(T... args) { return (args op ...); // i.e. arg1 op ... op argN }
キーワード
typenameをテンプレートテンプレート引数に書くことが許される。今まで書けなかったのか。クラステンプレートのテンプレート引数推論
オブジェクト生成時にコンストラクターへの実引数からクラステンプレートのテンプレート引数を推論する機能だ。例えば
std::vector<int> v {1, 2, 3}のつもりでstd::vector {1, 2, 3}と書ける。この機能に伴い、推論補助という機能が追加。ここはあとでやる。
この機能に伴い、デフォルトテンプレート引数のみを持つクラステンプレートは、生成時に
<...>部分を省略できるようになった。標準ライブラリーでの適用例を習得しておくこと。
非型テンプレート引数の型に
autoが許される。実際に推論された型を欲しいときにはdecltype()を利用する。非型テンプレート引数リストの定数式評価が許される。
具体的にはポインターが該当する。ポインター(や配列や関数)のうち、定数式評価が可能なものならば非型テンプレート引数として与えてよい。
これも消化し切れていない。のちほど。
using宣言のパック展開。基底クラスのメンバーをまとめてパック展開する使い方が許される。cpprefjp より引用:
#include <iostream> struct ForLong { void operator()(long v) { std::cout << "ForLong:" << v << std::endl; } }; struct ForString { void operator()(const std::string& v) { std::cout << "ForString:" << v << std::endl; } }; template <typename... T> struct ForAll : T... { using T::operator()...; void operator()(int v) { std::cout << "ForAll:" << v << std::endl; } };
変数テンプレートの「デフォルトテンプレート」が許される。cpprefjp の例を一部改変:
#include <cassert> template <typename T=int> T x; int main() { auto y = x<>; assert(y == 0); return 0; }
定数式¶
static_assert()の第二引数の省略が許される。元ネタのassert()に引数が一つしかないのだから考えられる。先述したように
constexprラムダ式が使えるようになる。先述したように
if constexpr文が使えるようになる。
名前空間¶
ダブルコロンを連結した名前を書くことでスコープを入れ子にすることなく部分名前空間を定義することが許される。
namespace aaa::bbb::cc { // ... }
名前空間に対して属性を与えることが許される。
namespace [[deprecated]] aaa { // ... }
usingディレクティブでパック展開が許される。次のように
using宣言の行にカンマ区切りで識別子を並べることができる。using std::cout, std::endl;
例外¶
関数の型に例外仕様が含まれるようになった。
ここで言う例外仕様とは
noexcept()によるものしか差さない。旧式のthrow()はもう忘れろ。noexcept(false)な関数ポインターをnoexcept(true)な関数ポインターにキャストすることは許されない。端的に言うと、noexceptの違いしかない関数を多重定義することは許されない。ラムダ式の型においてもこの仕様が適用される。
この仕様変更により、C++14 まで適法だったコードが違法になることもある。
旧式の例外仕様削除。つまり
throw(xxx)と書けなくなる。代わりにnoexcept(bool)を利用することができる。例外を送出するか否かが本質的なのだ。
属性¶
属性
[[fallthrough]]が追加。先述のとおり。属性
[[maybe_unused]]が追加。コンパイラーの未使用変数の警告を抑止する。[[maybe_unused]] int x = 0; [[maybe_unused]] void f(); template <class T> [[maybe_unused]] inline void g();
属性
[[nodiscard]]が追加。関数の戻り値を呼び出し側が無視してはならないことを指示する。ユーザー定義型に与える方法と関数宣言に与える方法がある。struct [[nodiscard]] error_info {}; error_info f() { return error_info{}; } [[nodiscard]] int g() { return 0; }
名前空間に属性を与えることが許される。先述のとおり。
列挙型の列挙子に属性を与えることが許される。その場合には列挙子とカンマの間に属性を記す。
属性内の名前空間の指定をいちどにできる構文が追加。属性の先頭部分に
using名前空間:の順に記述し、その後に続けて属性の名前を記述する。// [[CC::opt(1), CC::debug]] void f(){} と同じ [[using CC: opt(1), debug]] void f(){}
標準が定義していない属性であり、コンパイラーにとっても不明な属性はコンパイラーは単に無視するものとする。
プリプロセッサ¶
__has_include という関数型マクロが追加される。これはインクルードするファイルが存在するかを確認するのに用いられる。従来は、欲しいヘッダーファイルに定義されている定数が定義されているか、のようなテストでその存在を確認していた。今回追加のこのマクロにより、コンパイラー(プリプロセッサー)がヘッダーファイル自体の存在をテストすることができるようになる。
使わなそうだから習得しなくていいだろう。
削除¶
トライグラフ削除。これは使わなかったはずなので気にしなくていい。
キーワード
registerの削除。これが修飾する変数は文字通りレジスターに格納されるという振る舞いだったはずだが、マニアックなライブラリー実装者くらいしか使うことはなかったのでは?演算子
bool::operator++前置後置どちらも削除。こんなオーバーロードがあったとは知らなかった。cpprefjp に書いてあるテキストが面白い。
先述したように旧式の例外仕様は廃止。
小さな変更¶
定義済みマクロ
マクロ
__cplusplusの値が201703Lに更新。マクロ
`__STDCPP_DEFAULT_NEW_ALIGNMENT__が追加。使わないので忘れていい。
機能テストマクロ。C++17 の機能がサポートされているかを判定するのに用いる。量が多いので割愛。
次の条件を満たす例外仕様のあるラムダ式から関数ポインターに変換する際に、変換後のものに同等の例外仕様を与えるものとする。
ラムダ式はキャプチャーを持たない。
ラムダ式は汎用ラムダ式ではない。
UTF-8 文字リテラル(文字列ではなく文字)が許される。
u8'A'のような書き方をすればいい。ただしコードポイントの範囲に制限がある。
charの表現できるサイズに収まらなければならない。