[C++]WG21月次提案文書を眺める(2021年07月)

文書の一覧

全部で39本あります。

N4894 Business Plan and Convener's Report

ビジネスユーザ向けのC++およびWG21の現状報告書。

P0288R8 move_only_function (was any_invocable)

ムーブのみが可能で、関数呼び出しのconst性やnoexcept性を指定可能なstd::functionであるstd::any_invocableの提案。

以前の記事を参照

このリビジョンでは、P2265R1を受けて、名前をany_invocableからmove_only_functionへ変更した事と配置するヘッダを独自ヘッダから<functional>へ変更した事などです。

この提案はこのリビジョンを持ってLWGに転送され、C++23入りを目指してLWGでレビューされます。

P0847R7 Deducing this

クラスのメンバ関数の暗黙のthis引数を明示的に書けるようにする提案。

以前の記事を参照

このリビジョンでの変更は、CWGのレビューを受けて提案する文言を変更した事です。

この提案はCWGでのレビューを終え、次の全体会議で投票にかけられることが決まっています。何事もなければそこでC++23に入ります。

P1206R4 Conversions from ranges to containers

任意のrangeをコンテナへ変換/実体化させるためのstd::ranges::toの提案。

このリビジョンでの変更は、

  • コンテナ型をrangeから構築する為のタグ型std::from_range_tとそれを受け取るコンストラクタを標準の多くのコンテナへの追加
  • ranges::toの提案する文言の改善
  • ranges::toによる構築時、可能なら構築対象コンテナの.reserve()を呼び出すようにした
  • 動機の項目を書き直した

ことなどです。

P1726R5 Pointer lifetime-end zap (informational/historical)

Pointer lifetime-end zapと呼ばれる問題の周知とそれについてのフィードバックを得るための報告書。

以前の記事を参照

このリビジョンでの変更は、zapという言葉を、provenanceとlifetime-end pointer zapの適用可能な側面の和集合として定義しなおし、それに従って文書を書き直したことなど、ポインタのprovenanceの概念に関連する説明を追加した事や、解決策に関する事を追記したことなど、多岐に渡ります。

この文書は提案ではありませんが、この問題の解決策がP2414R0で提案されています。

P2036R2 Changing scope for lambda trailing-return-type

ラムダ式の後置戻り値型指定において、初期化キャプチャした変数を参照できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロ(追加しない)と実装経験(ない)についての説明のセクションを追記した事です。

P2066R8 Suggested draft TS for C++ Extensions for Minimal Transactional Memory

現在のトランザクショナルメモリTS仕様の一部だけを、軽量トランザクショナルメモリとしてC++へ導入する提案。

以前の記事を参照

このリビジョンの変更点は、LEWGでのレビューのために「notable design decisions」を追加した事です。

P2093R7 Formatted output

std::formatによるフォーマットを使用しながら出力できる新I/Oライブラリstd::printの提案。

以前の記事を参照

このリビジョンでの変更は

  • SG16での投票結果の追記
  • 最新のWDをベースとするように変更
    • これによって、フォーマットのコンパイル時チェックが導入された
  • 無効なコードポイントの置換について、ユニコード規格を参照するようにした
  • 「The Unicode® Standard Version 13.0 – Core Specification」についての参照を提案する文言に追加
  • 文字コードが混在しているときの動作を明確にした

ことなどです。

P2167R1 Improved Proposed Wording for LWG 2114 (contextually convertible to bool)

contextually convertible to boolと言う規格上の言葉を、C++20で定義されたboolean-testableコンセプトを使用して置き換える提案。

必要なかったため、std::valarrayの比較演算子に対する変更を削除した事、タイトルを改善したことなどです。

P2198R2 Freestanding Feature-Test Macros and Implementation-Defined Extensions

フリースタンディング処理系でも使用可能なライブラリ機能について、機能テストマクロを追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案するポリシーの推奨事項の変更や、提案する文言の変更などです。

P2242R3 Non-literal variables (and labels and gotos) in constexpr functions

constexpr関数において、コンパイル時に評価されなければgotoやラベル、非リテラル型の変数宣言を許可する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正とサンプルコードを変更した事です。

この提案は既にCWGのレビューを終えており、次の全体会議で投票にかけられることが決まっています。

P2249R1 Mixed comparisons for smart pointers

スマートポインターの比較演算子に生ポインタとの直接比較を追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGでのレビューを受けて設計の根拠を明確にしたこと、提案する演算子オーバーロードHidden Friendsにしたことなどです。

P2273R2 Making std::unique_ptr constexpr

std::unique_ptrを全面的にconstexpr対応する提案。

以前の記事を参照

このリビジョンでの変更は、std::make_unique_for_overwriteconstexpr実装可能性について説明を追記した事、LEWGでの投票の結果を記載した事などです。

どうやら、2つのunique_ptrの順序付け比較演算子は除外されるようです(nullptrとの比較は提案されている)。

P2290R2 Delimited escape sequences

文字・文字列定数中の8進・16進エスケープシーケンスおよびユニバーサル文字名について、その区切りが明確になるような形式を追加する提案。

前回の記事を参照

このリビジョンでの変更は、提案する文言の改善と機能テストマクロが不要であることの説明の追記、この提案がWG14へも提出されたことについて記載されたことなどです。

SG22でのミーティングでは、そこにいるWG14メンバはこの提案をCで採用することに前向きなようです。

P2295R5 Support for UTF-8 as a portable source file encoding

C++コンパイラが少なくともUTF-8をサポートするようにする提案。

以前の記事を参照

このリビジョンでの変更は、SG16のガイダンスに従って提案する文言を改善した事です。

この提案はSG16からEWGへ、C++23に導入することを目指して転送されました。

また、SG22のミーティングでもこの提案が紹介され、そこにいたメンバーはCとC++がこの提案について一貫したことを採用する点で合意が形成されているようです。

P2300R1 std::execution

P0443R14のExecutor提案をベースにした、任意の実行コンテキストで任意の非同期処理を実行するためのフレームワークの提案。

以前の記事を参照

このリビジョンでの変更は

  • sender_ofコンセプトの追加
  • schedulerの特性を取得するCPO、std::this_thread::execute_may_block_callerの追加
    • そのschedulerが現在のスレッドをブロックするかをboolで取得する
  • schedulerの特性を取得するCPO、get_forward_progress_guaranteeの追加
    • そのschedulerによって作成された実行エージェントが、forward progress guaranteeを満たすかを列挙値で取得する
  • unscheduleアダプタの削除
  • typoやバグの修正

などです。

P2316R1 Consistent character literal encoding

#ifプリプロセッシングディレクティブの条件式において、文字リテラルC++の式の意味論と同等に扱えるようにする提案。

以前の記事を参照

このリビジョンでの変更は、EWGの要請によりコンパイラ実装者に連絡し実装に問題が無い事を確認したことを追記した事と、機能テストマクロが必要ない理由を記載したことなどです。

SG22のWG14メンバは、この提案をWG14でも採用することに前向きなようです。

P2338R1 Freestanding Library: Character primitives and the C library

<charconv>std::char_traitsをはじめとするいくつかのヘッダをフリースタンディングライブラリ指定する提案。

以前の記事を参照

このリビジョンでの変更は

  • POSIXエラーのハンドリングについて追記
  • WG14がintmax_tを非推奨としているため、<cinttypes>を除外
  • SDCCでの実装状況の確認に関する追記
  • 一部の機能をオプションとする代替案についての説明の追記
  • wchar_t関連関数の説明の追記

などです。

P2347R1 Argument type deduction for non-trailing parameter packs

関数テンプレートの実引数型推論時に、引数リスト末尾にないパラメータパックの型を推論できるようにする提案。

以前の記事を参照

このリビジョンでの変更は提案する文言やサンプルコードの修正です。

P2350R1 constexpr class

constexpr対応クラスの簡易構文の提案。

以前の記事を参照

このリビジョンでの変更は指定子の現れる順番についての説明の追記と提案する文言の修正です。

この提案では、クラス名の後に続くときは必ず: final constexprとなるように、finalconstexprの指定に順序を設けています。これによって実装が簡単になり教えやすくなるため、順序を自由にする必要はないとの主張です。

P2362R1 Remove non-encodable wide character literals and multicharacter wide character literals

エンコード可能ではない、あるいは複数文字を含むワイド文字リテラルを禁止する提案。

以前の記事を参照

このリビジョンでの変更は、タイトルの変更、提案する文言の改善、機能テストマクロについての追記などです。

この提案はSG16からEWGへ、C++23に向けて導入ることを目指して転送されました。

P2392R1 Pattern matching using "is" and "as"

現在のパターンマッチングをクリーンアップし、使用可能な所を広げる提案。

以前の記事を参照

このリビジョンでの変更は

  • is, asに対するインデックスアクセスの動作を明確化
  • オーバーロードされた演算子は副作用を無視することを明確化
  • typoやバグ修正

などです。

EWGでの投票では、この提案に作業時間をかける事(おそらくC++23に向けて)について合意がとれています。

P2401R0 Add a conditional noexcept specification to std::exchange

std::exchangenoexcept指定を追加する提案。

std::exchangeはその動作のほぼ全てが指定されていますが、noexceptは指定されていません。

template<class T, class U = T>
constexpr T exchange(T& obj, U&& new_val) {
  T old_val = std::move(obj);
  obj = std::forward<U>(new_val);
  return old_val;
}

このため、std::exchangeを使用してムーブコンストラクタを実装した時など、自然にnoexceptになるべきところでならなくなってしまっています。また、noexcept(std:exchange(...))falseとなり、そのようにnoexcept指定をする場合もnoexceptになりません。

指定されている実装を見れば、例外を投げ売るのは1行目のTのムーブ構築と、2行目のU -> Tのムーブ代入です。そのため、std::exchangeが例外を投げるかどうかはstd::is_nothrow_move_constructible<T>std::is_nothrow_assignable<T&, U>によって簡単に求めることができるため、標準にもそう指定することを提案しています。

これらのことはすでにMSVC STLでは実装されています。

P2402R0 A free function linear algebra interface based on the BLAS (slides)

P1673R3 A free function linear algebra interface based on the BLASの解説スライド。

どういう動機からBLASベースのAPIを追加し、どのような設計になって、どう変化してきたのか、等を解説しています。

P2403R0 Presentation on P2300 - std::execution

P2300R1 std::executionの機能紹介スライド。

P2300によって導入される非同期処理を構成するための各種アルゴリズムの解説が行われています。

P2404R0 Relaxing equality_comparable_with's and three_way_comparable_with's common reference requirements to

各種異種比較を定義するコンセプトのcommon_reference要件を緩和する提案。

異種比較系コンセプトとは、std::equality_comparable_withstd::three_way_comparable_withstd::totally_ordered_withの3つです。これらのコンセプトは2つの型の間にそれぞれが表現する二項関係が成り立っている事を表すコンセプトです。

しかし、これらのコンセプトは現在のところ、ムーブオンリーな型について正しく機能していません。

// Tはなんか型とする
static_assert(std::equality_comparable_with<std::unique_ptr<T>, std::nullptr_t>); // コンパイルエラーとなる

int main() {
  std::unique_ptr<int> p;
  p == nullptr; // OK
}

これらのコンセプトはその定義中にstd::common_reference_withコンセプトによる制約を含んでおり、そのcommon_reference_withコンセプトが型に対して実質的にcopyableである事を要求しているために起こります。

3つのコンセプト定義にはどれも、std::common_reference_with<const std::remove_reference_t<T>&, const rstd::emove_reference_t<U>&>のような制約式があります。remove_reference_tの部分を単にT, Uに置き換えるとstd::common_reference_with<const T&, const U&>となります。std::common_reference_with<T, U>T, Uがどちらもそれらのcommon_referenceであるCRに変換可能(std::convertible_to<CR>)である事を指定しています。

std::common_reference_t<const std::unique_ptr<T>&, const std::nullptr_t&>std::unique_ptr<T>になるので、std::unique_ptr<T>std::nullptr_tに対してはstd::convertible_to<const std::unique_ptr<T>&, std::unique_ptr<T>>const std::unique_ptr<T>& -> std::unique_ptr<T>への変換)が要求されることになりますが、これはコピーコンストラクタを呼び出し、std::unique_ptrはムーブオンリーなので変換可能ではないためcommon_reference要件を満たすことができず、異種比較系コンセプトはfalseとなります。

一般化するとこのことは、common_reference_t<const T&, const U&>Tとなり、T(const U&)のコンストラクタは利用可能でないがT(U&&)は利用可能であるようなT, Uについて同じことになります。これはT, Uを逆にしても同じ事です。

この3種類のコンセプトは<ranges>を始め色々な所で使われており、また基礎的なコンセプトであるため色々な所で使われていくでしょう。すると、意図せずこの問題に遭遇する確率は上がっていく事でしょう。

このcommon_reference要件は同値関係についての数学的な考察から来ているようです。ある集合AとBの間に同値関係を定義することは、代わりにその和集合A ∪ Bの上に同値関係を定義する場合にのみ意味を持ちます。そのことに基づいてC++では、型T, Uの間の同値関係はTUに共通する何らかのsupertypeの上で動作している、ととらえます。このsupertypeを導出するのがcommon_referenceであり、common_reference要件はsupertype上で同値関係が定義されている事を要求しています。そのため、実行時にcommon_referenceへの変換が発生する、もしくは必要となるわけではありません。

common_reference要件の問題点は、このsupertypeの要求を2つの型の間のcommon_referenceとして表現してしまっていることにあります。CV修飾や形成可能な参照型によらずにsupertype要件を表現できれば、この問題を解決する事ができ、コンセプトはより洗練されます。

この問題は次の2つの問題に分けて考える事ができます(C = std::common_reference_t<const T&, const U&>とします)。

  1. TがムーブオンリーでCTが同じ型となる
  2. CTではなく、Tの右辺値からのみ構築できる

これらの問題の両方で、T(U)Cに変換可能である必要がありますが、それは数学的な要件であって実行時に実際に変換されません。そのため、変換関係を表現するためにおかしな事をする必要はありません。

1つ目のケースは、CTはCV参照修飾を除いて同じ型であることがわかるため、convertible_to<const T&, C>の要件を緩和して、const T&Cremove_cvrefした後で同じ型になる場合を受け入れるようにすることで解決できます。これは、const T&const C&と同じ型であるときはconst T& -> const C& -> Cのような変換(const T&Cの一時オブジェクトにバインドすることでCを構築)が可能であるためです。これは実際にやったら危険なことですが、実際にはこの変換は行われません。

2つ目のケースは、convertible_to<const T&, C>を緩和してconvertible_to<const T&, C> || convertible_to<T&&, C>のように、Tのコピーを必要としない有効な変換を探すようにすることで解決できます。実際こんな変換を勝手にやられたら困りますが、ここでもやはり実行時にこのような変換は行われません。

これらの解決は、TUに置き換えて同じ事が言えます。

この提案は、これらの事を考慮したsupertype要件を表現するcommon-comparison-supertype-with<T, U>という説明専用のコンセプトによって現在の異種比較系コンセプトのcommon_reference要件を置き換えることでこのような問題の解決を図るものです。その際、構文的要件だけでなく意味論要件の変更も必要となります。
この変更は破壊的なものですが、影響を受けるのは極端なコードだけであり、ムーブオンリータイプで異種比較系コンセプトが正しく動作するようになる利点が上回ると筆者の方は主張しています。実際、libc++とMSVC STLの内部テストを用いてこの提案による変更の実装をテストしたところ、この提案の変更によって失敗するテストはなかったようです。

P2405R0 nullopt_t and nullptr_t should both have operator and operator==

nullopt_tstd::optional<T>nullptr_tstd::unique_ptr<T>, std::shared_ptr<T>の間で各種異種比較を定義するコンセプトが動作するようにする提案。

nullopt_tstd::optional<T><=>/==によって比較することができます。しかし、異種比較系のコンセプト(std::equality_comparable_with, std::three_way_comparable_with, std::totally_ordered_with)はそれらの型についてfalseとなります。

// Tはなんか型とする
static_assert(std::three_way_comparable_with<std::optional<T>, std::nullopt_t>);  // NG
static_assert(std::equality_comparable_with<std::optional<T>, std::nullopt_t>);   // NG

int main() {
  std::optional<int> opt;

  // ともにOK
  auto cmp = opt <=> std::nullopt;
  auto eq = opt == std::nullopt;
}

先ほどP2404R0と似た問題に見えますが、これはnullopt_t自身に何ら比較演算子が定義されていないことによリます。異種比較系のコンセプトは型T, Uの間の比較についてTおよびU自身がまず同等の比較演算が可能である事を求めます。nullopt_tはそうではないため、実際の比較可能性とは異なる結果を生成してしまっています。

前述のように、これらのコンセプトは<ranges>を始め色々な所で使われているためこれらのコンセプトがきちんと機能していないと、optionalnulloptについて比較が必要となるところでコンパイルエラーを起こしてしまいます。

これらの事は、nullptr_tとスマートポインタの間にも同じことが言えます。

この提案は、nullopt_tnullptr_t<=>/==による比較を定義することでこれらの問題の解決を図るものです。どちらの比較もstd::strong_oredering::equalを返すdefault<=>を定義した形の比較となり、自身との同値性について(== <= >=)だけtrueを返すものです。

なお、nullptr_tとスマートポインタの間でこれらコンセプトをきちんと動作させるには、先ほどのP2404の解決が同時に必要となります。

P2406R0 Fix counted_iterator interaction with input iterators

std::counted_iteratorを安全に使用可能にする提案。

次のプログラムは実行完了するのに非常に時間がかかります。

#include <ranges>
#include <iostream>

int main() {
  for (auto i  : std::views::iota(0)
               | std::views::filter([](auto i) { return i < 10; })
               | std::views::take(10))
  {
    std::cout << i << '\n';
  }
}

やっていることは0~9の整数値を出力しているだけです(色々突っ込みどころはありますが)。

このviews::takeは前段のrangeイテレータ)がrandom_accessでない場合にstd::counted_iteratorを使用しています。問題があるのは2段目のviews::filterの条件です。

非常にわかりづらいですが、これは範囲for文によってイテレータ操作に展開されており、for文によるループは終了する時に最後の要素の次までイテレータが進行します。ループが9を出力した後、次のようなことが起こることが期待されます。

  1. takeイテレータインクリメント
    1. filterイテレータインクリメント
      1. 条件を満たす次の要素を探索し、その位置を指すイテレータを返す
      2. 条件がtrueとなるまでiotaイテレータをインクリメント
  2. takeイテレータの終端判定
    1. std::counted_iteratorの内部カウンタの残量が0かをチェック、0となるのでtrueを返す
  3. ループの終了

実際にはviews::filterの条件がi < 10であることによって、上記手順の1-1、views::filterのインクリメントは9を超え10未満の要素(整数)を探索し続けます。従って、この探索は終わることが無く(最大値に到達すると終わるのかもしれません・・・)、views::takeイテレータのインクリメントは終了しません。

この例は作為的ではありますが、同じことはviews::filterの返す要素の数が不透明である場合に起こり得ます。すなわち、views::filterによってフィルタされて残る要素の数がtakeする数よりも小さい場合、同じことが起こり終了しない可能性があります。

このことが実際に問題となるのはむしろ、basic_istream_viewのようなinput_iteratorに対して使用したときです。

#include <ranges>
#include <iostream>
#include <sstream>

int main() {
  auto iss = std::istringstream("0");

  // 0を読んだ後、takeイテレータが進行するが、istream_viewが次のストリーム入力を待機するために終了しない
  for (auto i : std::ranges::istream_view<int>(iss)
              | std::views::take(1))
  {
    std::cout << i << '\n';
  }
}

input_iteratorが終端に達した後でインクリメントされると何が起こるのかはイテレータによるため、これはもっとわかりづらい形で顕在化するかもしれません。

この提案は、内部カウンタが0付近(長さが0となる近傍)でのstd::counted_iteratorの振る舞いを変更することでこの問題に対処します。std::counted_iteratorの内部カウンタが0になったときに内部イテレータをインクリメントしないようにし、内部カウンタが0->1になるときもデクリメントしないようにするほか、base()の動作も長さが0の時は保持するイテレータを進めて返すように変更します。

ただし、random_access_iteratorに対しては現在の振る舞いを維持します。なぜなら、random_access_iteratorとなるrangeview)はその要素の全てがいつでも利用可能な状態にある事を期待できるためです。

この変更はABI破壊を招くため、それを受け入れるかC++20に逆適用するかしない場合、これと同じことをする新しいクラスを追加することを提案しています。

P2407R0 Freestanding Library: Partial Classes

一部の有用な標準ライブラリのクラス型をフリースタンディング処理系で使用可能とする提案。

使用可能にしようとしているのは次のクラスです

  • std::array
  • std::string_view
  • std::variant
  • std::optional

フリースタンディングライブラリ機能とすることを提案している理由は、これらのクラスがとても有用であるからです。

これらのクラスには例外を投げうる関数(.at()std::get()など)が含まれていますが、フリースタンディング環境ではそれらを= delete;としておくことを提案しています。

P2408R0 Ranges views as inputs to non-Ranges algorithms

非Rangeアルゴリズムイテレータに対する名前付き要件を、イテレータコンセプトで置き換える提案。

C++20で<algorithm>に追加された、std::ranges名前空間の下にあるコンセプトで制約されたアルゴリズム群の事をRangeアルゴリズムと呼び、そうで無い従来のものを非Rangeアルゴリズムと呼び分けます。

Rangeアルゴリズムはコンセプトを用いて各種要件が指定されているのに対して、非Rangeアルゴリズムは名前付き要件(Cpp17ForwardIteratorなど)という文書で要件が指定されています。両者はほとんど同じ事を指定していますが微妙に異なり、コンセプトによって定義されるC++20のイテレータイテレータ要件によって定義されるC++17以前のイテレータは互換性、特に後方互換性がなく、非RangeアルゴリズムC++20イテレータを使用する事は推奨されません。

この提案は従来の非Rangeアルゴリズムに対する要件もコンセプトを用いて置き換える事で、C++20のイテレータを従来の非Rangeアルゴリズムで使用可能にするものです。その目的は、C++20のviewから取得したイテレータに対してParallel Argorithmを使用可能とすることにあります。

std::vector<int> data = ...;
auto v = data | std::views::transform([](int x){ return x * x; });
int sum_of_squares = std::reduce(std::execution::par, begin(v), end(v));

auto idxs = std::views::iota(0, N);
std::transform(std::execution::par, begin(idxs), end(idxs), begin(sqrts),
               [](int x) { return std::sqrt(float(x)); });

問題となるのは、従来のイテレータカテゴリチェック機構(すなわちstd::iterator_traits)を使用してC++20イテレータを使用した時、つまりはC++17以前のイテレータを使用している場所でC++20イテレータを使用した時、C++20イテレータC++17互換イテレータとして振る舞うためにC++20のそれとは異なるイテレータカテゴリを返します。それは多くの場合input_iteratorであり、実際にはそれよりも強いカテゴリとして扱うことができるにも関わらず性質が制限されます。

また、forward以上の強さのC++20イテレータC++17イテレータの最大の非互換はイテレータiに対する*iの結果型(reference)が参照型でなくても良いかどうかです。C++17以前はreferenceは必ず参照型でしたが、C++20以降はそうではなくても(例えばprvalue)構いません。この提案の最大の問題点はこの点です。

非Rangeアルゴリズムに指定されている要件をコンセプトを用いて書き換える事は、これらの問題に抵触しません。コンセプトを通じた経路ではC++20イテレータは正しくC++20イテレータとして扱われ、C++17以前は間接参照の結果が必ず参照型なのでイテレータコンセプトにおいてそこは問題となりません。そして、イテレータを入力にしか使用しないアルゴリズムreference型に依存せずに書くことが(あるいは書き直すことが)できるはずです。

なおこの変更はCpp17ForawardIterator以上の要件に対してのみ行われます。したがって、現在Cpp17InputIteratorを求めているところではC++20イテレータを使用できません。これは、input_iteratorC++17とC++20間の非互換が他のカテゴリに比べて大きいためです。

P2409R0 Requirements for Usage of C++ Modules at Bloomberg

ブルームバーグ社内における経験から、モジュールの実装およびそれを利用するために必要な要件についてまとめた報告書。

ブルームバーグ社内には数万の独立したC++リポジトリが存在しており、それらのプロジェクトは積極的な依存関係の再構築を行うパッケージマネージャのアプローチによって統合され、ディストリビューションスナップショットと呼ばれるものによって全てのプロジェクトの一貫性が保たれています。ディストリビューションスナップショットはビルド済みの成果物が含まれており、変更が必要となるときは変更されるソースコードによる最小のビルドコンテキストを構成した上で、成果物をディストリビューションスナップショットに対して更新します。

ブルームバーグ社内のこのような経験は一般のオープソースプロジェクトと大きく変わるものではないため、この文書の内容はブルームバーグ社内の経験に大きく影響されるもののブルームバーグに特化したものではありません。

その上で、同様のプロジェクト構造を持つ組織において、モジュールを使用していくために求められる次のような要件を報告しています。

  1. システムに存在するモジュール数に関係なく、C++ソースコードを含むファイルを開くことなく、一定のI/Oコストで現在のビルドの外側に存在しているモジュールの存在がテスト可能でなければならない
    • 例えば、モジュール名から決定される特定のファイルの存在をチェックするなど
  2. システムに存在するモジュール数に関係なく、一定のI/Oコストで現在のビルドの外側に存在しているモジュールを利用する方法を発見することができる
    • 例えば、モジュール名から決定される特定のファイルを読み込むことなど
  3. モジュールソースコードをパースすることなく、現在のビルドの外側に存在しているモジュールの依存関係を把握することが可能である
    • 例えば、モジュール名から決定される特定の依存関係記述ファイルを読み込むことなど
  4. モジュールの検出機能は、同じプラットフォームで動作するコンパイラや静的解析ツールにの間で相互運用可能である
  5. モジュールの検出機能には、モジュールのインターフェースをパースするための十分な指示が含まれている
  6. コンパイルコマンドは、ディスク上のファイルが相互運用可能な方法で発見・パース可能であることに加えて、翻訳単位のセマンティクスを正しく再現するのに十分でなくてはならない
  7. モジュールの検出機能には、ビルドシステムの外側でモジュールファイルをパースするコストを削減するための、相互運用可能なフォーマットが含まれている

P2410R0 Type-and-resource safety in modern C++

C++CoreGuidelineをベースとして、完全なタイプ&リソースセーフなC++プログラミングのためのルールの概要。

これは提案文書ではなく、タイプ&リソースセーフなC++を書くために意識すべき原則や注意すること、またそれを促進する静的解析の重要性を説明する文書です。

P2411R0 Thoughts on pattern matching

C++へのパターンマッチング機能についてのビャーネ先生の所感。

パターンマッチングに否定的なわけではなく、機能の方向性についてどのようなものが望ましいのかの考えをまとめた文書です。

  • 構造化束縛の延長線上にあること
  • 構文は統一的かつジェネリックで、宣言的なものであること
  • パターンマッチングを、既存のライブラリコードを用いたマッピングで定義することは避けるべき
  • 非常に稀な特殊ケースに過度に対応させる必要はない
  • パフォーマンスを損ねるものであってはならない

パターンマッチングはC++のコードをよりクリーンかつシンプルに書けるようにすることができる機能を提供できる貴重な機会ですが、その導入に失敗してしまえばC++の複雑さを増大させユーザー離れを招きかねないためその設計は慎重になるべき、という事を言っています。

ビャーネ先生はP2392is, asによるパターンマッチングの方向性を支持しているようですが、それをパターンマッチング外部に一般化するのをC++23に間に合わせようとする必要はないとも述べています。

P2412R0 Minimal module support for the standard library

標準ライブラリモジュールについて最小のサポートをC++23に追加する提案

C++20でモジュールが追加されましたが、標準ライブラリはモジュール化されていません。これは次のような問題を生じています

  • モジュールの使用を妨げている
  • 標準ライブラリのモジュール(定義したり、実装固有のものなど)を使用するプログラムは、そのモジュールを定義せずに移植できない
  • 標準ヘッダのように簡単にモジュールを教えられない
  • モジュールを利用した現実的な例の不足
  • プラットフォーム固有の、準標準モジュールが登場してしまう

標準ライブラリは優れたスタイルの例であるはずですが、特定の機能を使用するために特定のヘッダをインクルードもしくはインポートする必要があり、どの機能がどのヘッダにあるのか覚える必要があるなど多くの人にとって負担になっています。結果的に、これはライブラリ機能を提供する際のアンチパターンになってしまっています。また、標準ヘッダのインクルードは思わぬコンパイル時間の増大を招きます。

標準ライブラリをモジュールとして提供する事でこれらの問題を解決し、標準ライブラリによってモジュールの定義と利用のモデルを提供することを目指す提案です。

次のようなモジュールを提供する事を提案しています。

  • std.fundamental
  • std.core
  • std.io
  • std.os
  • std.concurrency
  • std.math
  • std

特に重要なのが最後のstdモジュールで、これをインポートすれば標準ライブラリのすべての機能にアクセスできます(C互換ヘッダを除く)。このstdモジュールを少なくとも追加した上で、その他細粒度のモジュールは合意が取れるものだけをC++23に導入していく事を目指しています。

標準ヘッダはエクスポートしてはならない実装詳細を含んでおり、stdモジュールは標準ヘッダユニットの集合体であってはならず、標準で規定されている名前だけがエクスポートされている必要があります。import std;なグローバル名前空間を汚染すべきではなく、マクロをエクスポートすべきでもありません。

ヘッダユニットは結局機能に対して適切なヘッダをインポートしなければならないという問題を解決せず、マクロをリークし、複雑な依存関係とそれによるコンパイル時間の増大、などの問題を解決できません。ヘッダユニットは移行期のメカニズムとして利用することができますが、それに頼りすぎるとヘッダが抱える問題を引きずり続けるリスクがあります。

提案文書には、MSVCのモジュール実装を使用したヘッダインクルードとモジュールインポートによる標準ライブラリ使用時のコンパイル時間の測定例が記載されています。

#include import import std; 全ヘッダのインクルード 全ヘッダのインポート
<iosteream> 0.87s 0.32s 0.08s 3.43s 0.62s
9ヘッダ 2.20s 0.77s 0.44s 3.53s 0.99s

あくまで1つの例であり、まだ最適化されたものではありません。しかし、この結果は標準ライブラリをモジュールとして提供することのメリットの一つを端的に表しています。

P2413R0 Remove unsafe conversions of unique_ptr

std::unique_ptrにおける、危険な暗黙変換を禁止する提案。

この提案のいう危険な変換とは次のようなものです。

#include <memory>

struct Base {};
struct Derived : Base {};

int main() {
  std::unique_ptr<Base> base_ptr = std::make_unique<Derived>();
}

Baseを公開継承しているDerivedのポインタはBaseのポインタに暗黙変換することができますが、Baseには仮想デストラクタがないため、そのままBaseのポインタに対してdeleteを呼ぶと未定義動作となります(派生クラスのデストラクタが呼ばれない)。

この問題はstd::unique_ptrの変換コンストラクタが適切に制約されていないことから来ており、より詳細にはstd::default_deleteの変換コピーコンストラクタが単にstd::is_convertible_vのチェックしかしていないことによります。

namespace std {
  template <class T>
  struct default_delete {

    // default_deleteの変換コピーコンストラクタの実装例
    template <class U>
      requires is_convertible_v<U*, T*>
    default_delete(const default_delete<U>&) noexcept {}

    /*...*/
  };
}

生ポインタ間の変換可能性しかチェックしておらず、その後のoperator()の呼び出し(delete)が未定義とならないかどうかを気にしていません。

この提案は、ここにさらに要件を加えることで、冒頭のような危険な変換を禁止するものです。

namespace std {
  template <class T>
  struct default_delete {

    // default_deleteの変換コピーコンストラクタの実装例
    template <class U>
      requires is_convertible_v<U*, T*> &&
               (
                 is_similar_v<U, T> || 
                 has_virtual_destructor_v<T>
               )
    default_delete(const default_delete<U>&) noexcept {}

    /*...*/
  };
}

is_similar_vは説明専用のメタ関数で、2つの型がsimilarな関係にあるかを調べるものです(similarとは2つの型がCV修飾の違いを除いて等しい、みたいな意味です)。

これによってdefault_delete<T> -> default_delete<U>への変換は、T* -> U*への変換が可能でありかつ次のどちらかの場合に可能となります。

  1. TUsimilarな関係にある
  2. Tが仮想デストラクタを持っている

これによって、冒頭のコードはどちらの条件も満たさないためコンパイルエラーとなります。

この変更によって壊れるコードは元々未定義動作を含んでいたものだけであるはずで、実際libc++をフォークしこの変更を適用した上でLLVMコンパイルしたところlibc++のテストが1件だけコンパイルエラーとなり、それは冒頭のコードのような未定義動作を含んでいたものでした。

ただし、C++20のDestroying Deleteを用いたコードではこの変更が破壊的となる可能性があります。

#include <memory>
#include <new>

struct Base {
  void operator delete(Base* ptr, std::destroying_delete_t);
};

struct Derived : Base {};

void Base::operator delete(Base* ptr, std::destroying_delete_t) {
  ::delete static_cast<Derived*>(ptr);
}

int main() {
  std::unique_ptr<Base> base_ptr = std::make_unique<Derived>();
}

このコードでは、Baseのポインタに対するdelete時にデストラクタが呼び出されず、代わりにユーザー定義deleteの処理において静的にディスパッチされ適切なデストラクタを呼び出しています。

このようなコードは稀でしょうがDestroying Deleteの適正な使用法の一つであるため、解決が必要です。提案では、default_deleteに対するカスタマイゼーションポイントを追加することで、特定の型のペアに対して変換を許可する案を提示しています。

P2414R0 Pointer lifetime-end zap proposed solutions

Pointer lifetime-end zapと呼ばれる問題の解決策の提案。

現在のC/C++の規定では、あるオブジェクトを指すポインタはそのオブジェクトの寿命が尽きた時に無効(あるいは不定)となり、その使用(ポインタ値のロードとストアやキャストやポインタ値の比較)は未定義動作となります。しかし、古くから使用されているアルゴリズム、特に並行アルゴリズムではそのような不正なポインタを使用することに依存しているものが多くあり、そのようなアルゴリズムは未定義動作に陥っています。並行アルゴリズムでは、あるオブジェクトからポインタが取得された後でそのポインタが使用されるまでの間に、別のスレッドによってそのオブジェクトが破棄されていることが起こり得、さらにはそのポインタの指す場所に別のオブジェクトが再配置され、デリファレンスによってそれを読みだしてしまう事すら起こり、それに依存するアルゴリズムも存在しています。

そのような規定はコンパイラあるいは診断ツールによる追加の診断や最適化を可能としますが、今日マルチスレッドハードウェアが一般的になっていることから、そのような最適化(特にリンク時最適化)によって並行アルゴリズムの動作が妨げられると、実行時にバグを見えない形で埋め込むことになり、影響はより深刻になります。特に、そのようなアルゴリズムは既に広く使用されていることから、既存のコードをコンパイルしなおしたときにそのようなバグが埋め込まれる可能性があります。

この様な問題は、ポインタがそれが指すオブジェクトの寿命終了とともに消失(zap)するように見えることから、Pointer lifetime-end zap(あるいは、lifetime-end pointer zap)と呼称されます。

この提案は、Pointer lifetime-end zapを解決するために可能ないくつかの解決策について提案するものです。

提案では、無効(不定)なポインタの使用とゾンビポインタのデリファレンス(一度無効となったポインタの指す位置にオブジェクトが再構築された後の参照)の2つのパートに問題を分割し、それぞれに可能ないくつかの解決策を提示しています。

  • 無効(不定)なポインタの使用
    1. 無効なポインタの使用の許可
      • ポインタを単にアドレス値の様な値であるとみなしそれ以上の意味論を与えない
      • PointerのProvenanceの概念と衝突する
    2. 無効なポインタを使用可能であるとマークする
      • usabel_ptr<T>の様なラッパ型を使用して、ポインタのProvenanceを断ち切る
      • 現在の規定を変更しない
    3. 無効化した後再使用するポインタに予めマークしておく
      • std::atomic等の既存のライブラリ機能をポインタで使用するときに特別扱いすることでusabel_ptr<T>と同様の意味論を与える
    4. 予めマークされた割り当て/解放操作によって取得されるオブジェクトへのポインタについて、無効化した後の利用を許可する
      • コンパイラオプションなどによって、そのようなアロケータへのマーキングはコードの外で行う
  • ゾンビポインタのデリファレンス
    1. ゾンビポインタのデリファレンスの許可
      • ポインタを単にアドレス値の様な値であるとみなしそれ以上の意味論を与えない
      • 最適化を阻害する可能性があり、標準の変更も伴う
    2. ゾンビポインタのデリファレンスが可能であるとマークする
      • 「無効なポインタ使用」の解決策2に、ゾンビポインタのデリファレンスも許可する
    3. ゾンビポインタとしてデリファレンスしうるポインタに予めマークしておく
      • 「無効なポインタ使用」の解決策3に、ゾンビポインタのデリファレンスも許可する
      • 2に比べて既存コードへの影響が小さくなる
    4. ゾンビポインタとしてデリファレンスしうるポインタに予めマークしておく
      • 「無効なポインタ使用」の解決策4に、ゾンビポインタのデリファレンスも許可する
      • 3に比べて更に既存コードへの影響が小さくなる
    5. CAS成功時に期待される値が上書きされるモデル
      • 通常のCAS操作では期待される値は上書きされない
      • ゾンビポインタを期待値としてCASが成功したときにゾンビポインタのProvenanceを再計算させることで有効化する
      • 意味論のみの変更であり、実際には書き換えなどは行われない

この提案ではまだこれのどれを選ぶのか、また選ばないかは決まっていません。

P2415R0 What is a view?

viewコンセプトの要件を緩和する提案。

これはstd::generator<T>がO(1)で破棄可能という要件を満たせない事から議論が始まっています。

viewコンセプトは範囲を所有しない軽量なrangeを定義するコンセプトで、構文的には次のように定義されます。

template<class T>
  concept view =
    range<T> &&
    movable<T> &&
    enable_view<T>;

そして、意味論要件として次の3つが要求されています。

  • Tのムーブ構築/代入は定数時間(O(1))
  • Tのデストラクトは定数時間
  • Tはコピー不可であるか、Tのコピー構築/代入は定数時間

この要件は、viewであるTが範囲を所有せず必然的に軽量な型であることを要求するものです。

viewコンセプトがアルゴリズム関数のようなrangeを受け取る関数の引数を制約するために使用されることはほばなく(その用途には~_rangeコンセプトとフォワーディングリファレンスが用いられる)、viewが構築・代入や破棄が軽量であることを求めるのはrangeアダプタの構築のコストを増大させないためです。

auto rng = v | views::some
             | views::operations
             | views::here;

この様なrangeアダプタの構築はまだ何もしておらず、将来のループのために準備をしているだけですが、これがvの要素をコピーしたりするものであると準備するだけのこのコードのコストはかなり大きなものになってしまいます。viewコンセプトの意味論要件は、そのようなことが起きずrangeアダプタの構築がvの要素に触れることは無いことを保証し、rangeアダプタ自身がviewであり入力にviewを受け取ることから、そのことはrangeアダプタのすべてのレイヤで保証されます。

viewコンセプトを満たさない型をviewとしてしまうことは未定義動作に繋がりますが、それによって実際に起こることはrangeアダプタの構築時に余分なコストが発生しパフォーマンスが低下することです。

しかし、次の様な型を考えてみるとviewコンセプトの破棄に対する要求は本当にパフォーマンスに配慮したものなのかに疑問が生じます。

struct bad_view2 : view_interface<bad_view2> {
  std::vector<int> v;
  
  bad_view2(std::vector<int> v) : v(std::move(v)) { }
  
  // movable, but not copyable
  bad_view2(bad_view2 const&) = delete;
  bad_view2(bad_view2&&) = default;
  bad_view2& operator=(bad_view2 const&) = delete;
  bad_view2& operator+(bad_view2&&) = default;
  
  std::vector<int>::iterator begin() { return v.begin(); }
  std::vector<int>::iterator end()   { return v.end(); }
};

このbad_view2はムーブ構築と代入はO(1)でcopyableではなくviewコンセプトの構文要件を満たしていますが、O(1)で破棄可能ではないためviewのモデルではありません。

std::vector<int> get_ints();

auto rng = bad_view2(get_ints())
         | views::filter([](int i){ return i > 0; })
         | views::transform([](int i){ return i * i; });

このrngの構築にはstd::vector<int>のムーブ2回が発生し、std::vector<int>の破棄は3回行われます。破棄がO(1)とならないのは最後のrngの破棄時です。

このコードは次のように書くこともできます。

auto ints = get_ints(); // 変数に保持しておく
auto rng = ints
         | views::filter([](int i){ return i > 0; })
         | views::transform([](int i){ return i * i; });

この場合は現行のviewコンセプト的にも何の問題もなく、rngの構築時にもstd::vector<int>のムーブを伴わないので効率的に思えます。しかし、実際にはstd::vector<T>の破棄がrngから中間オブジェクトであるintsに移されただけで破棄のコストが無くなったわけではありません。そして、rngintsを所有せず参照しているためダングリングの危険に配慮する必要があり、(ref_viewがポインタで参照するため)ポインタの間接参照のコストがかかります。
これらのデメリットはbad_view2では問題とならず、全体を見比べてみるとbad_view2は合法な後者のコードよりもパフォーマンスでも安全性でも勝っています。

この提案では、このbad_view2の様な型を許可するために、viewコンセプトの破棄に関する意味論要件を次のどちらかように変更する事を提案しています。

  • M個の要素を持ったTのオブジェクトがN回ムーブされたとき、それらN個のオブジェクトの破棄はO(N+M)
  • ムーブ元のTのオブジェクトの破棄は定数時間

この要件によって先程のbad_view2の様な型やstd::generator<T>は無事にviewとなるようになります。

そしてこれによって可能となる次のような型を追加し、views::allの効果を書き換えることも提案しています。

template <range R>
  requires is_object_v<R> && movable<R>
class owning_view : public view_interface<owning_view<R>> {
  R r_; // exposition only
  
public:
  owning_view() = default;
  constexpr owning_view(R&& t);
  
  owning_view(const owning_view&) = delete;
  owning_view(owning_view&&) = default;
  owning_view& operator=(const owning_view&) = delete;
  owning_view& operator=(owning_view&&) = default;

  constexpr R& base() & { return r_; }
  constexpr const R& base() const& { return r_; }
  constexpr R&& base() && { return std::move(r_); }
  constexpr const R&& base() const&& { return std::move(r_); }

  constexpr iterator_t<R> begin() { return ranges::begin(r_); }
  constexpr iterator_t<const R> begin() const requires range<const R>{ return ranges::begin(r_); }
  
  constexpr sentinel_t<R> end() { return ranges::end(r_); }
  constexpr sentinel_t<const R> end() const requires range<const R> { return ranges::end(r_); }


  // + overloads for empty, size, data
};
  
template <class R>
owning_view(R&&) -> owning_view<R>;

このstd::ranges::owning_view<T>は要するに先ほどのbad_view2と同じものです。

このowning_view<T>を用いて、views::allが従来std::ranges::subrangeを返していたところを書き換えます。それによって右辺値のviewではないrangeを安全かつ効果的にviews::allによってview化できます。views::allはほとんどのrangeアダプタで使用されているため、これによるメリットは殆どのrangeアダプタが享受できます。

P2416R0 Presentation of requirements in the standard library

現在の規格書の、要件(requirement)の記述方法を変更する提案。

たとえばコンテナ要件やイテレータ要件など、現在は表と文書で記述されているものを箇条書きリストとその詳細文書のような記述に変更するものです。

意味論の変更は意図されておらず、純粋に文書としての体裁の変更です。

P2417R0 A more constexpr bitset

std::bitsetconstexpr対応させる提案。

C++11にて、std::bitsetの一部のコンストラクタとoprator[]constexpr指定されており、限定的に定数式で使用可能でした。他の部分がconstexpr指定されていないのは、std::bitest<T>::referenceのデストラクタがトリビアルではなかったための様です(トリビアルとすることもできたがABI破壊を伴うため敬遠されていた)。

C++20において定数式での動的メモリ確保が許可され、同時にconstexprデストラクタが許可されました。それによって、::referenceのデストラクタを含めたstd::bitsetの全てのメンバ関数constexpr化する障害はなくなっています。

std::bitestはビットマスクやビットフラグを表すのに便利なクラスで定数式においても同様であり、定数式でも利用可能にすることで同様のクラスを再発明する必要が無くなるなどの利点があります。

この提案は、std::bitsetstd::bitest<T>::referenceのすべてのメンバ関数および、std::bitsetの一部の演算子オーバーロード<<を除いた残り)にconstexprを付加するものです。

なお、この提案の前にも同様の提案(P1251R1)が提出されており、そちらは改訂を待った後でLWGに転送される予定でした。しかし、著者の方のレスポンスが無くなり進行していなかったため、この提案が新しい提案として再提出されました。

おわり