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
の表現できるサイズに収まらなければならない。