文書の一覧
www.open-std.org
提案文書で採択されたものはありません。全部で28本あります。
constexpr if
やstatic_assert
の引数でのみ、整数型からbool
型への暗黙の縮小変換を定数式で許可する提案。
現在定数式での整数の暗黙変換では、文脈的なbool
変換も含めて縮小変換が禁止されており次のようなコードはコンパイルエラーになります。
enum Flags { Write = 1, Read = 2, Exec = 4 };
template <Flags flags>
int f() {
if constexpr (flags & Flags::Exec)
return 0;
else
return 1;
}
int main() {
return f<Flags::Exec>();
}
template <std::size_t N>
class Array {
static_assert(N, "no 0-size Arrays");
};
Array<16> a;
一方対応する実行時コードは普通にコンパイルでき、期待通りに動作します。
if (flags & Flags::Exec)
{}
assert(N);
このような一貫しておらず直感的ではない挙動を修正するために、constexpr if
とstatic_assert
の条件式に限って、文脈的なbool
変換時の縮小変換を許可しようというものです。
そもそもこれらの条件式でさえもbool
への縮小変換が禁止されていたのは、noexcept
式での縮小変換を禁止した時に巻き込まれてしまったためのようです。関数f()
の例外仕様が別の関数g()
と同じ(あるいはそれに従う)場合、noexcept
を二つ重ねて書きます。しかし、その場合に書き間違えて関数名だけを書いたり、1つにしてしまってg()
がconstexpr
関数だったりすると思わぬバグを生みます。
int f() noexcept(noexcept(g()));
int f() noexcept(g);
int f() noexcept(g());
このような些細な、しかし気づきにくいバグを防ぐために定数式での文脈的なbool
変換の際は縮小変換を禁止することにしました。しかし、noexcpet
以外のところではこれによって(最初に上げたような)冗長なコードを書くことになってしまっていました。
この欠陥報告(CWG 2039、C++14)を行ったRichard Smithさんによると、本来はnoexcept
式にだけ適用するつもりで、static_assert
には表現の改善のみで縮小変換禁止を提案してはいなかったそうですが、その意図に反して両方で縮小変換が禁止されてしまいました。結果、おそらくその文言を踏襲する形でconstexpr if
やexplicit(bool)
にも波及したようです。
型の修飾情報などを操作するためのいくつかの新しいメタ関数の提案。
主に次の2種類のものが提案されています。
remove_all_pointers
- 型に付いている情報をコピーする
copy_*
メタ関数
using remove_ptr = std::remove_all_pointers_t<int************>;
using copy_ptr1 = std::copy_all_pointers_t<int***, double>;
using copy_ptr2 = std::copy_all_pointers_t<int***, double*>;
using copy_r1 = std::copy_reference_t<int&, double>;
using copy_r2 = std::copy_reference_t<int&, double&>;
using copy_r3 = std::copy_reference_t<int&, double&&>;
using copy_const1 = std::copy_concst_t<const int, double>;
using copy_const2 = std::copy_const_t<const volatile int, double>;
using copy_cvr = std::copy_cvref_t<const volatile int&&, double>;
この他にもcopy_volatile
とかcopy_extent
、copy_pointer
などが提案されています。
copy_*
系メタ関数の引数順は<From, To>
になっており、全てに_t
付きのエイリアスが用意されています。また、対象の修飾がコピー先にすでに付いている場合はそれはそのままに追加でコピーする形になり、コピー元に対象の修飾がない場合は何もコピーしません。
筆者の方々の経験からプロクシクラス作成やカスタムオーバーロードセットを構築するツールの実装に有用であった型特性を提案しているそうです。
C++コア言語/標準ライブラリに拡張浮動小数点型のサポートを追加する提案。
機械学習(特にディープラーニング)では多くの場合それほど高い精度が求められないため、float
(32bit浮動小数点数)よりもより小さい幅の浮動小数点型(時には整数型)を利用することでその時間的/空間的なコストを抑えることが行われており、それを支援する形でハードウェア(GPU/CPU)やソフトウェア(CUDA/LLVM-IR)でのサポートが充実してきています。
現在のC++には3種類の浮動小数点型だけが定義されておりそれ以外のものは何らサポートがあリません。そのため、拡張浮動小数点型は算術・変換演算子をオーバーロードして組み込み型に近い挙動をするようなクラス型を定義することでサポートされています。しかし、そのような方法は完全ではなく面倒で、効率化のためにインラインアセンブラやコンパイラ固有のサポートが必要とされます。
これらの問題はユーザー定義のライブラリで解決できるものではなく、コア言語でのサポートが必要です。そして、拡張浮動小数点型が求められる場所ではC++が使用される事が多くこれらの問題を解決するに足る価値(ポータビリティや効率性の向上など)があるので、拡張浮動小数点型の言語/ライブラリサポートを追加しようという提案です。
ただし、現在のところ拡張浮動小数点型のスタンダードとなるものは確定しておらず、将来どれが使われていく(あるいは廃れる)のか予測することは困難であるため、何がいくつ定義されるかは実装定義とされます。そのため、bfloat16
とかfloat16
みたいな具体的な型は提供されませんが、代わりに似た形のエイリアスが実装定義で提供されます。
変更は既存の浮動小数点型の振る舞いを保ったままで追加の浮動小数点型をより安全に利用可能かつ拡張可能である(ハードウェアに依存しない)ようにされています。
- 拡張浮動小数点型は
double
に昇格されない
- 拡張浮動小数点型の関わる暗黙変換では縮小変換を許可しない
- 式のオペランドとなっている2つの浮動小数点型を統一する算術型変換(Usual arithmetic conversions)では、どちらのオペランドももう片方の型に変換できない場合はill-formed
float, double
等は従来のルール通りにより幅の広い型に自動昇格する
float f32 = 1.0;
std::float16_t f16 = 2.0;
std::bfloat16_t b16 = 3.0;
f32 + f16;
f32 + b16;
f16 + b16;
std::float16_t x{2.1};
char out[50]{};
if (auto [ptr, ec] = std::to_chars(out, std::end(out), f16); ec == std::errc{}) {
std::cout << std::string_view(out, ptr - out) << std::endl;
std::float16_t outv;
if (auto [_, ec2] = std::from_chars(out, ptr, outv); ec2 == std::errc{}) {
std::cout << outv << std::endl;
}
}
using namespace std::float_literals;
std::complex<std::bfloat16_t> z = {1.0bf16, 2.0bf16};
これら型エイリアスが定義されるヘッダには<fixed_float>
か<stdfloat>
という名前を提案しているようですが、筆者の方々はあまり気に入っていないようでより良い名前やふさわしい既存のヘッダを考慮中のようです。いい名前が思いついたらコントリビュートチャンスです。
この提案文書は拡張浮動小数点型の既知のレイアウトとその名前についての提案でしたが、今回P1467R4(1つ前のの拡張浮動小数点型に対する提案)にマージされたため、内容は空です。それが行われたことを記しておくために存在しているようです。
[utility]、<ranges>
、<iterator>
から一部のものをフリースタンディングライブラリに追加する提案。
[utility]は範囲が広いですが、ほとんどpair, tuple
を対象にしています。また、動的メモリ確保を必要としたり例外を送出しうるもの、iostream
のようにOSのサポートを必要とするものは当然含まれていません。<utility>, <tuple>, <ratio>
ヘッダの全てと、特筆する所ではstd::unique_ptr
やstd::function
のフリースタンディング化が提案されています。
<ranges>
、<iterator>
からは(i|o)stream_iterator
、(i|o)streambuf_iterator
やistream_view
等に関わるもの以外の全てを追加することが提案されています。
また、これらのものには機能テストマクロが用意されています。
筆者の方は、<optional>
や<variant>
などを今後別の提案で詳細に検討していくつもりのようです。
ライブラリ機能のフリースタンディング化に慎重な検討が必要になるのは、フリースタンディング処理系とホスト処理系とである関数呼び出し時のオーバーロードセットが変化することで暗黙のうちに動作が変わってしまうことを防ぐためです。変わったとしてもちゃんとエラーになるのかや選択されるオーバーロードが変化しないかなどを慎重に検討せねばならないようです。とても大変そうです・・・
<cstring>
と<cwchar>
の関数にconstexpr
を追加する提案。
これらのヘッダに定義されている文字列操作関数をconstexpr
にすることを意図しています。ただし、ロケールやグローバルオブジェクトの状態に依存したり、スレッドローカルな作業域を持つような関数は除外しています。また、std::memcpy
やstd::memmove
などのメモリ操作系の関数もconstexpr
にすることが提案されています。これらは引数にvoid
ポインタを取るためそのままだと定数式で使えないのですが、コンパイラマジックにより定数式で使用可能にしてもらうようです。
<cstring>
と<cwchar>
はC++として独自実装している処理系とCのコードを流用している処理系が存在しているようですが、前者はそのままconstexpr
を付加し、後者はコンパイラの特別扱いによってABIを破損することなくconstexpr
対応できるだろうということです。
識別子(identifier)の構文において、不可視のゼロ幅文字や制御文字の使用を禁止する提案。
前回の記事を参照
onihusube.hatenablog.com
前回(R3)との変更点は文書にSummaryが追加されたことと、提案する字句トークン(プリプロセッシングトークン)のEBNF定義の修正だけのようです。
この提案はすでにCWGでの文言レビューを待つだけになっており、C++23に導入される可能性は高いです。その場合、C++においては識別子(クラス・変数・関数等の名前)に絵文字を使用できなくなります(欠陥報告になる可能性があるので以前のバージョンも含めて)。
std::initializer_list
に添字演算子[]
とdata()
関数を追加する提案。
std::initializer_list
は軽量な生配列のプロキシクラスではありますが、要素アクセスにはイテレータを使用するしかないなど少し使いづらい所があります。その解消のために、添字アクセスと先頭領域へのポインタ取得をサポートしようという提案です。
struct Vector3 {
int x, y, z;
Vector3(std::initializer_list<int> il) {
x = *(il.begin() + 0);
y = *(il.begin() + 1);
z = *(il.begin() + 2);
}
Vector3(std::initializer_list<int> il) {
x = il[0];
y = il[1];
z = il[2];
}
};
void f(std::initializer_list<int> il) {
const int* head = il.begin();
const int* head = il.data();
}
NRVO(Named Return Value Optimization)によるコピー省略を必須にする提案。
C++17からはRVO(Return Value Optimization)によるコピー省略が必須となり、関数内で戻り値型をreturn
ステートメントで構築する場合にコピーやムーブを省略し、呼び出し元の戻り値を受けている変数に直接構築するようになっています。一方、関数内で変数を構築してから何かしてその変数をreturn
する場合のコピー省略(NRVO)は必須ではなく、コンパイラの裁量で行われます(とはいえ、主要なコンパイラは大体省略します)。
struct Heavy {
int array[100]{};
Heavy() {
std::cout << "default construct\n"
}
Heavy(const Heavy& other) {
std::copy_n(other.array, 100, array);
std::cout << "copy construct\n"
}
Heavy(Heavy&& other) {
std::copy_n(other.array, 100, array);
std::cout << "move construct\n"
}
};
Heavy rvo() {
return Heavy{};
}
Heavy nrvo() {
Heavy tmp{};
for (int i = 0; i < 100; ++i) {
tmp.array[i] = i;
}
return tmp;
}
int main() {
Heavy h1 = rvo();
Heavy h2 = nrvo();
}
[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ
(言語バージョンをC++14にすると完全にコピー省略のない世界を見ることができます。また、-fno-elide-constructors
を外すとコピー省略された結果を見ることができます。)
このような場合のnrvo()
の呼び出しのようにNRVOが可能なケースではNRVOを必須にしよう、という提案です。NRVo可能なケースというのは簡単に言うと全てのreturn
文が同じオブジェクトを返すことが分かる場合の事で、提案文書にはこの提案によっていつNRVOが保証されるかのいくつかのサンプルが掲載されています。
この提案はCWGでの文言調整フェーズに進んでおり、C++23に入る可能性が高そうです。
ラムダ式の全体をmutable
とするのではなく、一部のキャプチャだけをmutable
指定できるようにする提案。
前回の記事を参照
onihusube.hatenablog.com
前回との差分は一部のサンプルコードが変更されたことと初期化キャプチャ時パック展開でのmutable
指定が提案に含まれた事、EWGIの議論で示された懸念事項が追記された事です。
C++20よりラムダ式の初期化キャプチャ時にパラメータパックをキャプチャ出来る様になっているので、そこでもmutable
が出来るようにしようとしています。これを用いるとパラメータパックだけをラムダ式中で再ムーブする時に、全部をmutable
にしなくても良くなります。
template <class... Args>
auto delay_invoke_foo(Args... args, State s) {
return [s, mutable ...args = std::move(args)] {
return foo(s, std::move(args)...);
};
}
追加された懸念事項は、明示的なconst
キャプチャをする場合に、ラムダ式のムーブで暗黙にコピーが行われるようになる事です。クラスのメンバにconst
メンバがあってもムーブコンストラクタ自体は使用可能ですが、const
メンバはコピーされます。コピーコンストラクタは多くの場合例外を投げうるので、これによって思わぬところで例外が発生するようになってしまう可能性があります。
auto l1 = [const str = std::string{"not movable"}](){return str;};
auto l2 = std::move(l1); // キャプチャしたメンバstrはコピー構築される、場合によっては例外を投げうる
この提案によってもたらされるラムダ式の対称性と一貫性の向上による効用と、このような足を撃ち抜く可能性を導入することによる弊害のどちらがより大きいのかは解決されておらず、より議論が必要となりそうです。
std::string
の単一の文字代入を非推奨とする提案。
std::string
にはchar
1文字を受け取る代入演算子が定義されています。
constexpr basic_string& operator=(charT c);
しかし、この代入演算子は特に制約されておらず、char
に暗黙変換可能な型に代入を許します。その代表的なものはint
やdouble
の数値型です。
std::string s{};
s = `A`;
s = 66;
s = 67.0;
すなわち、int
やdouble
への暗黙変換を実装している任意のユーザー定義型も代入可能です。
そもそもstd::string
にchar
1文字を代入できる必要があることが疑わしい上にコンストラクタのインターフェースとも一貫しておらず、この様な変換が起きることはほとんどの場合意図したものではなくバグの原因であるので非推奨にしようという主張です。
ただし、削除することまでは提案されていません。
ほかの選択肢としては
- 削除する
- その場合、
nullptr
の代入が可能になってしまうのでケアする必要がある
- コンセプトによる制約を行う
cpp
template<same_as<charT> T>
constexpr basic_string& operator=(T c) ;
int
からの変換だけを許可するようにして、他の変換は不適格とする。
- 筆者の方が見てきたこれらの変換が問題となっていたケースはほぼ全て
int
からの変換だったので解決策としては弱いだろう、とのこと
R0の際に行われたLEWGでの投票では、非推奨とすることに合意が取れていて、今回はそれを受けて標準のための文言を追加したようです。
std::format
によるフォーマットを使用しながら出力できる新I/Oライブラリstd::print
の提案。
C++20で導入されたstd::format
はフォーマットを指定しつつ文字列を構成できるものですが、その結果はstd::string
で得られ出力機能は備えていません。そのままiostream
を使えば出力できますが、一時オブジェクトの確保が必要になる上、iostream
によってフォーマット済み文字列を再びフォーマットすることになり非効率です。
std::cout << std::format("Hello, {}!", name);
std::printf("Hello, %s!", name);
auto msg = std::format("Hello, {}!", name);
std::fputs(msg.c_str(), stdout);
この提案では、このような場合に一時オブジェクトを作成せず、フォーマットとI/Oで別の関数を呼び出す必要もなく、より効率的な出力を行うstd::print
関数を提案しています。
std::print("Hello, {}!", name);
これはすなわちiostream
に変わる新しい出力ライブラリとなります。このライブラリは次のことを目標にしています。
これはすでに{fmt}
にて実装されていて、その実装により得られたベンチマーク結果が掲載されています。既存のI/Oと比較すると速度とバイナリフットプリントの両面で良好な結果を得られているようです(ただ、純粋なフットプリントだけはprintf
に及ばないようです)。
CWGとEWGの間で使用されているwording reviewに関するルールの修正と、それをLWGとLEWGの間でも使用するようにする提案。
前回の記事を参照
onihusube.hatenablog.com
このリビジョンでの変更は、CWG/LWGにおける標準のための文言レビューと本会議での投票の間に、最終確認のためのTentatively Readyという作業フェーズを追加することを提案している点です。
C++20までに非推奨とされた機能をレビューし、標準から完全に削除するかあるいは非推奨を取り消すかを検討する提案文書。
この提案は規格書中のAnnex.Dというセクションに記載されている機能だけを対象としていて、そこにあるもの以外を削除するわけでもなく、そこに新しく追加する機能について検討するものでもありません。
まだ検討中で、削除が決まった物は無いようです。
集成体(Aggregate)を名前付きのstd::tuple
であるとみなし、標準ライブラリにおけるstd::tuple
のサポートを集成体に拡張する提案。
std::tuple
は任意個数の型をひとまとめにして扱える大変便利なものではありますが、コア言語のサポートが無く全てをライブラリ機能によって実現しているため使いづらい事が多くあります。一方、集成体はC言語から引き継がれたいくつかの条件を満たした構造体で、std::tuple
を利用するシーンでは集成体を利用した方が便利だったりする事が多々あります。
struct auth_info_aggreagte {
std::int64_t id;
std::int64_t session_id;
std::int64_t source_id;
std::time_t valid_till;
};
using auth_info_tuple = std::tuple<
std::int64_t,
std::int64_t,
std::int64_t,
std::time_t
>;
template <class T>
constexpr bool validate() {
static_assert(std::is_trivially_move_constructible_v<T>);
static_assert(std::is_trivially_copy_constructible_v<T>);
static_assert(std::is_trivially_move_assignable_v<T>);
static_assert(std::is_trivially_copy_assignable_v<T>);
return true;
}
constexpr bool tuples_fail = validate<auth_info_tuple>();
constexpr bool aggregates_are_ok = validate<auth_info_aggreagte>();
ただ、集成体には言語サポート(集成体初期化、必然的なtrivial性など)がある代わりに、ほぼライブラリサポートがありません。std::get
などを利用できず、ジェネリックなコードにおいては少し使いづらい事があります。
namespace impl {
template <class Stream, class Result, std::size_t... I>
void fill_fileds(Stream& s, Result& res, std::index_sequence<I...>) {
(s >> ... >> std::get<I>(res));
}
}
template <class T>
T ExecuteSQL(std::string_view statement) {
std::stringstream stream;
T result;
impl::fill_fileds(stream, result, std::make_index_sequence<std::tuple_size_v<T>>());
return result;
}
constexpr std::string_view query = "SELECT id, session_id, source_id, valid_till FROM auth";
const auto tuple_result = ExecuteSQL<auth_info_tuple>(query);
const auto aggreagate_result = ExecuteSQL<auth_info_aggreagte>(query);
std::get
等std::tuple
に対するライブラリサポートはtuple-like
な型(例えばstd::pair
やstd::array
)ならば利用可能であるので、一般の集成体をtuple-like
な型として利用可能にすることで集成体にライブラリサポートを追加しよう、という提案です。
tuple-like
な型の条件はstd::tuple_size
によってその長さが、std::tuple_element
によってその要素型が、そしてstd::get
によってインデックスに応じた要素を取得できる事です。標準ライブラリにおいて、任意の集成体に対してこれらを用意(あるいは自動生成?)しておくようにする事で集成体にライブラリサポートを追加します。コア言語に変更は必要ありませんが、コンパイラによるサポートは必要そうです。
そして、それによってstd::tuple
を用いている既存のコードは一切変更する事なく集成体でも利用できるようになります。
constexpr std::size_t elems = std::tuple_size<auth_info_aggreagte>::value;
using e2_t = std::tuple_element_t<2, auth_info_aggreagte>;
auth_info_aggreagte a = { 1, 2345, 6789, {}};
auto& e3 = std::get<3>(a);
std::byte
によるバイナリシーケンスのI/Oのための新ライブラリ、std::io
の提案。
前回の記事を参照
onihusube.hatenablog.com
このリビジョンでの変更は、いくつかの機能の追加とそれを用いた既存機能の修正などです。
alignof
を型だけではなくオブジェクトに対しても使用出来るようにする提案。
alingas
によってオブジェクトと型に対してアライメントを指定する事ができますが、alignof
でアライメントを取得できるのは型だけです。この挙動は一貫しておらず、GCCではオブジェクトに対してもalignof
出来るようになっているためC++標準としても正式に許可しようとする提案です。
struct alignas(32) S {};
int main () {
alignas(64) S obj{};
std::size_t type_alignment = alignof(S);
std::size_t obj_alignment = alignof(obj);
}
さらに、既存のアライメントに関しての空白部分やC言語との非互換性を改善する提案も同時に行なっています。
オブジェクトの型のアライメントとオブジェクトのアライメント指定について。
typedef struct U U;
struct U {
}__attribute__((aligned (32)));
int main() {
_Alignas(16) U u;
_Alignas(64) U v;
_Alignof(v);
alignas(16) U u;
alignof(u);
alignas(64) U v;
alignof(v);
}
- (1) : 型よりも弱いアライメントを指定する
alignas
ではオブジェクトを定義できないはずだが、C++にはこの場合の規定がない
- 型のアライメント要求よりも弱いアライメント指定はエラーと明確に規定する
- (2) : オブジェクト型に対する
alignof
は現在許可されていない
型のアライメントとメンバ変数のアライメントについて。
typedef struct V V;
typedef struct S S;
typedef struct U U;
struct V {} __attribute__((aligned (64)));
struct S {} __attribute__((aligned (32)));
struct U {
S s;
V v;
} __attribute__((aligned (16)));
int main() {
_Alignof(U);
alignof(U);
}
- (1) : 型へのアライメント要求がそのメンバのアライメント要求よりも弱い場合の規定がC++にはない
- 型へのアライメント要求がそのメンバのアライメント要求よりも弱い場合はエラーと明確に規定する
- (2) : (1)の場合にアライメントをどうするのかの規定もない(ただし、構造体のアライメントはメンバのアライメントによって制限されるということを示す記述はある)
- エンティティ(型)のアライメントはそのメンバと同じかそれよりも強くなければならない、と明確に規定する。
こうしてみると、C言語がしっかりとしている一方でC++は深く考えてなかった感があります・・・
Networking TSのassociated_executor
からデフォルトのExecutorを取り除く提案。
前回の記事を参照
onihusube.hatenablog.com
このリビジョンでは、単純にassociated_executor
からsystem_executor
を削除してしまうと、Networking TS内にある別の機能であるdefer, dispatch, post
が深刻な影響を受けてしまうようで、それについての問題点と対策が追記されています。他には、5月に行われたSG4でのレビューについて追記されています。
元のシーケンスの各要素にインデックスを紐付けた要素からなる新しいシーケンスを作成するRangeアダプタviews::enumrate
の提案。
前回の記事を参照
onihusube.hatenablog.com
このリビジョンでは、インデックスの型の指定が変更されました。以前は1つ前の範囲の差分型(difference type)をインデックスの型に使用していましたが、1つ前の範囲のranges::size()
の返す型が取得できる場合はそれを、できない場合は差分型と同じ幅の符号なし整数型を使用する、という風に変更されました。要は常に符号なし整数型を使用するようになったという事でしょう。
std::string
、std::string_view
をnullptr
から構築できないようにする提案。
std::string
とstd::string_view
にはconst char*
を受けるコンストラクタ用意されており、nullptr
を直接渡すとそのコンストラクタが選択され、未定義動作に陥ります。こんなコードは書かないだろうと思われるのですが、筆者の方の調査によればLLVMを含む少なくとも7つのプロジェクトでこのようなコードが発見されたそうです。
実装によっては実行時アサーションによってエラーにするものもあるようですが、std::nullptr_t
を受けるコンストラクタをdelete
する事でそのような診断をコンパイル時に行おうとするものです。
契約プログラミングにおいて事前/事後条件をC++コードとして記述する際、チェックされない条件を記述する構文についての提案。
現在C++標準ライブラリでは処理の事前条件や事後条件を文章で指定していますが、契約プログラミングによってそれらをC++コードとして記述することが出来るようになります(予定)。その際、実行時であってもそのチェックが難しいか出来ない条件については、注釈という形で書いておくことが出来るようになっています。例えば、文字列のnull終端要求や、イテレータのend
への到達可能性などがあります。
bool is_null_terminated(const char *);
void use_str(const char* s)
[[expect: s != nullptr]]
[[expect axiom: is_null_terminated(s)]];
void use_opt_str(const char* s)
[[expect axiom: s == nullptr || is_null_terminated(s)]];
この様に、axiom
と指定された条件は注釈であり実行時にチェックされません。
この提案はこの構文を変更し、事前・事後条件に注釈であることを書くのではなく、関数宣言の方に注釈のためのものであることを表示するようにするものです。
axiom is_null_terminated(const char *);
void use_str(const char* s)
[[expect: s != nullptr]]
[[expect: is_null_terminated(s)]];
void use_opt_str(const char* s)
[[expect: s == nullptr || is_null_terminated(s)]];
このようにする事で、注釈となる条件とそうでないものを混ぜて書きながら実行可能な条件をチェックしてもらう事が出来るようになリます。OR条件の場合は条件を複数に分割して書く訳にもいかないので特に有用です。
axiom
とマークされた関数は契約の構文の中でのみ使用でき、何らかの述語としてbool
値を返す関数だがチェックが困難であることを表現し、実行時には単にtrue
を返す条件として扱われます。それ以外はほとんど通常の関数と同様に扱えるものです。ただし、そのために記述する順番には制約がかかります。
void use_opt_str(const char* s)
[[expect: is_null_terminated(s) || s == nullptr]];
現在のC++の字句規則をクリーンアップし、ユニコードで記述されたソースコードの振舞を明確にする提案。
現在のC++の字句規則はユニコード以前の世界で定義されたもので、文字コードを具体的に指定せずに構成されています。しかし、それによって実装定義の部分が広くなり実装による差異が多く発生していたり、そもそも人間に理解しづらかったりしています。
この提案はそれらを改善しユニコードの振る舞いをより明確にしつつ、実装間の差異をなるべく縮小することを目指したものです。全部で12個の提案が含まれています。
C++コンパイラ書く人とかC++コンパイラになりたい人は読んでみると面白いかもしれません。
SG16(Unicode Study Group)のミーティングにおける議論の要旨をまとめた文書。
例えば先ほど出てきていたP1949: C++ Identifier Syntax using Unicode Standard Annex 31などの提案やIssue等についての議論の様子が記載されています。
進行中のExecutor(P0443)提案中のbulk_execute
のインターフェースを改善する提案。
bulk_execute
はバルク実行のためのカスタマイゼーションポイントオブジェクトで、カスタムされたbulk executorを使用することによってハードウェアやOSが提供するバルク実行API(例えば、SIMDやスレッドプール)によって効率的なバルク処理を行う事を可能にするためのものです。
template<class Executor, class F, class Range>
void my_for_each(const Executor& ex, F f, Range rng) {
sender auto s = execution::bulk_execute(ex, [=](size_t i) {
f(rng[i]);
}, std::ranges::size(rng));
execution::sync_wait(s);
}
ただ、bulk_execute
は提案の初期から存在しており、P0443は途中で遅延実行のためにsender/recieverによるアプローチを採用しましたが、bulk_execute
はそれらの変更に追随しておらずインターフェースが一貫していませんでした。この提案はそれを解決するものです。主に以下の3点を変更します。
- 既存の
execute
(CPO)とセマンティクスを統一し、bulk_execute
は与えられた作業を即座に実行する実行用インターフェースとする
- 遅延実行用
bulk_execute
であるbulk_schedule
(CPO)を導入する(execute
に対するschedule
と同様)
bulk_schedule
によって返されるsenderに対する要件を制約し明確化するmany_receiver_of
コンセプトを導入する
- このsenderでは
set_value()
が繰り返し呼び出される事を許可する
これらの変更の提案はP0443R13に対してのもので、現在のExecutorライブラリの要件やコンセプト、セマンティクスを大きく変更しません。bulk_execute
とexecute/schedule
やsender/recieverとのセマンティクスの一貫性を改善し、Executorライブラリをより使いやすくするものです。
namespace std::execution {
void bulk_execute(executor auto ex,
invocable<executor_index_t<decltype(ex)> auto f,
executor_shape_t<decltype(ex)> shape);
}
auto executor = ...;
std::vector<int> ints = ...:
bulk_execute(executor,
[&](size_t idx) { ints[i] += 1; },
vec.size());
namespace std::execution {
sender auto bulk_schedule(executor auto ex,
executor_shape_t<decltype(ex)> shape,
sender auto prologue);
}
auto executor = ...;
std::vector<int> ints = ...:
auto increment =
bulk_schedule(executor, vec.size(), just(ints)) |
transform([](size_t idx, std::vector<int>& ints) {
ints[i] += 1;
});
execution::submit(increment, null_receiver{});
C++20で全面的に削除されたContractsのうち、議論の余地がなく有用であった部分と削除の原因となった論争を引き起こした部分とに仕分けし、有用であった部分だけを最初のC++ Contractsとして導入する事を目指す提案。
C++20において最終的にContractsが削除されることになってしまったのは、主に以下の機能が議論を巻き起こし合意が取れなくなったためです。
- 継続モード
- ビルドレベル
- 上記も含めた、制御がグローバルであること
- Literal semantics(in-source controls)
- 個々の契約に対して個別にチェックするか否かを指定したり、それがグローバルフラグの影響を受けないようにしていた
- Assumption
- (上記の事によって)
axiom
ではないのにチェックされていない契約条件の存在が想定される
提案ではC++20Contractsからこれらの部分を除いた広く合意の取れていた有用な部分をMVP(Minimum Viable Product)と呼称し、MVPを最初のContractsとして導入し、そうでない部分(上記の5項目)についてはより時間をかけて議論し、追加の機能として導入していくことを提案しています。
2020年6月のHistory Of Programming Languages (HOPL) で発表されるはずだったBjarne StroustrupさんによるC++の歴史をまとめた論文の紹介文書。
6月のHOPLカンファレンス延期されましたが論文は公開されているようです。英文PDF168Pの超大作ですが、とても興味深そうな内容です(翻訳お待ちしております)。
HOPLは15年毎に開催されるようで、C++はHOPLで3回紹介されたただ一つの言語になり、BjarneさんはHOPLで3回論文を書いたただ一人の人になったようです。次は2035年ですが、C++はそこでも登場することができるでしょうか・・・?
Contractsのユースケースを「何のために使用するか」と「どうやって使用するか」2つにカテゴライズし、報告されている既存のユースケースをカテゴライズする文書。
これは提案文書ではなく、SG21(Contracts Studt Group)での議論のための報告書です。
新しい標準アルゴリズムであるstd::swap_if
とstd::predictable
の提案。
std::sort
に代表される標準ライブラリ中の多くのアルゴリズムには次のような典型的な条件付きswap
が頻出します。
if (*right < pivot) {
std::swap(*left, *right);
++left;
}
この様なswap-if
操作は実装を少し変更するだけで、分岐予測のミスによるパイプラインストールを回避しパフォーマンスを2倍以上改善できるらしく、std::swap_if
はそのためのより効率的なswap-if
操作を提供するものです。
次のような実装になるようです。
template <movable T>
bool swap_if(bool c, T& a, T& b) {
T tmp[2] = { move(a), move(b) };
b = move(tmp[1-c]), a = move(tmp[c]);
return c;
}
bool
値がfalse == 0
、true == 1
であることを利用して、条件分岐を配列のインデックスに帰着させています。
これを用いると先ほどの典型的な操作は次のように書けます。
left += swap_if(*right < pivot, *left, *right);
ただし、現在のC++コンパイラはこの様なコードに対して必ずしも最適な(cmov
を使った)コードにコンパイルすることができず、せいぜい次善のコードを出力する場合が多いようです。ただ、その場合でも通常のswap-if
によるstd::sort
よりも高速なので、標準ライブラリとしてstd::swap_if
を規定し効率的な実装が提供されるだけでも典型的なswap-if
操作の性能向上が図れます。
また、std::swap_if
を規定することはコンパイラによるのぞき穴最適化の機会を提供することに繋がり、将来的に多くのコンパイラが最善のコードを出力できるようになるかもしれません。
ただし、std::swap_if
の上記の様な実装は多くのケースでは高速ですが、特定のデータに対してはかえって低速になります(例えば、ほとんどソート済みの配列のようなデータ列など)。それが事前に予測できる場合、通常の分岐によるswap-if
操作にフォールバックできる必要があります(現在のハードウェアでは、その閾値は90%以上の確度が必要)。
2つ目のstd::predictable
はそのための述語ラッパー型です。
template <predicate Predicate, bool is = true>
struct predictable {
std::remove_reference<Predicate>::type pred;
explicit predictable(Predicate&& p) : pred(p) {}
template <typename... Args>
constexpr bool operator()(Args&&... args) {
return ::std::invoke(p, args...);
}
};
template <typename>
constexpr bool is_predictable = false;
template <predicate P, bool is>
constexpr bool is_predictable<predictable<P,is>> = is;
標準ライブラリの述語を引数に取るアルゴリズムでは、これを用いて述語をラップして渡し、アルゴリズム中でそれを検出してstd::swap_if
を使用するかをコントロールします。
auto v = std::vector{ 3, 5, 2, 7, 9 };
std::sort(v.begin(), v.end());
std::sort(v.begin(), v.end(),
std::predictable([](int a, int b) { return a > b; }));
std::predictable
は単なる述語ラッパーであるため、従来の述語を取るアルゴリズムは何ら変更することなくこれを受け入れ、使用できます。一方で、std::swap_if
を使用しかつ述語を取るアルゴリズムでは、これを検出することで最適な実装を選択できるようになります。
これによって、標準ライブラリにstd::swap_if
を使用するかしないかを選択するための従来のアルゴリズム名それぞれに対応する新しい名前を導入したり、既存のアルゴリズムの規定を変更したりすることなく、標準アルゴリズムの多くでパフォーマンス向上と最適な実装の選択を同時に達成できるようになります。
現在の標準の無効な(指すオブジェクトの生存期間が終了した後の)ポインタについての矛盾した規定を正す提案。
[basic.stc] p4には、「Any other use of an invalid pointer value has implementation-defined behavior.(無効なポインタ値の他の使用には実装定義の振舞がある)」とあり、その注釈には「Some implementations might define that copying an invalid pointer value causes a system-generated runtime fault.(一部の実装では、無効なポインタのコピーを行うとシステム生成の実行時エラーが発生する、と定義している場合がある)」とあります(これらの規定のことをpointer zapと呼んでいるようです)。
一方でこの事は、[basic.types] p3にある規定及びポインタ型がtrivially copyableであることと明らかに矛盾しています。
提案はいくつかの例を示すとともに、これら規定を削除して無効なポインタはtrivially copyableであり比較可能と規定するか、ポインタ型はtrivially copyableではないと規定するか、どちらかを選択すべきと主張しています。提案としては前者が提案されています。
#include <assert.h>
#include <string.h>
int main() {
int* x = new int(42);
int* y = nullptr;
memcpy(&y, &x, sizeof(x));
assert(x == y);
assert(*y == 42);
}
[basic.types] p3にある例をint*
に特殊化したコードで、ポインタ型はtrivially copyableであるためこのコードは有効であり、y
はx
と同じものを指すようになります。
#include <assert.h>
#include <string.h>
int main() {
int* x = new int(42);
int* y = nullptr;
unsigned char buffer[sizeof(x)];
memcpy(buffer, &x, sizeof(x));
memcpy(&y, buffer, sizeof(x));
assert(x == y);
assert(*y == 42);
}
先ほどのサンプルを中間buffer
を介して行ったもの。[basic.types] p2にあるように、ポインタ型はtrivially copyableであるためこのコードは有効です。
#include <assert.h>
#include <string.h>
#include <stdint.h>
int main() {
int* x = new int(42);
int* y = nullptr;
uintptr_t temp = reinterpret_cast<uintptr_t>(x);
y = reinterpret_cast<int*>(temp);
assert(x == y);
assert(*y == 42);
}
[expr.reinterpret.cast] p5にあるように、ポインタ値を整数型にキャストしてから再びポインタ値に戻した場合でもポインタとしては有効であり続けます。
他にも込み入った例が全部で10パターン紹介されています。しかしここで見ただけでもわかるように、標準は少なくとも有効なポインタから無効なポインタへのその値のコピーは有効であることを示しており、([basic.stc] p4にあるような)無効なポインターのコピーが実装定義であるという規定を削除すべきという主張のようです。
次
onihusube.hatenablog.com
この記事のMarkdownソース