文書の一覧
全部で43本あります。
多次元配列に対するstd::span
である、mdspan
の提案。
以前の記事を参照
このリビジョンでの変更は
std::span
と調和するように変更
- レイアウトマッピングクラスの変換コンストラクタを、暗黙変換できない
extent
によってexplicit
になるように修正
mdspan
の変換コンストラクタを、保持するメンバとテンプレートパラメータが明示的に変換可能であるかによってexplicit
になるように修正
submdspan
の文言改善
- レイアウトについての文言の一貫性改善
- アクセサクラスとマッピングクラスからのデフォルト構築可能性を削除
layout_stride
の変換コンストラクタを修正
extents/mdspan
の整数値からの推論補助にexplicit
を追加
- 要素型と
extent
を含まないようにmdspan
の制約を調整
- 機能テストマクロの追加
submdspan
のsubslice引数をpair
の代わりにtuple
を取るように変更
mdspan::unique_size
を削除
- 整数型のstride配列に対して柔軟になるように、
layout_stride
のコンストラクタを修正
extent/mdspan
のコンストラクタがrank_dynamic
または整数rank
指定値(あるいはそのサイズの配列)のいずれかを受けられるように変更
mdspan
がtrivially default constructibleであるという指定を削除
mdspan
がnon-owningであるという単語を削除(例えば、shared_ptr
をポインタとして使用する事ができる)
- 1次元
layout_left
からlayout_right
の相互変換を追加
- ランク0の
layout_left, layout_right, layout_stride
それぞれの間の暗黙変換を許可
などです。
この提案はC++23入りを目指して、LEWGからLWGへ転送する採決を取るために12月のLEWG投票待ちをしています。
エラーハンドリングを戻り値で行うための型、std::expected<T, E>
の提案。
以前の記事を参照
このリビジョンでの変更は、std::unexpected
の変換コンストラクタを削除した事、制約と効果を全体的に合理化した事、expected<void, E>
のために部分特殊化を定義した事、などです。
この提案は、C++23入りを目指してLWGでのビュー中です。
要素が削除されない限りそのメモリ位置が安定なコンテナであるstd::hive
(旧名std::colony
)の提案。
以前の記事を参照
このリビジョンでの変更は、使用経験とconstexpr
の検討について追記した事です。
<cmath>
と<cstdlib>
の一部の関数をconstexpr
対応する提案。
以前の記事を参照
このリビジョンでの変更は、「non-constant library call」という用語を導入して、Cライブラリ関数がnon-constant library callとなる条件とnon-constant library callがコア定数式ではないことを追記する形で、提案する文言を改善したことです。
non-constant library callはFE_INEXACT
以外の浮動小数点例外を発生させるようなC標準ライブラリ関数の呼び出しとして規定され、コア定数式となる(non-constant library callではない)C標準ライブラリ関数の呼び出しセマンティクスは、その呼び出しの引数型の浮動小数点数型に対して適用可能なC標準のAnnex Fで規定されたもの、として指定されています。また、math_errhandling & MATH_ERRNO == true
となる場合のC標準ライブラリ関数の呼び出しもまたnon-constant library callとなります。
すなわち、浮動小数点環境に影響を与えるorの影響を受けるような<cmath>
(<cstdlib>
)の関数の呼び出しは、定数式で実行不可となるようになります。
std::aligned_storage
とstd::aligned_union
を非推奨とする提案。
std::aligned_storage
とstd::aligned_union
を含めた在野の類似のものは、次のような問題があるため本質的に危険だと思われます
- その使用は未定義動作を引き起こす
- これらの型はストレージを提供するのではなく、指定されたサイズをもつ型を提供する
- 保証が間違っている
- 規格では型が指定されたサイズ以上であることを指定しているだけで、サイズ上限が無い
- APIが適切ではない
- APIが適切ではないため、利用にあたって同じような事前作業が繰り返される
std::aligned_storage
とstd::aligned_union
は共通して次のような問題があります。
- その領域上に構築された値のアクセスには
reinterpret_cast
する必要がある
-
constexpr
化できない
- 未定義動作を容易に引き起こす
::type
が自動解決されない
::type
のサイズに上限が無い
2つ目の問題は意味が分かりづらいですが、std::aligned_storage
(std::aligned_union
)は::type
として指定されたサイズとアライメントをもつ型を提供するものです。従って、std::aligned_storage
(std::aligned_union
)のオブジェクトを作成しても意味はなく、さらに間違ってそれを使用してしまうと悲惨なことになります。これは、std::aligned_storage_t
(std::aligned_union_t
)を使用すれば防止できますが、それが提供されていてもstd::aligned_storage
(std::aligned_union
)を直接使うという間違いを阻止する手段がありません。ここに1つ目の問題が重なり、間違って使用されても気づけない可能性があります。
3つ目は単に標準の規定の欠陥です。どちらも規定では::type
は少なくとも要求されたサイズ以上であることを指定しており、その上限は指定されていません。それによって、必要以上のメモリが想定外に使用される可能性があります(特にstd::aligned_storage
(std::aligned_union
)を配列の要素にした場合に影響が大きくなる)。
std::aligned_storage
の問題
さらに、std::aligned_storage
に固有の次のような問題があります
- テンプレート引数として構築したい型
T
を直接取らない
- 第二引数(アライメント指定)にデフォルト引数が指定されている
std::aligned_storage
はテンプレート引数として2つのstd::size_t
値を取ります。1つ目は領域の最小サイズ指定、2つ名は領域のアライメント指定です。しかし、2つ目のアライメント指定が1つ目のサイズ指定と無関係に指定されることはまれであり、std::aligned_storage
の用法を考えればむしろ構築したい型T
は固定で、std::aligned_storage<sizeof(T), alingof(T)>
と指定するのが適切なはずです。
このように、現在のAPIは本来必要な構築したい型T
を取らないだけでなく、アライメント指定にはデフォルト値が指定されています。オーバーアラインされた型をサポートする必要はなく、デフォルト値が有効なのはそれが適正であることをたまたま利用者が知っている場合だけです。
Folly/Boost/Abseilの3つのライブラリにおけるaligned_storage
(std::aligned_storage
likeなものも含む)の使用を調査したところ、95例のうち69例でaligned_storage<sizeof(T), alingof(T)>
のように使用されていたようです。他にもインターネット上で検索可能なところでも同様の用法が支配的であることが確認できます。
std::aligned_union
の問題
std::aligned_union
にも固有の次のような問題があります
- 第一引数(サイズ指定)は無意味
- サイズとアライメントの推論が
std::aligned_storage
と一貫していない
std::aligned_union
はサイズパラメータと可変長の型のリストを取り、それらの型の中の最大のサイズとアライメントを使用したストレージ型を用意します。第一引数のサイズ指定はstd::aligned_union
の最小サイズ指定であり、引数リストの全ての型のサイズがその値よりも小さい時でも、std::aligned_union
の提供する型のサイズはそれ(第一引数)よりも小さくなりません。
しかし、この最小サイズが必要になるのは非常にまれであり、ほとんどの場合はstd::aligned_union<0, Ts...>
のように使用されます。この0
の指定の意味はstd::aligned_union
を使い慣れていない場合には分かりづらく、その意図がサイズ0の型が欲しいのか単にAPIを満足するためだけに指定されているのか解読するのは困難です。
そして、std::aligned_union
が領域サイズとアライメントを勝手に計算してくれるのはいいことではありますが、そのことがstd::aligned_storage
のAPIと逆になっています。これはstd::aligned_union<0, T>
の様な使用法(サイズとアライメントを自動で求めてほしい)につながり、このコードを書いた人以外の人が見た時に、std::aligned_storage<sizeof(T), alingof(T)>
の代わり使用しているのか、将来型を追加することを見越しているのか、APIの矛盾という前提によってその意図を把握することは困難となります。
これらの問題から、この提案ではstd::aligned_storage
とstd::aligned_union
を非推奨にしようとしています。
また、可能であればstd::aligned_storage
は次のように置き換えることを推奨しています。
namespace std2 {
template <typename T>
using aligned_storage = alignas(T) std::byte[sizeof(T)];
}
ただし、alignas(T)
の指定はusing
宣言では意味がなく正しく機能しないため、この提案ではこのような代替を導入することは提案していません。代わりに、ユーザーに対して現在std::aligned_storage
とstd::aligned_union
を使用しているところを次のように置換することを推奨しています
template <typename T>
class MyContainer {
private:
alignas(T) std::byte t_buff[sizeof(T)];
};
template <typename T>
class MyContainer {
private:
alignas(Ts...) std::byte t_buff[std::max({sizeof(Ts)...})];
};
こうしたときでもreinterpret_cast
の使用は避けられませんが、既にそれが必要とされるところで引き続き必要になるだけなので、新規に導入するよりも悪影響は少ないはずです。
この提案はすでにLWGのレビューを終え、次の全体会議で投票にかけられることが決まっています。
↓
C++コア言語/標準ライブラリに拡張浮動小数点型のサポートを追加する提案。
以前の記事を参照
R6での変更は、
- SG22/EWGでの議論に基づいて、通常の算術変換(usual arithmetic conversion)のルールをC23の動作と一致するように変更
- オーバーロード解決のセクションを大幅に変更し、提案する方向性を「最小の安全な変換を優先」から「同じ変換順位を優先」に変更
std::is_extended_floating_point
などの型特性を削除
- 拡張浮動小数点数型のエイリアスなどを配置するヘッダを
<stdfloat>
に変更
- C23の
_FloatN_t
名に関する説明を追記
- 型エイリアスとそのリテラルサフィックスについて予備的な文言を追加
このリビジョンでの変更は
- 拡張浮動小数点数型のためのリテラルサフィックスを言語機能として定義するように変更
- 機能テストマクロを追加
- C23の
_FloatN_t
名がC++でも使用可能であることを要求しないことを決定
- IEEE/IEC浮動小数点数標準を参考文献(bibliography)から参照規格(normative reference)へ移動
- タイプエイリアスの順番を論理的な順序になるように並び替えた
などです。
因果関係を逆転するような過度な最適化を防止するためのバリアであるstd::observable()
の提案。
現代のコンパイラは未定義動作を活用して(未定義動作が現れないことを前提として)積極的な最適化を行うことがあります。それが起こると、未定義動作を回避するためのチェックやテストのコードをコンパイラが排除することがあります。
#include <cstdio>
#include <cstdlib>
static void bad(const char *msg) {
std::fputs(msg, stderr);
#ifdef DIE
std::abort();
#endif
}
void inc(int *p) {
if(!p) bad("Null!\n");
++*p;
}
このコードでは、DIE
マクロを事前定義していない場合に#1
の行を削除する最適化が可能です(ただし、現在これを行うコンパイラはないようです)。なぜなら、#1
の分岐はtrue/false
どちらのパスを通ったとしても次の行の++*p;
に結局は到達し、p
がnullptr
である場合のデリファレンスは未定義動作であるため、コンパイラはp
がnullptr
とならないと仮定することができ、遡って#1
のif(!p)
は常にfalse
となるためtrue
の分岐は実行されない、と導くことが可能だからです。「p
がnullptr
である場合のデリファレンスは未定義動作であるため、コンパイラはp
がnullptr
とならないと仮定することができ」のような無茶な導出を支えているのは、未定義動作を含むプログラムは全体が未定義動作になるという規定([intro.abstract]/5)により、それによって未定義動作が起こりうる時にそれが起こらないとみなしてプログラムを書き換える最適化が許可されます。
なお、DIE
マクロが定義されていればbad()
の実行は戻ることがなく、従ってif(!p)
がtrue
となる分岐はそこで実行が終了するため先ほどのような推論はできなくなります。
このような最適化あるいは未定義動作は、C++20で導入されかけていた契約プログラミングサポートを取り下げさせた原因の一つとなりました
void f(int *p) [[expects: p]] [[expects: *p<5]];
C++20契約プログラミングでは、契約条件が破られている時でも実行を継続する継続モードという実行モードが規定されており、その場合には1つ目の契約条件の実行後に2つ目の契約条件がチェックされることになり、先程と同様に未定義動作を起こらないものと仮定して1つ目の契約条件は常にtrue
とみなしてしまうことが可能となります。
C++20契約プログラミングには契約違反時の動作をカスタムするための違反ハンドラーというものが規定されており、違反ハンドラを最適化に対してブラックボックス扱い(すなわち、違反ハンドラは戻ってくるとは限らない)とすることでこのような問題に対処することが模索されていたようです(結局はその議論も含めて紛糾したためC++20から取り下げられました)。
一番最初の例のコードは、volatile
変数を用いた次のようなテクニックによって最適化から保護することが可能となります。
inline void log(const char *msg) {
std::fputs(msg, stderr);
}
bool on_fire() {
static volatile bool fire;
return fire;
}
void f(int *p) {
if (p == nullptr) log("bad thing 1");
if (on_fire()) std::abort();
if (*p >= 5) log("bad thing 2");
}
volatile
変数の読み取りはC++仮想機械(実装が模倣すべき振る舞いをする仮想適正実装)が規定する観測可能な振る舞い(observable behavior)の一部であり、観測可能な振る舞いは最適化の後でも必ず実行される必要があります。#1
のif
の条件では関数呼び出しを介してvolatile
変数の読み取りが行われており、その読み取りは最適化の対象となりません。そのため、true
パスのstd::abort()
は到達しないことがわかっていてもon_fire()
の実行およびif (on_fire())
文を最適化によって除去することはできず、次の行の*p
に全てのパスで到達すると仮定できないことから、先ほどのような最適化が抑止されます。
ただし、コンパイラはp
がnullptr
である場合にon_fire()
がtrue
を返さない限り未定義動作となることを推察することができ、その場合on_fire()
よりも前にstd::abort()
を持ってくることができます(未定義動作は起こらないのだから、on_fire()
がtrue
を返すと仮定してもよい + プログラムが未定義となる場合にはそのプログラムは観測可能な振る舞いを実行しなくても良い)。その場合は、未定義動作を実行することなくそれを検出することができます。
とはいえこのような分析に実装が従う必要はなく、このテクニックには保証がありません。
この提案はこのテクニックを一般化し、最適化抑止の保証を与えたstd::observable()
を導入することで、これら因果関係を逆転するような最適化をコントロールできるようにしようとするものです。
namespace std {
void observable() noexcept;
}
std::observable()
の呼び出しは最適化における一種のブロックとして動作して、std::observable()
によるある1つのブロックが未定義動作を含まずに完了した場合、そのブロックはブロック内に含まれる観測可能な振る舞いを示すことを要求します。ブロックが未定義動作を含む時に未定義となるのはそのブロック内に留まり、コード上の因果関係を遡って未定義化が波及することはありません。より正確には、std::observable()
(およびプログラムの終了)は1つの観測可能なチェックポイント(observable checkpoint)として規定され、そのようなチェックポイントの後方に未定義動作がある場合でもチェックポイント前方の観測可能な振る舞いを実行しなければならない、のように規定されます。
先ほどのC++20契約プログラミングの例では次のように使用して、いかなる場合でも1つ目の契約条件が評価されることを保証できます。
void f(int *p) [[expects: p]] [[expects: (std::observable(), *p<5)]];
他にstd::observable()
を適用可能な明らかな場所は、その成否にかかわらずリターンするI/O操作の後、エラーをハンドルするコードの中です。そのような場所では未定義動作が発生する可能性が高いはずです。
この提案は、EWGのレビューを通過しており、LEWG/SG1/SG22での確認を待ってCWGに転送される予定で、今のところC++23を目指しているようです。
コンパイラにコードの内容についての仮定を伝えて最適化を促進するための[[assume(expr)]]
の提案。
プログラマーはあるコードについて特定の仮定が成立する事を知っている場合があり、そのような情報をコンパイラに伝えることができれば、コンパイラの最適化の一助となる可能性があります。そして、全ての主要なC++処理系はその手段を提供しています。
- clang :
__builtin_assume(expr)
- MSVC/ICC :
__assume(expr)
- GCC :
if (expr) {} else { __builtin_unreachable(); }
int divide_by_32(int x) {
__builtin_assume(x >= 0);
return x/32;
}
この例では、コンパイラは通常符号付整数の可能な全ての入力で正しく動作するコードを出力しますが、__builtin_assume(x >= 0)
によってx
が負およびゼロのケースを考慮しなくても良いことがわかるため、コンパイラは正の場合のみで正しく動作するコード(5ビット右シフト)を出力します。
このように高い有効性が期待できますが、各実装の独自拡張でありその意味論や振る舞いも微妙に異なっているなどポータブルではありません。この提案はこの既存の慣行となっている機能を標準化するとともに、既存実装とC++標準の両方にうまく適合するように統一された構文と意味論を与えようとするものです。
構文は__builtin_assume(expr)
をベースとした属性構文[[assume(expr)]]
を提案しています。
int divide_by_32(int x) {
[[assume(x >= 0)]];
return x/32;
}
この属性はどこにでも書けるわけではなく、空のステートメントに対して([[fallthrough]]
と同様)のみ指定でき、かつ関数内部でのみ使用できます。[[assume(expr)]]
のexpr
は評価されないオペランドであり、副作用を持つ式を指定することもできますが、決して実行されません。そして当然ですが、expr == false
となるような入力に対しては未定義動作となるため、この仮定が満たされるようにするのはプログラマの責任となります。
このような仮定は、契約プログラミングにおける事前条件とよく似たものに思えます。しかし、契約の目的は事前条件と事後条件をコード上で記述できるようにするとともに、実行時にチェックすることでバグを発見するものであり、インターフェースなどAPIの境界の部分で使用されるものです。この機能(仮定の伝達)の目的はコードの特定の部分における事前条件(不変条件)をコンパイラに伝えるもので、特定の実装の詳細として使用されます。また、誰もが広く使用するものではなく、パフォーマンスが必要となるところで専門家だけが使用するものです。
また、契約の事前条件を仮定とみなすことでパフォーマンスが向上するということを示した調査はなく、むしろ低下させるか全く変化がないことを示した調査は存在しています。そのため、事前条件のアサーションと仮定を同じ言語機能で表現すべきではなく、提案中の契約プログラミングの構文とは異なったものをここでは提案しています。また、将来的に契約プログラミングに仮定の能力を与える場合でも、この機能をベースとしてそれを指定することができます。
文字列リテラルのエンコーディングを実行時エンコーディングに変換する際、文字表現が失われる場合をコンパイルエラーとする提案。
以前の記事を参照
このリビジョンでの変更は、マルチキャラクタリテラルの各要素が基本文字集合のメンバに限定されるのではなく、1つのコード単位として表現可能なように文言を修正したこと、マルチキャラクタリテラル関連の変更が視覚的な曖昧さを避ける為だけのものであることを強調するように文章を変更したことなどです。
範囲を等間隔の要素からなる範囲に変換するRangeアダプタstride_view
の提案。
stride_view
のような機能はSTLに存在しておらず、C++20のRangeライブラリにもこれを簡単に合成する方法はありません。それによって次のような処理のfor
ループからアルゴリズムへの移行が妨げられています。
for (auto i = 0; i < std::ssize(v); i += 2) {
v[i] = 42;
}
for (auto i = 0; i < std::ssize(v); i += 3) {
v[i] = f(v[i]);
}
for (auto i = 0; i < std::ssize(v); i += 3) {
for (auto j = i; j < std::ssize(v); i += 3) {
if (v[j] < v[i]) {
std::ranges::swap(v[i], v[j]);
}
}
}
stride_view
によってこれらの処理は次のように書くことができるようになります。
std::ranges::fill(v | std::views::stride(2), 42);
auto strided_v = v | std::views::stride(3);
std::ranges::transform(strided_v, std::ranges::begin(strided_v), f);
stdr::stable_sort(strided_v);
C++23にstride_view
がない場合、必要とするユーザーはそれを得ようとして自作を試み、filter_view
が最適だと思うかもしれません。
auto bad_stride = [](auto const step) {
return views::filter([n = 0, step](auto&&) mutable {
return n++ % step == 0;
});
};
この実装は少なくとも次の2つの問題があり、間違っています
filter_view
に渡す述語はstd::predicate
のモデルでなければならず、副作用は認められない。
- このラムダは後方への移動を考慮しておらず
bidirectional_range
の入力range
に対して動作しない。
- ラムダが
std::predicate
のモデルとなっておらず、それによって出力range
がbidirectional_range
のモデルにもならないため、これは診断不要の未定義動作となる。
stride_view
は利便性が高いく、欠けていればこのように誤った実装をされる可能性が高いため、<ranges>
追加しなければならないということで、C++23に向けて追加しようとする提案です。
提案されているstride_view
は、入力範囲のrandom_access_range
を継承するようになっています。その際問題となるのは、指定された数で割り切れない長さを持つ範囲に対するstride_view
の後退時で、ナイーブな実装(指定された数飛ばしてイテレータを進行/後退する実装)だと終端に到達した時に正しく後退することができません。
auto x = std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
std::ranges::copy(std::views::stride(x, 3), std::ostream_iterator<int>(std::cout, " "));
std::ranges::copy(std::views::stride(x, 3) | std::views::reverse, std::ostream_iterator<int>(std::cout, " "));
auto y = std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::ranges::copy(std::views::stride(y, 3), std::ostream_iterator<int>(std::cout, " "));
std::ranges::copy(std::views::stride(y, 3) | std::views::reverse, std::ostream_iterator<int>(std::cout, " "));
ナイーブな実装だと、stride_view(stride)
をreverse
した時には終端イテレータからのstride
飛ばしの後退をすることになりますが、入力範囲がstride
で割り切れない長さの場合先頭から進行した時と異なる要素をイテレートすることになります。
これを防ぐためには、ステップ数を記憶しておき、それを利用して正しい位置を求めるようにします。
iterator& advance(difference_type n) {
if (0 < n) {
step_ = ranges::advance(current_, n * stride_, ranges::end(underlying_range_));
return *this;
}
}
この形式のranges::advance
は指定された距離(n * stride_
)に対して進めなかった距離を返します。すなわち、入力範囲の終端以外のところではstep_
はゼロです。入力範囲の終端かつ入力範囲長がn
で割り切れない場合のみstep_
は非ゼロ(正)になります。進行時はこのadvance(n)
を使用して、最後に進めなかった距離をstep_
に記録しておきます。
iterator& advance(difference_type n) {
if (n < 0) {
auto stride = step_ == 0 ? n * stride_
: (n + 1) * stride_ - step_;
step_ = ranges::advance(current_, -stride, ranges::begin(underlying_range_));
}
}
後退時はadvance(-n)
のように使用して、step_
がゼロであれば端点の考慮は必要なたいめn * stride_
分(これは負になる)入力範囲のイテレータ(current_
)を現在位置から後退させます。step_
が非ゼロなら元の範囲の終端を超えた位置まで進行しようとしていたことがわかるので、(n + 1) * stride_ - step_
のようにして終端位置から後退する距離を調整します。
これらの工夫によって、stride_view
は入力範囲のrandom_access_range
を継承できるようになります。
ユニバーサル文字名として、16進エスケープシーケンスの代わりにユニコードの規定する文字名を使用できるようにする提案。
C++11から、基本文字集合に含まれない任意のユニコード文字をポータブルに表すためにユニバーサル文字名を使用できます。
char32_t c = U'\u0100';
char8_t c = u8"\u0100\u0300";
とはいえ、16(8)進エスケープシーケンスではそれがどの文字を指すのか直感的ではありません。この提案は、16進数値列の代わりにユニコードの規定する文字の名前を使用してユニバーサル文字名を構成できるようにしようとするものです。
char32_t c = U'\N{LATIN CAPITAL LETTER A WITH MACRON}';
char8_t c = u8"\N{LATIN CAPITAL LETTER A WITH MACRON}\N{COMBINING GRAVE ACCENT}";
16(8)進エスケープシーケンスによるユニバーサル文字名では、その内容を表示するために同時にコメントを追記する場合、時間経過とともにコードとコメントの同期が取れなくなりがちですが、このように文字の名前(エイリアス)で指定する事でそのようなコミュニケーション手段を取る必要がなくなります。
この機能は名前付文字エスケープ(Named character escape)と呼ばれます。構文は、文字/文字列リテラル中で\N{...}
の中に文字の名前を指定する形です。
使用可能な文字名はユニコード規格によって提供され、ユニコード規格に追随しており安定性が保証されている次のものを参照します。
ユニコードが将来追加しうる名前との衝突を回避するために、この提案では使用可能な文字名を実装が拡張することを許可しないようにしています。
名前指定のマッチングについてはUAX44-LM2を参照しており、これによって大文字と小文字を区別しない、ハイフンの省略、アンダースコアのスペースへの置換など、柔軟な指定が可能となっています。例えば、次の名前は全てU+200B {ZERO WIDTH SPACE}を示すものとして扱われます
ZERO WIDTH SPACE
ZERO-WIDTH SPACE
zero-width space
ZERO width S P_A_C E
この提案はSG16およびEWGのレビューを通過し、C++23導入を目指してCWGへ転送されるための投票待ちをしています。
std::format
によるフォーマットを使用しながら出力できる新I/Oライブラリstd::print
の提案。
以前の記事を参照
このリビジョンでの変更は、ベースとするドラフトを最新のものに更新し、それに伴ってP2418の変更を適用したことです。
この提案はLEWGのレビューを通過し、LWGに送られるための投票待ちをしています。
フリースタンディング処理系でも使用可能なライブラリ機能について、機能テストマクロを追加する提案。
以前の記事を参照
このリビジョンでの変更は、(この提案がC++23で入るとして)フリースタンディングと示される機能テストマクロの値を更新するものをC++20以前の機能のみに限定したこと(その機能の導入・更新とフリースタンディング化を区別するため)、C++23のライブラリ機能でフリースタンディング指定可能なものに対応する機能テストマクロを追加したこと(__cpp_lib_byteswap, __cpp_lib_constexpr_typeinfo, __cpp_lib_invoke_r, __cpp_lib_is_scoped_enum, __cpp_lib_ranges_zip, __cpp_lib_to_underlying
)などです。
スマートポインターの比較演算子に生ポインタとの直接比較を追加する提案。
以前の記事を参照
このリビジョンでの変更は、LEWGのレビューを受けて設計上の決定を明確にするために説明を改善したこと、std::unique_ptr/std::shared_ptr
から派生したクラスを除外するための制約を追加したことです。
std::unique_ptr
を全面的にconstexpr
対応する提案。
以前の記事を参照
このリビジョンでの変更は、LWGのレビューを受けて提案する文言のフォーマットを調整したこと、unique_ptr
同士の比較演算子を対象に追加したことなどです。
この提案はすでにLEWG/LWGのレビューと投票を完了し、次の全体会議で投票にかけられることが決定しています。
std::ranges::cbegin/cend
を拡張して、常にconst_iterator
を返すようにする提案。
以前の記事を参照
このリビジョンでの変更は、views::as_const
をviews::all_const
に名前を変更したこと、エイリアステンプレート(std::ranges::const_iterator_t/std::ranges::range_const_reference_t
)と機能テストマクロを追加したことなどです。
この提案はこのリビジョンを持ってLEWGでのレビューを完了し、LWGに送るためのLEWGの投票待ちをしています。
任意の範囲を手軽に出力できる機能を追加する提案。
以前の記事を参照
このリビジョンでの変更はP2418の変更を適用したこと、文字列を適切に引用符で括る方法や連想コンテナをフォーマットする方法について議論と機能を拡充したこと、あらゆるフォーマット指定子の紹介とそれをより広く機能させるための議論の追加、設計に集中するために提案文言を一旦削除、などです。
新しいアルゴリズムとしてstd::ranges::contains
を追加する提案。
このリビジョンでの変更は、std::basic_string_view/std::basic_string
の.contains()
メンバ関数削除を提案しなくしたこと、命名についての説明を追加したことなどです。
<charconv>
とstd::char_traits
をはじめとするいくつかのヘッダをフリースタンディングライブラリ指定する提案。
以前の記事を参照
このリビジョンでの変更は
strtok
をC++のフリースタンディングライブラリとして追加
<ratio>
への誤った言及を削除
<cinttypes>
の提案からの削除完了
- その他C向けの変更
などです。
コンパイル時にのみ使用され、実行時まで残らない文字列リテラルについての扱いを明確化する提案。
以前の記事を参照
このリビジョンでの変更は、ベースとなる規格ドラフトを更新したことです。
契約が破られた時に継続しないコントラクトサポートを追加する提案。
以前の記事を参照
このリビジョンでの変更は、不適切な契約によって契約チェックが無限ループに陥る問題について追記したこと、関数の戻り値型がvoid
である場合に事後条件に名前(戻り値のキャプチャ)を導入できないように文言を修正したことなどです。
この提案による契約サポートは事前条件と事後条件のみを対象としていて、クラス不変条件についてのサポートは欠けています。その場合、クラス不変条件とはすべてのパブリックメンバ関数の事前条件である、と考える人がいるかもしれません。すると、例えば次のようなコードが生まれる可能性があります
class Container {
public:
bool invariant() const { return (size() == 0) == empty(); }
int size() const [[pre: invariant()]];
bool empty() const [[pre: invariant()]];
};
このクラスのsize()/empty()
はどちらの関数を呼んだとしても、自身及び片方が事前条件invariant()
のチェックで呼び出されるため、無限ループに陥ります。この問題の考えられる解決策は、これが危険であることを周知・教育しこのようなコードが書かれないことを信頼する、あるいは契約のチェック時には再帰的に契約チェックを行わないようにする、の二つが考えられます。
2つ目のアプローチでは、事前条件の中でそれ自体が事前条件をなしている関数を呼び出すことはできず、すべての事前条件が広い契約(wide contract)を持たなければならないことを示しています。
2つ目のアプローチはBoost.Contracts、1つ目のアプローチはD言語でそれぞれ実装されています。
一部の有用な標準ライブラリのクラス型をフリースタンディング処理系で使用可能とする提案。
このリビジョンでの変更は、機能テストマクロを追加したこと、std::optional
のmonadic oeprationとstd::string_view::contains
についても同様にフリースタンディング化することにしたことなどです。
↓
非Rangeアルゴリズムのイテレータに対する名前付き要件を、イテレータコンセプトで置き換える提案。
以前の記事を参照
R3での変更は、プロクシイテレータを正しくハンドルできるように変更したことです。
これによって、Cpp17XXXIterator要件からイテレータコンセプトを使用してイテレータチェックを行うように変更するのは、constant iteratorに対してのみになります(以前はすべてのイテレータ)。プロクシイテレータでは、*it = v
がユーザーの期待通りに振舞うように設計されていますが、*it = std::move(v)
やswap(*it1, *it2)
などの変異操作は予期しない振る舞いをする可能性があります。なぜなら、通常のイテレータのdecltype(*it)
がT&
であるのに対して、プロクシイテレータのそれは別のもの(おそらくprvalue)となるためです。例えば、zip_view::iterator
の場合はdecltype(*it)
は参照型のstd::pair/std::tuple
であってstd::pair/std::tuple
の参照ではありません。
C++20 Rangeアルゴリズムはこのようなプロクシイテレータを正しく扱うことができるように設計されています。例えばムーブ/swap
にiter_move
やiter_swap
CPOを使用することで、プロクシイテレータによってカスタムされたムーブ/swap
操作を呼び出します。非Rangeアルゴリズム(つまり従来のアルゴリズム)ではその考慮はされておらず、イテレータのデリファレンス結果に直接ムーブ/swap
操作を呼び出します。
ただし、プロクシイテレータはconstant iterator(要素アクセスのみ可能で書き換えができないイテレータ)としては正しく動作します。問題となるのはプロクシイテレータように設計されておらず、かつmutable iterator(要素の書き換えができるイテレータ)を必要とするアルゴリズムだけです。したがって、Cpp17XXXIterator要件を非Rangeアルゴリズムに渡されるmutable iteratorのために維持すれば、そこでプロクシイテレータを使用することは未定義動作となります。
R3ではこれに対処して、ForwardIterator要件が要求されるところでは、mutable iteratorが必要となる場合はCpp17ForwardIterator要件を満たす、そうでない場合はforward_iterator
のモデルとなる、というように変更しています。
R4(このリビジョン)での変更は、実装経験を収集し追記したこと、それに基づいてこの提案の既存実装への影響セクションを更新したことです。
筆者の方はこの提案の内容をGCC(libstdc++)、MSVC(MSVC STL)に適用したうえでテストを行い、その結果いくつかの問題は見つかったもののこの提案の内容を実装可能であると報告しています。
パターンによってrange
のrange
となっているようなシーケンスを接合して平坦化するRangeアダプタ、views::join_with
の提案。
以前の記事を参照
このリビジョンでの変更は、機能テストマクロを追加したことです。
指定された述語によって元のシーケンスの可変個の要素を組にした要素からなるシーケンスを生成する、views::chunk_by
アダプタの提案。
以前の記事を参照
このリビジョンでの変更は、LWGのフィードバックを反映したことです。
この提案は、すでにLEWG/LWGのレビューを終えており、次の全体会議で投票にかけられることが決まっています。
入力シーケンスの各要素をstd::move
するようなview
である、views::move
の提案
以前の記事を参照
このリビジョンでの変更は、名前をviews::all_move
へ変更したこと、機能テストマクロを追加したことです。
2021年の11月に予定されている、LEWGでの全体投票の予定表。
以下の13の提案が投票にかけられる予定です。
LEWGでの作業を完了してLWG(CWG)へ転送することを確認するための投票です。
属性likeな構文に代わるコントラクト構文の提案。
以前の記事を参照
このリビジョンでの変更は
- タイポ修正と単語や文書の改善
- 文法の変更
preconditions
、postconditions
、assertions
に分割し明確化
postconditions
の戻り値パラメータをオプションにした
[]
の指定が採用されない場合、メンバ関数におけるキャプチャデフォルトを[&, this]
となるように変更
- 値によるキャプチャでは、対応するラムダ本体は常に
mutable
であることを明確にした
- サンプルの拡張
- 文書から「lambda」を削除し、「closure」に統一した
- 提案する文法はラムダクロージャの構文に依存しているが、ラムダ本体の構文には依存していないため
- 「future extensions」のセクションを副節に分割
- 副作用消去の異なるモデルを紹介し、このテインのモデルよりも緩いものを採用すると何が失われるかを説明
- 戻り値の破棄の例を意味のあるものに変更
attribute-specifier-seq
の場所を変更して、契約指定そのものに対する属性指定を許可
- コントラクト仕様そのものに対するテストとファジングについてのセクションを追加
- 省略形ラムダとの関連を追記
- 「capture design space」セクションを追加
- 検討、否定されたアイデアに関するセクションを追加
- 契約指定がチェックされないモードでもODR-usedであることを明確化
などです。
fstream
に排他モードでファイルオープンするフラグ、std::ios_base::noreplace
を追加する提案。
C11ではfopen
で書き込みモードでファイルを開く際のフラグにx
を追加できるようになりました。これによって、ファイルが排他モードでオープンされ、既存のファイルが存在する場合はオープンに失敗するようになります。
FILE *fp = fopen("foo.txt", "wx");
これはいわゆるTime of Check, Time of Use(TOCTOU)という問題に対処するためのものです。
FILE *fp = fopen("foo.txt","r");
if(!fp) {
fp = fopen("foo.txt", "w");
fclose(fp);
} else {
fclose(fp);
}
このようなコードにおいて、1-2行目のファイルの存在チェックから3行目の書き込み用ファイル作成(オープン)までの間にその名前のファイル(あるいはシンボリックリンクなど)が作成されてしまうと、4行目以降の処理において意図しないところに書き込みを行ってしまう可能性があります。x
を追加した排他モードの書き込みファイルオープンでは、fopen
においてファイルの存在チェックとファイル作成を同時に行うことでTOCTOUに対処し、既存ファイルが存在する場合に上書きを行わないようになります。
C++はC11を参照しているのでfopen
に対するx
フラグはすでにサポートされていますが、std::fstream
で同じことをする標準的な手段はなく、TOCTOUを回避しようとする場合に使用することができません。x
フラグはglibcで早期からサポートされており、時期POSIX標準でも導入される予定です。また、C++の初期(標準化以前)のstd::ofstream
ではnoreplace
フラグがサポートされていました(これはおそらくPOSIXのO_EXCL
から来ており、C90との互換のために標準化されませんでした)。また、MSVCではios_base::_Noreplace
としてサポートされています。
これらの理由から、C++(std::ofstream
)でも排他モードの書き込みファイルオープンをサポートすべき、という提案です。
排他モードフラグはstd::ios_base::noreplace
として追加されます。
int main() {
std::ofstream ofs("file.txt", std::ios_base::out | std::ios_base::noreplace);
if (!ofs) {
std::cout << "file exist\n";
return -1;
}
}
↓
コルーチンの動的メモリ確保を避ける最適化を制御し、起こったことを検出するAPIを追加する提案。
以前の記事を参照
R1での変更は
このリビジョンでの変更は、
must_elide()
の戻り値型をbool
から3要素のenum
へ変更
- 背景をさらに追記
must_elide()
を非constexpr
とすることについて議論を追加
must_elide()
で動的メモリ確保省略を強制できないケース(コルーチン本体が見えていない時)をどうするかなどの問題について議論が必要であるため、この提案はまだ方向性を決定していません。
single_view
がコピー不可なクラスのオブジェクトでも保持できるようにする提案。
現在のsingle_view
はstd::copy_constructible
コンセプトによって要素型を制約しているため、コピー構築不可能な型を要素とすることができません。
foo make_foo();
std::views::single(make_foo())
この制約はどうやら、最初期のview
定義(copyable
かつmovable
)を満たすための制約のようですが、途中でview
の定義は変更され現在はmovable
であればよくcopyable
は必ずしも求められていません。したがって、single_view
のこの要件はstd::move_constructible
まで弱める事ができるはず、という提案です。
これによって冒頭のサンプルのような、ムーブオンリーな型を要素とするsingle_view
が作成可能となります。
クラス型を非型テンプレートパラメータ(NTTP)の条件にアダプトするための新しい構文operator template()
の提案。
C++20からNTTPの制限が緩和され、一部のクラス型をNTTPとして取れるようになりました。新たな制限はNTTPとなれる型をstructural typeとして制限しており、それは次のいずれかに該当するものです
- スカラ型
- 左辺値参照型
- 次の条件を満たすリテラル型
- 全ての基底クラス及び全ての非静的メンバ変数は
public
かつmutable
ではない
- 全ての基底クラス型及び全ての非静的メンバ型はstructural typeであるか、配列型である
クラス型のNTTPは3つ目の条件を満たすものに限られています。これによってstd::pair
やstd::array
などの型をNTTPとする事ができるようになりますが、std::tuple, std::optional, std::variant
やstd::string, std::vector
などの型をNTTPとすることはできず、これらの型をこの条件にアダプトさせることも困難です。
特に問題となるのは全メンバがpublic
であるという制約です。この制約はテンプレートの等価性判定のための制約で、あるクラス型のNTTPのテンプレートとしての等価性判定をメンバ毎の比較で行える型を指定するものです。型として追加の意味論を持つためにそのような比較が適切ではない型では、そのメンバ変数はprivate
とされる事が一般的です。
そういう意味論の下では、std::tuple, std::optional, std::variant
などはそのメンバの比較によるテンプレート等価性の判定が適切ではありますが、これらの型はそのメンバを全てpublic
として実装されません。これらの型をNTTPとして扱うために必要なことは、C++20で導入されたメンバ毎比較によるテンプレート等価性判定にアダプトするための仕組みだけです。
将来的にはstd::string, std::vector
などの型もNTTPとして活用できると便利ですが、std::tuple, std::optional, std::variant
と同じアプローチではこれは達成できません。例えば、std::string
はポインタ3つ(あるいはポインタ2つとサイズ2つ)をメンバとして実装される事が多いですが、単にそれらメンバの比較によってテンプレート等価性判定をしてしまうと次のような問題があります
template <std::string S>
struct X {
bool operator==(X) const = default;
};
X<"hello"> a;
X<"hello"> b;
a = b;
異なるstd::string
オブジェクトのメンバのポインタ値は異なる領域を指すため、このa b
は異なる型を持ちます。これはstd:string
及びテンプレート等価性の意味論にそぐわないため、std::string, std::vector
などの型をNTTPとして扱うためには別のアプローチが必要そうです。
この提案のアプローチはoperator template()
という演算子を追加する事で、ある型のNTTPのテンプレート等価性を別のstructural typeに移譲するものです。
class A {
private:
int m_i;
struct Repr {
int i;
};
constexpr A(Repr r) : m_i(r.i) {}
constexpr auto operator template() const -> Repr {
return {m_i};
}
public:
constexpr A(int i) : m_i(i) { }
};
template <A a>
struct X { };
T::operator template()
はstructural typeな型R
を返さなければならず、R
はT
の表現として機能する必要があり、T
はR
から構築可能である必要があります。この例では、A::operator template()
の返す型A::Repe
によってA::Repr{1} == A::Repr{1}
となるため、A{1} == A{1}
となります。
この例は説明的なもので、実際には次のようにより簡易化できます。
class A {
private:
int m_i;
constexpr auto operator template() const -> int {
return m_i;
}
public:
constexpr A(int i) : m_i(i) { }
};
template <A a>
struct X { };
int
はすでにstructural typeなので、それをラップする型は必要ありません。
これをtuple
likeな型に対して書くのは非常に面倒な作業となるので、operator template()
はdefault
実装可能です。
class A3 {
private:
int i;
int j;
constexpr auto operator template() const = default;
public:
constexpr A3(int i, int j) : i(i), j(j) { }
};
template <A3 a> struct X { };
default
定義のoperator template()
を持つ型は、C++20の集成体をNTTPとして使用した時と同様に、全ての基底クラス及び非静的メンバ変数についての比較によってテンプレートの等価性判定が可能であることを表明し、それによってテンプレート等価性判定が行われます。唯一の違いは、基底クラス及び非静的メンバ変数がprivate
であっても構わない点です。ただし、これは再帰的ではなく、全ての基底クラス及び非静的メンバ変数がstructural typeである事が求められます。
class B {
int i;
};
class D : B {
int j;
constexpr auto operator template() const = default;
};
template <D d>
struct Y { };
operator template()
は関数のように見えますが、あくまでコンパイラがテンプレートの等価性判断(及びマングリング方法)をどうするかを指定する注釈にすぎません。したがって、実際にこれが呼び出される事はなく、呼び出された時の振る舞いなどは規定されず、default
のoperator template()
の戻り値型を気にする必要はありません。ユーザー定義型C
をマングリングに参加させる(C
によってテンプレート等価性を判定する)には単にC
を直接使用すればokです。
class A {
private:
C c;
D d;
struct Repr { C c; };
constexpr auto operator template() const { return Repr{c}; }
explicit constexpr A(Repr);
};
C
がstructural typeであればその性質に到達する方法にかかわらず(C
がint
のエイリアスであったりoperator template()
を持っていたりにかかわらず)、A::Repr
はstructural typeでありC
のstructural性を正しく反映します。ここでも、operator template()
を呼び出す必要はありません。
std::tuple, std::optional, std::variant
はこのoperator template()
を使用してメンバごとの比較によってテンプレート等価性を判定できるようになり、簡単にNTTPにアダプトする事ができます。一方、std::string, std::vector
は現在定数式での動的メモリ確保が一時的(実行時に持ち越せない)なため、operator template()
を正しく定義したとしてもNTTPとして使用する事ができません。それを解決する提案は進行中ですがまだ採択されていないため、この提案ではこの2つの型に対しては何もしません。
定数式での非一時的な動的メモリ確保が許可されていないことから非default
のoperator template()
を急ぐ必要はないため、この提案ではC++23に向けてクラス型でdefault
のoperator template()
を定義可能にし、そのクラス型は全ての基底クラス及び非静的メンバがstructural typeであれば自身もstructural typeとなるようにすることを提案しています。また、それをstd::tuple, std::optional, std::variant
にも定義して、要素型が全てstructural typeであればこの3つの型もstructural typeとなるようにすることも提案しています。
ここまでの説明のように、この提案では非default
のoperator template()
を使用してstd::string, std::vector
などの型をNTTPとして扱えるようにする方向性が示されていますが、ここではそれは提案されません。将来的に定数式での非一時的な動的メモリ確保が許可された後で解禁する予定です。
std::numeric_traits
に代わる数値特性クエリAPIとして提案されているP1841R1から、value_exists
とvalue_or
を取り除く提案。
P1841R1に関しては以前の記事を参照
P1841R1に提案されているvalue_exists
は数値特性Trait
がT
について利用可能かどうかを調べるもので、次のような定義になります。
template <template<class> class Trait, class T>
constexpr bool value_exists = requires { Trait<T>::value; };
これは例えば、value_exists<finite_max, int>
のように使用しますが、LWGにおける議論の過程でTrait
とT
を別々に受け取る設計について疑問が提起されたようです。
template <class T>
constexpr bool value_exists = requires { T::value };
static_assert(value_exists<finite_max<int>>);
つまりこのように、Trait<T>
の形で受けた上で静的メンバ::value
の存在チェックをする形の方が理解しやすく使いやすいのでは?という事です。
value_or()
は数値特性Trait
がT
について利用可能でない場合に、指定された値へフォールバックするためのものです。
template <template <class> class Trait, class T, class R = T>
inline constexpr R value_or(R def = R()) noexcept;
これは例えば、value_or<finite_max, int>(100)
のように使用します。この問題は、value_or()
の戻り値型は引数として渡した値の型R
であるため、T
と異なる可能性がある事です。例えば、value_or<finite_min, double>(1)
はint
型の結果となり、これは想定される振る舞いではないでしょう。
これらの理由により、P1841からvalue_exists
とvalue_or
を取り除き、他の数値特性のみを採用することを提案しています。これらのユーティリティが必要になったら、また後で議論をすれば良いとのことです。
CPOやniebloidなどの関数オブジェクトに対しての命名ルールに関する提案。
P2322R5 ranges::fold
の議論の過程で、その命名に関する議論が起こりました。名前付についての議論は、名前が主観的になるとともにその名前の技術的な側面が主観的な側面によって曖昧になってしまうため、とても厄介な議論です。この提案は、そのような議論をなるべく回避するために、主として関数オブジェクトに対する命名についての標準的な方法を提案するものです。
物事のある集合に名前をつける際は、ネスト構造を追加することで簡単になります。この提案の言うStructured naming(構造化された名前付)とはそのようなネスト構造を適切に反映した命名のことであり、例えば次のようなものです。
構造化された名前 |
構造化されていない名前 |
std::vector<T>
std::vector<T>::value_type
std::list<T>
std::list<T>::const_iterator
std::chrono
std::chrono::steady_clock
std::chrono::steady_clock::time_point
|
std_vector<T>
std_vector_value_type<T>
std_list<T>
std_list_const_iterator<T>
std_chrono
std_chrono_steady_clock
std_chrono_steady_clock_time_point
|
この構造化されていない例の命名は多くの人が適切ではないと考えると思われますが、それは私たちが無意識下で想定している普遍的で適切な命名構造への期待に反しているからこその反応だと思われます。
現在LEWGで合意されたP2322R5の関数群は次のような命名となっています。
fold_left()
fold_left_first()
fold_right()
fold_right_last()
fold_left_with_iter()
fold_left_first_with_iter()
ここに載っていないものも含めて、fold
系操作はさらに増加する可能性があります。その際、このように構造化されていない命名は組合せ爆発とともに複雑化します。とはいえ将来的に追加されるものも含めて、それらの変種が別々のオーバーロードとして提供されるのは妥当なことであり、問題となるのはその命名のみです。
プレーンな関数名では構造化されていない命名を避けることは困難でしたが、このfold
は関数オブジェクトとして実装されることが示唆されており、他の提案でも関数オブジェクトやCPOの命名について構造化されていないものがあります。関数オブジェクトであれば、メンバとして関数オブジェクトをネストさせることができるはずです。それによって、非構造化名を使用せざるを得なかった関数に対しても構造化された命名をすることができます。
構造化された名前 |
構造化されていない名前 |
fold.left()
fold.left.with_iter()
fold.left.first()
fold.left.first.with_iter()
fold.right()
fold.right.last()
|
fold_left()
fold_left_with_iter()
fold_left_first()
fold_left_first_with_iter()
fold_right()
fold_right_last()
|
他のところでは、P2300のCPOに対してもこれを適用できそうです。
構造化された名前 |
構造化されていない名前 |
std::execution::receiver.set_value()
std::execution::receiver.set_error()
std::execution::receiver.set_done()
std::execution::sender.connect()
|
std::execution::set_value()
std::execution::set_error()
std::execution::set_done()
std::execution::connect()
|
ただし、sender
とreciever
は同じ名前空間でコンセプトとして提供されているため、実際にはこのような命名は行えません。この提案の構造化された命名の問題点は、コンセプト定義とのこのような衝突を回避すること(コンセプトの構造化された命名)ができなければコンセプトとCPOの命名についてLEWGの時間を無駄に消費してしまう点です。
この提案はP2322R5をブロックし命名を変更しようとするものではないですが、この方向性が受け入れられるならば事後的にP2322R5の命名を構造化されたものに変更することを目指しているようです。
契約プログラミングの構文について、属性likeな構文は契約の指定に適しているかを考察する文書。
現在、契約プログラミングのサポートの議論は「P2388R4 Minimum Contract Support: either No_eval or Eval_and_abort」にて行われており、そこではC++20の契約プログラミングの時からの属性に似た構文を採用しています。
int f(int i)
[[pre: i >= 0]]
[[post r: r >= 0]];
一方、それに対してラムダ式に似た構文の提案(「P2461R1 Closure-based Syntax for Contracts」)も出ています。
int f(int i)
pre{i >= 0}
post(r){r >= 0};
また、例えば次のような構文を容易に思いつくことができます(提案はされていません)
int f(int i)
pre(i >= 0)
post(r: r >= 0);
この提案は、現在の属性likeな契約構文が契約の指定にとって適しているのかを吟味するものです。主に以下のように分析しています。
- 無視できる
- 現在のC++の属性についての規定では、「無視する」の意味が曖昧
- 属性の無視について規定しなおすことを提案している
- 宣言的or命令的
- 契約が数学的な意味での述語である(宣言的)なら属性構文は適している
- 契約がチェックされる(命令的)なら、属性構文は直観に反する
- 並べ替え可能
- 1つの属性中の2つの属性(
[[A, B]]
)は並べ替え可能だが、2つの属性([[A]] [[B]]
)は並べ替えられない(意味が変わる)
- これは契約指定と互換性がある(ショートサーキットされるかが変わるため契約は並べ替えられない)
- 順序
- 属性構文では、他の属性と契約の順序についての問題が発生する
int f1(int i)
[[pre: i > 0]]
[[using gnu: fastcall]]
[[post r: r > 0]];
int f2(int i)
[[using gnu: fastcall]]
[[pre: i > 0]]
[[post r: r > 0]];
int f3(int i)
[[pre: i > 0]]
[[post r: r > 0]]
[[using gnu: fastcall]];
- コンテナとしての
[[]]
- 人々が抱いている(可能性のある)直感は、
[[]]
が0か1以上の属性をカンマ区切りで指定できるコンテナ(リスト)であるというもの
- 型と効果の分析
- 属性を使用して、型に対する効果の注釈を行うEffect systemをいくらでも考えられる
- そのような型と効果の静的分析という観点からは、属性構文は自然に見える
- 関数型に現れるか
- 属性が関数型に影響を与えるのかを明確にする必要がある
- 契約チェックとUB
- 契約指定に違反したときはある種の未規定の動作となるが、その未規定の動作は実際にはコンパイラオプションによって制御されている。契約指定に違反しないプログラムのセマンティクスには影響を与えず、それは属性の無視可能な側面に合致する。
- メタ注釈
- 属性構文を採用しない場合、契約そのものに属性指定できる
- リフレクションでの検出
- 属性指定されたものをリフレクションで検出可能とするかどうか
- コロンの使用
総合的には属性は不利なのでは?と思わせる内容ですが、この文書はどちらを提案しているわけでもありません。
C++23の設計完了に向けたLEWGの作業予定や進捗を示す文書。
C++23は2023年発行予定ですが、そこに向けた提案は2022年2月7日までに採択されなければなりません。LEWGのリソースは限られており、それをC++23に入る可能性のある提案に集中させる必要があり、この文書はそのような提案をリストアップしたものです。
C++23に向けて取り組む必要のある提案
- 引き続き優先的に作業を行う提案
- 引き続き優先的に作業を行うが、著者による改訂待ち提案
- 実装経験などにより成熟しており、少しのレビューで承認されそうな提案
優先度を高くする必要はないが、サイズが小さめで労力がかからなそうな提案
LEWGとしてはおそらく、これ以外の提案に(一時的に)リソースを割かなくなるため、これ以外の提案がC++23に入る可能性はほぼありません。なおこれは、ライブラリについての提案のみなので、コア言語に関してはまた別の話です。
例外からのスタックトレースの取得の提案について、問題点とその解決策についての提案。
P2370については以前の記事を参照
P2370R0では、任意の例外オブジェクトからスタックトレースを取得可能とすると多大な実行時コストがかかることを認めた上で、std::this_thread::capture_stacktraces_at_throw()
によってその使用をスイッチできるようにすることを提案しています。しかし、そのアプローチはスレッドローカル変数へのアクセスを伴っており、スレッドローカル変数へのアクセスには実行時コストがかかります(参考)。現在の例外機構では例外そのもののコストに比べればスレッドローカル変数へのアクセスコストは無視できるものであるため問題とされないことが多いようですが、将来的に「P0709 Zero-overhead deterministic exceptions: Throwing values」が採用されるとそれが問題となることが予想されます。それを考慮しなくても、P2370の主張する最小の実行時コストはゼロではありません。
また、P2370の方法ではそれを有効化するのに再コンパイルとリンクが必要となりますが、独自の例外発生メカニズムを持つサードパティのライブラリの場合、再コンパイルとリンクされて再出荷されるのに年単位の時間がかかる可能性があります。さらに、例外を内部で使用しているライブラリではユーザーがこの機能を有効化した時に、ライブラリ内部で完全に捕捉されている例外であってもオーバーヘッドがかかることになるため忌避される可能性があります。その場合はライブラリから漏れる例外に対処する必要がないとすれば、APIのエントリポイントで無効化しておく、という方法がとられる可能性があります。
C++の例外処理は通常、言語に依存しない低レベルの機能の上に構築されており、それはWindowsでは構造化例外、Itanium ABIではLevel I Base ABIです。これらの低レベルの機能では通常、例外処理は2段階の過程を経て行われています。1段目は「検索」フェーズで、例外が投げられた地点から適切なハンドラを見つけるためにスタックを調べます。2段目は「巻き戻し」フェーズで、例外が投げられた地点から選択されたハンドラまでクリーンアップ(デストラクタ呼び出し)しながら戻ります。重要なのは次の2点です。
- 検索フェーズではスタックの内容を変更しない
- ハンドラの識別は動的であり、コンパイラ/ライブラリによって見つかった関数が呼ばれる
これらの点から、次のような代替メカニズムを考案できます
- ユーザーコードでは、特別な関数または新しい構文を利用して、特定の
catch
ブロックについてスタックトレースが必要である事をマークする
- コンパイラはそのようにマークされた
catch
ブロックを認識すると、そのcatch
ブロックが例外ハンドラとして選択された時にその選択の直前(検索フェーズ)でスタックトレースを取得するための適切なコード/データを発行できる
- ユーザーコードでは、巻き戻しフェーズの後の例外処理において、保存されたスタックトレースを取得することができる
このアプローチの利点は次の2つです
- 透明性
- 例外を投げるコードを修正したり再コンパイル・リンクする必要がない
- このメカニズムは例外をキャッチする側の変更のみに依存している
- ゼロコスト
- 例外スロー時の検索フェーズでマークされたハンドラに到達しなければ、動作に影響がない
これを実現するための構文として次のいずれかを提案しています
std::stacktrace::from_current_exception()
の特別扱い
- 欠点 :
from_current_exception()
の呼び出しがcatch
ブロックの外に意図せず移動すると機能しなくなる。
catch
ブロックのデフォルトパラメータ
- 欠点 : 新しい構文である事、キャッチする複数の型を指定するものとして勘違いされる可能性がある
catch-with-init
- 欠点 : 新しい構文である事、検索フェーズで一般的なユーザーコードを実行するのは危険
- 検索フェーズの露出
- 欠点 : 新しい構文であり悪用される可能性がある、検索フェーズで一般的なユーザーコードを実行するのは危険
void f() noexcept(false);
int main() {
try {
f();
} catch (const std::exception& ex) {
std::cout << ex.what() << "\n" << std::stacktrace::from_current_exception() << std::endl;
}
try {
f();
} catch (const std::exception& ex, std::stacktrace st = std::stacktrace::from_current_exception()) {
std::cout << ex.what() << "\n" << st << std::endl;
}
try {
f();
} catch (auto st = std::stacktrace::current(); const std::exception& ex) {
std::cout << ex.what() << "\n" << st << std::endl;
}
try {
f();
} catch (const std::exception& ex) if (auto st = std::stacktrace::current(); true) {
std::cout << ex.what() << "\n" << st << std::endl;
}
}
これは少なくともWindowsのABIとItanium ABIで実装可能である事を確かめているようですが、その他のABI(プラットフォーム)で実装可能かどうかは不明であり情報を求めています。
システムの文字エンコーディングを取得可能とする提案(P1885)に対して、設計の欠陥を指摘する提案。
P1885については以前の記事を参照。
要約すると、以下の3点を問題としてあげています
- "UTF-16"というエンコーディング名を別の目的で再利用しているため、混乱が生じている
- Windowsで以前に使用されていたUCS-2ワイドエンコーディングを適切に表現できない
- オブジェクト表現を指定しようとしており、C++抽象化を壊している
これに対して、次のような改善を提案しています。
- IANAレジストリに登録されているエンコーディング方式(encoding scheme)のオクテット(バイト)が、
std::text_encoding
におけるコードユニットとみなされる事を規定する
- 現在または将来のIANAのエンコーディング割り当てとの衝突を避けるために負の列挙値を持つ追加のエンコーディング(
WIDE.UTF16, WIDE.UTF32, WIDE.UCS2, WIDE.UCS4
)を規定する
std::text_encoding::(wide_)literal()
からは、sizeof(char_type) == 1
でないとIANAのエンコーディング方式を返せない、という注意を追記
CHAR_BIT == 8
またはsizeof(wchar_t) > 1
に関する制限の削除
P2491 進行状況
WG21ミーティングへCOVID19パンデミックが与えた影響についての報告書。
筆者の方の個人的な経験を元にして書かれていて、WG21ミーティングがオンラインへ移行したことによって参加のためのコストが低下し、準備に時間を取れるようになったことで積極的に議論に参加できるようになったとのことです。筆者の方にとってはオンラインミーティングにはメリットしかなく、再び対面ミーティングに移行してしまうと参加のハードルが上がることから傍観者に追いやられてしまうことを危惧しているようです。
コア言語機能について、忘れられていた機能テストマクロの更新を行う提案。
忘れられていたのは、P0848R3 Conditionally Trivial Special Member FunctionsとP1330R0 Changing the active member of a union inside constexpr
の2つで、どちらもC++20のコア言語機能です。
P2231R1 Missing constexpr
in std::optional
and std::variant
の実装にあたってこれらの機能が使用可能である必要があり、実装はそれを検知できる必要がありますが、その手段が提供されていない事がわかりました。
この提案では、__cpp_concepts
と__cpp_constexpr
の値を202002L
(C++20規格完成の年月)に指定します。C++20から時間が経ってしまっていますが、__cpp_concepts
の値は更新されておらず、__cpp_constexpr
の値は更新されている(P2242R3)ものの、GCCのみがこれを実装済みでかつP1330R0も実装済みであるので問題ないようです。
おわり