P0100R0: Comparison in C++
C++に様々な比較用のカスタマイズ可能な共通ライブラリを追加する提案。
一口に比較といっても、patial order, weak order, total order、そしてequivalenceがある。アルゴリズムやデータ構造によって、要求する比較の強さや、提供できる比較の強さは異なる。従来のC++では、このような異なる強さの比較が存在するにもかかわらず、すべての比較のcustomization pointは、単一の演算子のオーバーロードで提供されてきた。
この提案では、以下のような関数をstd名前空間に追加し、必要によってライブラリがオーバーロードして上書きすることによって、customization pointを提供するものである。
template<typename T> bool partial_less(const T&,const T&);
template<typename T> bool weak_less(const T&,const T&);
template<typename T> bool total_less(const T&,const T&);
また、同値比較としては以下の関数が追加される。
template<typename T> bool partial_unordered(const T&,const T&);
template<typename T> bool weak_equivalence(const T&,const T&);
template<typename T> bool total_equal(const T&,const T&);
SG6による数値まわりの提案をまとめた文書。
メタプログラミングにより数値計算をして、コンパイル時に値を保持するのに必要な数値型のビット数を求めることができる。しかし問題は、その求めたビット数から整数型を選択するライブラリが提供されていない。そこで、この提案では、そのようなライブラリを提案する。
exact_2uint<32> a; // きっちり32bitの符号なし組み込み整数型
least_2int<19> a; // 少なくとも19bitを持つ符号付き整数型
fast_2int<7> a; // 少なくとも7bitを持つ高速な符号付き整数型
この提案で興味深いのは、符号付き整数型は、負数を2の補数で表現すると厳密に規定されていることだ。
浮動小数点数型は、以下の通り、
exact_2ieeefloat<64> a; // きっちり64bitの浮動小数点数
fast_2ieeefloat<32> a; // 高速な少なくとも32bitの浮動小数点数
least_2ieeefloat<80> a; // 少なくとも80bitの浮動小数点数
浮動小数点数はIEE 754で規定されている浮動小数点数表現を使うと厳密に規定されている。
そして、符号付き整数型、符号なし整数型、浮動小数点数型の最大のビット数を得るためのプリプロセッサーマクロ、MAX_BITS_2INT, MAX_BITS_2UINT, MAX_BITS_2IEEEFLOATも追加される。プリプロセッサーマクロである理由は、コンパイル時に分岐できるようにするためだ。
オーバーフローを検出できる整数演算ライブラリと、演算結果を2倍のサイズの整数型で返してくれるライブラリと、演算結果を同サイズの整数型2つに分けて返してくれるライブラリの提案。
結論から言うと、クソコード生成ライブラリ。
例えば、2つの符号付き整数を加算する場合は以下のようになる。
int a , b
std::cin >> a >> b ;
int sum{} ;
bool b = std::overflow_add( &sum, a, b ) ;
if ( b )
std::cout << "overflow detected." << std::endl ;
else
std::cout << "result: " << sum << std::endl ;
overflow_xxxは、第一引数に結果を格納する整数型へのポインターを取り、第二引数以降に演算する整数の値を取る。演算結果がオーバーフローした場合、trueが返され、ポインターには書き込まれない。演算結果がオーバーフローしなかった場合、falseが返され、演算結果がポインターを関節参照して書き込まれる。
提案は他にも、倍精度演算を提供している。結果は、演算に使った型の倍のサイズの整数型でうけとるか、2つの整数型で受け取ることができる。型は、<cstdint>から持ってくるか、P0102で提案されている型を使うなどする。この提案では整数型は規定しない。
std::int32_t a , b ;
std::cin >> a >> b ;
std::int64_t sum = std::wide_add( a, b ) ;
2つの整数型で受け取る場合、上位ビット列を戻り値で返し、下位ビット列を第一引数のポインターを介した間接参照で書き込んで返す。下位ビット列は必ず符号なし整数型になる。
std::int32_t a, b ;
std::cin >> a >> b ;
std::uint32_t lower ;
std::int32_t upper = std::split_add( &lower, a, b ) ;
ちなみに、論文著者はLawrence Crowlで、GCCが提供しているオーバーフロー検知機能付きの演算ライブラリを引数の順序などを少し手直しした他はそのまま持ち込んできた様子だ。きわめて原始的でわかりにくい設計をしていて、これを使ったコードはLinusも激怒するほどのクソになる。
本の虫: Linus Torvals、クソコードにブチギレ
オーバーフロー検知機能付きの演算はほしいが、ライブラリで提供するのは無理がある。思うに、コア言語でのサポートが必要だろう。例えば以下のような
try {
int sum = overflow_cast<int>( a + b ) ;
} catch( std::overflow_exception e )
{
// オーバーフローが発生した
}
任意のワード長での整数演算ができるmulti_int/multi_uintの提案。
template<int words> multi_int;
template<int words> multi_uint;
実装が提供する最大の組み込み整数型ですら演算精度が足りない場合、複数の整数型のオブジェクトを組み合わせて、多倍長精度演算を行う必要がある。そのような演算を手で書くのは難しい。標準で提供されているべきである。
multi_int/multi_uintは、組み込み型が提供している演算子を同じ意味でオーバーロードしている。また、通常の整数と同じように、暗黙にboolに変換できるようboolへの変換関数もある。
提案では、このような演算を整数型の配列に対して行う低級な関数も提供している。これによりユーザーは自前の多倍長演算も実装できる。提案では関数のシグネチャに(が足りずに文法が間違っていることと、divがないように見える。
無限精度整数ライブラリも標準にほしいが、こちらもほしい。
数値演算に対する丸め方法とオーバーフロー時の挙動を指定できる機能。
浮動小数点数の丸め方法はすでに、numeric_limitsのround_styleで取得したり、fenv.hで設定できる。しかし、既存の方法ではある重要な場合における挙動がかけている。
もし、値が表現可能な2つの値から等しく離れていた場合、どちらの方向に丸められるのかということだ。
論文では、これに対して、以下の方向を考察している。
負の無限大方向 |
正の無限大方向 |
ゼロ方向 |
ゼロから離れる方向 |
偶数方向 |
奇数方向 |
実行時間最速 |
生成コード最小 |
どうでもいい |
この考察を元に、提案では以下のenumがある。
enum class rounding {
all_to_neg_inf, all_to_pos_inf,
all_to_zero, all_away_zero,
all_to_even, all_to_odd,
all_fastest, all_smallest,
all_unspecified,
tie_to_neg_inf, tie_to_pos_inf,
tie_to_zero, tie_away_zero,
tie_to_even, tie_to_odd,
tie_fastest, tie_smallest,
tie_unspecified
};
このうち、all_away_zero, all_to_even, all_to_odd, tie_to_neg_inf, tie_to_pos_inf, and tie_to_zeroは、conditionally supportedとなっている。規格準拠の実装はサポートする義務がない。
このモードは、T round(mode,U)という関数に渡すことで使用する。
この提案はオーバーフローが起きたときの挙動を指定できるT overflow( mode, T upper, U value )も提案している。
モードは以下のようになっている。
enum class overflow {
impossible, undefined, abort, exception,
special,
saturate, modulo_shifted
};
impossible: 完全に正しいプログラムでありオーバーフローは数学的に決して起こらない。コンパイラーへのヒントを与えるのにも使える
undefined: オーバーフローは極めてまれにしか起こらない。オーバーフローが起こった時の挙動は未定義
abort: オーバーフローが起こったらabortする。
quick_exit: オーバーフローが起こったらquick_exitを呼び出す
exception: オーバーフローが起こったら例外を投げる
special: オーバーフローが起こったことをしめす特殊な値を返す(IEEE 754の規定する浮動小数点数などに存在する)
saturate: 有効な値の範囲でもっとも近い値を返す
modulo_shifted: 0からzまでの範囲の場合、結果はx mod (z+1)となる。2の補数表現による挙動
こちらの設計のほうがまだ比較的読みやすい。
また、提案では丸め方法とオーバーフロー挙動を同時に指定できる関数も提案している。
2進数固定小数点数ライブラリの提案。
提案を読んだがどうにも使い方が難しい設計と命名だ。筆者に数学的素養がないためかもしれないが、もう少し誰でも簡単に使える設計になっていた方がいいのではないか。
論文は2進数固定小数点を標準でサポートしている既存の言語としてAda, COBOL, CORAL 66, JOVIAL, PL/1を挙げている。CORALとJOVIALは初めて聞いたプログラミング言語だ。
constexprが暗黙にcosntを意味しなくなったので、std::arrayがconstexpr対応ではなくなってしまったのを修正する提案。
SIMDやGPGPUなどの軽量実行媒体では、TLSはうまく動かない。これは既存のライブラリが動作しない問題を引き起こす。
最も問題になるのは、math.hだ。多くのmath関数は、エラー通知にerrnoを使う。math_errhandling & MATH_ERRNOが非ゼロである場合、errnoに具体的なエラー内容が書き込まれる。errnoは、互換性のために必要だが、その特性からTLSが使われている。
SIMDやGPGPUによる並列化が行われるコードは、特に数学関数を使いたいはずだ。しかし、数学関数はerrnoに書き込むために競合を起こして使えない。
この問題はどうにかして解決しなければならない。軽量実行媒体でもTLSを使えるようにする案もあるが、ここでは、数学関数側で対処する案が考察されている。
1. 制約を加える
軽量実行媒体の文脈では、数学関数はmath_errhandlingやerrnoにアクセスしない。
2. errnoを関数の引数を経由して受け取る
errnoにアクセスするすべての数学関数に、例えば以下のようにオーバーロードを追加しなければならない。
double acos(double x, int *errnm);
3. errnoを戻り値で返す。
tupleなどを使わなければならない。
std::tuple< double, errno_t > acos( double x ) ;
そもそも、errnoでエラー処理をしているプログラマーは少ないと思うので、1.が最も妥当な案だと思う。本当にエラー処理をしたければ実装依存の方法を使うのではないか。
3つもの大きめの変更が提案されているので、提案を分割してほしい気がする。
この文書では、Opaque Aliasを提案している。
1. Opaque Alias
かつて、強いtypedefとも呼ばれていた機能。以下のようなエイリアス宣言に似た文法を持つ。
using identifier = access-specifier type-id opaque-definition
access-specifierは、お馴染みのpublic/protected/privateだ。それぞれ、以下のような意味を持つ。
- private: 暗黙の型変換を認めない
- public: is-a関係。暗黙の型変換を認める
- protected: is-implemented-as関係。opaque型の定義の中でのみ暗黙の型変換を認める
以下が利用例だ。
// intのopaque alias
using type = public int ;
type x = 0 ; // OK 暗黙の型変換
// OK、ただしyの型はint
auto y = x + x ;
publicのopaque aliasは、あらゆる箇所で内部型との暗黙の型変換を許可する。そのため、int型で初期化できる。"x+x"の結果の型がintになるのは驚くかもしれないが、仕組みを知れば当然だ。まず、operator +(type, type)は宣言されていないので、typeがintに暗黙に型変換されたうえで、組み込みのint opeartor +(int,int)が呼ばれる。そのため、結果はint型になる。
privateを使えば、一切の暗黙の型変換が禁止される
using type = private int ;
type x1 = 0 ; // エラー、暗黙の型変換は禁止
type x2 = reinterpret_cast<int>(0) ; // OK
type x3{ 0 } ; // OK
auto y = x2 + x2 ; // エラー
operator =やoperator +のような基本的な操作で、完全に暗黙の型変換を許すか、全面的に許さないかだけでは使いづらい。また、式を評価した結果の型についても、柔軟に決めたい。そこで、opaque aliasには、opaque-definitionを記述することができる。
using type = public int
{
type opeartor + ( type x, type y )
{
return type { int{x} + int{y} } ;
}
} ;
type x{ 0 } ;
// yの型はtype
auto y = x + x ;
// zの型はint
// 暗黙の型変換でint operator - ( int, int )が呼ばれる
auto z = x - x
opaque定義では、このようにトランポリン関数を定義できる。こうすることによって、任意の演算子の挙動を変えられる。operator -は定義していないので、暗黙の型変換により組み込みのint operator -(int,int)が呼ばれる。
目的によっては、operator -を使う事自体が想定できない場合がある。そのために、deleted定義が使える。また、引数の型を変更するだけのトランポリン関数の場合、default化することもできる。
using type = public int
{
type operator +( type, type ) = default ;
type operator -( type, type ) = delete ;
} ;
type x{ 0 } ;
// yの型はtype
auto y = x + x ;
// エラー、deleted定義
auto z = x - x ;
// OK
// 暗黙の型変換でint operator * (int, int)が呼ばれる
auto w = x * x ;
あらゆる演算子に対してdeleted定義を書くのは面倒だ。そこで、アクセス指定子にprotectedを指定すると、opaque定義以外の暗黙の型変換をしないようになる。
using type = protected int
{
type opeartor +( type, type ) = default ;
} ;
type x1 = 0 ; // エラー、暗黙の型変換は使えない
type x2{ 0 } ; // OK
auto y = x - x ; // エラー
ちなみに、opaque aliasのopaque aliasも作れる。
using A = private int ;
using B = public A ;
A a{} ;
B b{} ;
// OK
// BはAからのpublicなopaque alisなので、
// BからAに暗黙に型変換できる
a = b ;
// エラー
// Bにoperator =( A )は定義されていない
// Aは暗黙の型変換を許さない
b = a ;
opaque aliasをクラス型に対して適用する場合、メンバー関数に対しては、自動的にトランポリン関数が作られる。
using type = private std::vector<int> ;
type t ;
t.push_back( 0 ) ;
// 元のクラス型をtype型にしたトランポリン関数を呼び出す。
// トランポリン関数は元のメンバー関数を呼び出し結果を返す
type y( t ) ;
opaque aliasはテンプレート宣言できる。
template < typename T >
using vec = private std::vector<T> ;
template <typename T >
using INT = protected int
{
INT operator +( INT, INT ) = default ;
INT operator +( INT, T ) = default ;
INT operator +( T, INT ) = default ;
}
エイリアス型と元の型は、is_sameやis_base_ofなどの型の関連性を調べるtraitsでは、異なる型で派生関係にもないと判断される。typeidも別の型になる。is_integralなどの単一の型を調べるtraitsでは、エイリアス型は元の型と同じになる。
sizeofの結果は元の型と同じで、メモリレイアウトなどは全て同じ。opaque aliasを使うことによる実行時コストは発生せず、reinterpret_castを使って元の型にキャストしても動作する。
また、P0109は、関数エイリアスを提案している。
関数に別名を付けたい需要は昔からある。例えば、<algorithm>はオブジェクトの比較に、operator *lt;を使う版と、比較関数を受け取る版の関数が存在する。それぞれ、やっていることは同じなのにコードが異なる。
template < typename T >
constexpr const T & min ( const T & a, const T & b )
{
return a < b ? a : b ;
}
template < typename T, typename Compare >
constexpr const T & min( const T & a, const T & b, Compare comp ) ;
{
return comp( a, b ) ? a : b ;
}
何らかの方法で、compに関数エイリアスとしてoperator <を割り当てることができれば、同じように自然に書ける。
template < typename T, typename Compare >
constexpr const T & min( const T & a, const T & b, Compare comp ) ;
{
using operator <() = comp ;
return a < b ? a : b ;
}
また、P0109は、派生に対する拡張も提案している。配列とCV修飾されている型を除く、基本型から派生できるようにする拡張だ。
// OK
struct X : int ;