1 ページで眺める C++17
この記事は C++AdC 2017 19 日目の記事です。
n 番煎じ過ぎてみなさんがやらなかったやつをやります。
色々と cpprefjp と 江添さんのやつ を参考にしています。記述が被っている場所すらあるかもしれません。ごめんなさい。
言語機能
コードでの説明が必要なやつから紹介。だんだん雑になるかも。
構造化束縛(structured bindings)
今まで std::tuple
を扱うのは面倒で、例えば
std::tuple<int, double, std::string> tup = f(); // auto tup = f(); でも良い auto x = std::get<0>(tup); auto d = std::get<1>(tup); auto s = std::get<2>(tup);
または
int x; double d; std::string s; std::tie(x, d, s) = f();
みたいに書く必要があった。これは微妙に使う気が起きない。C++17 からはこう書ける
auto [x, d, s] = f();
これなら使う気がぐんぐん湧いてくる。
これは tuple に限った機能ではなく、配列、条件を満たすクラスなどにも使える
auto [a1, a2, a3] = { 5, 1, 3 }; struct Pos { int x = 10, y = 10, z = 10; }; auto [x, y, z] = Pos();
便利だね。
ただ
int a = 5, b = 3; [a, b] = std::minmax(a, b);
みたいなことは出来ない。あくまで構造化束縛は変数の定義/初期化のみの構文に留まっている。
こういう便利そうな機能が微妙な仕様で終わってるあたり C++ らしくていとあはれなり(社会性フィルター)。
条件式書くときの構文変更
書くたびになんか微妙な気がするこういうコード。
auto result = f(); if (result.isOK()) { // 処理 }
C++17 からはこう書ける
if (auto result = f(); result.isOK()) { // 処理 }
この構文は if
だけでなく、while
や switch
などでも使える。
template 引数推論
初期化時にちょっと嫌になるこういうやつ
std::vector<std::vector<int>> vec(m, std::vector<int>(n, x));
C++17 からはこう書ける
std::vector vec(m, std::vector<int>(n, x));
これは std::vector
の template 引数が推論されるようになったから。
ある程度は自動で推論してくれるし、もし自動では出来ないような推論をさせたい場合
template <typename T> struct Vec { std::vector<T> vec; // コンテナが渡されたとき、それを std::vector に持ち替えたい template <typename C> Vec(const C& rhv) : vec(rhv.begin(), rhv.end()) {} }; template <typename C> Vec(const C&) -> Vec<typename C::value_type>; // コンテナの value_type を T とする
のように Hoge(引数) -> Hoge<推論する型>
という構文で推論させることができる。
これを利用して明示的に文字数を指定しないでコンパイル時文字列を扱えるようになったりするのを 組んでみた けど、割りと面白かった。
RVO の保証
今までこんなコードを書いた時、返り値のオブジェクトがコピーされる可能性があった。
std::vector<int> f() { auto c = hoge(); // 何かしらのコンテナが帰ってくる return std::vector<int>(c.begin(), c.end()); // ここで返り値のコピーが発生する可能性がある }
なぜ「可能性」なのかというと、これをコピーしないようにする最適化、Return Value Optimization (RVO) が規格上認められていたからなのだが、C++17 からはこの関数 f の返り値を利用して値を初期化する場合。すなわち
auto v = f();
のようなユースケースの場合はこの最適化が強制されるようになった。
これにより、上記のコードで必ずコピーが発生しないようになる。
訂正: 記事公開時点では
std::vector<int> f() { std::vector<int> vec = hoge(); // なんか処理 return vec; }
のようなコードで RVO が保証されるかのような書き方をしていましたがこれは誤りでした。関数の返り値が prvalue
カテゴリでかつ変数の初期化に使われるときのみ RVO が保証されるようです。
ご指摘 していただいた銀天すばるさんに感謝。
template
template で任意の型のコンパイル時定数を渡したい人に朗報
template <auto X> struct Hoge { // 色々 };
のようなコードを書くことで
Hoge<30> a; Hoge<42ll> b; void f() { std::cout << "Hello, world!" << std::endl; } Hoge<f> c;
みたいなコードを書くことができる。局所的に役立ちそう。
constexpr if
あの C++ TMP を象徴する機能、SFINAE の地位を半分奪うかもしれない素晴らしい機能。
template < typename T, std::enable_if_t< std::is_same_v<std::decay_t<T>, int>, std::nullptr_t > = nullptr > void f(T) { std::cout << "T is int" << std::endl; } template < typename T, std::enable_if_t< !std::is_same_v<std::decay_t<T>, int> && std::is_integral_v<T>, std::nullptr_t > = nullptr > void f(T) { std::cout << "T is integer but not int" << std::endl; } template < typename T, std::enable_if_t< !std::is_integral_v<T>, std::nullptr_t > = nullptr > void f(T) { std::cout << "T is not integer" << std::endl; }
みたいなよくある地獄コード(実際はもうちょっとスマートに書けるかも)が、なんとこうなる。
template <typename T> void f(T) { if constexpr (std::is_same_v<std::decay_t<T>, int>) std::cout << "T is int" << std::endl; else if constexpr (std::is_integral_v<T>) std::cout << "T is integer but not int" << std::endl; else std::cout << "T is not integer" << std::endl; }
なんということでしょう。もうあのクソオーバーロード解決をしなくて良いのです。めでたしめでたし。
ところが一つ罠があり、この constexpr if
の操作は Two Phase Lookup の二段階目で行われるため、パースした段階で明らかにエラーである
template <typename T> void f() { if constexpr (false) static_assert(false); else std::cout << "fine" << std::endl; }
とかのコンパイルが通らない。この解決策としてはコードを
template <typename T> void f() { if constexpr (false) static_assert(std::is_integral_v<T> && false); else std::cout << "fine" << std::endl; }
こうすると static_assert
の中身が一応 T
に依存するので、コンパイルが通るようになる。前からある問題とは言えとても C++ らしいエラーの出し方でいとあはれなり。
畳み込み演算(Fold Expression)
今までこう書いてたやつが
template <typename T> auto sum(T x) { return x; } template <typename T, typename... Types> auto sum(T x, Types... args) { return x + sum(args...); }
こう書けるようになる
template <typename... Types> auto sum(Types... args) { return (... + args); }
これは (((args#0 + args#1) + args#2) + ...)
みたいに展開される(表記は江添さんが使ってるやつです)。
(args#0 + (args#1 + (args#2 + ...)))
みたいに展開したい場合
template <typename... Types> auto sum(Types... args) { return (args + ...); }
とすればいい。
注意点として、式全体を ()
でくくらないとエラーになる。
template <typename... Types> auto sum(Types... args) { return ... + args; // 括弧がないのでエラー! }
あと、畳み込む演算自体は結構自由にできる。こんなんでもいい。
template <typename... Types> auto sum(Types... args) { return (... + f(args).hoge()); }
例では +
演算子を使っているが演算子ならだいたい使える。
余談: これを悪用してみたいなぁとか考える人向けの某中3女子さんのツイートがこちら
C++17 fold expression に対応した比較式ユーティリティを書いてみた。 https://t.co/2uoV1cxDhw
— 狂える中3女子ボレロ村上/陶芸C++er (@bolero_MURAKAMI) 2017年10月28日
constexpr ラムダ
- コードの説明ないけど重要そうだから上の方入れた
- とうとう
constexpr
なラムダ式が作れるようになった - 特にキーワードも必要なく、単にコンパイル時に実行できそうなラムダはコンパイル時の文脈で使える
- つまり
constexpr
な変数とかstatic_assert
の条件式内でラムダ式が使える - 色々できそうだが、なんとSFINAEでの悪用を防ぐためにtemplate引数の中には書けないので注意
ラムダ式の *this キャプチャ
以下のコードには重大なバグがある
struct Hoge { int x; auto f() { return [this]() { return this->x++; }; } } int main() { std::function<int()> f; { Hoge hoge{5}; f = hoge.f(); std::cout << f() << std::endl; } std::cout << f() << std::endl; }
そう、最後のラムダ式内で this
をキャプチャしたはいいものの、最後の f()
の呼び出し時には hoge の寿命は切れている。つまりキャプチャしたはずの this
の中身が開放されているので見事未定義動作を踏み抜くことになる。
これを回避するため C++17 では this
の中身である *this
を直接キャプチャできるようになった。
struct Hoge { int x; auto f() { return [*this]() { return this->x++; }; // *this がコピーキャプチャされるので寿命も安心 } } int main() { std::function<int()> f; { Hoge hoge{5}; f = hoge.f(); std::cout << f() << std::endl; } std::cout << f() << std::endl; }
ネストされた名前空間定義の省略
いままで書いていた
namespace A { namespace B { namespace C { // ここに色々書く } } }
みたいな非常に微妙にアレなやつが
namespace A::B::C { // ここに色々書く }
と書けるようになった。嬉しい。
属性の追加
[[fallthrough]]
と [[nodiscard]]
、[[maybe_unused]]
が追加された。
[[fallthrough]]
は switch 文の中で用いて
switch (hoge) { case A: // なんか処理 [[fallthrough]] case B: // なんか処理 break; default: break; }
みたいにすると case A
の最後の部分で警告が出なくなる。
[[nodiscard]]
は関数の定義につけると、その関数の返り値が使われていないときに警告を出す。標準ライブラリでも malloc
などいくつかの低レベルのメモリ操作関数にこの指定がついた。
[[maybe_unused]]
は変数の定義につけるとその変数がその先使われていなくても警告を出さない。デバッグとかに便利かも。
一部演算子の評価順序の固定
int& f(int& x, int v) { std::cout << v; return x; } int x = 0; f(x, 2) = f(x, 1);
このようなコードを書いたとき、今までは演算子の右辺左辺の評価順序が未規定だったのでこの結果は 12
にも 21
にもなり得た。
C++17 からは例えば b = a
のとき a => b
の順に評価されるされるのが保証された。結果さっきのコードの実行結果は必ず 12
になる。
このようにして評価順序が固定される演算一覧はこんな感じ。全部 a => b
の順に評価される。
a.b
a->b
a->*b
b = a
b @= a // @ は任意の演算子
a[b]
a << b
a >> b
なお期待されていた f(a, b, c)
などの関数呼び出しにおける引数 a, b, c
の評価順序の固定はされなかった。未だに未規定です。
その他
コードで説明する感じでもない色々を箇条書きしていく。
noexcept 指定が関数の型の一部に
- 今まで
void f(int x)
とvoid g(int x) noexcept
は同じvoid(int)
型だった - C++17 からはこれが区別され、上の例でいえば
g
の型はvoid(int) noexcept
になる void(int) noexcept
からvoid(int)
には暗黙の変換がされるが、逆はされない- template 周りで既存のコードが動かなくなる可能性があるので注意
- こんなところに入れてるけど結構重要な変更
byte 型の追加
- 他の整数からの暗黙変換が不可能な 8bit 整数型、
byte
型が追加された - ストレージ用の型なのか、四則演算が定義されていない
inline 変数
inline Hoge x;
とかでinline
な変数が作れるようになった- ヘッダーオンリーライブラリの幅が広がるね
static_assert の記法追加
static_assert(cond, "")
がstatic_assert(cond)
と書けるようになった
using の可変長引数の展開が可能に
using Args::operator()...;
みたいな事ができます- というか今までできなかったのに気づいたときは驚いた
UTF-8 文字リテラル
u8'A'
で UTF-8 でのA
を表現できる- ただし、1 バイトの文字しか扱えないため
u8'あ'
などはエラー - なぜ
char8_t
が入らないのにこんなものを入れたのか
16 進数の浮動小数点リテラル
0x0.01p0 => 1/256 = 0.00390625
みたいな感じ。p
以下は 10 進数で指数部を指定する
属性名前空間を using できるように
[[using ns : foo, bar]] int x;
みたいにすると[[ns::foo, ns::bar]] int x;
と同じ動作になる
トライグラフの削除
- 詳しくは こちら
ライブラリ
全体的に触れるだけにします。ファイルシステムに関しては説明を放棄しています。
あと並び順はあまり重要度と関係がないため、実は重要な事が下に小さく書かれてたりするかも。
std::string_view
今まである文字列の部分文字列を取得したいときには substr
を使って次のようにしていた。
std::string str = "kitty on your lap."; std::cout << str.substr(2, 3) << std::endl; // => tty
これは便利ではあるが、一方で substr
の返り値が std::string
であるために、その生成を生成するための文字列コピーが走るというパフォーマンス的な問題が生じていた。
要はただ文字列の一部を参照するためだけにそのコピーを作るのは無駄という話。
というわけで文字列の参照のみを持つ std::string_view
というのが入った。<string_view>
を include することで使用可能になる。
これを使うとさっきのコードも
#include <string_view> //... std::cout << std::string_view(str).substr(2, 3) << std::endl;
こうすれば無駄なコピーが走ることはない。めでたしめでたし。
ただし、string_view
が参照している文字列を書き換えることは出来ない。つまり以下のコードは通らない
auto sv = std::string_view(str); sv[0] = 'a'; // エラー!書き換え不可能
std::optional
無効かもしれない値を表現するクラス、std::optional
ができた。<optional>
を include すると使える。
以下のようにして使う
#include <optional> // ... std::optional<int> f(int x) { int* res = hoge::proc(x); // ポインタを返す外部の関数 if (res) return *res; else return std::nullopt; }
以下のような事もできる
std::optional<int> opt = f(); std::cout << opt.value_or(42) << std::endl; // nullopt なら 42 として扱う
ようやく入った。しかし C++ なので opt.map(f)
みたいなやつはない。
std::variant & std::visit
プログラムを書いているとたまに union
を使いたい場面が出て来る。でも union
は型の概念を破棄しているに等しいクソだった。
C++17 ではまともな union
にあたる std::variant
が追加された。これも <variant>
ヘッダを include すると使える。
std::variant<int, double, std::string> var; var = 42; var = 5.0; var = std::string("kitty on your lap"); var = 'a'; // これはエラー
値を扱うにはこうする
if (auto p = std::get_if<int>(var)) std:cout << *p << std::endl; else if (auto p = std::get_if<int>(var)) std:cout << *p << std::endl; else if (auto p = std::get_if<int>(var)) std:cout << *p << std::endl;
しかしこれは流石に面倒なので std::visit()
という関数が用意されている。
std::visit([](auto&& x) { std::cout << x << std::endl; }, var);
神的に便利になる。型で処理をスイッチしたい場合はそういうクロージャーを作ってそのインスタンスを渡してやれば OK。便利 (大事なことなので 2 回言いました)
std::any
上の 2 つは適当に機能を制限していたが、それをぶっちぎってなんでも入る型 std::any
が追加された。<any>
を include すると使える。
使い方は std::variant
のもっと自由な感じなので省略。ただし値を取り出すときには std::any_cast()
を使う必要がある。
初学者であるほど使いたがりそうだが、初学者であるほど使ってはいけない機能個人的 No.1。本当に必要な時に伝家の宝刀として。
ファイルシステムを扱う機能が追加
ファイルシステムを扱う機能群としてとうとう <filesystem>
ヘッダが入った。また、この機能群はほぼ全て std::filesystem
名前空間上に定義される。
これに関しては 江添さんのやつ が非常に詳しい。というか僕には手に負えないので重要ではあるが存在の紹介にとどめさせていただきたい。
std::apply
std::visit()
の tuple 版っぽい std::apply()
が追加された。これで tuple の各要素に対する一様な操作が以下のように書ける。
std::tuple<int, double, std::string> tup = f(); std::apply([](auto&& x) { std::cout << x << std::endl; }, tup);
便利。
std::make_from_tuple
tuple の各要素をある型 T
のコンストラクタに突っ込みたいときがあるが、今まではいちいち補助関数を書く必要があった。
面倒なので std::make_from_tuple()
が追加された。
struct Hoge { Hoge(int, double, std::string); // 多変数を取るコンストラクタ }; std::tuple<int, double, std::string> tup = f(); Hoge hoge = std::make_from_tuple<Hoge>(tup); // こんな感じ
連想配列系コンテナ関連
ノード単体を扱う機能がついた
std::map
やstd::unordered_map
、std::multimap
や対応する set 系コンテナに、その要素のノードを扱う型であるnode_type
が追加された。- これは一見
iterator
のように見えるが遷移操作ができず、単に要素自体を表す型になる。 - これに合わせて、引数に渡した要素をコンテナから破棄してその
node_type
を取得するextract()
メンバと、2 つのコンテナを連結させるmerge()
メンバが追加された
その他のメンバ追加
try_emplace()
とinsert_or_assign()
が追加されたtry_emplace()
はemplace()
と違って要素が新たに挿入されなくても引数に渡した変数がムーブされないinsert_or_assign()
はoperator []
とほぼ同じだが、返り値が (要素の参照, 要素が追加されたかどうか) の pair になっている
<functional>
関連の色々
検索アルゴリズムの追加
<functional>
ヘッダに検索アルゴリズムがいくつか追加された。- ボイヤー・ムーア法とかがある
std::not_fn
- 「bool に変換できる型を返す関数の返り値を反転させる関数」を返す
std::not_fn()
が追加された - 名前が楽しくなさそうという印象が強い
- 合わせて
std::not1()
やstd::not2()
、std::unary_negate()
などは非推奨化された
std::invoke
- 「関数を呼び出す関数」である
std::invoke()
が追加された - 嬉しい人には嬉しい
<type_traits>
関連の色々
std::void_t
- テンプレート引数に何を渡しても
void
になる型、std::void_t
が入った - 一見意味がなさそうに見えるが、SFINAE で便利になる。
変数テンプレートの追加
std::is_hoge<T>::value
とかstd::is_hoge<T>{}
の代わりにstd::is_hoge_v<T>
と書けるようになった
std::invoke_result への移行
std::invoke_result<F, Args...>
で、型F
の関数を型がArgs...
の引数で呼び出したときの返り値の型が取得できる- 元々
std::result_of
があったが、ちょこちょこ問題があったためこれは非推奨化され、代わりにstd::invoke_result
が出来た
std::bool_constant
- コンパイル時の型として区別できる bool 定数を表現するための
std::bool_constant
が入った - 今までなかったのが驚き
論理演算系の追加
- 否定、論理積、論理和 などが入った
- 例えば論理積なら
std::conjunction<std::is_foo<T>, std::is_bar<T>>::value
みたいな感じで書ける
その他にもなんか色々ある
メモリ系の色々
std::shared_ptr が配列に対応
- タイトル通り。なぜいままでなかったのか
shared_ptr::weak_type
std::shared_ptr<T>
のメンバ型にstd::weak_ptr<T>
に対するエイリアス、shared_ptr::weak_type
が入った
memory_resource
- メモリの確保/解放に関する新しい管理機能群が入ったヘッダ
<memory_resource>
が出来た - 要は次世代版アロケータ
メモリ管理機能機能の追加
<memory>
に色々入った
キャッシュライン定数が追加
<new>
ヘッダにキャッシュ効率を色々頑張りたい時用のコンパイル時定数、std::hardware_destructive_interference_size
とstd::hardware_constructive_interference_size
が入った
その他
重要なものも混ざっています
並列アルゴリズム
- みんな大好き(?)
<algoriothm>
ヘッダの関数の並列化バージョンが入った std::sort(std::execution::par, std::begin(vec), std::end(vec))
みたいにすると使える
コンテナに関するフリー関数の追加
<iterator>
ヘッダにstd::size()
やstd::empty()
、std::data()
が追加された- それぞれ(その機能が提供されていれば)配列を含めるコンテナの要素数、コンテナが空かどうか、要素の配列の先頭のポインタを取得する
std::scoped_lock
- 今まで複数の変数を lock_guard するのは名前付けとかが面倒だった
- C++17 では
std::scoped_lock locks(x, y, z);
みたいにすることでx, y, z
全てに対する lock_guard を取得できるようになった
std::shared_mutex
- 書き込みが少なくて、読み込みが多い変数の mutex を効率的に行う
shared_mutex
が追加された - flag とかに使えそう(?)
std::as_const
const
でない変数にconst
を一時的に付与したい時が割りとあるが、const auto& cx = x
みたいにするかconst_cast
をするしかなかった- 不便なので
<utility>
にstd::as_const()
が入った
std::clamp
<algorithm>
ヘッダにある値を一定範囲に丸める関数、std::clamp()
が入ったstd::clamp(val, min, max)
で値が[min, max]
の範囲に入るよう丸められる- 数だけでなく一般に使えるので
<cmath>
じゃないっぽい
<cmath>
ヘッダに色々入った
- ベータ関数だのルジャンドル多項式だののすごい数学関数がいっぱい入った
- 三次元版の
std::hypot()
とかも入った
std::gcd() & std::lcm()
std::gcd()
とstd::lcm()
が入った<cmath>
ヘッダではなく<numeric>
ヘッダなので注意- 僕は性根が腐っているので早くこれに気付かずに死んでいく
using namespace std
er の顔が見たい
emplace 系関数の返り値が付いた
- 今まで
vector::emplace_back
などの emplace 系関数の返り値はvoid
だった - C++17 からはこれが挿入された要素の参照を返すように
- つまり
auto& back = vec.emplace_back(5);
みたいな事が出来る
std::uncaught_exceptions
- まだキャッチされていない例外の個数を取得する
std::uncaught_exceptions
が入った - 同時に
std::uncaught_exception
は非推奨になった - 名前の区別がつきにくくて困る
その他の非推奨になったもの
std::iterator
shared_ptr::unique()
<codecvt>
ヘッダstd::allocator
の幾つかのメンバ
以上
どうでしょうか。色々抜けはあるかもしれませんが大体こんな感じだと思います。
個人的にはまあダメなところを上げれば concept がないだとか range がないだとか structured bindings が微妙だとか色々いえますけど、良く言えば「次に繋がるアップデート」になるかなと思います。
ただ地味に重大な問題として未だに C++17 を完全に実装したコンパイラが存在しない気がします。
というわけでみなさんも良き C++ ライフを。明日は I さんの「ネタはあるんですが,時間的余裕がない可能性が高く,万が一書けない場合は @azaika_ が代わりに」です!!!