C++標準化委員会の文書のレビュー: P0030R0-P0039R0
[PDF] P0030R0: Proposal to Introduce a 3-Argument Overload to std::hypot
C++11で追加されたstd::hypotに3引数版のオーバーロードを追加する提案。
std::hypot( x, y )は、\(\sqrt{ x^2 + y^2}\)を、計算過程でオーバーフロー、アンダーフローが発生しない方法で計算する関数だ。
hypotの応用方法として、二次元空間における2点間の距離の計算、\(\sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}\) に使える。
struct point
{ double x, y ; } ;
double dist( point a, point b )
{
return std::hypot( a.x - b.x, a.y - b.y ) ;
}
しかし、多くの分野では、三次元空間における2点間の距離を計算、、\(\sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2 (z_q - z_2)^2}\)したいことがよくある。hypotは2引数なので、現状では以下のように使わなければならない
struct point
{ double x, y, z ; } ;
double dist( point a, point b )
{
return std::hypot( a.x - b.x, std::hypot( a.y - b.y, a.z - b.z ) ) ;
}
hypotに3引数版のオーバーロードを追加すると、以下のように書ける。
std::hypot( a.x - b.x, a.y - b.y, a.z - b.z ) ;
論文では、Variadic templatesを利用した任意個の引数を取る汎用的な関数の提案も考えたが、そのような関数の利用例が明らかではないとして、今回の提案では3引数版のみにとどめたという。
P0031R0: A Proposal to Add Constexpr Modifiers to reverse_iterator, move_iterator, array and Range Access
array, reverse_iterator, move_iteratorをconstexprに対応させる提案。
P0032R0: Homogeneous interface for variant, any and optional
現在提案されているvariant, any, optionalには、機能は同じだがメンバー名や引数などが異なるメンバーがあるので、統一を図る。
P0033R0: Re-enabling shared_from_this
現行のenable_shared_from_thisの文面では、挙動が明確に規定されていないコードが存在してしまう。
enable_shared_from_thisは、shared_ptrで管理する型の基本クラスとすることで、shared_from_thisというメンバー関数を追加できる。このメンバー関数を呼び出すと、そのオブジェクトを所有している既存のshared_ptrが返される。以下のように使える。
// CRTPとして使う
struct X : public std::enable_shared_from_this<X>
{ } ;
int main()
{
auto sp1 = std::make_shared<X>() ;
// sp1と所有権を共有するshared_ptrが得られる
auto sp2 = sp1->shared_from_this() ;
}
ところが、現行の文面では、以下のようなコードの挙動が未定義になってしまう。
int main()
{
struct X : public enable_shared_from_this<X> { };
auto xraw = new X;
shared_ptr<X> xp1(xraw); // #1
{ // 破棄時にdeleteしないので安全
shared_ptr<X> xp2(xraw, [](void*) { }); // #2
}
xraw->shared_from_this(); // #3
}
shared_from_thisの前提条件として、オブジェクトを所有するshared_ptrのオブジェクトが存在しなければならない。このコードでは、存在している。問題は、複数のshared_ptrが独立して存在しているということだ。ただし、後から構築される#2のオブジェクトのデリーターは何もしないので、このコードは二重にdeleteするという問題はない。すると、このコードは合法なのだろうか。
しかし、合法だとして、この場合はどちらのshared_ptrと所有権を共有するオブジェクトが返るのだろうか。#1だろうか、#2だろうか。
#1が返る場合、#2のshaared_ptrの構築時には、xraw->_weak_thisがアップデートされないことになる。#2が返る場合、アップデートされる。
問題は、shared_from_thisのpostconditionに照らし合わせると、どちらの挙動もweak_ptrがらみのpostconditionを満たせないので、規格上挙動が規定されていない。
さて、既存の実装であるDinkumware, GNU, LLVMの実装は、いずれも#2が_weak_thisをアップデートする挙動になっている。これは設計上意図的なものではない。
一方、Boostの実装では、ユーザーの意見を取り入れた結果、#2は_weak_thisをアップデートしない挙動に意図的にしている。つまり、一番最初に作られたshared_ptrと所有権を共有するshared_ptrを返す。
現在、既存の実装の挙動に依存したコードはなく、Boostの挙動の方が実際の需要があるため、Boostの挙動にあわせる変更を提案している。
また、この提案では、weak_ptrを得るweak_from_thisの追加も提案されている。
P0034R0 – Civil Time
軽量な日付ライブラリの提案。
Boost. Date Timeはあまりにも巨大すぎるし、作者が標準ライブラリに追加することに興味を示していないので、軽量な日付ライブラリを提案している。
このライブラリは、グレゴリオ暦以前の日付はtime_tのオーバーフロー、time_t以上の精度でうるう秒を扱うことは考慮しない。タイムゾーンが使える。
P0035R0: Dynamic memory allocation for over-aligned data
待望のオーバーアライメントしてくれる確保関数の提案。
C++の現行規格では、確保関数はオーバーアライメントする必要はない。例えば、floatのアライメントが4バイトで、SIMD命令の都合上、floatの配列を16バイトアライメントしたいとする。以下のようなコードを書いても、16バイトアライメントされる保証はない。
class alignas(16) float4 {
float f[4];
};
float4 *p = new float4[1000];
C++の規格上は4バイトアライメントされれば十分なのだ。オーバーアライメントは規格上必須ではない。
シグネチャは以下のようになる。
namespace std {
enum class align_val_t: size_t;
}
void* operator new(std::size_t size, std::align_val_t alignment);
void* operator new(std::size_t size, std::align_val_t alignment,
const std::nothrow_t&) noexcept;
void operator delete(void* ptr, std::align_val_t alignment) noexcept;
void operator delete(void* ptr, std::align_val_t alignment,
const std::nothrow_t&) noexcept;
void* operator new[](std::size_t size, std::align_val_t alignment);
void* operator new[](std::size_t size, std::align_val_t alignment,
const std::nothrow_t&) noexcept;
void operator delete[](void* ptr, std::align_val_t alignment) noexcept;
void operator delete[](void* ptr, std::align_val_t alignment,
const std::nothrow_t&) noexcept;
P0036R0: Unary Folds and Empty Parameter Packs (Revision 1)
folding expressionに空のパラメーターパックを指定した時のフォールバックを、operator &&, operator ||, operator , だけに留める提案。
デフォルトで0が変えるとまずい場合がある。
P0037R0: Fixed_Point_Library_Proposal
固定小数点ライブラリの提案。
template < class ReprType, int Exponent >
class fixed_point ;
ReprTypeは内部でつかう整数型で、Exponentは、ビットを指定された数だけシフトする。fixed_pointの精度は、pow(2, Exponent)であり、最小値と最大値は、pow(2, Exponent)にstd::numeric_limites<ReprType>::min()/max()を掛けた値になる。
Exponentを指定するのは面倒だ。固定小数は、基数ビット数と小数ビット数を記述することで指定できる。プログラマーの多くはこの記述方法を好む。
そこで、fixed_pointのエイリアスが用意される。
template <unsigned IntegerDigits, unsigned FractionalDigits = 0, bool IsSigned = true>
using make_fixed ;
template <unsigned IntegerDigits, unsigned FractionalDigits = 0>
using make_ufixed ;
例えば、8bitの符号なしの固定小数で、基数と小数のビット数にそれぞれ4ビット割り当てたい場合は、
make_ufixed<4, 4> value{ 15.9375 } ;
32bit、符号あり、基数が2ビット、小数が29ビットにしたい場合は、
make_fixed<2, 29> value { 3.141592653 } ;
固定小数と組み込みの整数型は相互に明示的に変換できる。
丸め誤差は発生する。
make_ufixed<4, 4>(.006) == make_ufixed<4, 4>(0)
このコードは丸められた結果trueとなり得る。
整数に普通に使える演算子は固定小数にもオーバーロードされていて普通に使える。シフト演算子と比較演算子以外では、二項演算子は固定小数同士や、固定小数と他の演算型を組み合わせてオペランドに取れる。
組み合わせが同じ固定小数型ではない場合、簡単なプロモーション風のルールによって、戻り値の型が決定される。
- どちらの実引数も固定小数の場合、大きなサイズの型が選ばれる。どちらか片方が符号付きならば、符号付きになる
- どちらかの実引数が浮動小数点数型の場合、結果の型は入力と同じかそれ以上大きい、最小の浮動小数点数型になる
- どちらかの実引数が整数型の場合、結果は他方の固定小数型になる
例
make_ufixed<5, 3>{8} + make_ufixed<4, 4>{3} == make_ufixed<5, 3>{11};
make_ufixed<5, 3>{8} + 3 == make_ufixed<5, 3>{11};
make_ufixed<5, 3>{8} + float{3} == float{11};
提案では、このプロモーションルールの理由を、挙動を簡単に推測できるためとパフォーマンス上の理由から、各ルールについて以下のように説明している。
- 固定小数のみが使われていた場合に、最小の計算のみが行われることを保証する。
- 型を混ぜた演算のプロモーションルールをまねた。固定小数の範囲から外れるようなexponentを持つ値も作り出せるようにし、浮動小数点数から整数へのコストのかかる変換を避ける
- 入力として与えられた固定小数型を維持する。処理に重要なのは意図的に固定小数型である
シフト演算子は右辺に整数型を取り、オーバーフロー、アンダーフローしない新しい型を返す。
比較演算子は、プロモーションルールにしたがって入力を共通の型に変換した上で、比較して、結果をtrueかfalseで返す。
符号付きと符号なしの固定小数のオーバーフローは、未定義の挙動となる。符号なしの整数型のオーバーフローの挙動が規定されている基本型とは異なる。
// オーバーフローの例
make_fixed<4, 3>(15) + make_fixed<4, 3>(1)
アンダーフローが発生して精度が失われることは許容されている。
make_fixed<4, 3>(15) + make_fixed<4, 3>(1) ;
この結果は7になるが、これは許容されている。
ただし、すべてのビットがアンダーフローによって失われた場合、その値は「フラッシュされた」状態になり、未定義の挙動となる。
固定小数の精度を上下させるpromote/demote関数がある。これは、基数部と小数部のビット数をそれぞれ、2倍、1/2倍する。
// make_fixed< 4, 4 >
promote( make_fixed< 2, 2 >(1) ) ;
// make_fixed< 2, 2 >
promote( make_fixed< 4, 4>(1) ) ;
オーバーフローを防ぐために精度を上げつつ計算する関数が用意されている。
単項演算子としては、trunc_reciprocal, trunc_square, trunc_sqrt, promote_reciprocal, promote_square
二項演算子としては、trunc_add, trunc_subtract, trunc_multiply, trunc_divide, trunc_shift_left, trunc_shift_right, promote_add, promote_sub, promote_multiply, promote_divide
trunc_は、入力よりも大きくはない結果を返すが、exponent部分をオーバーフローを避けるために変更される。
promote_は、オーバーフローとアンダーフローが起きないほど十分に大きな型を結果として返す
_multiplyと_squareは、64bit型には提供される保証がない。
_multiplyと_squareは、入力が最小負数である場合、未定義の挙動となる。
_squareは符号なし型を返す
_divideと_reciprocalは、ゼロ除算チェックは行わない
trunc_shift_は、最初の入力と同じ型を返す。
関数はまだ追加される余地がある。
P0038R0: Flat Containers
Boost.ContainerにあるFlat Associative Container(flat_map, flat_set, flat_multimap, flat_multiset)の追加。
フラット連想コンテナーとは、従来の連想コンテナーであるmapやsetのように、キーと対応する値をもっていて、キーによって値を高速に検索できる特性を持つ。従来のノードベースの連想コンテナーと違い、連続したストレージ上に要素が確保される。
実装方法は2つある。ひとつは要素を常にキーでソート済みにしておくこと。もうひとつはヒープ構造を用いること。Boostの実装はソートを使っている。ヒープを使った実装はキャッシュのローカル性を保ちやすいので、理論上優れた実装であるとされ、会議では関心が高かったが、実装経験が少ないため、さらなる検証が必要であるとしている。
P0039R0: Extending raw_storage_iterator
raw_storage_iteratorを改良する提案。
raw_storage_iteratorにムーブ代入演算子を追加し、ムーブに対応させる。
raw_storage_iteratorを作るのは面倒なので、factory関数を追加する。
template<class T>
auto make_storage_iterator( T&& iterator)
{
return raw_storage_iterator<std::remove_reference<T>::type, decltype(*iterator)>( std::forward<T>(iterator));
}
raw_storage_iteratorの目的は、主にplacement newで使うためだが、placement newの文法はraw_storage_iteratorをサポートしていない。これを直接サポートする。
template<class T, class U>
void* operator new(size_t s, raw_storage_iterator<T,U> it) noexcept
{
return ::operator new(s, it.base() );
}
template<class T, class U>
void operator delete ( void* m, raw_storage_iterator<T,U> it) noexcept
{
return ::operator delete(m, it.base() );
}
ドワンゴ広告
この記事はドワンゴ勤務中に書かれた。来月は有給を取りまくる必要のある事情がある。
ドワンゴは本物のC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0