日経ソフトウエア5月号 「constexpr」が開くコンパイル時プログラミングの世界
中3女子です。
だいぶ間が空いたが、日経ソフトウエア5月号の特集記事として
僕の書いた 「constexpr」が開くコンパイル時プログラミングの世界 が掲載されたことを報告する。
「プログラムは実行時にだけ処理が行われるもの」。
そう思い込んでいませんか。それは大きな誤解です。
最新のC++コンパイラを使えば、プログラムを実行する前のコンパイル時に処理を完了できるのです。
それを可能にするC++の機能が「constexpr」です。
本特集では、constexprの魅力を紹介します。
日経ソフトウエア
Amazon.co.jp: 日経ソフトウエア 2014年 05月号: 日経ソフトウエア: 本
constexprの紹介記事だが、入門から実装技法、Sproutライブラリの紹介からコンパイル時レイトレーシングまで、きわめて幅広い範囲を網羅している。
この内容を8ページ(見開き4ページ)に圧縮するのにはかなり苦心した。
後半はさすがに駆け足になってしまったが、constexpr入門者にとっても上級者にとっても、それなりに読み応えのある内容になったとは思う。
これまで、活字媒体でconstexprについて書かれた文章は、プログラミングの魔導書Vol.2 での江添氏のconstexpr入門などがあるが、『「constexpr」が開くコンパイル時プログラミングの世界』は、現時点でもっともconstexprについて詳しく書かれた日本語文章であると自負している。
執筆にあたって第一に心掛けたのは、規格上の正確さと簡潔さだ。
自分の知るかぎり、活字媒体と電子媒体とにかかわらず、プログラミング記事におけるC++の記述は正確さに欠けるものが多い。
もちろん、記事によっては、字数や想定読者といった観点から、正確さよりも直観的分かりやすさを優先せざるを得ない場合もあるだろう。
例えば、スコープに関する解説でADLに少し触れる際に、Two-phase name lookup や friend に関する挙動の詳細な定義を、常に述べるべきだとは僕も思わない。
重要なのは、前提と対象を常に明確にすることだ。
例を挙げるなら、「○○と書くとエラーになる」は、明確な文章とは言えない。
これでは、規格上の問題なのか、それとも特定の処理系に起因するのか、そもそも何故エラーになるのか、といった情報が一切含まれない。
「ill-formed である」「undefined behavior であり、対象の処理系でコンパイルエラーになる」「well-formed だが、処理系で未実装」といったように明確に書くべきである。
とはいえ、厳密さに言葉を費やすには、読者がその言葉(少なくとも名詞)を承知していることを前提にしなければならない。
そのため想定読者のレベルは、いくらか高く見積もらざるを得ない。
記事はもともとC++の基本的な文法がわかる程度を想定読者とする旨書いていたが、身内のレビューで「そのりくつはおかしい」旨の指摘をされ、「ストラウストラップのプログラミング入門」を読了した程度と修正することとした。
C++の基本的な文法がわかる程度の平均的なC++erであれば、当然C++11の規格やドラフトには目を通してそれなりに理解をしているはずだが、記事を読むのはC++erばかりとは限らないし、そもそも「基本的な文法」を一般に定義することは難しい。当然の仕儀といえる。
そのような客観的な指摘から、学術的な文章作法や、重要な規格上の厳密さについてまで、レビューをお願いした方々には大変お世話になった。
やはり客観的な視点というのは重要だ。
文章を書くときに、知らず知らずのうちに説明無しに暗黙の前提を置いたり、思い込みにもとづいて書いてしまうということはよくある。
そういった瑕疵は自分で読み直しても気付かない。
思い込みによって自分の脳内で勝手に文章を補完してしまうからだ。
ブログであれば後日あらためて訂正したり説明を追加することができる。
しかし紙媒体はそうはいかない。訂正記事を出すためのコストは計り知れない。
事前により多くの人に読んでチェックをもらうことの大切さを思い知った次第である。
それにしても、自分もブログエントリや勉強会発表やライブラリの公開といった形で知見を公開しているが、明らかに自分の提供する利益よりも貰う利益のほうが多いように思う。
集合知とは元々そうしたものかもしれないが。
ともあれ、特に世話になった方々にはそのうち何らかの形でお返しをしたい。
編集の大森氏にも感謝をしたい。
constexprという現状ニッチ過ぎる感のある機能について、依頼を戴いたことはもとより、執筆にあたってもほとんど自由に書かせて貰った。
元々依頼された時点では、Brainf*ckやPietのような難解プログラミング言語や、テンプレートメタプログラミングといったパラダイムを紹介する「ちょっと変わったプログラミングの始め方」シリーズ企画の一つとしてconstexprを取り上げるという扱いであった。
ところが、初稿を提出して後に、独立した特集記事として取り上げるという連絡が来たのである。
事情は訊いていないので定かではないが、「混ぜるな危険」と判断したのかもしれない。
いずれにせよ、自分としては喜ばしい結果である。
また、大森氏には、ライターとしての後記にも、編集後記で適切なフォローを戴いた。
それにしても、ことここに至って、なぜ自分はconstexprにこうまで熱心になっているのか、理由をあらためて考えても謎だ。
モチベーションは分からない。
ただ、義務感がある。市民の義務であるという感覚が。
コンパイルは祈りに似ている。世界よ斯くあれという祈りだ。
さあ、constexprによってコンパイル時プログラミングの世界を広げよう。
プログラムの処理が、実行時という枠から完全に自由になった世界により一歩近づくために。
C++14 時代の constexpr プログラミング作法
この記事は、C++ Advent Calendar 2013 の参加記事です。
前回は 西山信行 さんの記事でした。
まもなく C++14 時代が到来しようとしている。
ただでさえ実用的な言語機能である constexpr が C++14 での制限緩和によって神になろうとしている。
C++11 での constexpr の制限は、必然的に副作用のない関数型プログラミングを強制し、とても興味深いものではあった。
コードの可読性云々は主観によるのでここでは置くが、時には非効率な実装にならざるをえないケースもあったのは事実だ。
C++14 での constexpr は、古典的な実行時処理に慣れた者達にも非常に親しみやすいコードが書けるようになる。
ドラフトの直接的な内容については、すでに自分をはじめ有識者が解説を書いているので、そちらを参照するのが手っ取り早い。
-
- constexpr 関数の制限緩和
C++14 constexpr関数の制限緩和 - Faith and Brave - C++で遊ぼう
-
- 定数式の条件
N3337 5.19 Constant expressions - ここは匣
-
- リテラル型
C++14 のリテラル型 - ボレロ村上 - ENiyGmaA Code
ではどのように書けるようになるのか、手っ取り早い例を示す。
副作用のないアルゴリズムの実装
Sprout/sprout/algorithm/find.hpp at master · bolero-MURAKAMI/Sprout · GitHub
#include <iterator> #include <type_traits> #include <sprout/config.hpp> #include <sprout/iterator/operation.hpp> #include <sprout/iterator/type_traits/category.hpp> #include <sprout/utility/pair/pair.hpp> namespace sprout { namespace detail { template<typename RandomAccessIterator, typename T> inline SPROUT_CONSTEXPR RandomAccessIterator find_impl_ra( RandomAccessIterator first, RandomAccessIterator last, T const& value, typename std::iterator_traits<RandomAccessIterator>::difference_type pivot, RandomAccessIterator found ) { return found != first ? found : pivot == 0 ? (*first == value ? first : last) : sprout::detail::find_impl_ra( sprout::next(first, pivot), last, value, (sprout::distance(first, last) - pivot) / 2, sprout::detail::find_impl_ra( first, sprout::next(first, pivot), value, pivot / 2, first ) ) ; } template<typename RandomAccessIterator, typename T> inline SPROUT_CONSTEXPR typename std::enable_if< sprout::is_constant_distance_iterator<RandomAccessIterator>::value, RandomAccessIterator >::type find( RandomAccessIterator first, RandomAccessIterator last, T const& value, std::random_access_iterator_tag* ) { return first == last ? last : sprout::detail::find_impl_ra(first, last, value, sprout::distance(first, last) / 2, first) ; } template<typename InputIterator, typename T> inline SPROUT_CONSTEXPR sprout::pair<InputIterator, bool> find_impl_1( sprout::pair<InputIterator, bool> const& current, InputIterator last, T const& value, typename std::iterator_traits<InputIterator>::difference_type n ) { typedef sprout::pair<InputIterator, bool> type; return current.second || current.first == last ? current : n == 1 ? *current.first == value ? type(current.first, true) : type(sprout::next(current.first), false) : sprout::detail::find_impl_1( sprout::detail::find_impl_1( current, last, value, n / 2 ), last, value, n - n / 2 ) ; } template<typename InputIterator, typename T> inline SPROUT_CONSTEXPR sprout::pair<InputIterator, bool> find_impl( sprout::pair<InputIterator, bool> const& current, InputIterator last, T const& value, typename std::iterator_traits<InputIterator>::difference_type n ) { return current.second || current.first == last ? current : sprout::detail::find_impl( sprout::detail::find_impl_1( current, last, value, n ), last, value, n * 2 ) ; } template<typename InputIterator, typename T> inline SPROUT_CONSTEXPR InputIterator find( InputIterator first, InputIterator last, T const& value, std::input_iterator_tag* ) { typedef sprout::pair<InputIterator, bool> type; return sprout::detail::find_impl(type(first, false), last, value, 1).first; } } // namespace detail // 25.2.5 Find // // recursion depth: // O(log N) // template<typename InputIterator, typename T> inline SPROUT_CONSTEXPR InputIterator find(InputIterator first, InputIterator last, T const& value) { typedef typename std::iterator_traits<InputIterator>::iterator_category* category; return sprout::detail::find(first, last, value, category()); } } // namespace sprout
これは Sprout C++ Libraries による constexpr find アルゴリズムの実装である。
見てのとおり非常に長い。
C++14 限定ならば、以下のように書ける。
template<typename InputIterator, typename T> constexpr InputIterator find(InputIterator first, InputIterator last, T const& value) { for (; first != last; ++first) if (*first == value) { return first; } return last; }
これは、C++03 時代からの古典的な実行時処理の記述と全く変わらない。
上記のコードは C++14 ならば constexpr 関数としてコンパイル時にも実行時にも使うことができる。
なぜ C++11 版の実装ではこんなに長くなるのか?
なぜならば、長いほうが実装するのが楽しいからだ。
縛りプレイの楽しみである。
言語的制約をテクニックでかいくぐる過剰に複雑なコードは黒魔術めいたアトモスフィアを孕んで、宗教的恍惚にも似たエクスタシーにわれわれを導く。
C++er という人種はそのような快感に脳を灼かれたジャンキーにして狂信の徒なのである。
というのはもちろん半分冗談であって、実際的な理由は以下。
- インクリメントなどの副作用のある操作は C++11 では非定数式のため、イテレータ操作の別なインタフェースを用意する必要がある。
- コンパイラの実装定義の再帰深度の制限(推奨値:512)があるため、再帰のオーダーを抑えるために index_tuple や 倍分再帰 などのイディオムを駆使する必要がある。
といった理由が挙げられる。
C++14 では副作用もループ構文も扱えることから、こうした問題を考える必要は(C++14 が前提なら)無くなる。
こうなると、もはや C++14 constexpr で出来ないことを挙げたほうが早いだろうので、そうする。
C++14 constexpr で不可能であること
実用上の問題になるだろうことをざっくり挙げる。
まあ妥当と言ったところだろう。
C++11 からすれば、むしろ「これだけなのか」といった感想だ。
このほか、制御構文の中で goto だけが許可されないなどといったこともあるが、大きな問題ではないので省略する。
ところで、ラムダ式が定数式でないのは諸般の事情があるので仕方ないとは思うけれど、
何でも標準化委員会内で日本のメンバから「ラムダ式が定数式であると SFINAE に悪用される」との反対意見が出たとの話を耳にした。
これについては甚だ遺憾であり、何をか言わんや、腹を切って死ぬべきであろう。
例外処理と RAII に関しては、例外安全絡みの問題が生ずるので、後でくわしく述べる。
また、constexpr 関数やクラスの実装においていくつか些細な注意点がある。
C++14 constexpr での注意点
これらは、処理系依存な未初期化値を扱えないようにするための仕様である。
- 副作用を及ぼすオブジェクトは、定数式評価内で寿命が開始されたものでなければならない。
constexpr int& twice(int& x) { x += x; // 副作用 return x; } constexpr int f() { int x = 10; return twice(x); } constexpr int y = f(); // OK. "f()" の中だけで副作用は完結している。 struct X { static int x; }; int X::x = 10; constexpr int g() { return twice(X::x); } constexpr int z = g(); // error! "g()" の外側に副作用が及んでいる。
この仕様によって、副作用の制限緩和があっても定数式評価の中で副作用は完結するため、評価の前後で全体として副作用を持たないという一貫性が保持される。
糞仕様の改善によって、このような苦労 をせずとも普通のアクセッサが書けるようになる。
もし暗黙の const 修飾をあてにしたコードを書いていたら、今から明示的に const を付けるべきである。
ただし、トリビアル代入演算子は暗黙で constexpr 指定されない。(この問題は報告済みなので、仕様確定までに修正されるかもしれない)
- ポインタ同士の比較、減算ができるようになった。
C++14 からは、ポインタ同士の比較や減算が定数式になった。
例えば以下のようなコードが書ける。
constexpr int a[10] = {}; constexpr auto dist = (a + 10) - (a + 0); constexpr auto rel = (a + 10) > (a + 0);
ただし、同じ配列内の要素を指すポインタのように比較関係が明確なものではなく、無関係なポインタ同士の場合は ill-formed である。
constexpr int x = {}; constexpr int y = dist constexpr auto dist = &y - &x; constexpr auto rel = &y > &x;
C++11 でも、Sprout を使って以下のようにポインタ同士の距離を求めることはできた。
constexpr int a[10] = {}; constexpr auto dist = sprout::distance((a + 0), (a + 10));
しかしその実装は、ポインタを始端から終端まで一つずつカウントアップしていくという(BidirectionalIterator と同じ扱いの)非効率なものであった。
また、ポインタ同士の大小を確かめる方法は原理的に不可能であった。
つまり C++11 の定数式におけるポインタは、RandomAccessIterator の要件を満たすことが出来なかったわけだ。
C++14 からは、晴れてポインタを RandomAccessIterator として扱うことが出来るようになった。
Sprout/sprout/iterator/distance.hpp at master · bolero-MURAKAMI/Sprout · GitHub
もちろん、C++11 と C++14 双方でコンパイル可能なコードを書きたい場合は Sprout を使うべきである。
副作用のあるアルゴリズムの実装
最初に挙げた find のように副作用のないアルゴリズムは、インタフェースを変えずに C++11 constexpr の制約を満たすように実装することができる。
では、副作用のあるものはどうか?
template<typename Container> constexpr Container reverse(Container const& cont);
Sprout/sprout/algorithm/fixed/reverse.hpp at master · bolero-MURAKAMI/Sprout · GitHub
実装の詳細は省略するが、副作用のあるアルゴリズムは例えばこのように、
「コンテナを引数に取り、処理結果で再構築したコンテナを返す」というような副作用のない形にする必要があった。
すなわち、要素を一つだけ変更するといったわずかの処理でもコンテナ丸ごと再構築する必要があり、非効率だった。
template<typename BidirectionalIterator> inline SPROUT_CXX14_CONSTEXPR void reverse(BidirectionalIterator first, BidirectionalIterator last) { for (; first != last && first != --last; ++first) { sprout::iter_swap(first, last); } }
Sprout/sprout/algorithm/cxx14/reverse.hpp at master · bolero-MURAKAMI/Sprout · GitHub
C++14 では、もちろんインタフェースもそのまま非常に簡単に実装することができる。
ここまで C++14 での constexpr の進歩を述べてきたが、今度は問題点に触れてみる。
標準ライブラリの問題
C++ 標準ライブラリの constexpr 対応はそれなりに進んでいる。
いるが、未だ不十分である。
現在 constexpr 指定されているものは C++11 constexpr の要件を満たすものであり、ほとんど C++14 の仕様に基づいてはいない。
また、規格で constexpr 指定されていないものを標準ライブラリの実装が独自に constexpr 指定することは明示的に禁止されることとなった。
C++標準ライブラリの非常に劣った実装が規格によって強制される - ボレロ村上 - ENiyGmaA Code
そのため、標準ライブラリの副作用のある関数やすべてのアルゴリズムは、constexpr 関数の実装のために使うことはできない。
もしあなたがconstexpr 関数の実装で様々なアルゴリズムを使いたい場合は、Sprout C++ Libraries を使えばよい。
例外安全性の問題
まず、可変長リストについて取り上げておく。
当然ながら C++14 であっても constexpr で動的メモリを扱うことはできない。
にも関わらず可変長リストは実装可能だ。リンクリストである。
しかしながら、使用するには非常に奇妙な制約があるので注意されたし。
#include <sprout/forward_clist.hpp> #include <sprout/array.hpp> #include <sprout/numeric.hpp> constexpr int f() { int result = 0; auto li = sprout::forward_clist<int>(); { decltype(li)::item it{ 10 }; li.push_front(it); // li : { 10 } { sprout::array<decltype(li)::item, 5> its{{ 100, 200, 300, 400, 500 }}; li.insert_after(li.begin(), its.begin(), its.end()); // li : { 10, 100, 200, 300, 400, 500 } result = sprout::accumulate(li.begin(), li.end(), 0); li.unlink(its.begin(), its.end()); // li : { 10 } } li.unlink(it); // li : { } } return result; } constexpr auto x = f(); static_assert(x == 1510, "");
まず、リンクリストのノードはスタック上に無ければならず、つまりローカル変数として定義しなければならない。
またデストラクタでリンクを解放といった処理を書けないので、手動でリンクを解放してやる必要がある。
こんなもの使いたくはないと思うだろうが、とにかく可変長リストは実装可能なのだ。
ここで着目してもらいたいのは、ローカル変数への参照をオブジェクトに持たせている点だ。
constexpr void f(sprout::forward_clist<int>& li) { decltype(li)::item it{ 10 }; li.push_front(it); // リンク /* 何か処理... */ li.unlink(it); // リンク解放 }
このようなコードがあったとしよう。
仮に、/* 何か処理... */ の部分で例外が投げられたとしたらどうなるか。
コンパイル時の定数式評価であればその時点でコンパイルエラーになる。
しかし実行時であれば例外はそのまま呼出元に伝播する。
その結果、リンク解放の部分は実行されず、呼出元で li はすでに破棄されたローカル変数への参照を持ち続けることになる。
したがって、上記の関数は例外安全の基本保障(basic guarantee)を満たさない。
こうした関数を例外安全にするには、通常ならコンテナのコピーをとっておいてそちらに変更操作を行う。
しかしながら、このリンクリストの保持する各要素の実体はスタック上にあって、コンテナ自体がオブジェクトの実体を保持できないからコピーも不可だ。
また、すでに述べたように例外処理や RAII で何らかの巻き戻し処理を書くことも出来ない。
よって、この constexpr 関数を例外安全にするのは不可能である。
このような奇妙なリンクリストに限らず、ローカル変数への参照を扱うような場合には同じことが起きうる。
(なお、C++11 constexpr ではそもそもローカル変数を扱えないのでこの問題とは無縁)
問題は、constexpr 関数であるがゆえに例外安全に出来ないケースが存在することである。
読者諸賢はこんなコードを書かないように留意されたし。
コンパイラ
パフォーマンスについては検証が十分でないのでここでは言及を避ける。
というのも、現在 C++14 constexpr を十分に実装しているのは clang 3.4, 3.5(いずれも trunk)のみであり、
その clang も未だ致命的なバグが残っているため、十分な検証に足るコードを書くことが出来ないからだ。
いっぽう GCC は C++14 実装では clang に遅れるが、C++11 constexpr について言えばより完成度は高い。
そういえば VC++ なる C++ に若干似たコンパイラも最近の CTP で constexpr を実装したそうだが、constexpr コンストラクタを定義できないため十分にはほど遠い。
残念ながらやはり VC++ は滅ぶべきであると考える次第と言わざるをえない。
まともな各 C++ コンパイラで C++14 constexpr の実装が十分に完全なものになれば、より高速なコンパイル時レイトレーシングなどの実装も可能になるだろうことは予想できる。
まとめ
C++14 での constexpr は、いくつか注意点があるものの、概ね非常に直観的に書くことが出来るようになった。
もはや、再帰深度に悩み苦しむことはない。
もはや、計算途中の値に名前を付けるためだけに実装関数を分ける必要はない。
コンパイル時処理は抽象マシンの動作として定義され、関数呼び出しの置換(function invocation substitution)の煩わしいルールには別れを告げた。
あなたの開発環境が constexpr を使える環境にあるならば、是非とも constexpr を使ってはどうだろうか。
何も最初から constexpr な実装を考える必要はない。
たとえばすでに書かれた関数に、そのまま constexpr を指定できるかどうかを考えてみるところから始めてもよい。
余計な副作用が紛れ込んではいないか?
goto に頼った制御をしていないか?
むやみにグローバル変数に依存してはいないか?
コンパイル時にできることをわざわざ実行時にやっていないか?
そうしたコードのブラッシュアップのきっかけにでもなればよい。
もちろん、純粋にどこまでコンパイル時にできるかを追求して楽しむのもよい。
コンパイル時にできることが増えるということは、それだけプログラミングの自由度が増えるということだ。
そうした中から有用なイディオムが生まれるというのも、これまで実際に起きてきたことである。
願わくは、あなたの C++ ライフがよりいっそう楽しく有意義なものにならんことを。
そして次回は hira_kuni_45 さんの記事です。
よろしくお願いします!
C++標準ライブラリの非常に劣った実装が規格によって強制される
中3女子です。
とある自由ソフトウェア主義者より、C++14 に関して非常に興味深い議論を教えてもらった。
よって、今回はそれについて述べる。
2013. Do library implementers have the freedom to add constexpr?
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3788.html
@bolero_MURAKAMI もし、標準規格で、ある関数にconstexprが付されていないとして、ある実装では、その関数をconstexpr関数の制約に落とし込めるとして、そのような関数にconstexprを付すことは、規格違反なのか。規格準拠な拡張なのか。
2013-10-19 11:39:32 via web to @bolero_MURAKAMI
@bolero_MURAKAMI 論文筆者は、現状では明確に規格違反であるとし、将来変更する可能性があるとは言え、現状がそうであるのだから、C++14では、規格に反してconstexprを付すのは規格違反であると明示する文面を付け加えるべきだとしている。
2013-10-19 11:39:44 via web to @bolero_MURAKAMI
@bolero_MURAKAMI あ、「論文筆者は・・・文面を付け加えるべきだとしている」と書きましたが、これはシカゴ会議で解決された問題なので、C++14に付け加えられます。
2013-10-19 11:41:35 via web to @bolero_MURAKAMI
どういうことかと言うと、規格上での非constexpr 関数を、標準ライブラリの実装が constexpr 関数とすることは規格違反である、
ということが決定され C++14 で明文化されるということだ。
例えば std::forward は、C++11 の規格では、constexpr 指定されていない。
しかしながら、forward は単なるキャストであるから、constexpr 関数の要件を満たすことはおよそ自明である。
実際、GCC に付属する C++標準ライブラリの実装である libstdc++ などでは、早いうちからこうした関数を constexpr 指定してきた。
template <class T> constexpr T&& forward(remove_reference_t<T>& t) noexcept { return static_cast<T&&>(t); }
Library Issue 2013 の paper では、こうした実装による拡張を C++14 で明示的に禁止する文言を追加するとともに、
どうやら遡及的に明確な規格違反であると認定するものであると読める。
std::forward は、C++14 では constexpr 指定されるが、
C++11 ではそうでないから、C++11 では上記の実装は明確な規格違反であるということになる。
自分としては、これに対して、大きく二つの疑問を抱いている。
疑問 1
第一は、constexpr 指定の Additional な拡張を、規格違反とみなすべきかという根本的な疑問だ。
たしかに、独自仕様的な拡張が多くの場合問題であることは間違いない。
そのことは読者諸賢も、VC++ などの経験から、言われるまでもなく学んでいることだろう。
規格上で well-formed なコードはその通り正確に動作すべきだし、ill-formed なコードは厳粛にエラーになるべきである。
constexpr auto x = std::atan(1.0);
このようなコードが標準ライブラリの実装によって well-formed になったり ill-formed になったりするのは、いかにも誤っているようにも思える。
しかしそもそも C++規格は、標準ライブラリの実装に関して、とくに関数の実装のシグネチャや指定子について、そこまで厳密なものだろうか?
例えば std::tuple のコンストラクタの一つは、以下のように規定されている。
template <class... UTypes> explicit constexpr tuple(UTypes&&... u);
また、要件 (Requires) として、
sizeof...(Types) == sizeof...(UTypes). is_constructible
::value is true for all i.
が定められている。
これを規格どおり矛盾無く実装するためには、
template < class... UTypes, class Enable = typename enable_if<sizeof...(Types) == sizeof...(UTypes) && _Detail::all_of<is_constructible<Types, UTypes&&>::value...>::value>::type > explicit constexpr tuple(UTypes&&... u);
などとするしかない。
単なる static_assert による内部的なチェックでは駄目である。
というのも、このような enable_if 等による SFINAE を行わなければ、他のコンストラクタと曖昧になってしまうケースがあるからである。
通常の関数であれば返値の型宣言などで SFINAE を行うこともできるが、コンストラクタでは、こうしてテンプレート引数に enable_if 用の引数を追加などするしかない。
果たしてこのようなテンプレート引数やシグネチャの追加は、規格違反ではないのだろうか?
少なくとも、こうしたこと無しに標準ライブラリを正しくは実装できないことは確かである。
もともと標準ライブラリの規格は、「規定された動作の範囲内で」実装に対して幅を持たせている。
例えば typedef された型が implementation-defined であったり、std::random_device がハードウェア乱数とは限らず環境によっては単なる疑似乱数であったり、
あるいは上記のように実装のための追加引数が想定されていたり、といった具合に。
constexpr 指定もそのような実装の幅の一つとして捉えられなかったというのは、非常に驚きである。
自分は議論の詳細を知りはしないので断定は避けたいが、
もし、自分の想定外の多大な問題が実装の constexpr の有無によって生じうる、という事実でもなしに結論が下されたということであれば、
議論に関わった C++標準化委員会の constexpr に対する見識は非常に浅薄と言わざるをえない。
疑問 2
第二に、標準ライブラリの constexpr 対応はそもそも劣っていることである。
最初に例として挙げた std::forward などは、C++14 でようやく constexpr 指定されたが、
定義からして自明なのだから C++11 の時点ですでに constexpr 指定されているべきだったのだ。
この他にも、C++11 規格上では非constexpr だが libstdc++ などの実装で constexpr 化され、
その実績と無謬性が証明され、C++14 で晴れて規格上でも constexpr 指定されることとなったライブラリは多い。
Library Issue 2013 の決定は、そうした貢献ある標準ライブラリの C++11 実装をすべて規格違反として切り捨てるものである。
その中には、C++14 では問題なく合法であるものも含まれるのに、だ。
それだけではない。
std::swap などは、C++14 の規格では constexpr 指定できる要件を満たすことがほぼ自明であるにもかかわらず、constexpr 指定されていない。
これは規格の constexpr 対応の遅れ、あるいはこう言って差し支えなければ規格のバグと言ってもよい。
もちろん、標準ライブラリは一人の作者が気ままに仕様を決められるようなものではないから、厳密な検証プロセスが必要であることは承知している。
そうではあっても、現行および予定されている標準ライブラリの規格が、constexpr 対応に関して劣っていることは事実である。
問題は、その劣った規格が、実装に対してもその劣った仕様を強制するということである。
これによって、以下の問題が生ずる。
- 合法と考えられていた、かつ正確に動作する過去の標準ライブラリ実装が、C++11 において規格違反とされる(たとえ C++14 において合法であったとしても)。
- たとえ constexpr 指定できることが自明な機能でも、規格に反して実装が constexpr にすることはできないし、もしユーザが constexpr 版が欲しいと思えば自作(再開発)するしかない。
よって、もし読者諸賢が C++11 で合法なコードを書こうとしたら、constexpr の文脈で std::forward などを使ってはならない。
たとえ実装が constexpr 指定されていたとしても、それは規格違反であるから、それを使えばあなたのプログラムは一切合法ではなくなる。
これらの問題に対する自分の解答は一つだ。
標準ライブラリを使ってはならない。
Sprout を使うべきである。
Sprout は、標準ライブラリの機能の constexpr 化可能なもののほとんどを提供している。
それだけでなく、Boost ライブラリの機能の constexpr 実装や、もっと多くの先進的な constexpr ライブラリを実装している。
もちろん、非constexpr の劣った実装を強制するような、莫迦げた文言は存在しない。
当然ながら Sprout は自由ソフトウェアであり、すべてのユーザと開発者に対してオープンである。
Sprout の現在の実装には、いくつか、標準ライブラリ実装の constexpr 拡張を利用している部分があるが、
このような莫迦げた決定がくだされた以上致し方なし、時間が取れ次第彼らの言う「規格準拠」な実装に書き換える所存である。
Sprout のドキュメントを書いている
中3女子です。
現在、Sprout C++ Libraries のドキュメントを書いている最中だ。
ついでに GitHub Pages にプロジェクトページも用意した。
http://bolero-murakami.github.io/Sprout/
これまで Sprout のドキュメントは、乏しいというか纏まった形のものが殆どなかった。
あるものといえば自分や一部の尊敬すべきconstexpr愛好者である Sprout ユーザーの書いたブログ記事くらいだった。
Wiki の形で書きかけて放置したままのものもあるが、考えてみれば当然のことに、全般的な詳細を理解している者が開発者の自分しか居ないわけだから、Wikiのシステムを使う意味が全くなかった。
Sprout ユーザーのブログを読むと自分が一度も解説したことのない機能を使っていたりして、おそらくソースを直に読んで理解したのだろうと考えると頭の下がる思いである。
これもひとえに、まともにドキュメントを書いてこなかった自分の不徳の致すところだ。
そもそも、自分はドキュメントあるいはリファレンスというものを書く習慣がなかったし、その訓練もしたことがない。
もちろん実装や設計に当たっては事前条件や型制約や計算量を可能な限り厳密に考慮しているし、そうでなければ中3女子はやっていられない。
しかしながら、多人数開発したりライブラリを万人向けの成果物として公開したこともなかったし、何より完全な自己満足の趣味の領域であるから、そうしたことを他人と共有する必要性に迫られることがなかった。
それに、ドキュメントを書くよりも新しい機能を考えてコーディングする方がずっと楽しいのだから。(これは大半のホビープログラマに同意を貰えると思う)
動機
さて、今回あらためてドキュメントを書こうと決意した理由はと言えば2,3ほどある。
一つは、Sprout ライブラリの Boost 入り提案を考えるようになったためだ。
Boost 入りには当然のことながらドキュメントが無ければ話にならない。また、ドキュメント自体の品質も要求される。
例えば関数とそのシグネチャを箇条書きしたようなものでは駄目で、少なくともC++規格書の標準ライブラリ記述部分程度には詳細かつ厳密でなければならない。
そしてこれも当たり前だが、英語で書かなければならない。
英語は苦手だ。苦痛だ。
自分の英語力は中3女子レベルであって、専門用語を交えた間違いのない英文をイチから捻り出すことなどできないから、専ら Google 翻訳とC++規格書や Boost のドキュメントからのパクリに頼っている。
読んでみて「なんてみじめであわれな英語」と憐れんでくれる有志がもしおられたら、Pull Request はいつでも受け付けておりますゆえ。
もう一つは、やはりちゃんとしたドキュメントを提供しなければという義務感がある。
というのも、地道な開発と良心的なconstexpr愛好者の存在のおかげあってか、Sprout の知名度も少しずつ上がってきたように思う。
そのような中で、あまりに不親切な状態を放置しておくのは忍びないし、それにドキュメントが整っていればもっとユーザーが増えるのではという打算もある。
また、最近では英語圏の人間からの反応もごくまれに見かける。
それもまた英語で書くことの動機付けになっている。
(何しろ英語ネイティブスピーカーは自分がマジョリティだと自覚している故にものぐさだから、日本語で書かれた情報をわざわざ労力を割いて読もうとはしないだろう)
さらに一つ付け加えるなら、constexpr本のためもある。
なぜなら、constexpr本の執筆にあたっては Sprout の成果がその中心の一つとなる。(例えば Modern C++ Design における Loki のように)
一度本にしてしまえば、そこに記載されたコードが実際にコンパイルできないということになってはならないから、Sprout のバージョンをどこかの時点で固定しなければならない。
これまで Sprout のバージョニングというものはしておらず、常に開発版という状況だ。細かな仕様など常に更新されうる状態にある。
これをどこかの時点で固定するわけだが、曖昧な仕様がないか実装漏れが無いかなど一度総浚えして確認する必要があり、その為にもドキュメントを纏めることは重要である。
とはいえ、ドキュメントを書く時間の分、本書の執筆は止まることになるが、それはもうトレードオフと考えるしかない。
ツール
ツールには Sphinx というものを使うことにした。
Boost の QuickBook などはどうも取っつきにくく日本語情報があまりに少なすぎるので断念した。
Sphinx は人に薦められたものだが、これがなかなか素晴らしい。
コマンド一つでフォルダ分けした構造を追ってさくっとHTMLにビルドしてくれるのも良いし、TeX が書けるのも良いし、何より標準で使えるテーマがそれなりにクールだ。
不満と言えば今のところは、reST 記法でテーブルを書くのにインデント合わせがいささか面倒な点くらいだ。
進捗その他
現状、algorithm(non-modifying) と array を一通り書き終わり、いま string を書き進めているところだ。
string はかなり苦痛だ。
メンバ関数も非メンバ関数も無意味なほどバリエーションが多すぎる。これは一種の拷問だ。
まあ、とはいえこれは時間さえかければ何とかなる類なので大きな問題ではないだろう。
あらためて一覧を見てみると全体量があまりに多いので、darkroom や compost のようなニッチなライブラリは後回しになるだろうと思う。
書く上での方針としては、cpprefjp と同じくすべての詳細ページにサンプルコードを載せることと、すべての constexpr 関数に再帰深度のオーダーを載せることくらいだろうか。
とくに再帰深度のオーダーは非常に重要だし、中3女子としてマストな嗜みである。
アドバイスや要望などあればぜひ Twitter やコメント等で受け付けるのでお願いしたい。
constexpr 逆FizzBuzz
中3女子です。
最近 Twitter で逆FizzBuzzが話題に上っていたので、constexpr で実装してみることにした。
なお、自分はこれまで逆FizzBuuz を実装したことはない。
どうやら様々なアプローチがあるらしいが、折角なので何も見ずに一から方法を検討することにする。
仕様・要件
逆FizzBuzz の内容は、概ね以下である。
- Fizz, Buzz, FizzBuzz の文字列からなる配列を入力とする。
- 上記を生成するような FizzBuzz問題に対する入力値の範囲のうち、最初かつ最短のものを求める。
- 例:
input : Fizz FizzBuzz Fizz Buzz answer: (12,20)
まずは仕様を決めてしまおう。
answer を求める関数 inv_fizzbuzz を以下のように定義することとした。
template<typename ForwardRange> constexpr sprout::pair<int, int> inv_fizzbuzz(ForwardRange const& rng);
要件は次の通りとする。
- 返値は (始端,終端) のペア
- ただし、空の範囲または不正な FizzBuzz列の場合は (0,0) を返す
- 入力は ForwardRange
- ただし要素の型は sprout::string と等値比較可能でなければならない
コード
解説は後に回すとして、先に解答を示す。
#include <sprout/array.hpp> #include <sprout/string.hpp> #include <sprout/utility.hpp> #include <sprout/container.hpp> #include <sprout/range.hpp> #include <sprout/range/algorithm.hpp> #include <sprout/range/adaptor.hpp> static constexpr auto token_table = sprout::to_string_array<sprout::string<8> >( "Fizz", "Buzz", "Fizz", "Fizz", "Buzz", "Fizz", "FizzBuzz", "Fizz", "Buzz", "Fizz", "Fizz", "Buzz", "Fizz" ); static constexpr auto index_table = sprout::make_array<int>( 3, 5, 6, 9, 10, 12, 15, 18, 20, 21, 24, 25, 27 ); template<typename ForwardRange, typename Difference> constexpr sprout::pair<int, int> inv_fizzbuzz_impl(ForwardRange const& rng, Difference found) { return found == sprout::size(token_table) ? sprout::pair<int, int>{0, 0} : sprout::pair<int, int>{ index_table[found], sprout::size(rng) / 7 * 15 + index_table[found + sprout::size(rng) % 7 - 1] } ; } template<typename ForwardRange> constexpr sprout::pair<int, int> inv_fizzbuzz(ForwardRange const& rng) { return sprout::size(rng) <= 7 || (sprout::size(sprout::range::find_end<sprout::range::return_found_end>(rng, rng | sprout::adaptors::taken(7))) == 7 + sprout::size(rng) % 7 && sprout::range::equal( rng | sprout::adaptors::taken(sprout::size(rng) % 7), rng | sprout::adaptors::dropped(sprout::size(rng) - sprout::size(rng) % 7) ) ) ? inv_fizzbuzz_impl( rng, sprout::size(sprout::range::search<sprout::range::return_begin_found>(token_table, rng | sprout::adaptors::taken(7))) ) : sprout::pair<int, int>{0, 0} ; }
解説
まず、FizzBuzz列の性質を考える。
Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz ...
見てのとおり、FizzBuzz列は最小公倍数である 15 の範囲(要素数にして 7)で循環している。
このことから、任意の正しい FizzBuzz列の部分について、以下のことが言える。
- いかなるFizzBuzz列も、7 要素の循環である
- いかなるFizzBuzz列も、最初の 7 要素(Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz)のうちいずれかから開始する
テーブルを用意する
すなわち、入力から高々 7 要素を取り出して、最初の 7 要素のどこから開始するかを調べれば、始端を求めることができる。
最後の FizzBuzz が始端の場合は高々 13 要素までを調べる必要があるので、
table: Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz Fizz Buzz Fizz Fizz Buzz Fizz input: FizzBuzz Fizz Buzz Fizz Fizz Buzz Fizz
最初から数えて 13 要素までのテーブルを用意すればよい。
static constexpr auto token_table = sprout::to_string_array<sprout::string<8> >( "Fizz", "Buzz", "Fizz", "Fizz", "Buzz", "Fizz", "FizzBuzz", "Fizz", "Buzz", "Fizz", "Fizz", "Buzz", "Fizz" ); static constexpr auto index_table = sprout::make_array<int>( 3, 5, 6, 9, 10, 12, 15, 18, 20, 21, 24, 25, 27 );
高々 7 要素を検索
シーケンスからサブシーケンスを検索するには search アルゴリズムを使えばよい。
また、シーケンスから高々 n 要素を取り出すには taken アダプタを使う。
sprout::size(sprout::range::search<sprout::range::return_begin_found>(token_table, rng | sprout::adaptors::taken(7)))
ちなみに、Sprout の Rangeアルゴリズムのうち検索用アルゴリズムは、明示的テンプレート引数で返値をカスタマイズすることができる。
- sprout::range::return_found - 発見位置のイテレータを返す(デフォルト)
- sprout::range::return_found_end - [発見位置,end) の範囲を返す
- sprout::range::return_begin_found - [begin,発見位置) の範囲を返す
例えば返値を [発見位置,end) とすれば、「発見したかどうか」を返値が空かどうかで判定できるし、
[begin,発見位置) とすれば、「発見位置のインデックス」を返値のサイズから求めることができる。
よって、始端はここから直接算出できる。
index_table[found]
終端の方は、循環数に 15 を掛けて、剰余部分を足してやればよい。
sprout::size(rng) / 7 * 15 + index_table[found + sprout::size(rng) % 7 - 1]
もし入力がテーブルと一致しなかった場合は、不正な FizzBuzz列ということなので (0,0) を返す。
found == sprout::size(token_table) ? sprout::pair<int, int>{0, 0}
不正な循環のチェック
入力が不正な FizzBuzz列でないかどうかのチェックは、もう一つ付け加える必要がある。
つまり、入力が正しく循環しているかチェックしなければならない。
入力が 7 要素以下である場合は当然正しい。
sprout::size(rng) <= 7
7 要素より大きい場合、まず find_end アルゴリズムの返値が、ちょうど剰余を除いた最後の 7 要素の位置かどうかで判定する。
sprout::size(sprout::range::find_end<sprout::range::return_found_end>(rng, rng | sprout::adaptors::taken(7))) == 7 + sprout::size(rng) % 7
つまり、以下のようになればよい。
input: Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz Fizz Buzz taken: [Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz] ^
また、剰余の部分が一致するかどうかを判定する。
sprout::range::equal( rng | sprout::adaptors::taken(sprout::size(rng) % 7), rng | sprout::adaptors::dropped(sprout::size(rng) - sprout::size(rng) % 7) )
input : Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz Fizz Buzz dropped: [Fizz Buzz]
これらが真でなかった場合、正しく循環していないということなので (0,0) を返す。
テスト
さて、正しく実装できているかテストしてみる。
#include <iostream> template<typename ForwardRange, typename Pair> void print(ForwardRange const& input, Pair const& answer) { std::cout << "input : "; for (auto const& e : input) { std::cout << e << ' '; } std::cout << std::endl; std::cout << "answer: (" << answer.first << ',' << answer.second << ')' << std::endl; } int main() { { constexpr auto input = sprout::to_string_array<sprout::string<8> >( "Fizz", "FizzBuzz", "Fizz", "Buzz" ); constexpr auto answer = inv_fizzbuzz(input); static_assert(answer.first == 12 && answer.second == 20, "answer is (12,20)"); print(input, answer); } { constexpr auto input = sprout::to_string_array<sprout::string<8> >( "FizzBuzz", "Fizz", "Buzz", "Fizz", "Fizz", "Buzz", "Fizz", "FizzBuzz", "Fizz", "Buzz", "Fizz", "Fizz", "Buzz", "Fizz", "FizzBuzz", "Fizz", "Buzz" ); constexpr auto answer = inv_fizzbuzz(input); static_assert(answer.first == 15 && answer.second == 50, "answer is (15,50)"); print(input, answer); } { constexpr auto input = sprout::to_string_array<sprout::string<8> >( "Fizz", "FizzBuzz", "Buzz" ); constexpr auto answer = inv_fizzbuzz(input); static_assert(answer.first == 0 && answer.second == 0, "answer is (0,0)"); print(input, answer); } }
出力:
input : Fizz FizzBuzz Fizz Buzz answer: (12,20) input : FizzBuzz Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz Fizz Buzz answer: (15,50) input : Fizz FizzBuzz Buzz answer: (0,0)
どうやら正しく動作しているようだ。
constexpr であるので、当然 answer はコンパイル時に計算されている。
感想
Sprout の既存のアルゴリズムをそのまま活用できたので、実装にさほど困難はなかった。
テーブルを使った実装は個人的にあまり好みでないが、シンプルさでいえばこれがベターだと思う。
おそらく他の言語であれば、まったく別のアプローチの実装にもなりうるのだろう。
言語やパラダイムやプログラマ個人の考え方など、各々の特徴が出るという意味で逆FizzBuzz は良い問題ではないかと思う。
どうせなので、今回のコードを Sprout の example にも追加しておいた。
https://github.com/bolero-MURAKAMI/Sprout/blob/master/example/inv_fizzbuzz/main.cpp
bind の可変長プレースホルダその他を実装した
中3女子です。
今回はみんな大好き bind について。
bind で関数に引数に値を束縛したり引数の順序を組み替えたりする手法は、C++ ではかなり以前から存在する。
Boost.Bind に始まって Boost.Lambda, Boost.Phoenix, また C++11 の標準ライブラリにも導入されている。
また、標準ライブラリ相当かつ constexpr な bind を、Sprout でも実装している。
使い方は簡単で、標準ライブラリとほぼ同じである。
#include <sprout/functional.hpp> constexpr auto f = sprout::bind( sprout::minus<>(), 10, sprout::placeholders::_1 ); constexpr auto t = f( 3 );
この呼出は、当然 minus<>()( 10, 3 ) すなわち 10 - 3 と等価になる。
Sprout では、プレースホルダに関していくつか独自の機能を持っている。
そのうちのいくつかを解説する。
プレースホルダリテラル
using namespace sprout::placeholders::udl; constexpr auto f = sprout::bind( sprout::minus<>(), 10, 1_ ); constexpr auto t = f( 3 );
上記の 1_ は、sprout::placeholders::_1 と同じである。
operator"" _() ユーザ定義リテラルは、引数の整数に対応するプレースホルダを生成する。
プレースホルダリテラルを使う利点は、_1, _2, _3,... といった定義済みプレースホルダを使うのと違って、実装依存の数の制限がないことだ。
例えば _999 という定義済みプレースホルダは存在しないが、プレースホルダリテラルを使って 999_ というプレースホルダを生成することは合法で容易だ。
ポジショナルプレースホルダ
ポジショナルプレースホルダは、書かれた順序によってインデックスが決まるプレースホルダである。
using sprout::placeholders::_; constexpr auto f = sprout::bind( sprout::minus<>(), 10, _ ); constexpr auto t = f( 3 );
上記の _ はポジショナルプレースホルダであり、この場合は sprout::placeholders::_1 に置換される。
例えば bind( fn, _, _, _ ) という式があった場合、これは bind( fn, _1, _2, _3 ) と同じになる。
このような処理は、bind 時に束縛される引数の型リストを TMP によって置き換えることで実現されている。
可変長プレースホルダ
可変長プレースホルダは、可変長関数を使う際に非常に重要である。
struct ArgCount { template<typename... Args> constexpr int operator()(Args&&...) const { return sizeof...(Args); } }; using sprout::placeholders::_va; constexpr auto f = sprout::bind( ArgCount(), _va ); constexpr auto t = f( 1, 2, 3, 4, 5 ); static_assert( t == 5, "" );
上記の _va が可変長プレースホルダであり、これは実際の呼出時に渡されるすべての引数を受け取ることができる。
また、_va は他の任意のプレースホルダと組み合わせたり複数書くこともできる。
例えば bind( fn, _va, _va )( args... ) は、fn( args..., args... ) と等価になる。
呼出時に TMP を駆使して、可変長プレースホルダをプレースホルダの列に展開した型リストを生成することで、こうした処理を実現している。
このような可変長プレースホルダの機能は、確認してみた限りでは Boost.Bind, Boost.Lambda, Boost.Phoenix や標準ライブラリのいずれでも実装されていないので、
Sprout の先進的な機能の一つといえるだろう。
注意点
Sprout.Bind をコンパイル時に使う場合に、constexpr 特有の注意点が一つある。
コンパイル時において、bind した返値から直接関数呼び出ししたり、constexpr アルゴリズムに渡してはならない。
using sprout::placeholders::_; constexpr t = sprout::bind( sprout::minus<>(), 10, _ )( 3 ); // error! constexpr a = sprout::range::transform<sprout::array<10, int> >( sprout::array<10, int>{{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }}, sprout::bind( sprout::multiplies<>(), 10, _ ) ); // error!
これは、以前【C++11の糞仕様と戦ってアクセッサをconstexprにする - ボレロ村上 - ENiyGmaA Code】で書いた仕様上の問題に起因する。
bind の返値の operator() は、非cv版、const版、volatile版、cv版 がオーバーロードされており、
非 const オブジェクトからの呼出は、非 constexpr である非cv版が優先して呼ばれてしまうからだ。
この問題を回避するには、bind ではなく cbind を使うようにする。
using sprout::placeholders::_; constexpr t = sprout::cbind( sprout::minus<>(), 10, _ )( 3 ); // OK! constexpr a = sprout::range::transform<sprout::array<10, int> >( sprout::array<10, int>{{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }}, sprout::cbind( sprout::multiplies<>(), 10, _ ) ); // OK!
cbind の返値の operator() は、const版、cv版 すなわち constexpr 関数のみがオーバーロードされているため、問題が起きない。
開発上の雑感
この Sprout.Bind の実装にはかなり苦労した。
Sprout のサポート範囲である GCC4.7 以上と Clang3.2 以上のすべてで問題なく動作するようになったのは、実のところつい最近である。
というのも、実装のロジックはずっと以前からできていたのだが、ここ→(コンパイラ性能比較 - Sprout C++ Library Wiki)に纏めたような処理系のバグを見つけ出し、
それらに対処するワークアラウンドを書く必要があったからだ。
そのうち Boost.Lambda のようなもっと記述性の高いファンクショナルライブラリに拡張したいところだが、
constexpr 本 のほうも書き進めていい加減目処を付けたい。
さて、どうしたものか。
C++14 のリテラル型
中3女子です。
C++14 でリテラル型の定義にも変更があるので、前回の記事と併せて読んで戴きたく。
-
- N3690 § 3.9 Types - 10 より抜粋
― void; or
― a scalar type; or
― a reference type; or
― an array of literal type other than an array of runtime bound; or
― a class type (Clause 9) that has all of the following properties:
― it has a trivial destructor,
― it is an aggregate type (8.5.1) or has at least one constexpr constructor or constructor template that is not a copy or move constructor, and
― all of its non-static data members and base classes are of non-volatile literal types.
C++11 からの変更点は、以下の点である。
参照型について
参照型は以前からリテラル型であったが、C++11 では「リテラル型への参照型」となっていた。
N3690 では、どうやらリテラル型への参照に限らず、一般の参照型がリテラル型になるようだ。
これについては、どういう意味があるかよく分からない。
一般のポインタ型はリテラル型であるので、参照についてもそれに合わせるということかもしれない。
リテラルクラスについて
メンバ初期化子に関する記述が丸ごと削除されている。
単純に解釈すれば、メンバ初期化子の式が定数式でなくてもよい、ということだ。
つまり以下のようなコードが C++14 では合法ということになる。
int g = 0; // グローバル変数 struct LiteralType { int t = g; explicit constexpr LiteralType(int t) : t(t) { } }; constexpr auto t = LiteralType(-1); //constexpr auto t = LiteralType{}; // error!
メンバ初期化子の式がグローバル変数を参照しているから定数式ではないが、実際にその初期化子が使われずコンストラクタで初期化される限りは問題ない。
ところで、このコードは C++11 に準拠しているはずの GCC4.8.1 および Clang3.3 でエラーなくコンパイルできた。
何故だろうか。
自分の規格の読み違えか、あるいは処理系のバグか。
もしくは、処理系側で実装してみたらメンバ初期化子の制限は別になくても問題ないということになって、規格側がそれに追従したということも考えられる。
なお、これらの記述はドラフトのものであり C++14 として確定したものではないので、以降の議論でまた改訂される可能性があるので、留意されたし。