読者です 読者をやめる 読者になる 読者になる

ボレロ村上 - ENiyGmaA Code

中3女子です。

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



どういうことかと言うと、規格上での非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 拡張を利用している部分があるが、
このような莫迦げた決定がくだされた以上致し方なし、時間が取れ次第彼らの言う「規格準拠」な実装に書き換える所存である。