【一人 C++20 Advent Calendar 2019】C++ にコンセプトがやってくる!【25日目】
一人 C++20 Advent Calendar 2019 25日目の記事になります。
C++ にコンセプトがやってくる!
ついに C++ にコンセプトがやってきました!
C++11 の時代から長かった…。
コンセプトとはテンプレートパラメータに対して任意の制約を指定することができるようになる機能になります。
細かいところを話し出すと止まらないので簡単な例を記述すると以下のような使い方になります。
#include <string> #include <iostream> // T 型に対する制約を定義する template<typename T> concept printable = requires (T& t) { // T に対して任意のメンバ関数が呼べるかどうかの条件 t.name(); // 複数書くことで複数の制約を定義できる // また、以下のように書くことでメソッドの戻り値に対する制約も定義できる { t.sound() } -> std::string; }; // テンプレート引数を定義する時に typename や class の代わりに // concept で定義した制約名を記述できる // この場合は printable の条件の型のみをテンプレートで受け取ることになる template<printable T> void print(T const& obj) { std::cout << obj.name() << " : " << obj.sound() << std::endl; } // こちらは通常の関数テンプレートになる // printable の制約に当てはまらなかった場合にこちらを呼び出す template<typename T> void print(T const& obj) { std::cout << "出力できません" << std::endl; } struct cat { std::string name() const { return "ねこ"; } std::string sound() const { return "にゃーん"; } }; struct dog { std::string name() const { return "いぬ"; } std::string sound() const { return "わーん"; } }; struct X { int name() const { return 42; } int sound() const { return 42; } }; int main(){ // OK: printable の条件に当てはまってる print(cat{}); print(dog{}); // NG: printable の条件に当てはまらない print(X{}); return 0; } /* output: ねこ : にゃーん いぬ : わーん 出力できません */
C++ でメタプログラミングをやったことがある人なら必ず書いたことがあると思うんですが SFINAE を使ったテンプレート型の条件分岐ってめちゃくちゃむずかしいんですよね。
しかし、それがコンセプトを使うことで上のコードのようにめちゃくちゃすっきりと記述することができます。
そうそう、これだよこれ、これでいいんだよ。
もちろん上の使い方以外にも様々な用途でコンセプトを利用することができます。
詳しくは以下のまとめを読んでみてください。
いやー C++11 から長かったんですがやっとコンセプトが入りましたねえ…。
また、言語機能として追加されるだけではなくて標準ライブラリにもいくつか汎用的な制約が定義されています。
C++20 が楽しみですねー。
アドベントカレンダーを終えて
と、言うことで今日がクリスマス!そしてアドベントカレンダーが終わり!!!
勢いで始めたアドベントカレンダーだったんですがなんとか無事に完走することができました。
一つ一つの内容はとても薄っぺらいですが
始める前は C++20 って何が追加されるんじゃ???って感じだったんですが実際にどういう機能が追加されるのかを調べてみると細かい追加や修正がかなりされていることがわかりました。
特に「C++ で絶対に constexpr
するぞ!!!」という気持ちはめっちゃ伝わってきました。
まだ各コンパイラでは追加されてない機能やライブラリがあるので「ホンマに来年使えるようになるの???」みたいな気持ちがあるんですが、各コンパイラが対応されたらまた触ってみたいですねー。
それでは皆さん良いお年を。
参照
【一人 C++20 Advent Calendar 2019】状態を持たないラムダ式がデフォルト構築可能、代入可能となった【24日目】
一人 C++20 Advent Calendar 2019 24日目の記事になります。
状態を持たないラムダ式がデフォルト構築可能、代入可能となった
状態を持たないラムダ式がデフォルト構築可能、代入可能となりました。
次のようにラムダ式の型からオブジェクトを定義することができます。
#include <iostream> #include <map> int main(){ auto hello_world = [] { std::cout << "hello, world" << std::endl; }; // lambda の型から新しいオブジェクトを生成できる decltype(hello_world) hello_world2{}; hello_world2(); // 状態を持っている(キャプチャしている)場合は生成できない auto hello_world3 = [&] { hello_world(); }; // NG // decltype(hello_world3) hello_world4{}; return 0; } /* output: hello, world */
なるほど? これだけだと何が嬉しいんだと思いますよね。
こういう時に便利
std::map
は第三テンプレート引数に比較型を渡すことができます。
これは通常は関数オブジェクト型を定義し、 operator ()
などで処理を記述する必要がありました。
しかし、ラムダ式がデフォルト構築可能になったことで次のようにテンプレート引数に対して直接処理を記述できるようになります。
#include <iostream> #include <map> int main(){ // std::map は第三テンプレート引数に比較型を渡すことができる // 本来は関数オブジェクトクラスを定義してテンプレート引数に渡す必要があった // これを delctype を経由して直接ラムダ式で処理を記述できるようになった auto data = std::map< int, char, decltype([](auto a, auto b) { return a > b; }) > { { 1, 'A' }, { 11, 'J' }, { 12, 'Q' }, { 13, 'K' } }; for(auto&& [key, value] : data){ std::cout << key << ":" << value << std::endl; } return 0; } /* output: 13:K 12:Q 11:J 1:A */
テンプレート引数に直接式を記述したい!!!!って C++11 の頃からずーーーーーっとやりたいと思っていてそんなことをするライブラリも自前でつくったりしていました。
C++20 だと delctype
+ ラムダ式で直接テンプレート引数に処理を記述できるようになるのでかなりメタプログラミングが捗りそうですね…これだよこれを待っていたんだよ!!!
対応コンパイラ
- GCC 9
- Clang 8.0
参照
【一人 C++20 Advent Calendar 2019】C++ にモジュールがやってくる!【23日目】
一人 C++20 Advent Calendar 2019 23日目の記事になります。
C++ にモジュールがやってくる!
ついに C++ にモジュールがやってきます!
モジュールとはヘッダーファイル・インクルードに置き換わる『外部ファイル・実装を取り込む新しい仕組み』になります。
よくわからんので実際に簡単なコードを書いてみます。
今までのヘッダーファイル・インクルード
これまではヘッダーファイルで宣言や定義を行い、それを include
することで外部ファイルを取り込んでいました。
// math.h #ifndef MATH_H #define MATH_H template<typename T, typename U> auto plus(T a, U b){ return a + b; } #endif /* MATH */
// main.cpp #include "./math.h" #include <iostream> int main(){ std::cout << plus(1, 2) << std::endl; std::cout << plus(3.14, 3.14) << std::endl; return 0; } /* output: 3 6.28 */
モジュールを使った実装
モジュールを使用すると以下のように置き換えることができます。
// math.cppm // モジュールの宣言 export module math; // エクスポートの宣言 export template<typename T, typename U> auto plus(T a, U b){ return a + b; }
// main.cpp // インポートの宣言 // エクスポート宣言した関数が使用できるようになる import math; #include <iostream> int main(){ std::cout << plus(1, 2) << std::endl; std::cout << plus(3.14, 3.14) << std::endl; return 0; } /* output: 3 6.28 */
基本的には
- エクスポートで実装
- インポートで取り込み
っていう使い方ですかね。
見てわかるように今までの書き方とは全然違います。
わかりやすいところだとインクルードガードも必要なくなり、 include
のようなファイルパス指定もなくなります。
ビルド方法
インクルードする場合はファイルを指定して展開するだけなので簡単な構成の場合のビルドはそこまで難しくなかったのですが、モジュールを使った場合は少し複雑になります。
clang 8
で試してみたんですが以下のようにビルドします。
# clang 8 での動作確認 $ clang++- --version clang version 8.0.1-svn369350-1~exp1~20190820121219.79 (branches/release_80) Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin
# モジュールファイルを指定してプリコンパイル済みモジュールを生成する $ clang++ -std=c++2a -fmodules-ts --precompile math.cppm -o math.pcm
# プリコンパイル済みモジュールからバイナリファイル(.o) を生成 $ clang++ -std=c++2a -fmodules-ts -c math.pcm -o math
# 実行ファイルを生成 $ clang++ -std=c++2a -fmodules-ts -fprebuilt-module-path=. math.o main.cpp -o math
モジュールを取り込んで実行バイナリを生成するまでに3つのステップあります。
1. モジュールファイルからプリコンパイル済みモジュールを生成する
2. プリコンパイルモジュールからバイナリファイル(.o
)を生成
3. 2.
のバイナリファイルを取り込んで実行バイナリを生成
このような流れで最終的な実行バイナリを生成します。
また clang
でビルドする場合は -fmodules-ts
オプションが必要になります。
このビルドの仕方はここに書いてあるやり方を参考にしています。
他にも手段はあるんですかね?
あとこの仕組みだとコード補完や lint で使用する静的コード解析がどうなるのかが気になりますね。
プリコンパイル済みモジュールを読み込んで静的コード解析する感じになるんですかね?
その他の書き方
他にも以下のように namespace
を区切った書き方もできます。
C++17
// lib.h #ifndef LIB_H #define LIB_H #include <string> #include <iostream> namespace lib{ struct user{ std::string name; int age; }; inline std::ostream& operator <<(std::ostream& os, user u) { os << "name: " << u.name << ", age: " << u.age; return os; } inline int fact(int n){ return n > 1 ? n * fact(n - 1) : n; } } // namespace lib #endif /* LIB */
// main.cpp #include "./lib.h" int main(){ auto homu = lib::user{ "homu", 14 }; auto mami = lib::user{ "mami", 14 }; std::cout << homu << std::endl; std::cout << mami << std::endl; std::cout << lib::fact(5) << std::endl; return 0; }
C++20 + モジュール
// lib.cppm #include <string> #include <iostream> export module lib; // namespace 内の定義がエクスポート宣言される export namespace lib{ struct user{ std::string name; int age; }; inline std::ostream& operator <<(std::ostream& os, user u) { os << "name: " << u.name << ", age: " << u.age; return os; } inline int fact(int n){ return n > 1 ? n * fact(n - 1) : n; } } // namespace lib
// main.cpp #include <iostream> import lib; int main(){ auto homu = lib::user{ "homu", 14 }; auto mami = lib::user{ "mami", 14 }; std::cout << homu << std::endl; std::cout << mami << std::endl; std::cout << lib::fact(5) << std::endl; return 0; } /* output: name: homu, age: 14 name: mami, age: 14 120 */
慣れるまでは結構大変そうですがモジュールを使うと C++ の書き方がすごく変わりそうですね。
今までの概念ががらっと変わりそう…。
ここで書いたような例はモジュールのほんの一部でしかないので詳しくはこのスライドを読んでみるといいと思います。
まだ標準ライブラリとかには対応してないんですが、数年後の C++ は全然違うものになってそうですねー色んな意味で楽しみです。
参照
【一人 C++20 Advent Calendar 2019】実装依存の情報をまとめた <version> ヘッダーを追加【22日目】
一人 C++20 Advent Calendar 2019 22日目の記事になります。
実装依存の情報をまとめた ヘッダーを追加
実装依存の情報(実装固有のライブラリバージョンマクロなど)をまとめたヘッダーとして <version>
が追加されます。
今まではライブラリごとのヘッダーをインクルードする必要がありましたが <version>
をインクルードすることでライブラリ機能マクロなどをすべて使用できるようになります。
#include <version> #include <iostream> int main(){ // C++20 以前では対応するライブラリのヘッダーをインクルードする必要があった // C++20 では <version> をインクルードするだけですべて使用できるようになる std::cout << __cpp_lib_any << std::endl; std::cout << __cpp_lib_chrono << std::endl; std::cout << __cpp_lib_variant << std::endl; return 0; } /* output: 201606 201611 201606 */
ライブラリでワークアラウンド対応する場合に便利そう。
参照
【一人 C++20 Advent Calendar 2019】範囲ライブラリとして <ranges> が追加される【21日目】
一人 C++20 Advent Calendar 2019 21日目の記事になります。
今回もまだ実装しているコンパイラがないので軽めの内容です。
[範囲ライブラリとして が追加される]
今まではイテレータの組を利用してイテレーションを行っていたのですが、C++20 では新しく <ranges>
ライブラリが追加されます。
<ranges>
ではこれまでのイテレータ(std::begin
や std::end
)を関数に渡して使用するのではなくて配列に対して直接操作するような形でイテレーションする事が出来るようになります。
例えば filter
や transform
は以下のように書くことが出来るようになります。
#include <ranges> #include <iostream> #include <vector> int main(){ auto even = [](int n) { return n % 2 == 0; }; auto twice = [](int n) { return n + n; }; auto xs = std::vector{ 1, 2, 3, 4, 5, 6 }; for(int n // | でイテレーション処理を結合していく : xs | std::views:filter(even) // 引数の関数を元に絞り込み | std::views::transform(twice) // 引数の関数に要素を置き換える ) { std::cout << n << std::endl; } return 0; } /* output: 4 8 12 */
いわゆる PStade.Oven
や Boost.Range
みたいなやつですね。
同じく C++20 で追加される Concept をバリバリ使っているようなのでどうやって実装しているのかを調べてみると楽しそう
今まで C++ でイテレーション処理を行なうのがとてもめんどくさかったのでこれでかなり改善されるといいですねー。
参照
【一人 C++20 Advent Calendar 2019】std::result_of が削除される【20日目】
一人 C++20 Advent Calendar 2019 20日目の記事になります。
std::result_of が削除される
C++11 で追加され、C++17 で非推奨になっていた std::result_of
が削除されます。
変わりに今後は std::invoke_result
を使用する事が推奨されます。
#include <type_traits> struct plus{ template<typename T> auto operator ()(T a, T b) { return a + b; } }; int main(){ // C++20 ではエラー static_assert(std::is_same_v<std::result_of<plus(int, int)>::type, int>); static_assert(std::is_same_v<std::result_of<plus(float, float)>::type, float>); // C++20 では std::invoke_result を使用する static_assert(std::is_same_v<std::invoke_result<plus, int, int>::type, int>); static_assert(std::is_same_v<std::invoke_result<plus, float, float>::type, float>); return 0; }
result_of
は boost
を使っていた時代からお世話になっていたので削除されるのは感慨深いものがありますねえ。
std::invoke_result
といった今時のメタ関数は全然把握していたので C++20 とかで遊ぶ場合はどのメタ関数を使えばいいのかわからなくて苦労しそう。
参照
【一人 C++20 Advent Calendar 2019】新しいフォーマットライブラリ <format> が追加【19日目】
一人 C++20 Advent Calendar 2019 19日目の記事になります。
新しいフォーマットライブラリ が追加
printf
のようなフォーマットを指定して文字列を生成するライブラリが新しく追加されます。
例えば std::format(書式, 引数...)
のような形で呼び出し、関数の戻り値は std::string
になります。
まだ実装しているコンパイラがなかったのでここを参照して動作イメージを書いてみます。
基本的には書式内の {}
が引数の値に置き換わるような形で使用します。
// 必要なヘッダ #include <format> // {} で引数の値が展開される std::string result = std::format("name: {}, age: {}", "homu", 14); std::cout << result << std::endl; // output: name: homu, age: 14 // {} 内に数値を書くとその番号の引数で置き換える std::string result = std::format("age: {1}, name: {0}", "homu", 14); std::cout << result << std::endl; // output: age: 14, name: homu
他にも詳しく書式指定出来るので詳しくはこちらを参照して下さい。
またへんてこな書式を覚えるのかーってなるとめっちゃだるい…まあ慣れな気もしますが…。
とはいえ型安全らしいので便利といえば便利だと思います。
実際にライブラリが実装されてからいろいろと試してみたいところではありますねー。