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

文書の一覧

全部で74本あり、SG22(C/C++相互互換性に関する研究グループ)のCの提案を除くと73本になります。

採択された文書

ここにあるのは10月の全体会議でワーキングドラフト入りが承認された提案です。ただし、今月提案文書の改定がないものもあるのでこれで全部ではありません。

P0798R8 Monadic operations for std::optional

std::optionalmonadic interfaceのサポートを追加する提案。

以前の記事を参照

R7(未公開)の変更は

  • 機能テストマクロの変更
  • 各関数の制約の調整

R8の変更は

  • transformの受け取る関数の戻り値型にremove_cvを適用
  • or_elseから事前条件を削除
  • transformがコピー省略行えるように調整

などです。

P1147R1 Printing volatile Pointers

標準ストリームにおいて、volatileポインタの<<による出力をできるようにする提案。

標準出力ストリームを使用してvolatileポインタを出力すると、予期しない値が出力されます。

int main() {
           int* p0 = reinterpret_cast<         int*>(0xdeadbeef);
  volatile int* p1 = reinterpret_cast<volatile int*>(0xdeadbeef);

  std::cout << p0 << std::endl; // 0xdeadbeef
  std::cout << p1 << std::endl; // 1
}

標準出力ストリームに対するストリーム出力演算子<<)では、const void*(非volatileポインタ)の出力を行うオーバーロードはあり、普通のポインタはこれを使用してアドレスが出力されます。しかし、volatileポインタはこのオーバーロードを使用できず(CV修飾が合わないため)、ポインタ->boolの変換を介してbool値として出力されます。

この提案は、operator<<(const volatile void*)オーバーロードを追加することによってこの意図しない変換を防止し、volatileポインタを適切に出力できるようにしようとするものです。

P1272R4 Byteswapping for fun&&nuf

バイトスワップ(バイトオーダー変換)のための関数std::byteswap()の提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正のみです。

P2077R3 Heterogeneous erasure overloads for associative containers

連想コンテナに対して透過的な要素の削除と取り出し方法を追加する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックに基づいて提案する文言を修正したことです。

P2314R4 Character sets and encodings

規格文書中の ~ character setという言葉を明確に定義し直す提案。

以前の記事を参照

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

  • P1949の変更を適用する形で更新
  • CWGなどからのフィードバックによる提案する文言の改善

などです。

P2415R2 What is a view?

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

以前の記事を参照

このリビジョンでの変更は提案する文言の修正のみです(多分)。

P2418R2 Add support for std::generator-like types to std::format

std::generator-likeな型に対する<format>のサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善(主にFormatter要件を要求するところの調整)です。

P2432R1 Fix istream_view, Rev 1

std::views::istream_view<T>()の他のviewとの非一貫性を正す提案。

以前の記事を参照

このリビジョンでの変更は例の修正と、提案する文言の修正です。

P2450R0 C++ Standard Library Issues to be moved in Virtual Plenary, Oct. 2021

今回の会議で採択された標準ライブラリについてのIssue報告とその解決。

  1. 2191. Incorrect specification of match_results(match_results&&)
  2. 2381. Inconsistency in parsing floating point numbers
  3. 2762. unique_ptr operator*() should be noexcept
  4. 3121. tuple constructor constraints for UTypes&&... overloads
  5. 3123. duration constructor from representation shouldn't be effectively non-throwing
  6. 3146. Excessive unwrapping in std::ref/cref
  7. 3152. common_type and common_reference have flaws in common
  8. 3293. move_iterator operator+() has incorrect constraints
  9. 3361. safe_range<SomeRange&> case
  10. 3392. ranges::distance() cannot be used on a move-only iterator with a sized sentinel
  11. 3407. Some problems with the wording changes of P1739R4
  12. 3422. Issues of seed_seq's constructors
  13. 3470. convertible-to-non-slicing seems to reject valid case
  14. 3480. directory_iterator and recursive_directory_iterator are not C++20 ranges
  15. 3498. Inconsistent noexcept-specifiers for basic_syncbuf
  16. 3535. join_view::iterator::iterator_category and ::iterator_concept lie
  17. 3554. chrono::parse needs const charT* and basic_string_view<charT> overloads
  18. 3557. The static_cast expression in convertible_to has the wrong operand
  19. 3559. Semantic requirements of sized_range is circular
  20. 3560. ranges::equal and ranges::is_permutation should short-circuit for sized_ranges
  21. 3561. Issue with internal counter in discard_block_engine
  22. 3563. keys_view example is broken
  23. 3566. Constraint recursion for operator<=>(optional<T>, U)
  24. 3567. Formatting move-only iterators take two
  25. 3568. basic_istream_view needs to initialize value_
  26. 3570. basic_osyncstream::emit should be an unformatted output function
  27. 3571. flush_emit should set badbit if the emit call fails
  28. 3572. copyable-box should be fully constexpr
  29. 3573. Missing Throws element for basic_string_view(It begin, End end)
  30. 3574. common_iterator should be completely constexpr-able
  31. 3580. iota_view's iterator's binary operator+ should be improved
  32. 3581. The range constructor makes basic_string_view not trivially move constructible
  33. 3585. Variant converting assignment with immovable alternative
  34. 3589. The const lvalue reference overload of get for subrange does not constrain I to be copyable when N == 0
  35. 3590. split_view::base() const & is overconstrained
  36. 3592. lazy_split_view needs to check the simpleness of Pattern
  37. 3593. Several iterators' base() const & and lazy_split_view::outer-iterator::value_type::end() missing noexcept
  38. 3595. Exposition-only classes proxy and postfix-proxy for common_iterator should be fully constexpr

P2462R0 Core Language Working Group “ready” issues for the October, 2021 meeting

今回の会議で採択されたコア言語についてのIssue報告とその解決。

  1. 1249. Cv-qualification of nested lambda capture
  2. 1724. Unclear rules for deduction failure
  3. 1726. Declarator operators and conversion function
  4. 1733. Return type and value for operator= with ref-qualifier
  5. 2484. char8_t and char16_t in integral promotions
  6. 2486. Call to noexcept function via noexcept(false) pointer/lvalue
  7. 2490. Restrictions on destruction in constant expressions
  8. 2491. Export of typedef after its first declaration
  9. 2496. ref-qualifiers and virtual overriding

その他の文書

N4896 PL22.16/WG21 agenda: 4 October 2021, Virtual Meeting

2021年10月4日 08:00 (北米時間)に行われたWG21本会議のアジェンダです。

C++23のための3回目の全体会議です。

N4897 WG21 admin telecon meeting: September 2021

WG21の各作業部会の管理者ミーティング。

前回から今回の会議の間のアクティビティの報告がされています。

N4898 WG21 2021-10 Virtual Meeting Minutes of Meeting

2021年10月4日(北米時間)に行われた、WG21全体会議の議事録。

CWG/LWG/LEWGの投票の様子などが記載されています。

N4899 WG21 admin telecon meetings: 2022

次回以降のWG21の各作業部会の管理者ミーティング。

次は2022年1月24日に予定されています。

N4900 WG21 virtual plenary meeting(s): 2022

次回以降のWG21全体会議の予定表。

次は2月7日に予定されています。

N4901 Working Draft, Standard for Programming Language C++

C++23ワーキングドラフト第5弾。

N4902 Editors' Report - Programming Languages - C++

↑の変更点をまとめた文書。

10月の会議で採択された提案とコア言語/ライブラリのIssue解決が適用されています。

P0009R13 MDSPAN

多次元配列に対するstd::spanである、mdspanの提案。

以前の記事を参照

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

  • P2299R3の内容の適用
  • 提案する文言の修正
  • LEWGのガイダンスに従って、設計に関する議論を追記
  • ランク0mdpsnarequired_span_sizeのレイアウト修正
  • 少なくとも1つの要素数指定が0であるmdspanlayout_stride::required_span_sizeを修正
  • operator[]を多次元配列アクセスに使用
  • spanへの文言の参照を解消
  • layout_strideストライドとユニークレイアウトのための変換コンストラクタを追加
  • mdspanにポインタとエクステントからのコンストラクタを追加
  • layout policy mappingnothrow move constructible/assignableの要件を追加
  • accessor policynothrow move constructible/assignableの要件を追加
  • accessor policyaccessor policy pointerの要件を追加
  • mdspan/submdspanが何も例外を投げないという指定を削除

などです。

P0627R6 Function to mark unreachable code

到達不可能なコード領域であることを示す、std::unreachable()の提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の機能テストマクロ表現を修正したことです。

この提案はすでにLWGのレビューを終えており、次の全体会議で投票にかけられる予定です。

P1169R3 static operator()

関数呼び出し演算子operator())を、静的メンバ関数として定義できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加したこと、LWG Issue 3617の変更を前提とした文言の追加です。

P1467R5 Extended floating-point types and standard names

C++コア言語/標準ライブラリに拡張浮動小数点型のサポートを追加する提案。

以前の記事を参照

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

  • 文言と設計に関する文書を個別のセクションに分割
  • C言語との互換性に関するドキュメントの追記
    • 2つの言語での同じ浮動小数点数型を指す型名について
    • 2つの言語の間の拡張浮動小数型の変換(usual arithmetic conversion)の違いについて
  • 可変引数関数に渡した時のdoubleへの昇格を削除
  • <format>周りの文言に関する説明を追記(不要だった)
  • I/Oストリームに、long double以下の幅を持つ拡張浮動小数点数型のサポート追加
  • <charconv>/<cmath>の文言についての背景の追記
  • よく知られている浮動小数点数に対応する型エイリアスの名前を、std::floatN_tとする事を決定

などです。

P1642R7 Freestanding Library: Easy [utilities], [ranges], and [iterators]

[utility]<ranges><iterator>から一部のものをフリースタンディングライブラリに追加する提案。

前回の記事を参照

このリビジョンでの変更は、<atomic>がフリースタンディングであり続けるように文言を調整したこと、std::unreachableの追加に関する議論を追記したことです。

この提案では、<utility>全体をフリースタンディングとしていますが、std::unreachable<utility>に追加される予定のため衝突しています。std::unreachableをフリースタンディングとすることに対する投票が行われたようですが、反対する人はいなかったようです。

P1673R5 A free function linear algebra interface based on the BLAS

標準ライブラリに、BLASをベースとした密行列のための線形代数ライブラリを追加する提案。

このリビジョンでの変更は、mdspan()に代わって[]を使用するようになったため、その変更を反映したことです。

どうやらこの提案はC++26を目指すことになったようです。

P1854R1 Conversion to literal encoding should not lead to loss of meaning

文字列リテラルエンコーディングを実行時エンコーディングに変換する際、文字表現が失われる場合をコンパイルエラーとする提案。

文字列リテラルエンコーディングソースコードエンコーディング)と実行時エンコーディングが異なるケースは比較的よく発生し、その時の振る舞いは実装定義とされています。

#include <cstdio>

int main() {
  puts("こんにちは");
}

このコードをUTF-8で保存し、実行時エンコーディングをAsciiとしてコンパイルすると、MSVCでは警告のみでコンパイルが通るのに対してGCCではエラーとなり、その際MSVCはAscii内の代替文字(?)で文字を置換しています。

この事はC++プログラムの移植性を損ねており、文字列は意味や目的を持つ文書であるので実装がその意味を変えるべきではなく、このようなエンコーディングの縮小変換が起こる場合をill-formedと規定しようとする提案です。

この提案ではまた、複数の文字によって1文字を構成するユニコード文字(é🇯🇵など)がマルチキャラクリテラルとして読み取られて意図しない1文字になる場合をエラーとする事も同時に提案しています。

int main() {
  [[maybe_unused]]
  char c = '🇯🇵';
}

マルチキャラクリテラルに含まれる文字は基本リテラル文字集合の要素のみ、と規定する事でこのような文字がコンパイルエラーととなるようにします。これによって、結合文字などの不可視文字が排除されるため見た目の曖昧さが解消され、マルチキャラクリテラルの結果がintに収まるようになります。

P1885R8 Naming Text Encodings to Demystify Them

システムの文字エンコーディングを取得し、識別や出力が可能なライブラリを追加する提案。

以前の記事を参照

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

などです。

P2012R2 Fix the range-based for loop, Rev2

現在のrange-based forに存在している、イテレーション対象オブジェクトの生存期間にまつわる罠を修正する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善などです。

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

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

以前の記事を参照

このリビジョンの変更点は、LWGのフィードバックに対応して文言を修正した事です。

この提案は全体会議で承認され、「Minimal Transactional Memory TS」として発行されることになります。

P2248R2 Enabling list-initialization for algorithms

値を指定するタイプの標準アルゴリズムにおいて、その際の型指定を省略できるようにする提案。

以前の記事を参照

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

  • 理論的根拠について詳しく追記
  • 出力イテレータを使用するアルゴリズムのデフォルトvalue_typeについての説明の追記
  • テスト実装のリンク追加
  • *_scan系数値アルゴリズムについてのデフォルトを修正

などです。

P2249R2 Mixed comparisons for smart pointers

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正です。

P2255R2 A type trait to detect reference binding to temporary

一時オブジェクトが参照に束縛されたことを検出する型特性を追加し、それを用いて一部の標準ライブラリの構築時の要件を変更する提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロの追加、std::make_from_tupleをこの提案に沿って修正、提案する文言の変更などです。

P2264R1 Make assert() macro user friendly for C and C++

P2264R2 Make assert() macro user friendly for C and C++

assertマクロをC++の構文に馴染むように置き換える提案。

R1での変更は、ローカルなテスト実装を追記したこと、CとC++の提案する文言を分離したこと、フィードバックを受けた議論を追加したことです。

R2での変更は、さらに議論を追記したことなどです。

この提案はCとC++の両方に対して提出されていますが、Cの方の投票では一旦否決されているようです。

P2291R3 Add Constexpr Modifiers to Functions to_chars and from_chars for Integral Types in Header

std::to_chars, std::from_charsを整数変換に関してconstexprにする提案。

以前の記事を参照

このリビジョンでの変更は、ヘッダの肥大化防止やコンパイル時間削減のためコンパイラ組み込み命令で実装されうることについて追記したことです。

P2300R2 std::execution

P0443R14のExecutor提案を置き換える、任意の実行コンテキストで任意の非同期処理を構成・実行するためのフレームワークおよび非同期処理モデルの提案。

以前の記事を参照

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

  • 即時実行senderアルゴリズムの削除
  • connectカスタマイゼーションポイントとsender_traits<>を拡張して、awaitable(コルーチンPromise型)をtyped_senderとして扱えるようにした
  • as_awaitable(), with_awaitable_senders<>を追加して、コルーチン型がコルーチン内でsenderを待機可能にした
  • sender/awaitableの相互作用について説明を追記
  • sender/recieverのキャンセルサポートの設計についての説明を追記
  • 単純なsenderアダプタアルゴリズムの例を示すセクションを追加
  • 単純なschedulerの例を示すセクションを追加
  • 数独ソルバ、並行再帰ファイルコピー、エコーサーバー、の例を追記
  • bulkアルゴリズムforward progress保証を改善
  • 様々なsenderを使用して非同期シーケンスを表現する方法を説明するセクションの追加
  • senderを使用して、部分的に成功を表す方法を説明するセクションの追加
  • senderファクトリjust_error, just_doneを追加
  • senderアダプタdone_as_optional, done_as_error
  • sender/recieverの製品

などです。

今月の文書にもやりあっているのが見られますが、NetworkingTSのベースとなるExecutorとしてこの提案のschedulerがふさわしくないことから、議論が紛糾しているようで、進捗が芳しくなさそうです・・・。

P2322R5 ranges::fold

rangeアルゴリズムであるranges::foldの提案。

以前の記事を参照

このリビジョンでの変更は、プロジェクションを削除したこと、foldl_while系の操作を削除したこと、全てのオーバーロードがプロジェクションを取らないようになったこと、各関数の名前をfold_left, fold_left_first, fold_right, fold_right_lastに暫定的に変更したことなどです。

プロジェクションは、初期値を最初の要素から取得するタイプ(fold_left_first/fold_right_last)の場合に、プロクシオブジェクトを返すイテレータ(例えばzip)を適切に扱うには、余分なコピーが避けられない問題を回避するために削除されました。

このリビジョンの提出後、名前はfold_left, fold_left_first, fold_right, fold_right_lastで正式に決定し、LEWGの投票で合意が取れればLWGに転送される予定です。

P2324R1 Labels at the end of compound statements (C compatibility)

複合ステートメントcompound statement)の末尾にラベルを置けるようにする提案。

このリビジョンでの変更は、3つ提案していた文言を1つに絞ったこと、SG22での投票結果を記載したこと、実装経験について追記したこと、Cと同様の暗黙のnullステートメントを追加できるようにしたこと、などです。

P2327R1 De-deprecating volatile compound operations

非推奨となったvolatile値に対する複合代入演算子を再考する提案。

このリビジョンでの変更は、bitwise演算子の複合代入演算子|= &= ^=)の非推奨解除のみを提案するようにしたことです。

この提案はEWGの議論を通過し、CWGに送られるためのEWGの投票待ちです。なお、C++20へのDRとなるようです。

P2347R2 Argument type deduction for non-trailing parameter packs

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

以前の記事を参照

このリビジョンでの変更は、テスト実装を使用してより広いコンテキスト(クラス、関数テンプレートのアドレス取得、エイリアステンプレート、部分特殊化)でこの推論が上手くいくかの調査の結果を追記したことです。調査では、特に問題は見つからなかったようです。

P2348R2 Whitespaces Wording Revamp

ユニコードの仕様に従う形で、改行と空白を明確に定義する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正です。

この提案はSG16とEWGのレビューを通過し、CWGへ送るためのEWGの投票待ちをしています。

P2350R2 constexpr class

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

以前の記事を参照

このリビジョンでの変更は、例を追加したこと、EWGでの投票の結果を記載したこと、constexprクラスのstaticメンバ変数はconstexprとなることを明確にしたこと、constexpr(false)が必要な理由と不要な理由を記載したこと、などです。

メンバ関数の一部がCのAPIに依存していたりするとその関数はconstexpr化不可能ですが、そのクラスをconstexprクラスにしてしまうとそのようなメンバ関数が1つあるだけでエラーとなってしまいます。そのため、選択的に非cosntexprにするためにconstexpr(false)が必要だ、というフィードバックが寄せられたようです。ただし、その場合でもクラス外で定義することで回避可能であったり、P2448R0が採択されればその問題を回避できるとして、この提案ではconstexpr(false)を提案していません。

P2361R3 Unevaluated strings

コンパイル時にのみ使用され、実行時まで残らない文字列リテラルについての扱いを明確化する提案。

以前の記事を参照

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

  • プリプロセッシングトークンはコンテキストに依存してはならないことからunevaluated-stringプリプロセッシングトークンとしないように文言を改善した
  • 文字列リテラルの評価中にその文字列をnull終端することで、unevaluated-stringがnull終端されていないことを明確化
  • literal-operator-idの文言にunevaluated-stringを適用
  • externの文言にunevaluated-stringを適用し、リンケージ指定がユニコード文字を示すことを明確化
  • asmステートメントでの数値エスケープシーケンスの許可

などです。

P2384R1 2021 Spring Library Evolution Poll Outcomes

2021年の春(4月から6月にかけて)に行われた、LEWGの全体投票の結果。

以前の記事を参照。

このリビジョンの変更は引用する文書が間違っていたのを修正しただけです。

P2387R2 Pipe support for user-defined range adaptors

ユーザー定義のRangeアダプタに対して、パイプライン演算子|)サポートを簡単に行えるユーティリティを提供する提案。

以前の記事を参照

このリビジョンでの変更は、もう一つ機能テストマクロを追加した事です。

この提案はSG9とLEWGのレビューを終え、LWGに送られるための投票待ちをしています。C++23入りを目指しています。

P2388R3 Minimum Contract Support: either No_eval or Eval_and_abort

契約が破られた時に継続しないコントラクトサポートを追加する提案。

以前の記事を参照

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

  • 構文について、属性との混同、Cとの潜在的な非互換性について追記
  • 契約モード名をNo_eval、Eval_and_abort*に変更
  • "function parameter"とすべきところを"function argument"としていたところを修正
  • 事後条件で非const非参照仮引数を使用できるようにする解決策の案として、暗黙const扱いにする例を追加
  • セキュリティの懸念に対処するため、violation handlerが標準出力にメッセージを出力することを推奨する文言を削除
  • 事後条件で参照されている関数引数をconst_castした場合に未定義動作となる説明を追記
  • 契約条件部分式ごとの副作用削除についての議論の追記
  • 実装可能性についてのセクションを追加
  • 契約違反時にstd::terminate()ではなく、std::abort()を呼び出す理由について追記
  • 契約条件がimmediate contextにないことを明確にした
  • 構文の選択、副作用の説明、プログラミングモデルの有効性、に関する未解決の問題の追加

などです。

P2400R2 Library Evolution Report: 2021-06-01 to 2021-09-20

2021年6月から9月にかけての、LEWGでのミーティングについてのまとめ。

どれくらいミーティングを行ったのか、おおまかな機能単位についての進行状況、レビューを行った提案についての議論の状況などが記載されています。

P2408R2 Ranges iterators as inputs to non-Ranges algorithms

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

以前の記事を参照

このリビジョンでの変更は、タイトルの変更、input iteratoroutput iteratorの要件についてイテレータコンセプトを使用するようにしていた部分の削除、mutable iteratorに関する議論の追加、zip_viewイテレータなどについての説明の追記。

この提案はSG9のレビューを通過し、LEWGへ転送されました。

P2417R1 A more constexpr bitset

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

以前の記事を参照

このリビジョンでの変更は、設計についての議論と実装経験についてを追記したことです。

MSVC STLbitset実装をベースにcosntexpr化を行い、実装できることと、既存のテストをパスすることを確かめたようです。

P2435R1 2021 Summer Library Evolution Poll Outcomes

2021年の夏(7月から9月にかけて)に行われた、LEWGの全体投票の結果。

以前の記事を参照。

このリビジョンの変更は引用する文書が間違っていたのを修正しただけです。

P2445R0 forward_like

クラス型のメンバ変数について、const性も含めた正しい完全転送を行うstd::forward_likeの提案。

これはDeducing Thisによって頻出するであろうコードを正しく書くためのユーティリティです。

template<typename T>
struct wrap {
  T v;

  template<typename Self>
  auto value(this Self&& self) -> decltype(auto) {
    return std::forward<Self>(self).v; // あってる?
  }
};

この.value()メンバ関数thisself)の値カテゴリに応じてメンバvの返す型(値カテゴリ)を変化させたいわけです。C++20まではstd::optional.value()のようにconstと参照修飾によって4つのオーバーロードに分けていましたが、C++23以降はDeducing Thisによってthisに対応する引数を明示的に取れるようになった事から1つの関数にまとめることができるようになります。

その際、thisの状態によってメンバを完全転送するにはおおよそ上記のように書くことになるのですが、メンバが参照である場合やthisconstである場合に正しい結果になりません。次の表は上記のように書いた時の.value()の戻り値型がthisの状態とメンバの状態によってどうなるかを示したものです

thisself メンバ(v std::forwad(self).v
&&
& &
&& &&
const const &&
const & const &
const && const &&
const const &&
& const const &
&& const const &&
const const const &&
const & const const &
const && const const &&
& &
& & &
&& & &
const & &
const & & &
const && & &
&& &
& && &
&& && &
const && &
const & && &
const && && &
const & const &
& const & const &
&& const & const &
const const & const &
const & const & const &
const && const & const &
const && const &
& const && const &
&& const && const &
const const && const &
const & const && const &
const && const && const &

表中の空白はconstも参照修飾もない状態、すなわち単なる値として宣言されている場合です(thisの場合はコピーされている、すなわちprvlaueの状態)。

これを見ると、std::forward<Self>(self).vのようなコードが問題ないのは、vが非参照である時くらいのものです。メンバが参照である場合はthisconstが正しく伝わっておらず、メンバがconst参照の場合はthisの値カテゴリ(特に右辺値&&)が正しく伝播されていません。

std::forward_likeはこの伝播を正しく行い完全転送するためのもので、std::forwardとよく似ています。forward_likeを使うと先ほどのコードは次のように書き換えられます

template<typename T>
struct wrap {
  T v;

  template<typename Self>
  auto value(this Self&& self) -> decltype(auto) {
    return std::forward_like<Self>(self.v);
  }
};

次の表は、先ほどの表にforward_likeの結果を追記したものです

this メンバ forwad forward_like
&& &&
& & &
&& && &&
const const && const &&
const & const & const &
const && const && const &&
const const && const &&
& const const & const &
&& const const && const &&
const const const && const &&
const & const const & const &
const && const const && const &&
& & &&
& & & &
&& & & &&
const & & const &&
const & & & const &
const && & & const &&
&& & &&
& && & &
&& && & &&
const && & const &&
const & && & const &
const && && & const &&
const & const & const &&
& const & const & const &
&& const & const & const &&
const const & const & const &&
const & const & const & const &
const && const & const & const &&
const && const & const &&
& const && const & const &
&& const && const & const &&
const const && const & const &&
const & const && const & const &
const && const && const & const &&

std::forwadの時と比較すると、メンバが参照である場合にも正しく(理想的に)constと値カテゴリが伝播しているのが分かります。Deducing Thisで可能になることを考えればこのようなコードは良く書かれることが予想され、forward_likeの必要性はDeducing Thisの提案中でも指摘されていました。

std::forward_likeは簡単には次のように実装されます。

// T->Uへ参照(値カテゴリ)を伝播する
template <typename T, typename U>
using __override_ref_t = std::conditional_t<std::is_rvalue_reference_v<T>,
                                            std::remove_reference_t<U>&&, U&>;

// T->Uへconstをコピーする
template <typename T, typename U>
using __copy_const_t = std::conditional_t<std::is_const_v<std::remove_reference_t<T>>,
                                          U const, U>;

// forward_likeの結果型を求める
template <typename T, typename U>
using __forward_like_t = __override_ref_t<T&&, __copy_const_t<T, std::remove_reference_t<U>>>;


template <typename T>
[[nodiscard]]
constexpr auto forward_like(auto&& x) noexcept -> __forward_like_t<T, decltype(x)> {
  return static_cast<__forward_like_t<T, decltype(x)>>(x);
}

各メタ関数のT*thisの、Uはメンバの型が来ます。すなわち、なるべくthisの値カテゴリを利用してメンバの値カテゴリを指定しつつthisconstを伝播しようとするものです。

値カテゴリによって振る舞いを変化するラムダ式によるコールバックのサンプル

auto callback = [m=get_message(), &scheduler](this auto&& self) -> bool {
  return scheduler.submit(std::forward_like<decltype(self)>(m));
};

callback();             // retry(callback)
std::move(callback)();  // try-or-fail(rvalue)

この例のように、Deducing thisによってキャプチャしているラムダを扱う際にforward_likeは特に重要となります。

間接的な所有権を持つクラスにおける、所有するメンバの転送のサンプル

struct fwd {
  std::unique_ptr<std::string> ptr;
  std::optional<std::string> opt;
  std::deque<std::string> container;

  auto get_ptr(this auto&& self) -> std::string {
    if (ptr) {
      return std::forward_like<decltype(self)>(*ptr);
    }
    return "";
  }

  auto get_opt(this auto&& self) -> std::string {
    if (opt) {
      return std::forward_like<decltype(self)>(*m);
    }
    return "";
  }

  auto operator[](this auto&& self, size_t i) -> std::string {
    return std::forward_like<decltype(self)>(container[i]); // dequeは右辺値用[]を持たない
  }
};

この例では、ポインタを介した所有などによってthisconst性および値カテゴリの伝播が断ち切られているケースでも、Deducing Thisとforward_likeによって望ましい転送を実現しています。

P2447R0 std::span and the missing constructor

std::spaninitializer_listを受け取るコンストラクタを追加する提案。

現在のstd::spanにはstd::initializer_listを受け取るコンストラクタがなく、次のようなコードがエラーとなります。

void foo(std::span<const int>);

int main() {
  foo({1, 2, 3}); // error
}

この提案はstd::spanにはstd::initializer_listを受け取るコンストラクタを追加してこれがコンパイルできるようにしようとするものです。

この提案のモチベーションは、従来std::vectorを利用していた関数引数をstd::spanに置き換えた時に発生する問題を解決することにあります。

// C++17までのインターフェース
void foo(const std::vector<int>&);

// C++20からはspanを使い書き換えたい
void foo(std::span<const int>);

int main() {
  foo({1, 2, 3}); // C++17まではOK、C++20からはエラー
}

この変更が適用されると{}によって簡単にダングリングstd::spanを作れてしまうようになりますが、現状でもダングリングspanは割と簡単にできてしまい、std::spanviewである以上それは避けられないため、std::initializer_listを取れるようにすることのメリットの方が大きいという主張です。

また、std::initializer_listコンストラクタは{}初期化時に最優先で選択されることから、この変更は破壊的変更となります。しかし、std::initializer_listコンストラクタに制約を設けることでこの影響は低減でき、破壊的となるのは非常にレアケースとなるようです。

const std::vector<int>;

// この変更の後でも、これらの振る舞いは変化しない
auto sp1 = std::span{v};
auto sp2 = std::span{v.begin(), v.end()};
auto begin = v.data();
auto end = begin + v.size();
auto sp3 = std::span{begin, end};

// この提案以前はvoid*1つのspan
// この提案の後では、void*2つのspan
void* vp = nullptr;
span<void* const> sp4{&vp, &vp+1};

// この提案以前はany1つのspan
// この提案の後では、any2つのspan
std::any a;
std::any* ap = &a;
span<std::any> sa{&ap, &ap+1};

std::spanの主な用途の一つは関数引数で使用することです。その場合、spanの要素型をテンプレートパラメータにすることはできないためspanの要素型は明示的に指定されているはずです。従って、void*などの特殊な要素型を考慮する必要は通常ないため、破壊的変更はやはり問題とならないとのことです。

P2448R0 Relaxing some constexpr restrictions

constexpr関数がすべての引数について定数実行不可能となる場合でも、コンパイルエラーにしないようにする提案。

例えば、次のコードはC++20まではエラーとなりますが、C++23以降(P2231R1実装後)は定数実行可能となるためエラーになりません。

constexpr void h(std::optional<int>& o) {
  o.reset();  // C++20まで非constexpr
}

現在の仕様では、constexpr関数がそのあらゆる引数について定数実行可能ではない場合コンパイルエラー(もしくは診断不要のill-formed)とする事が求められています。そのため、constexpr関数内で非constexpr関数を呼び出す場合は常にコンパイルエラーとなります。

これを適応的にコンパイルするため、機能テストマクロを使用して次のようにすることができます

#if __cpp_lib_optional >= 202106
constexpr
#endif
void h(std::optional<int>& o) {
    o.reset();
}

しかし、この方法は適切でしょうか?

std::optionalのこの例に見えるように、ある関数がある時点でconstexprではなかったとしても将来的にconstexprになる可能性があります。constexprのルールはC++のバージョンごとに拡張されているため、それまでconstexpr対応できなかった処理でも、あるバージョンからはconstexpr化することができるようになり得ます。しかし、標準ライブラリのサポートはそれが可能になったタイミングから遅れてそれに対応し、在野のライブラリはさらに遅れるのが常です。またこのような機能テストマクロが非標準のものについて提供されることはほぼなく、在野のライブラリを使用している場合、このような対応は手作業で行う必要があります。

このような診断は定数式で実行可能なものが大きく制限されていたC++11の時代には有用だったのかもしれませんが、より多くのものが定数式で実行可能となった現在(C++23)の環境には即しておらず、constexprでの実行可否の診断は実際に定数式で実行する時にまで遅延しておく方が良いでしょう。この提案は、そのような規定を取り除こうとするものです。

P2451R0 2021 September Library Evolution Poll Outcomes

2021年の秋に行われた、LEWGの全体投票の結果。

以下の5つの提案が投票にかけられ、LWGに転送されることが可決されています。また、その際に寄せられたコメントが記載されています。

P2460R0 Relax requirements on wchar_t to match existing practices

wchar_tエンコーディングについての実態になじまない制約を取り除く提案。

現在のC++標準におけるwchar_tエンコーディングに関する規定は、ワイド文字エンコーディングのすべての文字を単一のコード単位としてエンコーディングする必要がある(すなわち、すべての文字がwchar_t1つに収まらなければならない)ことを要求しています。例えばWindowsではそれはUTF-16であり、UTF-16サロゲートペアを持つためその要件に違反していることになります。

さらに、wchar_tの値はサポートされているロケールで指定された拡張文字セットの個別の文字を表現できることも要求されているため、実行字文字集号がUTF-8の場合はすべてのユニコード文字をwchar_t1つで表現可能であることが求められ、実質的に2バイトエンコーディングを排除しています。

これらのことは実際の実装(主にWindows)に沿っておらず、C++を多様なOS上での開発に適さないものにしてしまっています。そのため、実装の慣行に従う形でこれを修正するのがこの提案の目的です。

この提案による変更は実装ですでに行われていることを標準化するだけなので、実装及びプログラマに影響はありません。

P2461R0 Closure-based Syntax for Contracts

属性likeな構文に代わるコントラクト構文の提案。

提案されている構文はpre, post, assertの3つの文脈依存キーワードに続いて、ラムダ式(に近い構文)によって契約条件を記述します。pre/postは後置戻り値型の後ろのrequires節と同じ位置に書き、assertは関数本体内に書きます。

auto plus(auto const x, auto const y) -> decltype(x + y)
  pre { x > 0 }
  pre {
    // オーバーフローのチェック
    // ここに書けるものは条件式だけなので、&&で結合する
    (x > 0 && y > 0 ? as_unsigned(x) + as_unsigned(y) > as_unsigned(x) : true) &&
    (x < 0 && y < 0 ? as_unsigned(x) + as_unsigned(y) < as_unsigned(x) : true)
  }
  // retはauto&&と宣言したかのような引数、戻り値が渡される
  post (ret) { ret == (x + y) }
{
  assert { x > 0 }; // この構文は現在有効(assert構造体の集成体初期化)
  auto cx = x;
  return cx += y;
}

ただし、この提案は純粋に構文のみを提案するもので、P2388の意味論をほぼ踏襲しており、P2388およびMVP(Minimum Viable Product)と呼ばれるコントラクトの最小セットに異を唱えるものではありません。この提案は、P2388で示されているいくつかの問題を解決可能な構文を探索し、提案するものです。

構文定義は次のようになっています

correctness-specifier:
    correctness-specifier-keyword correctness-specifier-introducer(opt) correctness-specifier-body

correctess-specifier-keyword:
    pre | post | assert

correctness-specifier-introducer:
    lambda-introducer(opt) return-value-id(opt)

return-value-decl:
    ( identifier )

correctness-specifier-body:
    { conditional_expression }

pre, post, assertの後にはラムダ式(のような)構文を置くことができて、ラムダキャプチャも機能します。省略された場合は[&]から始まったものとみなされています。ただし、現在合意が取れているMVPと呼ばれるコントラクトの最小セットに従う場合、ラムダ導入子は常に省略されなければなりません。 correctess-specifier-keywordに続く{}の中には条件式のみを書くことができて、これはrequires節と同じです。

ラムダ導入子を許可しなかったとしても、この構文ではP2388で導入される未定義動作を減らすことができるようです。

ラムダ導入子を許可すると、コピーキャプチャによってP2388に存在する事後条件で非const引数を参照できないという問題を解決することができます。これは事後条件から参照される実引数がタイミングによって変化しうることで条件の意味が変わってしまうことを防止するために現在(MVP)では禁止されています。

P2463R0 Slides for P2444r0 The Asio asynchronous model

P2444の解説スライド。

ASIOの非同期モデルとExecutor、非同期処理グラフの構成などが作者自らによって解説されています。

P2464R0 Ruminations on networking and executors

P0443のExecutorを使用するNetworking TSを標準化すべきではないという提案。

この提案で問題とされているのはNetworking TSの完了ハンドラがExecutorそのものがエラーを起こさないと仮定している事、およびP0443のexecutorにエラーや処理の成否を通知するチャネルが無いこと、Networking TSのExecutorと完了ハンドラによる非同期モデルはsender/recieverと比較するとジェネリックでも構成可能でも無いことです。

Networking TS(ASIO)の非同期処理ではI/O処理の継続作業を完了ハンドラで行います。完了ハンドラではAssociated Executorを取得し利用することでそのI/O処理や完了ハンドラの実行されているコンテキストに継続作業を投入することができます。このAssociated ExecutorはP0443のexecutorではありますがschedulerではなく、処理のスケジューリングに伴うエラーや、先行する処理の失敗を継続処理に通知する方法がありません。そして、Associated Schedulerのようなものを取得するAPIはなく、非同期処理グラブの構成もその場しのぎ的な書き方しかできません。また、実行コンテキストをAssociated Executor以外のExecutorのコンテキストに変更する方法もありません。

Executorライブラリの導入後、幾つものExecutorが作成されることになり、そこではExecutorの表現力不足に伴う問題を解決する事を繰り返し行う必要が出てきます。Executorはワークランナーの抽象化でしかなく、汎用的なエラー処理/検出の方法を提供するものではありません。従って、Networking TSのおよびP0443のexecutorとは、schedulerである必要があります。schedulerexecutorと同等の能力を持ちながら、recieverによる成否とスケジュールエラーの通知チャネル、senderによる非同期処理グラフのジェネリックな構成をサポートしています。

その場合、Networking TSの非同期処理は完了ハンドラの代わりにsenderを受け取るようになるでしょう。senderはあらかじめ構成された非同期処理のグラフを示す非同期エージェントであり、Networking TSから提供されるschedulerをそのまま使用することも、別のschedulerに実行コンテキストをスイッチすることもできます。さらに、senderによる構成では、前の処理およびスケジューリング段階でエラーが発生している場合後続の処理は実行されず、そのエラーをハンドリングする方法が提供されます。そして、それらの処理はジェネリックに構成することができ、特定のschedulerおよび特定の継続操作に依存せず、完了ハンドラのようにアドホック的ではありません。

// 擬似コードによる非同期I/Oと継続の例
void run_that_io_operation(scheduler auto sched,
                           sender_of<network_buffer> auto wrapping_continuation)
{
  // 特定のI/O操作
  launch_our_io();
  // I/O結果の取得
  auto [buffer, status] = fetch_io_results_somehow(maybe_asynchronously);
  // ユーザーの指定した継続を、ユーザーの指定したschedulerで実行する
  run_on(sched, queue_onto(wrapping_continuation,
                           make_it_queueable(buffer, status)));
}

ここでは、schedulerの具体的な型もsenderwrapping_continuation)の具体的な型も知る必要はありません。この関数の作者が詳しく知る必要があることはI/O操作に関することのみです。継続ラッパ(wrapping_continuation)が何をするかも知る必要がなく、それおよびそれがどこで実行されるのかを決めるのはこの関数を利用するユーザーです。

ユーザーはsenderの先に自由に継続を構成でき、そのユーザーがラップした処理を使用する別のユーザーもまた同様の方法によって継続をカスタマイズすることができます。この関数の作者は上位のユーザーが何をしたのかを知ることなくI/O処理を実行し、schedulersenderを投入でき、上位ユーザーはI/Oが何をしたのか継続をどう実行したのかを知ることなく、その結果から変換された指定した継続を実行することができます。

多数の実行戦略(executor/scheduler)とさらに多数の継続(完了ハンドラやsender)をサポートするために、Networking TSは多数のもの(追加の関数の変種)を必要としますがP2300(sender/reciever)では1つだけですみます。

P0443のexecutorはこれができません。executorはあらゆる種類の失敗やエラーを通知する方法を持ちません。そして、Networking TSでの入力実行戦略はexecutorのみです。

この提案が示しているのは次の2つの方向性です。

  1. Networking TSのExecutorモデルをP0443からP2300へ変更する
  2. C++23 Networking TSの標準化よりも、sender/reciever(P2300)の開発に集中する
    • ネットワークライブラリはその完成後にそれにフィットするものを標準化する

筆者の方は2つ目を推しているようです。

P2465R0 Standard Library Modules std and std.all

P2465R1 Standard Library Modules std and std.compat

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

これは以前のP2412R0を受けて、具体的にその内容を詰めるものです。

P2412R0のLEWGのレビューにおいて問題となった最大の事項は、import std;stdモジュール)がstd名前空間ではなくグローバル名前空間にあるものを含むかどうかという点でした。現在のC++標準では、<meow.h>::meowを確実に提供しstd::meowを提供するかは分かりません、<cmeow>::meowstd::meowの両方を確実に提供します。これは説明も対処も厄介な問題であり、またグローバル名前空間の暗黙の汚染をもたらします。標準ライブラリモジュールはこの問題を解決する唯一の機会だと言えます。

ただ、stdモジュールがグローバル名前空間にあるものを提供しない場合、既存のコードの標準モジュール対応が妨げられてしまいます。

そこでこの提案では、2つの標準ライブラリモジュールを提供することでユーザーに選択肢を与え、この問題に対処します。

  • import std;
    • C++標準ライブラリの全てをインポートする。グローバル名前空間を清潔に保ちたい場合
  • import std.compat;
    • std+Cライブラリヘッダの部分をインポートする。C由来のものがグローバル名前空間にインポートされる。既存コードベースの移行時など
    • import std;よりも重くなる可能性があるが、C互換ヘッダはサイズが小さいためそこまで問題にならないはず

stdstd.compatの違いはグローバル名前空間に何かをインポートするかどうかだけです。stdモジュール使用時、グローバル名前空間は清浄に保たれます。例えば、<cmath>std名前空間にもグローバル名前空間にもほぼ同じオーバーロードが定義されますが、import std;の場合はstd::~だけが使用可能となり、import std.compat;の時はそれに加えて::~のものも使用可能になります。

この提案は現在の内容でLWGに転送するために、次のLEWG全体投票にかけられます。

P2466R0 The notes on contract annotations

C++契約プログラミングのビジョンと設計目標について解説した文書。

関数コントラクト(function contract)という概念とコントラクト注釈(contract annotation)の関係を説明し、コントラクト注釈に求められることや役割について説明されています。また、事後条件で関数の非参照const引数を使用する場合の問題点と解決策の考察が行われています。

P2468R0 The Equality Operator You Are Looking For

==から導出される!=演算子とユーザー定義!=演算子が衝突してしまう問題を解消する提案。

次のコードはコンパイラによって扱いが変化します。

struct S {
  bool operator==(const S&) { return true; }  // (1) 非constメンバ関数
  bool operator!=(const S&) { return false; } // (2) 非constメンバ関数
};

int main() {
  bool b = S{} != S{};  // #1
}

#1の箇所ではユーザー定義!=(2)と(1)から導出された!=の2つが候補として上がります。Sの右辺値との比較でベストマッチものはないのでSの値への変換がどちらでも行われ、メンバ関数const修飾がなされていないことによってどちらの候補も最適とみなされ、その扱いがコンパイラによって変化しています。

  • GCC : (1)を使用する(警告なし)
    • 変換シーケンスの前に、パラメータ型が一致する複数の候補がある場合に生成された候補を無視する
  • clang : (1)を使用する(警告あり)
  • MSVC : (1)を使用する(警告なし)
    • 生成された候補を削除する

godbolt

現在の比較演算子a @ b)の自動導出に伴うオーバーロード解決のルールでは、@そのもの->生成された正順の式(a @' b)->生成された逆順の式(b @' a)の順で優先順位が高くなるというルールがあるのですが、このケース(@!=)では変換なしでベストマッチする!=が存在しないため、定義されているものと生成されたものとで優先順位が同等になってしまっているようです。

コンパイラから見ると先ほどの例は、次のようなコードと同じことをしています

struct S {
  friend bool f(S, const S&) { ... }
  friend bool f(const S&, S) { ... }
};
bool b = f(S{}, S{});

この提案では、現在の厳格なルールをMSVCにて実装(出荷されているMSVCの実装とは異なる)し、59のコードベースの調査を行なっています。その結果、20のプロジェクトでこの問題によるコンパイルエラーが観測されました。

template <typename T>
struct Base {
  bool operator==(const T&) const;
};

struct Derived : Base<Derived> { };

bool b = Derived{} == Derived{};

このケースでは、派生クラスから基底クラスへの変換が発生し、同様の問題に陥っています。これは受け入れられるべきです。

template <bool>
struct GenericIterator {
  using ConstIterator = GenericIterator<true>;
  using NonConstIterator = GenericIterator<false>;

  GenericIterator() = default;
  GenericIterator(const NonConstIterator&); // (1)

  bool operator==(ConstIterator) const;
};
using Iterator = GenericIterator<false>;

bool b = Iterator{} == Iterator{};

このケースは、IteratorGenericIterator<false>)の比較を暗黙変換コンストラクタ(1)を通すことによって、ConstIteratorGenericIterator<true>)の比較として実行しています。暗黙変換が行われることによって同様の問題が起きています。これは意図的であるので受け入れられるべきです。

struct Iterator {
  Iterator();
  Iterator(int*);

  bool operator==(const Iterator&) const; // (1)
  operator int*() const;  // (2)
};

bool b = nullptr != Iterator{};

このケースは、(2)によってIteratorからポインタへ暗黙変換することで、!=を提供しようとしています。暗黙変換が行われることによって同様の問題が起きています。しかしこの場合、逆順の==使用時にも暗黙変換によって組み込み==が使用されてしまい、より最適なはずのnullptrからIteratorを構築して==(1)が使用されないという問題があります。このケースはリジェクトされるべきです。

using ubool = unsigned char;

struct S {
  operator bool() const;
};
ubool operator==(S, S); // (1)

ubool b = S{} != S{};

これは厳密には先程までの問題と関係がありません。この例がエラーとなるのは、(1)の==の戻り値型がboolではないからです。!=の導出に使用される==の戻り値型は(cv)boolでなければならず、そうでない場合オーバーロード解決で選ばれるとコンパイルエラーとなります。

GCCの実装では、オーバーロード解決において変換を考慮する前に、候補集合の中に同じパラメータ型を持つ候補があり片方が生成された候補である場合に、その生成された候補を以降のオーバーロード解決で無視することで以前の振る舞いを維持しようとしています。このGCC実装をMSVCにも実装して再度テストしたところ、コンパイルに失敗したのは10プロジェクト(-10)となりました(上記の受け入れたい/リジェクトしたいケースは意図通りとなっている)。

この提案では、GCCの実装をベースとして実装の自由度も考慮して、「生成された演算子候補について、同じスコープで宣言された同じシグネチャを持つ別の候補が存在し、その候補が生成されていない(ユーザー定義である)候補である場合、生成された演算子オーバーロード候補から除外される」、というルールをオーバーロード解決のルールに追加することを提案しています。それによって、最初の例のようなコードは曖昧ではなくなり、上記のアクセプトしたい既存コード例はコンパイルされリジェクトしたいコード例はコンパイルされない、という振る舞いを手に入れることができます。

P2469R0 Response to P2464: The Networking TS is baked, P2300 Sender/Receiver is not.

P2464R0 Ruminations on networking and executors」の主張に反論する文書。

ASIO(NetworkingTS、以下ASIOとは=NetworkingTSも意味する)の定義する非同期モデルにおけるExecutorとは、非同期処理のtail callのカスタマイゼーションポイントであるという前提を説明し、P2300の抽象化はschedulerの具体的実装を知っていなければ一般的なアルゴリズムを構成できず、P2300は非同期モデルを定義するものではなく非同期処理グラフを作成するためのDSLに過ぎず、ASIO/NetworkingTSの非同期モデルはP2300のDSLを実装することができるより基礎的なモデルであると主張しています。

P2470R0 Slides for presentation of P2300R2: std::execution (sender/receiver)

P2300の紹介・解説スライド。

P2300のschedulersender/recieverによるアルゴリズムの紹介とそれによる非同期処理グラフの構成、thenアルゴリズムの実装解説、コルーチンとsenderの親和、senderにおけるキャンセルなどについて解説されています。

また、P2300の実装(libunifex)がFacebookにて広く実運用されているほか、NVIDIAがその実現にコミットしていくことや、Bloomberg社内でも好印象を得ていることが紹介されています。

P2471R0 NetTS, ASIO and Sender Library Design Comparison

P2471R1 NetTS, ASIO and Sender Library Design Comparison

ASIOとNetworkingTSとP2300(P0443)の設計を比較する文書。

それぞれのライブラリのモデルや要求することなどをコンセプトによって表現し、表にまとめて比較しています。

P2472R0 make_function_ref: A More Functional function_ref

function_refに適応的に型消去させるためのヘルパ関数make_function_ref()を追加する提案。

function_refは関数呼び出し可能なものに対するviewであり、主として関数引数で任意のCallableなものを受け取るときに使用します。そこでは、テンプレートによる受け取りとstd::functionによる受け取りのいいとこ取りのような性質を利用できます。

// テンプレートによる受け取り
// 関数のシグネチャなどの情報がなく、追加の制約が必要になる
// 可読性が低下し、複雑さが増大する
template<typeanme F>
void f(F&& f);

// std::functionによる受け取り
// 所有権が不明(`std::reference_wrapper`を保持しているかもしれない)、空かもしれない
// 型消去のための追加のオーバーヘッド(動的確保など)が必要となる
void f(std::function<bool(int)> f);

// function_refによる受け取り
void f(function_ref<bool(int)> f);

function_refを使用することによって、ステートフルなラムダを含めたあらゆるCallableを受け取れるようにしながら、求められている関数の情報が最小の形で引数型に表示され、追加のオーバーヘッドを回避できます。そして、function_refviewであり常に所有権を持ちません。

function_refは次のように実装されます。

template<typename F>
class function_ref;

template<typename R, typename... Args>
class function_ref<R(Args...)> {
  void* erased_object;
  R(*erased_function)(Args...);

public:

  template<typename F>
  function_ref(F&&);

  // ...
};

function_refはメンバとして2つのポインタを持ち、erased_objectは渡された関数のオブジェクトそのもの(ファンクタなど)のポインタを、erased_functionはその関数呼び出し演算子のポインタを保持します。

function_refはファンクタ/ステートフルラムダ、関数ポインタ/ステートレスラムダ、メンバ関数ポインタ、の3種類のCallableを保持することになりますが、2つのメンバがフルに活用されるのはファンクタ/ステートフルラムダの時だけで、それ以外のケースではerased_objectは有効利用されていません。メンバ関数ポインタに対するthisやフリー関数の最初の引数などを束縛した上でfunction_refに渡すにはラムダ式などでラップする必要がありますが、erased_objectを有効利用すればその必要なく一部の引数を型消去しながら束縛することができそうです。

このような単一関数に対する実行時ポリモルフィズムは、C#のデリゲートなどOOPの世界でよく見られ、C++においてもコミュニティから類似のもののアイデアがいくつか提示されています。それらは共通して、function_refの2つのメンバを直接指定して初期化するコンストラクタによって実装可能です。

この提案はその実現のため、function_refにその2つのポインタメンバを直接初期化可能なコンストラクタを追加した上で、その初期化をサポートするmake_function_ref()ヘルパ関数を追加するものです。

template<typename F>
class function_ref;

template<typename R, typename... Args>
class function_ref<R(Args...)> {
  void* erased_object;
  R(*erased_function)(void*, Args...);

public:

  template<typename F>
  function_ref(F&&);

  // 追加するコンストラクタ
  function_ref(void* obj, R(*func)(void*, Args...))
    : erased_object{obj}
    , erased_function{func}
  {}

  // ...
};


template<auto mf, typename T>
  requires std::is_member_function_pointer<decltype(mf)>::value
auto make_function_ref(const T& obj) {
  return function_ref(std::addressof(obj), mf);
}

template<auto mf>
  requires std::is_member_function_pointer<decltype(mf)>::value
auto make_function_ref() {
  return function_ref(mf);
}

template<auto f>
  requires is_function_pointer<decltype(f)>::value
auto make_function_ref() {
  return function_ref(f);
}

// 他多数のオーバーロード

make_function_refはNTTPとして(メンバ)関数ポインタを、関数引数で束縛対象のオブジェクトを受け取り、それらを指定してfunction_refの2つのメンバを初期化します。例えば次のように利用します

struct bar {
  void baz(bool b, int i, float f) {
      std::cout << "bar::baz" << std::endl;
  }

  void baznoe(bool b, int i, float f) noexcept {
      std::cout << "bar::baznoe" << std::endl;
  }
};

void third_party_lib_function1(tl::function_ref<void(bool, int, float)> callback) {
  callback(true, 11, 3.1459f);
}

void application_function_ref(function_ref<void(bar*, bool, int, float)> callback) {
  bar b;
  callback(b, true, 11, 3.1459f);
}

void free_baz1(bool b, int i, float f) {
  std::cout << "free_baz1" << std::endl;
}

int main() {
  bar b;

  // ステートフルラムダ(make_function_refがない場合の渡し方)
  third_party_lib_function1([&b](bool b1, int i, float f){ b.baz(b1, i, f); });
  // メンバ関数ポインタとthis(上記と同等な渡し方)
  third_party_lib_function1(make_function_ref<&bar::baz>(b));
  // 関数ポインタのみ
  third_party_lib_function1(make_function_ref<free_baz1>());

  // メンバ関数ポインタのみ(thisは後から渡す)
  application_function_ref(make_function_ref<&bar::baz>());
  application_function_ref(make_function_ref<&bar::baznoe>());
}

P2473R0 Distributing C++ Module Libraries

C++モジュールのビルド済み成果物の配布を容易にするための、ビルドツール・コンパイラ・静的解析ツール間の相互運用性のための共通フォーマットの提案。

この提案はP2409R0をベースとして、そこで挙げられている要件に沿うような規則を定めようとするものです。

パッケージマネージャを使用してビルド済みのライブラリを配布する環境では、ライブラリのコンテンツはディスク上のファイルとして表されているはずです。この提案はC++モジュールをそれらのビルド済みファイルとして配布できるようにするための規則を定めることを目的としています。

この提案の目標は以下の3点で、それ以外の点は目的としていません

  1. モジュールの発見可能性
    • ビルドシステムとパッケージマネージャ間で決定されたモジュール検索パスが与えられた時、それを用いてシステムで利用可能なモジュールを見つけるための規則を定める
  2. モジュールインターフェースのパース手順
    • C++モジュールでは、モジュールの解析とそれを使用する翻訳単位の区別が導入されていること、中間のバイナリモジュールインターフェースファイルが相互運用不可能であることから、この提案ではモジュールインターフェースソースファイルのパース方法を支持するメカニズムを定義する
  3. 依存関係の検出を最適化する規則
    • モジュールの依存関係グラフ構築の際に、プリプロセッサの実行を回避することは望ましい最適化の一つであるが必須ではない。この提案は、その選択をした実装者のための規約を定める

提案している規則は以下のようなものです。

モジュール名はファイル名と対応する

import宣言で使用されるモジュール名は次の規則によってファイル名に変換される。

モジュール名 検索ルートからの相対パス
foo foo.ixx
foo.bar foo/bar.ixx
foo.bar:baz foo/bar.part/baz.ixx

これは、モジュールのインストールプロセスがユーザーに変わって翻訳することを想定している。これにより、ビルドシステムの外部からモジュールを消費(importに対応した読み込み)する際に決定論的な探索が可能となる。

モジュールを消費する手順

モジュールを消費する手順はモジュールインターフェースファイルとともに(同じディレクトリに)配置され、拡張子は.meta-ixx-info

このファイルは次のキーを持つJSONオブジェクトとしてエンコードされる

  • include_path : インターフェース単位ファイルのプリプロセス時に必要な#include対象ファイルのリスト。標準ヘッダ類は含まれない。
  • definitions : 事前定義マクロのキーと値のペア。
  • imports : 対応するモジュールインターフェース単位によってインポートされているモジュール名のリスト。これは、ビルドシステムがモジュール単位を解析して依存関係を把握するのを回避するためのオプションのフィールド。
  • _VENDOR_extension : ベンダーはビルドシステムで使用される可能性のあるメタデータ拡張機能を指定するためにこのフィールドを使用できる

この規則の前提として、プリプロセス時に適用されるオプション以外のオプションは、モジュールを消費する翻訳単位とモジュールインターフェース単位のパースの間で一律に適用されなければならない。それは例えば、言語バージョンを指定するフラグやABI互換に影響するフラグなど。

また、モジュールを消費する翻訳単位からのインクルードパスとマクロ定義はインポートされるモジュールを消費する際に使用しないことも想定している。

バイナリモジュールインターフェースファイルの配布

バイナリモジュールインターフェース(BMI)の相互運用可能な範囲は非常に限られているが、コードがほぼ同じコンパイラによって生成される環境では重要な最適化となる。例えば、ほとんどのGNU/Linuxディストリビューションが該当する。ただし、BMIファイルがあるコンパイラに適用可能かどうかが識別可能である必要がある。したがって、このルールではコンパイラはサポートする互換性と同じくらい一意の識別子を提供する必要がある。

その識別子は、.bmi.vendor.idのようなパターンで用意する。例えば、g++がUUID-v4を使用してBMIファイルを再利用可能であることを表明するには、foo.barモジュールの場合ファイル名は「foo/bar.bmi.g++.20734238-4fc7-4725-bf22-be9700326774」になる。

この規則では、このような識別子のうち、コンパイラは1つの入力形式だけをサポートすることを想定している。コンパイラはファイル名の有効な拡張子となる任意の識別子を使用でき、複数の識別子パターンをサポート可能だが、ここでそれを規定すると探索の複雑さが増大する。

検索順序

モジュールの探索時は、最初にコンパイラ固有のBMI(標準ライブラリのものなど)を探索してから2回目の探索で.ixxファイルと.meta-ixx-infoファイルを探索できるはず。これにより、異なるビルドシステム間で使用するためにBMIファイルをキャッシュするローカルオーバーレイツールを作成できる。

BMIファイルの存在を最適化しない

ビルドシステムが現在のビルドと互換性のあるBMIファイルの存在を確認した時、依存関係の探索をやめて直接それを消費することが有効な最適化だが、そうすると、ビルドシステムは連携するツールからモジュールのインターフェースのパースを再現する方法の詳細を隠蔽してしまう。したがって、相互運用性を維持したいビルドシステムはモジュールインターフェースをパースする必要がないと判断した場合でもモジュールインターフェースのパースを継続して、その情報を現在のコンパイルデータベースのように利用する必要がある。

Discovery tooling

ライブラリとして提供されるモジュールの推移的な関係を解決するために、全ての必要なモジュールを解析するものが必要となる。そのために、c++-modules-configと呼ばれるツールを提案する。

このツールはモジュールの探索パス、BMIファイルの識別子、解決が必要なモジュール名のリストを入力とし、ビルドに関係するモジュールの完全なリスト、インターフェースのソース、インターフェースをパースするのに必要な手順、およびモジュールの依存関係を出力する。このツールは再帰的な探索を行い、ビルドシステムが消費する必要のあるシステムによって提供される全てのモジュールのパースを計画するのに十分な記述を返す。これにより、利用可能だが消費される予定のないモジュールは除外される。

このツールの出力はP1689R4のフォーマットと互換性を持たせる必要があり、またこの出力はmodule_config.jsonという名前で、現在compile_commands.jsonが保存されている場所と同じ場所に保存される必要がある。

P2475R0 WG21 2021-10 Virtual Meeting Record of Discussion

2021年10月4日(北米時間)に行われた、WG21全体会議のデイスカッション議事録。

CWG/LWG/LEWGの投票時のディスカッションの様子が記録されています。

P2477R0 Allow programmer to control and detect coroutine elision by static constexpr bool should_elide() and

コルーチンの動的メモリ確保を避ける最適化を制御し、起こったことを検出するAPIを追加する提案。

コルーチンはサスペンドと再開をサポートするためにその状態(コルーチンステータス)を保存しておく領域を必要とします。現在のC++コルーチンでは、この領域の確保は動的メモリ確保(operator new)によって行われ、それはコストがかかります。

それを軽減するために、HALOと呼ばれる動的メモリ確保を回避する最適化がデザインされており、これをcoroutine elisionと呼びます。clang/LLVMではそれはCoroElideとして実装されています。C++標準では名前付けされていないもののcoroutine elisionは許可されています(つまり必須ではない)。

これはコンパイラの最適化であり、必ず行われるわけではありません。そのため、コルーチンを利用するプログラマはこの最適化を制御することも、それが行われたことを察知することもできません。

また、組み込みのようにリソースが限られている環境ではcoroutine elisionを無効化したうえでpromise_type::operator newが静的ストレージのバイト配列からの領域を返すようにすることが好まれることがあるようです。しかし、coroutine elisionを無効化する方法もまた標準では提供されていません。

この提案では、coroutine elisionを制御するためのpromise_type::should_elide()coroutine elisionを検出するためのstd::coroutine_handle<>::elided()の2つの関数を追加することを提案しています。これによって、プログラマcoroutine elisionを制御し検出できるようになります。

should_elide()は次のようにcoroutine elisionの実行についてをコンパイラに指示します

  • promise_typeのスコープでshould_elide()という関数を見つけることができ、コンパイル時に評価した結果が
    • trueに評価された場合 : coroutine elisionが保証される
    • falseに評価された場合 : coroutine elisionが行われないことが保証される
  • should_elide()が見つからない、コンパイル時に評価できない場合
    • 従来通り(coroutine elisionコンパイラの裁量で行われる)

should_elide()promise_typeメンバ関数として実装されるもので、promise_typeはコルーチンの動作をカスタムするカスタマイゼーションポイントとしてユーザーが定義します。したがって、should_elide()もまた必要であればユーザーが定義するものであり、それはconstexpr関数として定義しなければなりません。

elided()std::coroutine_handleメンバ関数として追加され、coroutine elisionが行われていた時にtrueを返し、行われていない場合にfalseを返します。

namespace std {
  template<>
  struct coroutine_handle<void> { 
    // ...

    bool elided() const noexcept; // 追加
    
    // ...
  };

  template<class Promise>
  struct coroutine_handle<Promise> {
    // ...
    
    bool elided() const noexcept; // 追加
    
    // ...
  };
}

P2478R0 _Thread_local for better C++ interoperability with C

C++thread_local変数は動的初期化とデストラクタ呼び出しを伴うことができます。その初期化は、スレッド起動時に非ブロック(名前空間スコープ)変数に対して行われるか、最初に変数が使用されるときに行われる可能性があります。その主な実装は、変数が使用される場所に関数を挿入し、その関数が呼ばれることで非ブロック変数の遅延初期化を実行します。

そのような関数とその呼び出しによって実行時にサイズと時間のオーバーヘッドがかかります。これは変数が動的初期化もデストラクタ呼び出しも必要ないことが分かっていれば回避可能です。

このことは、単にオーバーヘッドがかかるだけではなく、Cの_Thread_localとの非互換を生み出しています。Cの_Thread_localC++thread_localとしての使用に必要なもの(初期化・破棄に必要な追加の関数など)を提供しません。このため、C_Thread_local変数をC++コードから参照しようとするとリンクエラーを起こすことがあります。

また、C++で定義された動的初期化されるか非トリビアルなデストラクタをもつ静的変数(非スレッドローカル)にCからアクセスすることは、C++から同じものにアクセスするのと比べて危険ではありません。なぜなら、主要な実装はプログラム起動時にそれらの初期化を行うためです。しかし、同様に宣言されているスレッドローカルな変数にアクセスした場合は同じことは言えません。Cからのアクセスは初期化前の変数を観測する可能性が高くなります。

この提案はC23に対して、_Thread_localthread_localキーワードで置き換えるのをやめるように提案するものです。上記のような問題があるため、安易にキーワードをC++と共通化してしまうことは危険です。一方、C++が上記問題の回避を行いたい場合にCと同様のセマンティクスで_Thread_localを使用する方向性を示しています。

P2479R0 Slides for P2464

P2464R0 Ruminations on networking and executorsの紹介スライド。

後半ではP2469に対する反論も行われています。

P2480R0 Response to P2471: "NetTS, Asio, and Sender library design comparison" - corrected and expanded

P2471R1の訂正や追記を行う文書。

P2471R1はASIO/NetrworkingTSとP2300との比較を行っている文書ですが、その内容について不正確な部分を修正したり追記したりする文書です。

P2481R0 Forwarding reference to specific type/template

テンプレートパラメータ指定時にconstと値カテゴリを推論可能にする構文の必要性について説明した文書。

std::tuple<Ts...>にはstd::tuple<Us...>からの変換コンストラクタが存在します。それは、Us...のそれぞれが対応するTs...のそれぞれに変換可能であれば利用可能となりますが、std::tuple<Us...>の状態によって4つの定義を必要とします。

template <typename... Ts>
struct tuple {
  template <typename... Us>
      requires sizeof...(Ts) == sizeof...(Us)
            && (is_constructible_v<Ts, Us&> && ...)
  tuple(tuple<Us...>&);

  template <typename... Us>
      requires sizeof...(Ts) == sizeof...(Us)
            && (is_constructible_v<Ts, Us const&> && ...)
  tuple(tuple<Us...> const&); // (1)

  template <typename... Us>
      requires sizeof...(Ts) == sizeof...(Us)
            && (is_constructible_v<Ts, Us&&> && ...)
  tuple(tuple<Us...>&&);      // (2)

  template <typename... Us>
      requires sizeof...(Ts) == sizeof...(Us)
            && (is_constructible_v<Ts, Us const&&> && ...)
  tuple(tuple<Us...> const&&);
};

これは簡易な例でしかないので正確ではありません。重要なことは、tuple<Us...>constの有無と値カテゴリの組み合わせで2x2=4つのコンストラクオーバーロードが必要となるうえ、制約もそれに応じて微妙に細部を調整しなければなりません。

これはかなり冗長なコードであるうえ、これらがオーバーロードであることから、ある適切なコンストラクタがその制約によって適切に候補から外れた時でも、別のコンストラクタが呼ばれることがあります。

void f(tuple<int&>);
auto g() -> tuple<int&&>;

void h() {
  f(g()); // ok!
}

これは、tuple<int&&> -> tuple<int&>の構築を行っているもので、int&& -> int&のような変換は不可能なので失敗してほしいし失敗するはずです。しかし実際には、(2)のコンストラクタが制約を満たさないために除外された後、(1)のコンストラクタが利用されます。g()の戻り値は右辺値であり、(1)のコンストラクタのtuple<Us...> const&でも受けることができて、その制約中のUs const&int&& const & -> int&となってint& -> int&の変換が可能であることから(1)のコンストラクタを呼び出してしまいます。

この問題を回避するためには、コンストラクタテンプレートを1つだけにする必要があります。必要なことは、tuple<Us...>を指定しながら、CV・参照修飾を推論してもらうことです。現在のところそれはできず、上記のように書くかテンプレートをこね回す必要があります。

template <typename... Ts>
struct tuple {
  template <typename Other>
      requires is_specialization_of<remove_cvref_t<Other>, tuple>
            && /* ???? */
  tuple(Other&& rhs);
};

しかし、このコンストラクタテンプレートの制約を正しく書く事は困難で、いい解決策はありません。結局、最低4つの似たようなオーバーロードを用意するのが一番簡易となります。

これと同じ問題は、std::getでも見ることができます。

template <size_t I, typename... Ts>
auto get(tuple<Ts...>&) -> tuple_element_t<I, tuple<Ts...>>&;

template <size_t I, typename... Ts>
auto get(tuple<Ts...> const&) -> tuple_element_t<I, tuple<Ts...>> const&;

template <size_t I, typename... Ts>
auto get(tuple<Ts...>&&) -> tuple_element_t<I, tuple<Ts...>>&&;

template <size_t I, typename... Ts>
auto get(tuple<Ts...> const&&) -> tuple_element_t<I, tuple<Ts...>> const&&;

これも一つの関数テンプレートにまとめることで記述を削減でき先ほどのような問題を回避できますが、やはりその制約を正しく書くことが困難で、std:tupleの派生型に対して機能しなくなってしまいます。

std::optional::transformのようなメンバ関数deducing thisを組み合わせた時にも、別の問題が浮かび上がります。

template <typename T>
struct optional {
  template <typename F>
  constexpr auto transform(F&&) &;

  template <typename F>
  constexpr auto transform(F&&) const&;

  template <typename F>
  constexpr auto transform(F&&) &&;

  template <typename F>
  constexpr auto transform(F&&) const&&;
};

このような冗長なコードは、deducing thisによって次のようにひとまとめにできます。

template <typename T>
struct optional {
  template <typename Self, typename F>
  constexpr auto transform(this Self&&, F&&);
};

しかし、今度はこのSelfの対象とする範囲が広すぎます。ここではstd::optionalの派生型を対象としたくはなく、やってほしいことはCV・参照修飾の推論だけです。

これらの問題は、テンプレートにおいてある程度型を指定しながらでもCV・参照修飾の推論が可能となってくれれば解決することができます。この文書では、いくつかの構文の候補を提示するとともにより良いアイデアを募っています(まだ提案には至っていません)。

以下の例では次のようなユーティリティを使用しています

#define FWD(e) static_cast<decltype(e)&&>(e)

template <bool RV, typename T>
using apply_ref = std::conditional_t<RV, T&&, T&>;

template <bool C, typename T>
using apply_const = std::conditional_t<C, T const, T>;

template <bool C, bool RV, typename T>
using apply_const_ref = apply_ref<RV, apply_const<C, T>>;

template <typename T, typename U>
using copy_cvref_t = apply_const_ref<
  is_const_v<remove_reference_t<T>>,
  !is_lvalue_reference_v<T>,
  U>;

T auto&&

これはコンセプトによるプレースホルダ型の制約(range auto&& r)を発展させて、コンセプトを指定する部分により具体的な型を指定できるようにしたものです

template <typename... Ts>
struct tuple {
  template <typename... Us>
  tuple(tuple<Us...> auto&& rhs)
    requires sizeof...(Us) == sizeof...(Ts)
          && (constructible_from<
                Ts,
                copy_cvref_t<decltype(rhs), Us>
              > && ...);
};
template <typename T>
struct optional {
  template <typename F>
  auto transform(this optional auto&& self, F&& f) {
      using U = remove_cv_t<invoke_result_t<F,
          decltype(FWD(self).value())>;

      if (self) {
        return optional<U>(invoke(FWD(f), FWD(self).value()));
      } else {
        return optional<U>();
      }
  }
};
  • 利点
    • 簡潔にやりたいことを実現・表現できる
  • 欠点
    • 正確な型を取得するにはdecltype(param)としなければならない
      • このため、requires節を関数の後ろにしか書けなくなる
    • concept autoの記法と矛盾する(この構文は変換を行わない)

T&&&

先ほどの、T auto&&の代わりにT&&&を導入するものです。(サンプルは省略)

  • 利点
    • tuple<Us...>&&&は派生型からの変換を行うことができる
  • 欠点
    • T auto&&と同じ
    • T&&&という参照トークンそのもの

const(bool)

noexcept(bool)explicit(bool)のように、const&&を条件付きにするものです。

template <typename... Ts>
struct tuple {
  template <typename... Us, bool C, bool RV>
    requires sizeof...(Us) == sizeof...(Ts)
          && (constructible_from<
                Ts,
                apply_const_ref<C, RV, Us>
              > && ...)
  tuple(tuple<Us...> const(C) &&(RV) rhs);
};
template <typename T>
struct optional {
  template <typename F, bool C, bool RV>
  auto transform(this optional const(C) &&(RV) self, F&& f) {
    using U = remove_cv_t<invoke_result_t<F,
        // apply_const_ref<C, RV, T>
        decltype(FWD(self).value())>;

    if (self) {
      return optional<U>(invoke(FWD(f), FWD(self).value()));
    } else {
      return optional<U>();
    }
  }
};
  • 利点
    • const性と参照修飾だけを指定していることが明確になる
    • requires節の配置自由度が保たれる
  • 欠点

Q修飾子

これは全く新しい形のテンプレートパラメータを導入するもので、これをqualifier(修飾子)と呼びます。これは、エイリアステンプレートを推論します

template <typename... Ts>
struct tuple {
  template <typename... Us, qualifiers Q>
    requires sizeof...(Us) == sizeof...(Ts)
          && (constructible_from<Ts, Q<Us>> && ...)
  tuple(Q<tuple<Us...>> rhs);
};
template <typename T>
struct optional {
  template <typename F, qualifiers Q>
  auto transform(this Q<optional> self, F&& f) {
    using U = remove_cv_t<invoke_result_t<F, Q<T>>>;

    if (self) {
      return optional<U>(invoke(FWD(f), FWD(self).value()));
    } else {
      return optional<U>();
    }
  }
};

ここでは、Q<T>QTはそれぞれ別々に推論されますが、Qは次のエイリアステンプレートのいずれかとして推論されます

template <typename T>
using Q = T&;

template <typename T>
using Q = T const&;

template <typename T>
using Q = T&&;

template <typename T>
using Q = T const&&;
  • 利点
    • 推論したCV・参照修飾を適用(利用)するのが容易
    • 正確な型の取得が容易
  • 欠点
    • 斬新な構文であり、奇妙
    • Qの名前が問題となる
    • constだけを推論したいときに、誤用の可能性がある

作者の方はこれらの解決策に納得してないらしく、より良い構文を募るためにこの文書を書いたようです。

おわり

[C++]`std::array`のエイリアステンプレートとCTAD

twitterに密かに置いていた質問箱に次のような質問をいただきました。

#include <array>

template<auto N>
using std_array_with_int = std::array<int,N>;

template<typename T>
using std_array_with_3 = std::array<T,3>;

int main() {
  [[maybe_unused]] std::array ar1 = { 1, 2, 3 };          // ok
  [[maybe_unused]] std_array_with_int ar2 = { 1, 2, 3 };  // ng、なして?
  [[maybe_unused]] std_array_with_3 ar3 = { 1, 2, 3 };    // ng、なして?
}

ようするに、std::arrayのテンプレートパラメータの一部だけを束縛したエイリアステンプレートでCTADを期待すると、謎のコンパイルエラーに悩まされています。

エイリアステンプレートのCTADが絡み非常に複雑な問題であり、簡易に回答できなさそうなのでこの記事を書きました。

CTADの仕組み(雑)

あるクラステンプレートCについてのCTADは、Cのコンストラクタを変換して得た関数テンプレート(引数はそのコンストラクタの引数、テンプレートパラメータはCのテンプレートパラメータにそのコンストラクタのテンプレートパラメータを合併したもの、戻り値型はそのコンストラクタのテンプレートパラメータのうちCのものに対応するパラメータで特殊化されたC<Ts...>)と、Cに定義された推論補助を関数テンプレートとして抽出し、初期化式(C = c{args...};)に与えられている実引数列(args...)を用いてそれらの関数テンプレートを呼び出したかのようにオーバーロード解決を行い、最適にマッチした1つの関数テンプレートの戻り値型からテンプレートパラメータが補われたC<Ts...>を取得するものです。

CTADはコンストラクタ呼び出しよりも前に行われ、必ずしも使用される推論補助(コンストラクタからの関数テンプレート)と実際に呼び出されるコンストラクタが対応していなくても良く、推論補助に関してはほぼ関数テンプレートのように書くことができます(コンセプトやSFINAEによる制約などを行えます)。

重要なことは、コンストラクタおよび推論補助を関数テンプレートとして扱って、実引数を用いてオーバーロード解決を行なって、残った1つの戻り値型から完全な型を取得するという手順です。CTADはクラス型・集成体型・エイリアステンプレートの3つに対して行われますが、バックエンド部(関数テンプレートとしてオーバーロード解決以降)はそれらで共通で、異なるのはその関数テンプレートをなんとか抽出するフロントエンド部分のプロセスです。

エイリアステンプレートのCTADの仕組み

エイリアステンプレートのCTADは、元の型の推論補助を変換してエイリアステンプレートの推論補助を導出し、それを利用することで行われます。そのアルゴリズムはおおよそ次のようになります

  1. エイリアステンプレートの元の型のコンストラクタと推論補助を(関数テンプレートとして)全て取得する
    • 以下の手順はそうして取得した関数テンプレート1つ1つに対して行われる(その1つをDとする)
  2. エイリアステンプレートの右辺とDの右辺(戻り値型)から、テンプレートパラメータの対応を推論する
    • この処理は、関数テンプレートのテンプレートパラメータ推論と同様に行われる(ただし、推論できないパラメータがある場合でも失敗しない)
    • 推論できないコンテキストが存在しうるため、この時に全てのテンプレートパラメータを推論する必要はない
  3. 2で得られた対応とエイリアステンプレートのテンプレートパラメータを、Dにフィードバックして置き換える
    • 2で全てのパラメータの推論が出来ていない可能性があるので、ここで得られた推論補助はエイリアステンプレートとDそれぞれからのテンプレートパラメータを含む可能性がある
  4. 結果の型(3で得られた推論補助の右辺)からエイリアスを推定し、3で得られた推論補助を書き換えることで、エイリアステンプレートに対する推論補助を生成する
    • 結果の型からエイリアスを推定するか、またどのように推定するかは実引数型に依存するため、追加の制約が必要となる場合がある

このような導出(あるいは推論補助の変換)は、エイリアステンプレートの元の型のコンストラクタと推論補助から抽出した関数テンプレート1つづつに対して行われ、そうして無事に導出できた(中には失敗するものがありうる)推論補助を、通常の推論補助と同じように扱ってエイリアステンプレートのCTADは行われます。

例1、単純な例

次のような型で動作例を見てみます。

// 今回の例の対象エイリアステンプレート
template<typename T>
using P = std::pair<int, T>;

このコードで、Pに対する推論補助がどのように導出されるかを先ほどの手順に沿って見てみます(以下、std::pairstdを省略します)。

2. エイリアステンプレートの右辺と元の推論補助の右辺から、テンプレートパラメータの対応を推論する

pairの推論補助は1つだけが用意されています(コンストラクタからのものは今回は無視します)

namespace std {

  // ...

  template<typename T1, typename T2>
  pair(T1, T2) -> pair<T1, T2>;
}

これとエイリアステンプレートP

template<typename T>
using P = pair<int, T>;

の右辺から、テンプレートパラメータのペア<T1, T2><int, T>から、T1 = int, T2 = T(とtypename T2 = typename T)の対応を得ます。

ここでは、pairの推論補助のテンプレートパラメータを持ち、その右辺の型(pair<T1, T2>)を引数型とする関数テンプレートに対して、エイリアステンプレートPの右辺の型(pair<int, T>)の値を引数として渡したときの関数テンプレートのテンプレートパラメータ推論とほぼ同じことを行なって、テンプレートパラメータの対応を得ます。

template<typename T1, typename T2>
void f(pair<T1, T2>);

f(pair<int, T>{});

このようなfT1, T2に推論される型を求めることで対応を得ます。この時、Tのように具体的な型ではないテンプレートパラメータに対しても推論を行い、テンプレートパラメータのまま対応させます。それによって、T1 = int, T2 = Tの対応が得られます。

3. 2で得られた対応とエイリアステンプレートのテンプレートパラメータを、元の推論補助にフィードバックして置き換える

pairの推論補助

template<typename T1, typename T2>
pair(T1, T2) -> pair<T1, T2>;

に、先ほど得た対応(T1 = int, T2 = T)とエイリアステンプレートのテンプレートパラメータをフィードバックします。

まずエイリアステンプレートのテンプレートパラメータをそのまま、推論補助のテンプレートパラメータリストの先頭にフィードバックすると

template<typename T, typename T1, typename T2>
pair(T1, T2) -> pair<T1, T2>;

となり、次にpairの推論補助とエイリアステンプレートPのテンプレートパラメータの対応(T1 = int, T2 = T)をフィードバックします。

template<typename T>
pair(int, T) -> pair<int, T>;

この時、対応が確定しているpairの推論補助のテンプレートパラメータについてはすでにテンプレートではないので(対応先が具体的な型ではなく、Pのテンプレートパラメータだったとしても)、テンプレートパラメータ宣言(typename T1, typename T2)は削除します。

4. 結果の型(2で得られた推論補助の右辺)からエイリアスを推定し、3で得られた推論補助を書き換えることで、エイリアステンプレートに対する推論補助を生成する

最後に、得られた推論補助とエイリアステンプレート

template<typename T>
pair(int, T) -> pair<int, T>;

template<typename T>
using P = pair<int, T>;

の対応から、エイリアステンプレートのテンプレート実引数を推論し、推論補助を完成します。ここでは、エイリアスを戻すような形で推論補助の型を置き換えます。

template<typename T>
P(int, T) -> P<T>;

P p = {1, 2};のように書かれたとき、こうして得られた推論補助を用いてエイリアステンプレートのテンプレートパラメータが推論されます。

例2、単に名前を短縮したいだけの例

// とても長い名前のクラス
template<class T>
class VeryLongNameXXXXX { /* ... */ };

template<class T>
VeryLongNameXXXXX(T) -> VeryLongNameXXXXX<decay_t<T>>;

// 名前を短縮するためのエイリアステンプレート
template<class A>
using MyAbbrev = VeryLongNameXXXXXX<A>;

この場合に、MyAbbrevに対して導出される推論補助を見てみます。

2. エイリアステンプレートの右辺と元の推論補助の右辺から、テンプレートパラメータの対応を推論する

VeryLongNameXXXXXの推論補助は1つだけです。

template<class T>
VeryLongNameXXXXX(T) -> VeryLongNameXXXXX<decay_t<T>>;

これと

// 名前を短縮するためのエイリアステンプレート
template<class A>
using MyAbbrev = VeryLongNameXXXXXX<A>;

これらの右辺からテンプレートパラメータの対応を求めます。それは、関数テンプレートの引数推論と同様に行われるので、

template<class T>
void f(VeryLongNameXXXXX<decay_t<T>>);

f(VeryLongNameXXXXXX<A>{});

このように呼んだ時のTの推論によって対応を求めますが、ご存知のように?これは推論できないコンテキストとされ、Tの推論はできません。したがって、ここでは何の対応も得られません。

3. 2で得られた対応とエイリアステンプレートのテンプレートパラメータを、元の推論補助にフィードバックして置き換える

先ほど得られた情報を元の推論補助にフィードバックするわけですが、何の対応も得られていないのでフィードバックは行われず、このステップは何もしません。よって、元の推論補助そのままが得られます。

template<class T>
VeryLongNameXXXXX(T) -> VeryLongNameXXXXX<decay_t<T>>;

4. 結果の型(2で得られた推論補助の右辺)からエイリアスを推定し、3で得られた推論補助を書き換えることで、エイリアステンプレートに対する推論補助を生成する

次の2つからエイリアステンプレートの推論補助を導出するわけですが

template<class T>
VeryLongNameXXXXX(T) -> VeryLongNameXXXXX<decay_t<T>>;

template<class A>
using MyAbbrev = VeryLongNameXXXXXX<A>;

これは何も難しいところはないでしょう

template<class T>
MyAbbrev(T) -> MyAbbrev<decay_t<T>>;

例3、規格書の意図的な例

template <class T, class U>
struct C {
  C(T, U);
};

template<class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>;

// 今回のエイリアステンプレート
template<class V>
using A = C<V*, V*>;

2. エイリアステンプレートの右辺と元の推論補助の右辺から、テンプレートパラメータの対応を推論する

Cの推論補助は1つだけです(コンストラクタからのものは無視)。

template<class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>;

これと

template<class V>
using A = C<V*, V*>;

これらの右辺からテンプレートパラメータの対応を求めます。

関数テンプレートで表すと

template<class T, class U>
void f(C<T, std::type_identity_t<U>>);

f(C<V*, V*>{});

のように呼んだ時のT, Uに行われるのと同じ推論が行われます。

ここで得られる対応は、T = V*のみで、Uとの対応は得られません(推論できないコンテキストです)。

3. 2で得られた対応とエイリアステンプレートのテンプレートパラメータを、元の推論補助にフィードバックして置き換える

先ほど得られた情報(T = V*)とエイリアステンプレートのテンプレートパラメータをCの推論補助

template<class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>;

にフィードバックします。

まずテンプレートパラメータをフィードバックし

template<class V, class T, class U>
C(T, U) -> C<T, std::type_identity_t<U>>;

テンプレートパラメータの対応(T = V*)をフィードバックします

template<class V, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>>;

4. 結果の型(2で得られた推論補助の右辺)からエイリアスを推定し、3で得られた推論補助を書き換えることで、エイリアステンプレートに対する推論補助を生成する

次の2つからエイリアステンプレートの推論補助を導出するわけです。

template<class V, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>>;

template<class V>
using A = C<V*, V*>;

単純にエイリアスを戻すと次のようになりそうです。

template<class V, class U>
A(V*, U) -> A<V>;

が、これだとエイリアステンプレートの意味が変わってしまっています。そこで、std::type_identity_t<U> = V*という対応が見えていますので、これの情報を制約として加えたものが最終的なエイリアステンプレートAについての推論補助となります。

template<class V, class U>
A(V*, U) -> A<V> requires std::same_as<V*, std::type_identity_t<U>>;

ここでの制約はわかりやすくした一例です。例えばGCCはこの時requires std::same_as<A<V>, C<V*, std::type_identity_t<U>>>のような制約を生成しているようです。

より正確に書けば、ここで導出されるのは元の型に対する次のような推論補助です。

template<class V, class U>
C(V*, U) -> C<V*, std::type_identity_t<U>> requires std::same_as<V*, std::type_identity_t<U>>;

実際は、エイリアステンプレートに対して推論補助を導出するのか、あくまで元の型に対するものを導出するのかは実装定義というか未規定で、GCCは元の型に対する推論補助を導出し使用するようです。

std::arrayエイリアステンプレート1

エイリアステンプレートに対するCTADがどうなっているのかを理解したところで、本題に戻ります。

まずは、std::array<T, N>Tを束縛するエイリアス

// 要素型を束縛するエイリアス
template<auto N>
using std_array_with_int = std::array<int, N>;

について導出される推論補助を求めてみます。

std::arrayには推論補助が1つしかなく、コンストラクタ(集成体初期化)からの推論補助相当のものは得られません(要素1つに対して初期化子複数となるため)。

namespace std {
  template <class T, class... U>
  array(T, U...) -> array<T, 1 + sizeof...(U)>;
}

この推論補助とエイリアスから、テンプレートパラメータの対応を求めます。

template <class T, class... U>
void f(std::array<T, 1 + sizeof...(U)>);

f(std::array<int, N>{});

明らかに要素数は推論できないコンテキストなので、得られるのはT = intという対応のみです。

対応が得られているので、まずエイリアステンプレートのテンプレートパラメータを推論補助へフィードバックし

template <auto N, class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;

T = intの対応をフィードバックします。

template <auto N, class... U>
array(int, U...) -> array<int, 1 + sizeof...(U)>;

そしてエイリアスを推定する(戻す)と

template <auto N, class... U>
std_array_with_int(int, U...) -> std_array_with_int<1 + sizeof...(U)>;

省略していますが、std::arrayの元の推論補助にはTU...のすべての型が同じであることが制約されているので、それも継承されています。

そして、この推論補助の問題点はauto Nを解決できないことです。どのような初期化式が与えられてもこのNが推論されることはなく、そのためコンパイルエラーを起こします。

GCCの導出する推論補助

GCCは、CTADでエラーが起きた時に使用した推論補助の候補をエラーメッセージ中に出力してくれます。

prog.cc: In function 'int main()':
prog.cc:12:53: error: class template argument deduction failed:
   12 |     [[maybe_unused]] std_array_with_int ar2 = {1,2,3};//ng
      |                                                     ^
prog.cc:12:53: error: no matching function for call to 'array(int, int, int)'
In file included from prog.cc:1:
/opt/wandbox/gcc-head/include/c++/12.0.0/array:267:5: note: candidate: 'template<auto N, class _Tp, class ... _Up> std::array(_Tp, _Up ...)-> std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, (1 + sizeof... (_Up))> requires  __is_same(std::std_array_with_int<N>, std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, 1 + sizeof ... (_Up ...)>)'
  267 |     array(_Tp, _Up...)
      |     ^~~~~
/opt/wandbox/gcc-head/include/c++/12.0.0/array:267:5: note:   template argument deduction/substitution failed:
prog.cc:12:53: note:   couldn't deduce template parameter 'N'
   12 |     [[maybe_unused]] std_array_with_int ar2 = {1,2,3};//ng
      |                                                     ^

今回のケースでGCCが導出して使用している推論補助は

template<auto N, class _Tp, class ... _Up>
array(_Tp, _Up ...) -> array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, (1 + sizeof... (_Up))>
  requires  __is_same(std::std_array_with_int<N>,
                      std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, 1 + sizeof ... (_Up ...)>)

というものです(見やすいように改変しています)。先ほどは省略していた関連制約が見えていますが、ほぼ求めた推論補助と同じものであることが分かります。ただし、テンプレートパラメータが1つ多いです。

GCC実装のstd::arrayの推論補助は次のように宣言されています。

template<typename _Tp, typename... _Up>
array(_Tp, _Up...) -> array<enable_if_t<(is_same_v<_Tp, _Up> && ...), _Tp>, 1 + sizeof...(_Up)>;

_Tp_Upの各型が同じであることをenable_ifによって制約しているわけです。CTADとこの推論補助はともにC++17で導入されたものであるため、この実装は妥当です。そして、テンプレートパラメータ名的に、先ほどエラーメッセージから拾い上げた推論補助に含まれる余分なテンプレートパラメータ_Tpはここからのもので、_Tp = intの対応が取れていないことがうかがえます。

これは、2ステップ目のテンプレートパラメータの対応を求める際に

template <class T, class... U>
void f(array<enable_if_t<(is_same_v<_Tp, _Up> && ...), _Tp>, 1 + sizeof...(_Up)>);

f(std::array<int, N>{});

となり、Tも推論できないコンテキストとなってT = intの対応が取れなくなるため起きています。そのためそれがフィードバックされず、導出された推論補助のテンプレートパラメータはauto N, class _Tp, class ... _Upの3つになっています。ただ、ステップ4ではenable_if_t<(is_same_v<_Tp, _Up> && ...), _Tp> = intの対応が取れることが分かるため、それについての制約が追加のrequires節にておこなわれているようです。

とはいえ、そうであっても結論は変わらず、結局auto Nが推定できないためこの推論補助は使い物になりません。そしてそれは、先ほどのGCCのエラーメッセージにも表示されています(最後のほうの「couldn't deduce template parameter 'N'」)。

std::arrayエイリアステンプレート2

次に、std::array<T, N>Nを束縛するエイリアス

// 要素数を束縛するエイリアス(区別のためにT->Eへパラメータ名を変更
template<typename E>
using std_array_with_3 = std::array<E, 3>;

について導出される推論補助を求めてみます。

namespace std {
  template <class T, class... U>
  array(T, U...) -> array<T, 1 + sizeof...(U)>;
}

この推論補助とエイリアスから、テンプレートパラメータの対応を求めます。

template <class T, class... U>
void f(std::array<T, 1 + sizeof...(U)>);

f(std::array<E, 3>);

素数の方は推論できないコンテキストなので、得られるのは、T = Eという対応です。

これを推論補助へフィードバックすると

template <typename E, class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;
template <typename E, class... U>
array(E, U...) -> array<E, 1 + sizeof...(U)>;

そしてエイリアスを推定する(戻す)と

template <typename E, class... U>
std_array_with_3(E, U...) -> std_array_with_3<E> requires (1 + sizeof...(U) == 3);

省略していますが、std::arrayの元の推論補助にはTE)とU...のすべての型が同じであることが制約されているので、それも継承されています。

あれ?これはなんかいけそうな雰囲気がしていますが・・・?

GCCの導出する推論補助

実際エラーとなってるGCCの出力を見てみましょう。

prog.cc: In function 'int main()':
prog.cc:13:51: error: class template argument deduction failed:
   13 |     [[maybe_unused]] std_array_with_3 ar3 = {1,2,3};//ng
      |                                                   ^
prog.cc:13:51: error: no matching function for call to 'array(int, int, int)'
In file included from prog.cc:1:
/opt/wandbox/gcc-head/include/c++/12.0.0/array:267:5: note: candidate: 'template<class T, class _Tp, class ... _Up> std::array(_Tp, _Up ...)-> std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, (1 + sizeof... (_Up))> requires  __is_same(std::std_array_with_3<T>, std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, 1 + sizeof ... (_Up ...)>)'
  267 |     array(_Tp, _Up...)
      |     ^~~~~
/opt/wandbox/gcc-head/include/c++/12.0.0/array:267:5: note:   template argument deduction/substitution failed:
prog.cc:13:51: note:   couldn't deduce template parameter 'T'
   13 |     [[maybe_unused]] std_array_with_3 ar3 = {1,2,3};//ng
      |                                                   ^
In file included from prog.cc:1:
/opt/wandbox/gcc-head/include/c++/12.0.0/array:95:12: note: candidate: 'template<class T> array(std::array<T, 3>)-> std::array<T, 3>'
   95 |     struct array
      |            ^~~~~
/opt/wandbox/gcc-head/include/c++/12.0.0/array:95:12: note:   template argument deduction/substitution failed:
prog.cc:13:51: note:   mismatched types 'std::array<T, 3>' and 'int'
   13 |     [[maybe_unused]] std_array_with_3 ar3 = {1,2,3};//ng
      |                                                   ^
In file included from prog.cc:1:
/opt/wandbox/gcc-head/include/c++/12.0.0/array:95:12: note: candidate: 'template<class T> array()-> std::array<T, 3>'
   95 |     struct array
      |            ^~~~~
/opt/wandbox/gcc-head/include/c++/12.0.0/array:95:12: note:   template argument deduction/substitution failed:
prog.cc:13:51: note:   candidate expects 0 arguments, 3 provided
   13 |     [[maybe_unused]] std_array_with_3 ar3 = {1,2,3};//ng
      |     

使用されている推論補助は

template<class T, class _Tp, class ... _Up>
array(_Tp, _Up ...) -> array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, (1 + sizeof... (_Up))>
  requires  __is_same(std::std_array_with_3<T>,
                      std::array<typename std::enable_if<(is_same_v<_Tp, _Up> && ...), _Tp>::type, 1 + sizeof ... (_Up ...)>)

先ほどど同じ理由で、エイリアステンプレートのTstd::arrayの推論補助の_Tpの対応が取れない結果、エイリアステンプレートのTが推論補助にフィードバックされてしまい、テンプレートパラメータがclass T, class _Tp, class ... _Upの3つになります。しかも、Tに対応する仮引数はないので(推論補助の左辺にTが現れていない)、結局auto Nと同じようにTを実引数から推論できず、コンパイルエラーを起こしています。

エラーメッセージを見ると、MSVCもほぼ同じ理由によってエラーとなっている様子です。 - godbolt

これは、推論補助に対する制約(_Tp_Up...のすべての型が同じ型である)をSFINAEによって行なっていることから生じています。

SFINAEを使わなくしたら行ける?

C++20からはコンセプトが使用でき、コンセプトによって制約してやればrequires節で(すなわち推論補助の右辺の外で)パラメータに対する制約を行えます。std::arrayの要素数を束縛するエイリアステンプレートの場合、そのようにすればCTADが正しく働く気がしてなりません。実験してみましょう。

#include <concepts>

template<typename T, std::size_t N>
struct my_array {
  T arr[N];
};

template<typename T, typename... Us>
  requires (std::same_as<T, Us> && ...)
my_array(T, Us...) -> my_array<T, sizeof...(Us) + 1>;


template<typename T>
using myarray_3 = my_array<T, 3>;

std::array相当のクラス(my_array)を作成して、これに対して要素数を束縛するエイリアステンプレート(myarray_3)を書いて試してみます。

int main() {
  // ok
  [[maybe_unused]]
  my_array a1 = {1, 2, 3, 4};

  // これは制約に引っかかって正しくエラーになる
  [[maybe_unused]]
  my_array a2 = {1, 2.0, 3, 4};
  
  // ok!
  [[maybe_unused]]
  myarray_3 a3 = {1, 2, 3};
}

無事正しく動きました。つまり、std::arrayの推論補助の実装でSFINAEを避ければ要素型の推論はできそうです。

(ただし、MSVCは受け入れてくれないようです)

ここでGCCがやっていることを確かめるために、わざとエラーを起こしてエラーメッセージを見てみます。

prog.cc: In function 'int main()':
prog.cc:24:28: error: class template argument deduction failed:
   24 |   myarray_3 a3 = {1, 2, 3.0};
      |                            ^
prog.cc:24:28: error: no matching function for call to 'my_array(int, int, double)'
prog.cc:10:1: note: candidate: 'template<class T, class ... Us>  requires (same_as<T, Us> && ...) my_array(T, Us ...)-> my_array<T, (sizeof... (Us) + 1)> requires  __is_same(myarray_3<T>, my_array<T, sizeof ... (Us ...) + 1>)'
   10 | my_array(T, Us...) -> my_array<T, sizeof...(Us) + 1>;
      | ^~~~~~~~
prog.cc:10:1: note:   template argument deduction/substitution failed:
prog.cc:10:1: note: constraints not satisfied
prog.cc: In substitution of 'template<class T, class ... Us>  requires (same_as<T, Us> && ...) my_array(T, Us ...)-> my_array<T, (sizeof... (Us) + 1)> requires  __is_same(myarray_3<T>, my_array<T, sizeof ... (Us ...) + 1>) [with T = int; Us = {int, double}]':
prog.cc:24:28:   required from here
prog.cc:10:1:   required by the constraints of 'template<class T, class ... Us>  requires (same_as<T, Us> && ...) my_array(T, Us ...)-> my_array<T, (sizeof... (Us) + 1)> requires  __is_same(myarray_3<T>, my_array<T, sizeof ... (Us ...) + 1>)'
prog.cc:10:1: note: the expression '(same_as<T, Us> && ...) [with T = int; Us = {int, double}]' evaluated to 'false'
prog.cc:4:8: note: candidate: 'template<class T> my_array(my_array<T, 3>)-> my_array<T, 3>'
    4 | struct my_array {
      |        ^~~~~~~~
prog.cc:4:8: note:   template argument deduction/substitution failed:
prog.cc:24:28: note:   mismatched types 'my_array<T, 3>' and 'int'
   24 |   myarray_3 a3 = {1, 2, 3.0};
      |                            ^
prog.cc:4:8: note: candidate: 'template<class T> my_array()-> my_array<T, 3>'
    4 | struct my_array {
      |        ^~~~~~~~
prog.cc:4:8: note:   template argument deduction/substitution failed:
prog.cc:24:28: note:   candidate expects 0 arguments, 3 provided
   24 |   myarray_3 a3 = {1, 2, 3.0};
      |   

ここでGCCが使用している推論補助は

template<class T, class ... Us>
  requires (same_as<T, Us> && ...)
my_array(T, Us ...) -> my_array<T, (sizeof... (Us) + 1)>
  requires  __is_same(myarray_3<T>, my_array<T, sizeof ... (Us ...) + 1>)

これは先ほど手(脳内CTADマシーン)で求めてみた推論補助の想定とほぼ一致しています。

std::arrayエイリアステンプレートでCTADするには結局どうすればいいんですか?

この質問は2つのパターンに分岐し、それぞれで答えが異なります。

  1. 素数を束縛したエイリアスの場合(最初のstd_array_with_int
    • 無理です(もしかしたら、推論補助の形を工夫すれば行けるかもしれませんが、わかりません・・・)
  2. 要素型を束縛したエイリアスの場合(最初のstd_array_with_3
    • 実装の推論補助をSFINAEを使わない形で書き直させれば行けます

1をなんとかするアイデアをお持ちの方がいましたら教えて欲しいです(多分LWG Issueとして提出できます)。

参考文献

謝辞

この記事の9割は次の方のご指摘によって成り立っています。

この記事のMarkdownソース

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

文書の一覧

全部で42本あり、SG22(C/C++相互互換性に関する研究グループ)のCの提案を除くと41本になります。

P0288R9 move_only_function (was any_invocable)

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の微修正のみの様です。

この提案はLWGのレビューを通過し次の全体会議で投票にかけられることが決まっています。

P0447R16 Introduction of std::hive to the standard library

要素が削除されない限りそのメモリ位置が安定なコンテナであるstd::hive(旧名std::colony)の提案。

以前の記事を参照

このリビジョンでの変更は、望ましいclear()の動作の説明を追加した事、リファレンス実装へのリンクを変更した事、commonではないrangeを受けられるようにrangeコンストラクタを修正した事などです。

P0627R5 Function to mark unreachable code

到達不可能なコード領域であることを示す、std::unreachable()の提案。

あるコードブロックについて、プログラマはそこが実行されないことを知っていたとしても、コンパイラにとってはそうではありません。そのような場合に、コンパイラにそれを通知する方法があるとより効率的なプログラムを作成できる可能性があります。例えば、switch文でよく見る事ができます。

void do_something(int number_that_is_only_0_1_2_or_3) {
  switch (number_that_is_only_0_1_2_or_3) {
  case 0:
  case 2:
    handle_0_or_2();
    break;
  case 1:
    handle_1();
    break;
  case 3:
    handle_3();
    break;
  }
}

このような場合、コンパイラは4以上の入力に対して処理をスキップするコードを生成します。この時、4以上の入力が決して来ないことがわかっていて、それをコンパイラに伝える事ができればそのような余分なコードの生成を抑止する事ができます。

他にも有用な場合が多々あるため、C++の処理系でもそのような機能を持っているものがあります。

  • GCC,clang,ICC : __builtin_unreachable()
  • MSVC : __assume(false)

このようなサポートのない実装でも意図的にゼロ除算を行い未定義動作を挿入する事で到達不能性を表現できますが、それは直感的ではなく推奨されません。この提案は、標準的な方法によって到達不可能であることを表現できるようにしようとするものです。

この提案では、std::unreachable()という関数によってそれを行います。

namespace std {
  [[noreturn]] void unreachable();
}

この関数は呼び出されると未定義動作であると規定されており(正確には、事前条件が常に満たされないように規定されている)、呼び出されているコンテキストは未定義動作であることからコンパイラはその場所が実行されないと仮定する事ができ、それによって到達不能性を表現します。

先ほどのswitch文では次のように使用できます。

void do_something(int number_that_is_only_0_1_2_or_3) {
  switch (number_that_is_only_0_1_2_or_3) {
  case 0:
  case 2:
    handle_0_or_2();
    break;
  case 1:
    handle_1();
    break;
  case 3:
    handle_3();
    break;
  default:
    std::unreachable();
  }
}

std::unreachable()の振る舞いが未定義動作であることを選択したのは次のような理由によります

  • 呼び出された時の特定のアクションを規定しないことで、実装は自由な振る舞いを選択できる
    • 例えば、デバッグビルドにおいてトラップを発動するなど
  • clangは__builtin_unreachable()の呼び出しを未定義動作であると規定している
  • 動作が未定義であることで定数式では実行できず、必然的にconstexprコンテキストで呼ばれた場合の振る舞いが規定される

また、[[unreachable]]のような属性ではなく関数が好まれた一つの理由として、その実装を後から変更する事が可能となるためというのが挙げられています。

P0849R8 auto(x): decay-copy in the language

明示的にdecay-copyを行うための構文を追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整のみのようです。

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

P1018R13 C++ Language Evolution status 🦠 pandemic edition 🦠 2021/08–2021/09

EWG(コア言語への新機能追加についての作業部会)が2021/08–2021/09の間に議論した提案やIssueのリストや将来の計画、テレカンファレンスの状況などをまとめた文書。

8月には以下の提案がEWGでの投票にかけられ、P2138R4以外はCWGに送られることが決まっています。

ただし、P2266R1にはclangにおける実装経験より破壊的変更となることが指摘されており、CWGあるいは本会議投票で否決される可能性があります。

P1072R10 basic_string::resize_and_overwrite

std:stringに領域(文字長)を拡張しつつその部分を利用可能にする為のメンバ関数resize_and_overwrite()を追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整です。

この提案はLWGのレビューを終え、次の全体会議で投票にかけられます。

P1885R7 Naming Text Encodings to Demystify Them

システムの文字エンコーディングを取得し、識別や出力が可能なライブラリを追加する提案。

以前の記事を参照

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

  • aliases()の戻り値型をstring_viewからaliases_viewに変更
    • aliases_viewcopyableでありviewかつborowed_rangeかつrandom_access_rangeである軽量の型で、そのvalue_type/referenceconst char*となります。
  • text_encoding::environment()が実行時のロケール変更の影響を受けるように変更
  • name()aliases()で返される名前の間の関連性を明確にした
  • name()nulltprを返すことがあるように変更
    • text_encoding{text_encoding::id::unknown}.name();の様なとき
  • 提案する文言の修正

などです。

P2012R1 Fix the range-based for loop, Rev1

現在のrange-based forに存在している、イテレーション対象オブジェクトの生存期間にまつわる罠を修正する提案。

以前の記事を参照

このリビジョンでの変更は、この提案による変更が破壊的変更になるかどうかの調査と議論の追加、回避策が解決策ではない理由の追加、コンパイラによる診断についての議論の追加、などです。

著者の方(Nicolai Josuttisさん)のツイートによると、この提案に関してこれ以上の作業はなされないようです・・・

P2036R3 Changing scope for lambda trailing-return-type

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正です。

この提案は現在CWGでのレビューを受けています。そこを通過すれば、全体会議での投票に進みます。

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

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

以前の記事を参照

このリビジョンの変更点は、いくつかのフィードバックに対応して文言を修正した事です。

この提案はLWCとCWGでのレビューが終了し、次の全体会議で投票にかけられるようです。そこで承認されれば、Transactional Memory TS(v2?)として発行されることになります。

P2093R9 Formatted output

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

以前の記事を参照

このリビジョンでの変更は、SG16の投票の結果を記載した事、提案する文言の修正、Pythonprintとの違いを明確にしたことなどです。

この提案は主にエンコーディング周りの事を詰めるためにSG16で作業されていましたが、それが完了しC++23に向けてLEWGに転送されることになりました。

P2128R6 Multidimensional subscript operator

多次元コンテナサポートのために添字演算子[])が複数の引数を取れるようにする提案。

前回の記事を参照

このリビジョンでの変更は、CWGの指示に基づいて提案する文言を修正した事です。

この提案は、このリビジョンを持ってCWGのレビューが完了しており、次の全体会議で投票にかけられます。

P2214R1 A Plan for C++23 Ranges

C++23に向けてのrangeライブラリの拡張プランについてまとめた文書。

前回の記事を参照

このリビジョンでは、R0以降の作業の進行を反映しいくつかの提案へのリンクを追加した事と

  • Rangeアダプタのパイプサポートのためのヘルパクラス(P2387R1)をTier 1へ追加
  • cache1(cache_latest)をTier 1からTier 2へ
  • as_constをTier3からTier1へ
  • flat_mapをTier1からTier3へ
    • 現在、transform(f) | joinが正しく同じことをするため
  • transform_maybeをTier1からTier3へ
    • cache1に依存しているため
  • 以前は省略されていたRangeアダプタをTier 3へ追加
  • shift_left/shift_rightをTier 1へ追加

などです。

P2266R2 Simpler implicit move

return文における暗黙のムーブを改善する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正と機能テストマクロの追加、EWGにおける投票の結果を記載した事、clangにおける実装経験について追記した事です。

この提案はCWGのレビューが終了し全体会議で投票にかけられることが決まっていますが、clangによる実装経験から破壊的変更の程度が大きく実装者から反対意見が提出されています。そのため、本会議において否決され再度の議論が必要となる可能性があるようです。

P2276R1 Fix cbegin

メンバ関数cbegin()とフリー関数のstd::cbegin()/std::ranges::cbegin()の不一致を修正し、std::spancbegin()サポートを復活させる提案。

以前の記事を参照

このリビジョンでは、R0の時に提案していたcbegin/cendに対する修正を提案しなくなりました。その代わりに、壊れたconst性を無効化し回避策を実装できるようにすることを目標とするようになりました。

R1では、次の4つの事を提案します

  1. std::spanメンバ関数cbegin()/cend()を追加
    • これはLEWGのレビューにおいて合意済
  2. コンテナが深いconst性(deep constness)を提供するかどうかを検出するコンセプトを提供する
  3. 浅いcosnt性によってバグを抱える場合、std::cbegin/std::cendを無効にする
    • 理想的にはコンパイルエラーとしたいが、少なくとも非推奨にする
  4. 浅いcosnt性によってバグを抱える場合、std::ranges::cbegin/std::ranges::cendを無効にする

なお、cbegin()/cend()だけでなく、c付き範囲アクセス関数(CPO)に対して同じことを提案しています。

そしてこれらの提案は全て、C++20に向けてのDefect Reportとする事が提案されています。

P2278R1 cbegin should always return a constant iterator

std::ranges::cbegin/cendを拡張して、常にconst_iteratorを返すようにする提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言を追加した事(ranges::cdataを含むようにしたこと、view_interfacecbegin/cendを追加した)、views::const_rangeviews::as_constに変更した事、深いconst性を持つviewを扱えるようにviews::as_constの定義を修正した事です。

P2314R3 Character sets and encodings

規格文書中の ~ character setという言葉を明確に定義し直す提案。

以前の記事を参照

このリビジョンでの変更は、CWGからのフィードバックを反映した事です。

P2316R2 Consistent character literal encoding

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正です。

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

P2322R4 ranges::fold

rangeアルゴリズムであるranges::foldの提案。

以前の記事を参照

このリビジョンでの変更は、説明が少し修正されただけの様です。

P2348R1 Whitespaces Wording Revamp

ユニコードの仕様に従う形で、改行と空白を明確に定義する提案。

以前の記事を参照

このリビジョンでの変更は、Vertical TabとForm Feedを垂直方向のスペースではなく水平方向のスペースとして扱うようにしたこと(ユニコード規格には従っていたが、現在の文言及び実装には準じていなかった)、\n\rなど名前が付いていない文字によるシーケンスについてのメモの追記、P2314R2がベースとなるように文言を調整した事などです。

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

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整とEWGにおける投票の結果を記載した事です。

P2363R1 Extending associative containers with the remaining heterogeneous overloads

連想コンテナの透過的操作を、さらに広げる提案。

以前の記事を参照

このリビジョンでの変更は、insertoperator[]などのオーバーロードstd::is_constructible_v<value_type, K&&, Args...>の様な制約を要求しないようにしたこと、提案する文言を追加した事です。

P2372R3 Fixing locale handling in chrono formatters

<chrono>のフォーマッタがロケール依存でありそれを制御できない問題を修正する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正などです。

この提案はLWGでのレビューを終え、次の全体会議で投票にかけられます。

P2388R2 Minimum Contract Support: either Ignore or Check_and_abort

契約が破られた時に継続しないコントラクトサポートを追加する提案。

以前の記事を参照

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

  • 同じ関数が異なるファイル(翻訳単位)で異なる契約がなされている場合、ill-formed(診断不用)とする事を明確にした
  • 参照ではない関数引数に対する事後条件についての議論を拡張
  • 機能テストマクロの追加
  • std::unreachable()との相互作用について記載
    • std::unreachable()は事前条件が満たされない事で到達不能性を表現しているため、そこに契約を用いると無限ループになる
  • UBサニタイザの説明を追加

などです。

事後条件において関数引数を参照する場合、問題となるのは次の様なコードです

// ユーザーが見る宣言
int generate(int lo, int hi)
  [[pre lo <= hi]]
  [[post r: lo <= r && r <= hi]];

// 開発者が見る定義
int generate(int lo, int hi) {
  int result = lo;
  while (++lo <= hi) // loが更新される
  {
    if (further())
      ++result;      // loよりもゆっくりとインクリメントされる
  }
  return result;
}

この時、generate()の戻り値は呼び出された時点のlo以上にはなりますが、関数終了時点のloよりも小さい可能性があります。すなわち、事後条件で関数引数を参照する場合どの時点の値を参照するかで条件の意味が変わってしまうのです。

契約された関数のユーザーが見るのは宣言であり、宣言には明確に「あなたが関数に渡したオブジェクトは変更されない(コピーして使用するので)」「戻り値の数値はあなたが渡した制限値の間に収まる(関数の実行中に作られた値ではない)」と書かれています(あるいはそう読み取ることができます)。人間は宣言をこの様に解釈するためツール(静的解析など)もそのように解釈すべきであり、C++の契約ランタイムチェックも同様にする必要があります。

また、非const非参照関数引数の事後条件での使用を許可してしまうと、事後条件がムーブ後オブジェクトを読んでしまう事に繋がります。

// 宣言
string forward(string str)
  [[post r: r == str]];

// 定義
string forward(string str) // disallowed in our proposal
{
  // ...
  return str; // implicit move
}             // 事後条件はムーブ後オブジェクトを読み取る(未定義動作)

この例ではよりユーザーの意図と外れていることが明確であり、ここでは未定義動作よりもコンパイルエラーが望ましいでしょう。

このため、この提案では非const非参照関数引数を事後条件で参照するのはill-formedとしています。

P2408R1 Ranges views as inputs to non-Ranges algorithms

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

以前の記事を参照

このリビジョンでの変更は、common_rangeではない範囲の使用について議論を追加した事です。

P2418R1 Add support for std::generator-like types to std::format

std::generator-likeな型に対する<format>のサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGでの投票の結果を記載したこと、提案する文言の改善などです。

この提案はC++20への欠陥報告(DR)とする事で合意がとれ、すでにLWGのレビューを終えて次の全体会議で投票にかけられることが決まっています。

P2419R1 Clarify handling of encodings in localized formatting of chrono types

<chrono>のフォーマットにおいて、実行時ロケールが指定するエンコーディングリテラルエンコーディングが異なる場合の振る舞いを規定する提案。

以前の記事を参照

このリビジョンでの変更は、SG16での投票結果を記載した事です。

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

P2430R0 Slides: Partial success scenarios with P2300

非同期処理における部分的な成功(Partial Success)を配信する(返す)際の、P2300のモデルにおける問題点について解説したスライド。

P2431R0 Presentation: Plans for P2300 Revision 2

P2300R2に向けて、これまで受け取ったフィードバック(疑問点)への回答をまとめたスライド。

P2432R0 Fixing istream_view

std::views::istream_view<T>()の他のviewとの非一貫性を正す提案。

std::views::istream_view<T>()は関数であり、型名ではありません。したがって、次のようなことはできません。

int main() {
  std::istringstream mystream{"0 1 2 3 4"}; 

  std::ranges::istream_view<int> v{mystream}; // 型名ではないので、このような初期化はできない
  std::ranges::basic_istream_view<int, char> v{mystream}; // これはok
}

istream_viewは、std::ranges::basic_istream_viewというviewの実体の型に対してstd::views::istream_viewというヘルパ関数が用意されています。一方で<ranges>の他のviewは、std::ranges::xxx_viewという実体の型に対してstd::views::xxxという関数オブジェクト(CPO)が用意されており、istream_viewだけがこれらの命名規則の外にあります。

そのため、上に書いたようなコードは他のviewとの一貫性のある自然な発想によって書かれますが、コンパイルエラーとなります。特に、forループと共に書かれた場合に理解不能なエラーメッセージを見る事ができるでしょう・・・

int main() {
  std::istringstream mystream{"0 1 2 3 4"}; 

  for (int v : std::ranges::istream_view<int>{mystream}) {
    ...
  }
}

この提案はこれらの問題を解決するべく、istream_view命名規則を他のviewと一貫させようとするものです。

この提案の後では、istream_view周りは次のようになります。

namespace std::ranges {

  // basic_istream_viewクラスはそのまま
  template<movable Val, class CharT, class Traits>
    requires default_initializable<Val> && stream-extractable<Val, CharT, Traits>
  class basic_istream_view : public view_interface<basic_istream_view<Val, CharT, Traits>>;

  // charとwchar_tを予め嵌めた型エイリアスを追加
  template<class Val> 
  using istream_view = basic_istream_view<Val, char>;

  template<class Val> 
  using wistream_view = basic_istream_view<Val, wchar_t>; 

  namespace views {

    // 現在のviews::istream_view<T>()を削除し、views::istream<T> CPOを追加
    template<typename T>
    inline constexpr /*unspecified*/ istream = /*unspecified*/;
  }
}

これによって、std::ranges::istream_view<T>は型名を示すようになりstd::views::istream<T>はそれを生成する関数オブジェクト(CPO)となり、その他のviewとの一貫性が保たれるようになります。

これらの解決はC++20へのDRとすることを提案しており、LEWGでのレビューでは合意が取れているようです。

P2435R0 2021 Summer Library Evolution Poll Outcomes

2021年の夏(7月から9月にかけて)に行われた、LEWGの全体投票の結果。

以下の5つの提案が投票にかけられ、P2138R4以外はLWGに転送されることが可決されています。また、その際に寄せられたコメントが記載されています。

P2436R0 2021 September Library Evolution Polls

2021年の秋(10月から12月にかけて)に予定されている、LEWGでの全体投票の予定表。

以下の5つの提案が投票にかけられる予定です。

LEWGでの作業を完了してLWG(CWG)へ転送することを確認するための投票です。上の3つの提案はC++20へ逆適用することを目指しています。

P2437R0 Support for #warning

警告を表示するための#warningプリプロセッシングディレクティブを追加する提案。

#warningディレクティブは#errorと同様にコンパイル時にプログラマに対してメッセージを発行する事ができますが、#errorとは異なりコンパイルを停止しません。これは、ライブラリ開発者などが利用者に向けて致命的ではない警告を表示するのに役立ちます。

主要なC/C++コンパイラのほとんどが既にこれを実装しておりデファクトスタンダードとなっています。加えて、C言語ではC23に向けてすでに#warningディレクティブが承認されているため、C++でも同じ機能をサポートする事でコンパイラ実装は2つの言語間で実装を共有でき、CとC++の相互運用性を高める事ができます。

// マルチスレッド対応していないライブラリのヘッダにおいて、それを警告するのに使用する
// シングルスレッドで使用する分には問題ないのでコンパイルを止めたくない
#warning This library currently has no thread support.

...

P2438R0 std::string::substr() &&

右辺値std::stringからのsubstr()を効率化する提案。

std::string::substr()関数は、元の文字列の一部を切り出した部分文字列を返す関数です。その際、切り出した部分文字列は新しいstd::stringオブジェクトにコピーされて返されます。

// コマンドライン引数の一部を取り出す
benchmark = std::string(argv[i]).substr(12);

// prvalueなstringの一部を切り出す
name_ = obs.stringValue().substr(0,32);

このように、元のstd::stringオブジェクトが右辺値である場合、substr()の処理では元の文字列の保持する領域を再利用してやることで余計なコピーとアロケーションを回避できます。これは、メンバ関数&&修飾を利用すれば可能であり、似たような最適化はstd::optionalなどに見ることができます。

現在、std::string::substr()にはconst修飾されたものだけが提供されており、この提案ではそれを&&, const &の2つに変更することを提案しています。

// 現在のsubstr()
constexpr basic_string substr(size_type pos = 0, size_type n = npos) const;

// この提案後のsubstr()
constexpr basic_string substr(size_type pos = 0, size_type n = npos) const &;
constexpr basic_string substr(size_type pos = 0, size_type n = npos) &&;

提案より、振る舞いの変化例

// aから部分文字列のstringをコピーして作成、aは変更されない(この振る舞いは変わらない)
auto a = std::string(/* */);
auto b = a.substr(/*  */);


auto foo() -> std::string;
// 現在 : 一時オブジェクトのstringから部分文字列のstringをコピーして作成
// 提案 : 一時オブジェクトのstringのリソースを再利用して部分文字列を保持するstringオブジェクトを作成
auto b = foo().substr(/* */);

// 現在 : 一時オブジェクトのstringから部分文字列のstringをコピーして作成
// 提案 : 一時オブジェクトのstringのリソースを再利用して部分文字列を保持するstringオブジェクトを作成
auto a = std::string(/* */).substr(/* */);

// 現在 : aから部分文字列のstringをコピーして作成、aは変更されない
// 提案 : aのリソースを再利用して部分文字列を保持するstringオブジェクトを作成、aは有効だが未規定な状態となる
auto a = std::string(/* */);
auto b = std::move(a).substr(/* */);

最後のケースだけはこの提案の変更によって破壊的変更となります。とはいえ現在このように記述するメリットはないためこう書くことはなく、書いたとしても明示的にmoveしているためaの値にはもはや関心が無いことを理解した上でコンパイラにそれを伝えているはずなので、この提案の変更によってその意図した振る舞いが得られることになります。

また、この変更は既存のsubstr() constcosnt &&&に置き換えるものなのでABiの破壊も伴います。しかし、ライブラリ実装は古い実装を同時に提供し続けておくことができるため、ABIの後方互換を保ちながらこの変更を適用可能であるようです。

また、std::stringはコンストラクタによってもsubstr()を使用したのと同じことを行うことができるようになっているため、この提案では同時に右辺値substr()オーバーロードに対応したコンストラクタを追加することも提案しています。

// 右辺値stringから部分文字列を切り出すコンストラクタ
constexpr basic_string( basic_string&& other, size_type pos, const Allocator& alloc = Allocator() );
constexpr basic_string( basic_string&& other, size_type pos, size_type count, const Allocator& alloc = Allocator() );

この提案のオーバーロードは、元のstd::stringオブジェクトのリソースを再利用することから、アロケータを適切に伝播しなければなりません。

std::pmr::string s1 = ....;
std::pmr::string s2 = std::move(s1).substr();

この場合、s1.get_allocator() == s2.get_allocator()とならないと、再利用したリソースを適切に開放することができません。これは、std::allocator_traits<A>::is_always_equal::value == trueとなる場合は常に再利用することができます。そうならない状態を持つアロケータでもそれを再利用(move)することで効率的なアロケータ伝播を達成できます。それ以外の場合(アロケータのムーブができないなど)は既存のsubstr()と同じ振る舞いとなるため、効率性はなくなりますが追加のオーバーヘッドはありません。

この提案はこれらの最適化を実装に強制するものではなくこのような最適化を実装が選択できるようにし、またそれを推奨するものであり、実装はどのように最適化するかを自由に選択することができます。従って、このアロケータの伝播をどのようにするかは実装定義とすることを提案しています。

P2439R0 Slides for P2415R1, what is a view?

P2415R1の解説スライド。

viewコンセプトの変遷と、viewコンセプトが保証し表現するものは何か、そして提案(P2415)の目的についてまとめられています。

P2440R0 ranges::iota, ranges::shift_left, and ranges::shift_right

Rangeアルゴリズムranges::iota, ranges::shift_left, ranges::shift_rightの提案。

これらのアルゴリズムは新しいものではなく標準ライブラリに古いタイプのものが既にありますが、C++20ではrange対応されていませんでした。

ranges::iota

ranges::iotaは効果が単純であるためすぐに追加できたのですが、<ranges>にはすでにviews::iotaが存在しており、その有用性が不明であったためC++20には追加されませんでした。

しかし、ranges::iotaは出力範囲の要素数に基づいて書き込む値の数が決定されますが、views::iotaはそうではなくその数を事前に決定する必要があるため、ranges::iotaは出力範囲があらかじめ得られている場合に効率的です。

追加されるのは次の二つの形式のオーバーロードです。

// in <numeric>

namespace std::ranges {
  // iotaの戻り値型
  template<class O, class T>
    using iota_result = out_value_result<O, T>;
  
  // イテレータペアを受け取る
  template<input_or_output_iterator O, sentinel_for<O> S, weakly_incrementable T>
    requires indirectly_writable<O, const T&>
  constexpr iota_result<O, T> iota(O first, S last, T value);

  // Rangeオブジェクトを受け取る
  template<weakly_incrementable T, output_range<const T&> R>
  constexpr iota_result<borrowed_iterator_t<R>, T> iota(R&& r, T value);
}

戻り値型であるiota_resultは範囲の終了位置を指すイテレータと計算値の最終値のペアとなる集成体です。

cpprefjpのサンプルを改変したコード例

#include <numeric>
#include <iostream>
#include <array>

int main() {

  // 0から始まる10要素のシーケンスを作成する。
  std::array<int, 10> ar;
  const auto [it, v] = std::ranges::iota(ar, 0);

  for (int x : ar) {
    std::cout << x << std::endl;  // 0123456789
  }

  std::cout << (it == ar.end()) << std::endl; // true
  std::cout << v; // 10
}

ranges::shift_left, ranges::shift_right

shift_left/shift_rightC++20を目指していたのですが、ranges::shift_leftの戻り値型(シフト後範囲を示すranges::subrange)が元の範囲の終端についての情報を失っている懸念から議論が長引き、C++20に間に合いませんでした。特に、番兵によって範囲の終端が示される場合、シフト後範囲の終端と番兵によって示される元の範囲の終端との間の要素はムーブされているため、元の範囲の終端を復元するのが難しいという懸念があったようです。

結局、次のような結論が得られたようです。

  • 戻り値のsubrangeが空でない場合、そのend()からシフト量nだけ進めることで終端を回復できる。
  • 戻り値のsubrangeが空の時(元の範囲のサイズよりシフト量の方が大きい時)、アルゴリズムは必ずしも元の範囲の終端を計算していない可能性がある。
    • この時必要なのはサイズのみ。それはlast - first(引数のイテレータペアがsized_sentinel_forのモデルとなる場合)かranges::size()(引数のrange型がsized_rangeのモデルとなる場合)によって計算できる。
    • そしてその場合、元の範囲は変更されないことが保証できる。

そして、ユーザーが自分で分解する必要のある複雑な型を返すよりも処理結果の部分範囲を示すsubrangeを返した方が使いやすく、範囲終端を計算する可能性があるがそれを返さないタイプのアルゴリズムには前例があります(ranges::cout, ranges::min/maxなど)。そして、問題が発生しないranges::shift_rgihtと戻り値型を一貫させることができます。

これらの理由からこの提案では、ranges::shift_left, ranges::shift_rightの戻り値型は当初の提案通りにシフト後の部分範囲を示すsubrangeとすることを提案しています。

提案ではranges::iotaと同様にそれぞれ2種のオーバーロードを追加します

namespace std::ranges {
  // イテレータペアを受け取るshift_left
  template<permutable I, sentinel_for<I> S>
  constexpr subrange<I> shift_left(I first, S last, iter_difference_t<I> n);

  // rangeオブジェクトを受け取るshift_left
  template<forward_range R>
    requires permutable<iterator_t<R>>
  constexpr borrowed_subrange_t<R> shift_left(R&& r, range_difference_t<R> n);

  // イテレータペアを受け取るshift_right
  template<permutable I, sentinel_for<I> S>
  constexpr subrange<I> shift_right(I first, S last, iter_difference_t<I> n);

  // rangeオブジェクトを受け取るshift_right
  template<forward_range R>
    requires permutable<iterator_t<R>>
  constexpr borrowed_subrange_t<R> shift_right(R&& r, range_difference_t<R> n);
}

前述の議論の通り、戻り値型はシフト後のsubrangeです(borrowed_subrange_tとは引数のrangeオブジェクトが右辺値であるなどダングリングイテレータの危険があるときに代わりのタグ型を返すものです)。

cpprefjpのサンプルを改変したコード例

#include <iostream>
#include <ranges>
#include <vector>
#include <algorithm>

int main() {
  // shift_left
  {
    std::vector<int> v = {1, 2, 3, 4, 5};

    std::ranges::range auto shifted_range = std::ranges::shift_left(v, 2);

    for (int x : shifted_range) {
      std::cout << x << ',';  // 3,4,5,
    }
    std::cout << std::endl;
  }

  // shift_right
  {
    std::vector<int> v = {1, 2, 3, 4, 5};

    std::ranges::range auto shifted_range = std::ranges::shift_right(v, 2);

    for (int x : shifted_range) {
      std::cout << x << ',';  // 1,2,3,
    }
    std::cout << std::endl;
  }
}

P2441R0 views::join_with

パターンによってrangerangeとなっているようなシーケンスを接合して平坦化するRangeアダプタ、views::join_withの提案。

views::joinrangerangeを単にそのまま平坦化(内側rangeの各要素からなるrangeに変換)するのに対して、join_withは指定されたパターンを挿入しながら平坦化します。

std::vector<std::string> vs = {"the", "quick", "brown", "fox"};

for (char c : vs | std::views::join_with(' ')) {
    cout << c;  
}
// "the quick brown fox"という文字列が出力される

for (char c : vs | std::views::join) {
    cout << c;  
}
// "thequickbrownfox"という文字列が出力される

この例では入力のvsstd::stringstd::vectorというrangerangeであり、内側のrangestd::striingです。views::joinによる平坦化は内側rangeの各std::stringをそのまま繋げたrangeに変換するものですが、views::join_withは内側の各rangeの間に指定されたパターン(ここではスペース1つ)を挿入して1本のrangeに変換します。

また、これはviews::splitの逆変換となっており、パターンpによるstr | views::split(p) | views::join_with(p)の様な変換は、もとのstrと同じシーケンスとなります(型は異なりますが)。

この様な平坦化は、views::joinが追加の引数を取るようにすることによっても実装できますが、rangerangeviews::joinRangeアダプタオブジェクトに渡したときの曖昧さを回避するために別の名前のviewとして導入しています(views::join(rr)rrを平坦化したいのかrrjoin_withしたのか不明瞭になる)。

上記例では単に文字(single_viewに変換されている)を渡していましたがjoin_withのパターンには任意のrangeを渡すことができ、そのvalue_type/referenceは入力rangeの内側rangevalue_type/referencecommon_typeを有している必要があり、そのcommon_typejoin_withvalue_type/referenceとなります。

join_withjoinと同様に入力としてprvalueの非viewrangeをキャッシュすることで処理することができます。その場合のjoin_withinput_rangeとなり、そうでない場合は入力rangeとその内側range及びパターンのrangeの共通部分となるカテゴリになります。

P2442R0 Windowing range adaptors: views::chunk and views::slide

元のシーケンスの各要素を指定した要素数のウィンドウによって参照するようなviewを生成する、views::chunk/views::slideアダプタの提案。

提案文書より、サンプルコード。

std::vector v = {1, 2, 3, 4, 5};

fmt::print("{}\n", v | std::views::chunk(2));   // [[1, 2], [3, 4], [5]]
fmt::print("{}\n", v | std::views::slide(2));   // [[1, 2], [2, 3], [3, 4], [4, 5]]

fmt::printは任意のrangeの直接出力が可能で、1つの範囲を[]で囲います。

この2つのviewはどちらも、rangerangeを生成するものです。

views::chunk

views::chunk(R, N)は入力範囲Rの各要素をN個づつまとめた組を要素とするrangeを生成します。その際、サイズNのウィンドウはR上でオーバーラップせずにchunkの各要素を生成します。Rの要素数Nで割り切れない場合、chunkの最後の要素のサイズはNよりも小さくなります。

views::chunkの各要素はviews::takeによって生成され、chunkvalue_type(外側range型)はviews::take(R, N)の結果によります。その場合(入力rangeforwardより強い場合)、入力rangeの諸性質をほぼそのまま受け継ぎます。

一方でviews::chunkinput_rangeの入力をサポートする事が提案されています。その場合、元となるイテレータとその反復状態はchunk_view自身によって管理され、入力rangeの要素はキャッシュされます。したがってその場合は、const-iterableではなくなるなど大きく性質が制限されます。

views::slide

views::slide(R, N)は入力範囲Rの各要素をN個づつまとめた組を要素とするrangeを生成しますが、views::chunkと異なりサイズNのウィンドウはR上で1要素分オーバーラップしてchunkの各要素を生成します。すなわち、slideM番目の要素はRM番目から(M+N-1)番目の要素を参照します。

これはviews::adjacentとよく似ていますが、こちらはウィンドウサイズNを実行時に指定できる点で異なっています。

views::slideの各要素はviews::countedによって生成され、value_type(外側range型)はstd::spanstd::ranges::subrangeのどちらかになります。その性質の多くは、入力となるrange型から継承します。

views::slideでは、chunkと異なりinput_rangeをサポートしません。この場合の要素のキャッシュは複雑となりキャッシュしない場合との差が大きくなり、ユーザーの関心のある要素以外の要素も全てコピーして保持する必要があるなど、あらゆる側面から高コストとなってしまうためです。このことは、views::adjacentで議論され決定されたことを引き継いています。

P2443R0 views::chunk_by

指定された述語によって元のシーケンスの可変個の要素を組にした要素からなるシーケンスを生成する、views::chunk_byアダプタの提案。

std::vector v = {1, 2, 2, 3, 0, 4, 5, 2};
fmt::print("{}\n", v | std::views::chunk_by(ranges::less_equal{}));   // [[1, 2, 2, 3], [0, 4, 5], [2]]

つまり、元のシーケンス上(例ではv)で連続する2つの要素について、指定された述語(例ではranges::less_equal<=比較)がfalseとなる所を区切りとしてchunkを生成します。逆に言うと、述語がtrueとなる連続要素が1つの組としてchunk_byの1要素になります。

これもrangerangeを生成するviewであり、views:chunkの各要素がviews::takeで実装されるのに対してviews::take_whileによって生成されるものと見るとわかりやすいかもしれません(実際にはsubrangeによって生成されますが)。

views::slideviews::adjacentと同様の理由により、chunk_byinput_rangeをサポートしません。chunk_by自身はbidirectional_rangeforward_rangeのどちらかとなり、入力rangecommon_rangeであるときにそれを継承しますが、sized_range, borrowed_rangeにはなりません。

また、views::splitなどと同様に、rangeコンセプトの意味論要件を満たすために最初のbegin()の呼び出し時にchunk_byの最初の要素を導出するためのイテレータを計算してキャッシュしています。このため、const-iterableではなくなります。

P2444R0 The Asio asynchronous model

Asioライブラリの非同期処理モデルについて解説した文書。

Asioは基礎的なソケット通信を行うためのデファクト的なライブラリですが、ネットワーキングにつきものの並行処理についてのサポートも充実しています。

この文書は、Asioの作者の方自らAsioの非同期処理モデルの概要を紹介するものです。

どうやらこれは、NetworkingTSのベースとなっているAsioの非同期モデルについて周知するとともに、現在のExecutor提案によって導入されようとしている(Networking TSの下地となる)非同期モデルとの相互運用性について議論するためのもののようです。

P2446R0 views::move

入力シーケンスの各要素をstd::moveするようなviewである、views::moveの提案

std::vector<string> words = {"the", "quick", "brown", "fox", "ate", "a", "pterodactyl"};
std::vector<string> new_words;

// wordsのstringをnew_wordsへムーブする
std::ranges::copy(words | views::move, std::back_inserter(new_words));

views::moveviews::transfom(std::move)とほぼ同等のものです。しかし実際には、std::moveが関数テンプレートである事からそのような渡し方はできません。

また、views::transfom(std::move)の入力となるrangereferenceprvalueであった場合、std::moveすることは無駄にprvalueの実体化を行うことになるため、効率的ではなくなる可能性があります。そのため、正確にそれを行うには入力rangereferencelvalueの時だけstd::moveする必要があります。それはイテレータに対してstd::ranges::iter_moveCPOが行なっている事ですが、iter_moveイテレータそのものに対して作用するため、こちらもそのままtransformに渡すことができません。

Range-v3にはrangeイテレータに対して変換を行うiter_transformがあります。しかし、そちらはムーブに特化したものではなくより汎用なものであり、入力のrangeの性質をより良く反映してしまいます。すなわち、views::moveによって生成されるrangeinput_rangeでなければなりません。これはviews::transfom(std::move)にも言えることです。

そして、views::moveranges::toによってrangeからコンテナへの変換をより効率的に行うのに大いに役立ちます。さらに、move_iteratorがすでに存在しているため、ムーブオンリーイテレータの設計について時間をかけずともすぐに実装することができます(実際に、提案ではmove_iteratorを使用して実装しています)。

これらの理由から、views::moveはファーストクラスのRangeアダプタとしてふさわしいものであるので、標準に追加しようとする提案です。

std::vector<string> words = {"the", "quick", "brown", "fox", "ate", "a", "pterodactyl"};

// そのままだとコピーになる
auto copy_vec = words | ranges::to<std::vector>;
// views::moveを適用することで適切にムーブできる
auto move_vec = words | views::move | ranges::to<std::vector>;

おわり

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

文書の一覧

全部で29本あります。

N4895 Working Draft, Extensions to C++ for Concurrency Version 2

Concurrency TS v2のワーキングドラフト第一弾。

先頃アクセプトされた、ハザードポインタRCUを反映したもので、今のところ他のものはありません。

これをベースとして実装経験を積んでから、標準ライブラリに導入される事になります。

P1018R12 C++ Language Evolution status 🦠 pandemic edition 🦠 2021/06–2021/08

EWG(コア言語への新機能追加についての作業部会)が2021/01–2021/03の間に議論した提案やIssueのリストや将来の計画、テレカンファレンスの状況などをまとめた文書。

8月は以下の提案がEWGでの投票にかけられる予定です。

これらの提案はほとんど、C++23入りを目指して提案をCWGに転送しようとするものです。

P1072R9 basic_string::resize_and_overwrite

std:stringに領域(文字長)を拡張しつつその部分を利用可能にする為のメンバ関数resize_and_overwrite()を追加する提案。

以前の記事を参照

このリビジョンでの変更は、std::stringstd::allocator_tratisconstruct/destroyメンバ関数を使用しない事を明記したことなどの、提案する文言の調整です。

この提案はLWGのレビューを終え、次の全体会議で投票にかけられます。

P1169R2 static operator()

関数呼び出し演算子operator())を、静的メンバ関数として定義できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、EWGのレビューで提起されたラムダ式に関する2つの問題について追記したことです。

1つ目は、staticラムダ式にキャプチャを許可するべきか?という問題です。ラムダ式本体でキャプチャを参照はしないが初期化キャプチャを利用したい、用途があるようです。例えば次のようなコードです

auto under_lock = [lock=std::unique_lock(mtx)]() static { /* do something */; };

現在(およびR1)の提案はstaticラムダはキャプチャを許可しないことになっています。それを緩和してこのような例を動作させることはできますが、それによってstaticラムダはステートレスラムダをstaticにするだけ、というこの提案の簡易さ(教えやすさ)が失われます。

レビューのミーティング中に投票が行われましたが、そこではこれを許可するコンセンサスは得られませんでした。そのため、この提案ではこの点について変更はありません。

2つ目の問題は、キャプチャレス(ステートレス)ラムダ式はデフォルトでstaticラムダである、と実装定義にすることは可能か(あるいは望ましいか)?という点です。

この提案による変更は後方互換性がなくABIを破壊するため、現在(およびR1)の提案はstaticはあくまでユーザーが指定するものです。実装定義のコンパイラオプションによってこの振る舞いがデフォルトになればユーザーの手間をかけずともこの提案による恩恵を受けることができるようになります。もしC++11時点でこの提案の内容が考慮され採用されていれば、ステートレスラムダ式はデフォルトでstatic担っていたはずです。

一方、実装定義でフォルトの振る舞いを変更すると、ラムダ式の移植性を損ねます。現在の仕様では、operator()の性質をはじめとする観察可能なプロパティは移植可能であるように規定されており、この提案によるstatic性も同様であるため、この性質がポータブルではないというのは奇妙でありラムダの設計に反している、と筆者の方は主張しています。

今のところ、2つ目の問題についての投票は行われておらず、提案もそれを可能なようにしてはいません。

P1206R5 Conversions from ranges to containers

P1206R6 Conversions from ranges to containers

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

R5での変更は

  • push_back_range/push_front_range関数の追加
  • 文言の微修正
  • パフォーマンスやベンチマークについてのノートの追記

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

  • push_back_range/push_front_range関数をprepend_range/append_rangeにリネームした

ことなどです。

prepend_range/append_rangeは任意のrangepush_front/push_backする関数で、一部の既存コンテナにメンバ関数として追加されます。

P1664R5 reconstructible_range - a concept for putting ranges back together

viewによって別のrangeに変換されてしまった範囲を、元のrange(と同じ型)に戻す操作、std::ranges::reconstructと関連するコンセプトの提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言を全体にわたって書き換えたこと、設計選択の動機についての追記、などです。

P1673R4 A free function linear algebra interface based on the BLAS

標準ライブラリに、BLASをベースとした密行列のための線形代数ライブラリを追加する提案。

このリビジョンでの変更は多岐に渡りますが、LEWGのレビューを受けての文言(ライブラリの規定)の更新や修正がメインです。

P1885R6 Naming Text Encodings to Demystify Them

システムの文字エンコーディングを取得し、識別や出力が可能なライブラリを追加する提案。

以前の記事を参照

このリビジョンでの変更は、UTF7IMAエンコーディングをリストに追加したこと、RFC3808の参照をIANA IANA Charset MIBへの参照で置き換えたこと、text_encoding::id列挙値の基底の型がstd::int_least32_tである理由の説明の追記、などです。

P2047R2 An allocator-aware optional type

Allocator Awarestd::optionalである、std::pmr::optionalを追加する提案。

このリビジョンでの変更は、pmr::optional<optional>へ移動したこと(以前は<pmroptional>)、pmr::optionalをよりアロケータについてジェネリックpolymorphic_allocator以外に対応する)にすることについての意見を追記したこと、フリー関数のswap()を追加したこと、などです。

アロケータジェネリック化に関しては、「C++標準ライブラリは狭いサブセットを決め打ちするのではなく、複数のセマンティクスを表現できるような基礎部品を提供するべき」のような意見が上がりましたが、コンセンサスが取られたものではないため今の所提案には反映されていません。

P2093R8 Formatted output

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

以前の記事を参照

このリビジョンでの変更は、LLVMraw_ostream(ここで提案されている文字化け防止メカニズムと似たことを実装している)への参照と言及を追記したことです。

P2280R3 Using unknown references in constant expressions

定数式での参照のコピーを許可する提案。

以前の記事を参照

このリビジョンでの変更は、R2でポインタに対して拡張された内容を元に戻した(ポインタについてはこの提案で扱わないことにした)ことです。ただし、thisポインタの定数式での利用についてはそのまま含まれています。

P2286R2 Formatting Ranges

任意の範囲を手軽に出力できる機能を追加する提案。

以前の記事を参照

このリビジョンでの変更は提案する文言の初稿を追加したことです。

P2291R2 Add Constexpr Modifiers to Functions to_chars and from_chars for Integral Types in Header

std::to_chars, std::from_charsを整数変換に関してconstexprにする提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正(constexpr追加し忘れやコメントの追加)です。

P2361R2 Unevaluated strings

コンパイル時にのみ使用され、実行時まで残らない文字列リテラルについての扱いを明確化する提案。

以前の記事を参照

このリビジョンでの変更は、unevaluated-string-literalという用語をunevaluated-stringに置換したこと、プリント不可能な文字を禁止しない事とunevaluated-stringが式ではない事についての説明の追記、提案する文言の修正、などです。

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

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

以前の記事を参照

このリビジョンでの変更は、ワイド文字列リテラルに関する一部の追加した文言を削除した事です。

P2370R1 Stacktrace from exception

現在投げられている例外オブジェクトに基づくスタックトレースを取得できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、capture_stacktraces_at_throw()の名前をset_capture_stacktraces_at_throw()に変更し、その状態を取得できるget_capture_stacktraces_at_throw()を追加した事、Windowsでの実装アイデアの追記、提案する文言の修正、などです。

P2372R2 Fixing locale handling in chrono formatters

<chrono>のフォーマッタがロケール依存でありそれを制御できない問題を修正する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正などです。

P2387R1 Pipe support for user-defined range adaptors

ユーザー定義のRangeアダプタに対して、パイプライン演算子|)サポートを簡単に行えるユーティリティを提供する提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加した事です。

P2388R1 Minimum Contract Support: either Ignore or Check_and_abort

契約が破られた時でも継続しないコントラクトサポートを追加する提案。

以前の記事を参照

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

  • 提案する文言の追加
  • 設計についての説明の追記 : 異なる翻訳単位で同じ関数に互換性のない契約がなされていたら
    • 呼び出されない限り存在は許可されるが、呼び出されたら未定義動作
  • 設計についての説明の追記 : 事後条件では、値or右辺値参照関数引数を参照する
  • Issueの解決 : オブジェクトは契約指定の式内ではconstとして扱われない
  • Issueの解決 : 実装はIgnoreモードでもコア定数式の契約を実行時にチェックしてもいい
  • 事後条件指定の代替構文案の追記
    • 事後条件で多くの変数を参照し名前を付けるために、[[post(r, a, b) : cond]]のような構文を将来的に利用できる
  • 設計詳細と理論的根拠のセクションの拡充

などです。

P2393R1 Cleaning up integer-class types

整数型とみなせる型を指すinteger-classの定義を修正する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを反映しLWG Issue 3575も修正するようにしたことです。

これによって、全てのinteger-class型はthree_way_comparable<strong_ordering>のモデルとなります。

この提案は次の全体会議で投票にかけられることが決まっています。

P2414R1 Pointer lifetime-end zap proposed solutions

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

以前の記事を参照

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

  • Abstractの変更
    • この提案の要約の追記
  • “What We Are Asking For”セクションに、atomicとvolatileについての前方参照を追加
  • atomic_usable_ref()を追加し、usable_ptr::refusable_refにリネーム
  • B5セクションをより明快になるように書きなおし

などです。

追記された要約によれば、この提案の目指すものは次の2つです。

  1. std::usable_ptr<T>の標準ライブラリへの追加
    • これはポインタlikeな型で、参照先の生存期間が終了した後も使用可能であることが保証される
  2. atomicvolatile操作を再定義し、lifetime-end pointerの無効性を許容するようにする

また、この提案はbag-of-bitsポインタセマンティクス(ポインタは単なる値である、というようなポインタ意味論)を導入するものではないことが明確化されています。

P2415R1 What is a view?

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

以前の記事を参照

このリビジョンでの変更は提案する文言を追加した事です。

SG9でのレビューでは全会一致でLEWGへの転送が決定されました。

P2418R0 Add support for std::generator-like types to std::format

std::generator-likeな型に対する<format>のサポートを追加する提案。

<format>rangeのフォーマットサポートを追加するP2286の作業中に、次のような問題が発見されました。

auto ints_coro(int n) -> std::generator<int> {
  for (int i = 0; i < n; ++i) {
    co_yield i;
  }
}

std::format("{}", ints_coro(10)); // error

std::formatは出力する値をconst T&で受け取りますが、std::generatorはconst-iterableでもcopyableでもないためそこから値を取り出す事ができず、エラーとなります。

同様の問題が起こりうる<ranges>の各種viewでは、const-iterableではないviewはcopyableであるためC++20では問題になりませんでした。

しかし、std::generatorも含めて他のコルーチン型では同様の問題が発生し、またviewの中にもconst-iterableでもcopyableでもないものがある可能性があります。

この提案では、std::formatをはじめとする関数の出力対象引数列の型をconst Args&&...からArgs&&...に変更する事で問題の解決を図ります。これによって次のようなメリットが得られます

  1. const-iterableではないviewはコピーを回避できる
  2. 一般的なlifetimeエラーを検出できるようになる

2番目のメリットは次のような事が可能になる事です

// format_joinは、第一引数のrangeの各要素を第二引数のデリミタで区切って出力する関数(未導入)
auto joined = std::format_join(std::vector{10, 20, 30, 40, 50, 60}, ":");
std::format("{:02x}", joined); // 現在はUB、この提案の後ではコンパイルエラー

そもそも<format>が出力対象引数をconst参照で受け取っていたのは、ビットフィールドをサポートするためでした

struct S {
  int bit: 1;
};

auto s = S();
std::format("{}", s.bit);   // 現在は有効、この提案の後ではコンパイルエラー
std::format("{}", +s.bit);  // intへ変換する、この提案の後でもOK

ビットフィールドはconst参照でしか参照する事ができないため、この提案によってこのようなコードはエラーとなります。しかしその場合でも、ビットフィールドの頭に+をつけて整数型にキャストする事で簡単に問題を回避でき、問題ないとの認識のようです。

この部分の6割は以下の型のご指摘によって出来ています

P2419R0 Clarify handling of encodings in localized formatting of chrono types

<chrono>のフォーマットにおいて、実行時ロケールが指定するエンコーディングリテラルエンコーディングが異なる場合の振る舞いを規定する提案。

std::locale::global(std::locale("Russian.1251"));
auto s = std::format("День недели: {}", std::chrono::Monday);
auto s = std::format("День недели: {:L}", std::chrono::Monday); // P2372以降

// 出力例(リテラルエンコーディングがUTF-8の場合)
// "День недели: \xcf\xed"

このようなコードにおいて、リテラルエンコーディング(文字列リテラルエンコーディング)がUTF-8の場合、グローバルロケールに指定されているRussian.1251エンコーディングとの間に不一致があります。しかし、この場合の振る舞いを標準は指定していません。

この提案は、この場合に結果が一貫したエンコーディングの下で正しく出力されるように、実装にトランスコードィングを許可するか、ロケールを置換する事で文字化けを防ぐように仕様を明確化するものです。

このリビジョンでは、文字列リテラルエンコーディングユニコードでありロケールの指定するエンコーディングと異なる場合、ロケールによる文字列置換結果は、文字列リテラルエンコーディングに変換されて出力される、ようにする事を提案しています。

std::locale::global(std::locale("Russian.1251"));
auto s = std::format("День недели: {}", std::chrono::Monday);
auto s = std::format("День недели: {:L}", std::chrono::Monday); // P2372以降

// 出力(リテラルエンコーディングがユニコードの場合)
// "День недели: Пн"

P2420R0 2021 Summer Library Evolution Polls

2021年の夏(7月から9月にかけて)に予定されている、LEWGでの全体投票の予定表。

以下の9つの提案が投票にかけられる予定です。

基本的にはLEWGでの作業を完了してLWG(CWG)へ転送することを確認するための投票です。

P2423R0 C Floating Point Study Group Liaison Report

C23に適用される予定の、浮動小数点数関連の変更についての要約文書。

  1. 2進浮動小数点数
    • 幅を示す整数値を返すマクロの追加
    • 浮動小数点環境アクセスのためのマクロと関数の追加
    • その他マクロと関数の追加
      • fromfpx, roundeven, fmaxmag, llogb, nextup, fadd, ffma, totalorder, canonicalize, setpayload, strfromdなど
    • Constant rounding modeの追加
      • #pragma STDC FENV_ROUNDディレクティブによって設定し、いくつかの標準関数が影響を受ける
    • signaling NaNのためのマクロ追加
    • 浮動小数点数値分類のためのマクロの追加
      • iscanonical, issignaling, iszeroなど
  2. 10進浮動小数点数(条件付きサポート)
  3. 交換・拡張浮動小数点数型(interchange and extended floating-point types
    • 2進と10進浮動小数点数型の交換と拡張のための個別の型
      • _Float32, _DecimalN, _FloatNx
    • リテラルサフィックス
      • fN/FN, fNx/FNx, dN/DN, dNx/DNx
    • 交換・拡張型に一般化された2進、10進浮動小数点数型情報取得マクロ
      • FLTN_MAX, DECNX_TRUE_MIN
    • 交換・拡張型に一般化された2進、10進浮動小数点数型の関数やタイプジェネリックマクロやその他のマクロ
      • coshfN, ceilfNx, sinhdNx, dMadddNx, strtofN, FP_FAST_FMADDFN, FLTN_SNAN
    • 交換・拡張型に一般化された10進浮動小数点数型用の関数
      • encodedecdN, quantizedNx
    • 交換・拡張型に一般化された2進複素数型及び虚数
      • _FloatN _Imaginary, _FloatNx _Complex
    • 交換・拡張型に一般化された2進複素数型用の関数
      • cexpfN, crealfNx
    • 評価メソットマクロの値を交換・拡張型を含めるように更新
      • _DecimalNに対してDEC_EVAL_METHOD N
      • _FloatNxに対してFLT_EVAL_METHOD N+1
    • 算術演算が定義されない交換型の間の変換のためのデコード/エンコード関数
      • decodefN, dMecndecdN
  4. 追加の数学関数

だいたい条件付きサポート(必須でない)だったりしますが、C23に向けてこれらの浮動小数点数関連の拡張が予定されています。おそらくC++にも影響してくるでしょう。

P2425R0 Expression Function Body

簡易な関数定義を式で直接書けるようにする提案。

ラムダ式を含む関数定義では、1行で済むような単純な関数を定義するシーンがよくあります。

// 1. Calling a member
std::all_of(vs.begin(), vs.end(), [](const auto& val) { return val.check(); });

// 2. Binding an object
std::all_of(vs.begin(), vs.end(), [id](const auto& val) { return val.id == id; });

// 3. Passing a lazy argument
auto get_brush_or(painter, []{ return Brush(Color::red); });

// その他多数の例が提案にあります、省略

この例はほんの一例でしかなく、noexceptやコンセプトチェックを欠いているなど正確なものではありません。このようなコードを正しく書くことは、記述量が増加するとともに考慮すべき事も多く、簡単な作業ではありません。

この提案の目的は、このような簡易な関数定義について記述量を削減するとともに簡易な記述で正しく書く事ができる構文を提供する事です。

コンセプトの導入によってSFINAEという高度なテクニックが非常に簡易な形で誰でも利用できるようになった事で、これらの問題の影響は時間経過とともに増大する事が想像されます。

void something(std::invocable<int> auto f);
void something(std::invocable<std::string> auto f);

// something()の呼び出しは曖昧であるため、コンパイルエラー
something([](auto arg){ return arg/2; });

このように、コンセプトのチェックを必要とするコードは今後増加していく事でしょう。これはもはやTMP的なコードではなくあらゆる場所で行われるようになるため、何も考えずに書いても正しく動くことの重要性はより高まります。

さらに、静的例外(P0709R0)の導入は例外指定の問題をさらに悪化させます。

auto func(const std::string&) throws; // 静的例外の指定
auto func(int);
...
std::transform(vs.begin(), vs.end(), vs.begin(), 
    [](const auto& val) { return func(val); }); // このラムダ式の例外指定は?

現在の提案の仕様では、静的例外指定された関数をラムダ式で包んで特に例外指定を行わない場合、動的例外に変換されます。これは明らかに望ましい振る舞いではありません。

これらの問題は、短縮ラムダ提案(P0573R2)の解決しようとした問題でもありましたが、それは次のような理由によりリジェクトされました。

  1. 通常のラムダ式と意味論が異なる。関数本体が同じでも、短縮形か否かによって戻り値型が異なる。
  2. 任意の先読みパースが必要となる。パーサーはラムダ式が短縮形かどうかを判定するためにラムダ式本体を先読みしなければならない。
  3. 後置戻り値型との不一致。ラムダ本体と後置戻り値型とでは解析が異なるため、短縮ラムダは意図通りに動作しない可能性がある
    • この問題はP2036R2で解決される(予定)

この提案は、この1つ目の問題を解決しようとするものでもあります。

短縮ラムダ提案では次の二つの形式が同じ意味となるように定義されました。

[]() noexcept(noexcept(expression)) -> decltype((expression)){ return expression; }
[]() => expression;

問題となったのは戻り値型のdecltype((expression))です。これによる推論は参照のセマンティクスをデフォルトとし、左辺値のreturnに対して左辺値参照型を推論します。一方、通常のラムダ式で戻り値型指定を省略した場合は値のセマンティクスがデフォルトであり、decltype((expression))の結果型をdecayした型が推論されます。

int i;

auto l = [](int* p) noexcept(noexcept(*p)) -> decltype((*p)) { return *p; };
// decltype(l(&i))はint&

auto l2 = [](int* p) { return *p; }
auto func(int*) { return *p; }
// decltype(l2(&i))とdecltype(func(&i))は共にint

auto l3 = [](int* p) => *p;
// decltype(l3(&i))はint&

また、[](auto val) => val;のように書くとローカル変数への参照を返します。これはバグであり望ましい振る舞いではありませんが、先ほどのポインタの例のように多くの場合は参照を返した方が便利だと思われます。

このように、短縮ラムダは通常のラムダと同じようにはならず、これが敬遠された理由の一つとなりました。

この問題への対処のためにこの提案では2つの方法を提案しています。

  1. 最小式は参照セマンティクス持ち、非最小式は値のセマンティクスを持つ
  2. 最小式も値のセマンティクスを持ち、オプトインで参照セマンティクスを与える

この提案では1つ目の方を主として推しています。

提案1(メイン)

この提案による記法の1つ(非最小式)は、単一の式のみから構成された関数の{}を取り払うことで導入されます。

// どこかで定義されているとして
const pixel& pixel_ref_at(const image& image, point p) noexcept;

// From
auto pixel_at(image& image, int x, int y) {
  return pixel_at(image, point{x, y});
}

// To (この提案)
auto pixel_at(image& image, int x, int y)
  return pixel_at(image, point{x, y});

1つ目の方法ではこれは次のようなコードと等価となります。

auto pixel_at(image& image, int x, int y) 
  noexcept(noexcept(std::decay_t<decltype(pixel_at(image, point{x, y}))>(pixel_at(image, point{x, y}))))
  -> decltype((std::decay_t<decltype(pixel_at(image, point{x, y}))>(pixel_at(image, point{x, y})))) 
    { return pixel_at(image, point{x, y}); }

戻り値型の扱いは同じ(値のセマンティクス)ですが、例外指定の正確さとコンセプト/SFINAEとの高い親和性が追加されています。

従ってこの例では、提案前後でも戻り値型は変化しませんが、正しいnoexceptハンドリングが追加されます。

returnが必要であることは、呼び出されて値を返すという通常の関数のセマンティクスを持つことの指標となっています。

int i;
auto l = [](auto* p) return *p; 
// decltype(l(&i))はint

記法の2つ目(最小式)は関数というよりは式である事を明示するもので、先ほどの記法からreturnを取り除いたものです。

auto l = [](auto* p) *p; 

// このコードと等価
auto l = [](auto* p) noexcept(noexcept(*p)) -> decltype((*p)) { return *p; };

この記法では戻り値型は参照のセマンティクスを持ち、関数よりもより式そのものに近い振る舞いをします。そしてこれはP0573R2)の短縮ラムダのセマンティクスそのものです。

この提案による非最小式は、完全な関数とこの最小式の中間に位置する記法として振る舞い、この2つの記法がラムダ式以外の部分に広く導入されることによって、最小式による短縮ラムダと通常のラムダの間の曖昧さを取り除こうとするものです。

// このような階層的な記法を提供し、関数記法と最小式記法の間にギャップを挿入する
[](auto* p) { return *p; }  // 値セマンティクス
[](auto* p)   return *p;    // 値セマンティクス
[](auto* p)          *p;    // 参照セマンティクス

そして、最小式の記法によってより実際の式の表記に近づけることで、本体が値を返すという関数のメタファーから逃れる事を目指します。これは=>を使用しない理由でもあります。=>は結局returnエイリアスであり、->の進化形でしかありません。->が型を返す事を示すのに対して=>は式を返す事を示しています。何かを返すという関数的な概念をここでは避けようとしており、より純粋な式として振舞う事を明示的にしようとしています。

なお、この記法を導入すると関数の修飾との区別が曖昧になるため、それがある場合は修飾と式を:で区切る事を提案しています。

auto Class::member() const: async;
[](int a) mutable: coro;

提案1によるサンプルコード

現在 この提案
class QPointF {
  // ...
  real& rx() { return xp; }
  real& ry() { return yp; }
  real x() const { return xp; }
  real y() const { return yp; }
   
  friend auto operator+(const QPointF &p1, const QPointF &p2) {
    return QPointF(p1.xp+p2.xp, p1.yp+p2.yp);
  }

private:
  real xp;
  real yp;
};
class QPointF {
  // ...
  auto rx() xp; 
  auto ry() yp;
  auto x() const return xp;
  auto y() const return yp;

  friend auto operator+(const QPointF &p1, const QPointF &p2)
    QPointF(p1.xp+p2.xp, p1.yp+p2.yp);

private:
  real xp;
  real yp;
};
現在 この提案
template< class C >
constexpr 
auto cbegin( const C& c ) noexcept(noexcept(std::begin(c)))
    -> decltype(std::begin(c)) { return std::begin(c); } 
template< class C >
constexpr 
auto cbegin( const C& c ) std::begin(c); 

提案2 (サブ)

こちらでは、提案1による最小式の戻り値型をデフォルトで値のセマンティクスとして、参照セマンティクスとするには追加の記法を用いるものです。

int i;
auto l = [](int* p) *p; 
// decltype(l(&i))はint

// このコードと等価
auto l = [](auto* p) noexcept(noexcept(std::decay_t<decltype(*p)>(*p))) 
  -> decltype(std::decay_t<decltype(*p)>(*p)) { return *p; }; 

先ほどのような通常の関数定義から{}returnを省いた構文を導入する事は同じですが、ここではこれはまだ値のセマンティクスを持ちます。これを参照のセマンティクスとするには式を()で囲みます。

int i;
auto l = [](int* p) (*p); 
// decltype(l(&i))はint&

// このコードと等価
auto l = [](auto* p) noexcept(noexcept(*p)) -> decltype((*p)) { return *p; }; 

変数を()で囲んで参照を取得することは、decltypereturnですでに確立されています。

auto l = [](int i) -> decltype(auto) { return (i); };  // 戻り値型はint&

struct Point { int x; int y; };
auto l2 = [](const Point& p) -> decltype(auto) { return (p.x); };  // 戻り値型はint&

int i;
decltype((i)) p = i; // pの型ははint&

このオプションの副次的な効果として、先ほど:が必要だったところで不要となります。

[object]mutable: object.func();  // 値を返す最小式記法、区切りが必要
[object]mutable (object.func()); // 参照を返す最小式記法、区切りは不要

提案2によるサンプルコード

現在 この提案
class QPointF {
  // ...
  real& rx() { return xp; }
  real& ry() { return yp; }
  real x() const { return xp; }
  real y() const { return yp; }
   
  friend auto operator+(const QPointF &p1, const QPointF &p2) {
    return QPointF(p1.xp+p2.xp, p1.yp+p2.yp);
  }

private:
  real xp;
  real yp;
};
class QPointF {
  // ...
  auto rx() (xp); 
  auto ry() (yp);
  auto x() const: xp;
  auto y() const: yp;

  friend auto operator+(const QPointF &p1, const QPointF &p2)
    QPointF(p1.xp+p2.xp, p1.yp+p2.yp);

private:
  real xp;
  real yp;
};

P2428R0 Slides: BSI issues with P2300

P2300の問題点や疑問点についての報告スライド。

欠いているアルゴリズムや、コールバックやキャンセルについてなど、いくつかの設計上と実装上の問題や疑問点についてまとめられています。

おわり

[C++]メンバ型のトリビアル性を継承するロストテクノロジー

std::optionalstd::variantは保持する型がトリビアルな型であれば、そのトリビアル性を継承することが規格によって求められており、その実装には非常に難解なテクニックが使用されます。しかし、C++20以降、このテクニックは過去のものとなり忘れ去られていく事でしょう。この記事はそんなロストテクノロジーの記録です。

メンバ型のトリビアル性を継承、とは?

テンプレートパラメータで指定された型の値をメンバとして保持するときに、そのテンプレートパラメータの型のトリビアル性を継承する事です。

template<typename T>
struct wrap {
  T t;
};

template<typename T>
void f(wrap<T>) {
  // 要素型Tがトリビアルであれば
  static_assert(std::is_trivial_v<T>);
  // wrap<T>もトリビアルとなってほしい
  static_assert(std::is_trivial_v<wrap<T>>);
}

トリビアルというのは、クラスの特殊メンバ関数がユーザーによって定義されていないことを言います(単純には)。これによって、trivially copyableならばmemcpyできるようになるとか、trivially destructibleならばデストラクタ呼び出しを省略できる、などの保証が得られます。

上記のwrap<T>型のように単純な型であれば単純にメンバとして保持しただけでも継承していますが、std::optionalのように複雑な型ではそうは行きません。しかしそれをなんとかする方法がちゃんと存在しています。

optional<T>簡易実装

この記事ではoptionalの簡易実装によってメンバ型のトリビアル性継承がどのように行われるのかを見ていきますので、ここでベースとなる簡易実装rev1を書いておきます。

template<typename T>
class my_optional {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;

public:

  // デフォルトコンストラクタ
  constexpr my_optional() 
    : dummy{}
    , has_value(false)
  {}

  // 値を受け取るコンストラクタ
  template<typename U=T>
  constexpr my_optional(U&& v)
    : data(std::forward<U>(v))
    , has_value(true)
  {}

  // コピーコンストラクタ
  my_optional(const my_optional& that)
    : dummy{}
    , has_value(that.has_value)
  {
    if (that.has_value) {
      new (&this->data) T(that.data);
    }
  }

  // ムーブコンストラクタ
  my_optional(my_optional&& that)
    : dummy{}
    , has_value(that.has_value)
  {
    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }
  }

  // コピー代入演算子
  my_optional& operator=(const my_optional& that) {
    auto copy = that;
    *this = std::move(copy);
    
    return *this;
  }

  // ムーブ代入演算子
  my_optional& operator=(my_optional&& that) {
    if (this->has_value) {
      this->data.~T();
    }

    this->has_value = that.has_value;

    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }

    return *this;
  }

  // デストラクタ
  ~my_optional() {
    if (has_value) {
      this->data.~T();
    }
  }
};

この実装はとりあえずoptionalっぽい働きはします。C++11で制限解除された共用体はそのメンバ型が非トリビアルな特殊メンバ関数を持つとき、対応する特殊メンバ関数deleteされます。そのため、それをラップする外側の型はそれを書いておく必要があります。optionalは遅延構築や任意タイミングでの無効値への切り替えが可能であり、それを実現するためには共用体を利用するのが最短でしょう。なお、状態を変化させるのは他のメンバ関数や代入演算子で行いますが、ここではそれは重要ではないので省略します。また、noexceptについては考えないことにします。

デストラク

簡易実装rev1はデストラクタがトリビアルではありません。Ttrivially destructibleであるならばデストラクタ呼び出しは省略できるので、my_optionalのデストラクタもトリビアルに出来そうです。そしてそれは、C++17の世界でmy_optionalconstexprとなるための必要十分条件です。

デストラクタのトリビアル性継承は要するに、Tトリビアルデストラクタを持つ場合にdefaultで、そうではない場合に独自定義、という風に分岐してやればいいのです。それはクラステンプレートの部分特殊化を用いて、次のように実装できます。

// デストラクタがトリビアルでない場合のストレージ
template<typename T, bool = std::is_trivially_destructible_v<T>>
struct optional_storage {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;

  // デストラクタは常に非トリビアルでdeleteされているので定義する
  ~optional_storage() {
    if (has_value) {
      this->data.~T();
    }
  }
};

// デストラクタがトリビアルである場合のストレージ
template<typename T>
struct optional_storage<T, true> {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;

  // デストラクタはトリビアルであり常にdeleteされないので、宣言すらいらない
};

template<typename T>
class my_optional : private optional_storage<T> {
public:

  // 他略

  // デストラクタ、この宣言も実はいらない
  ~my_optional() = default;
};

optional_storage<T>というクラスにデータを保持する部分を移管し、optional_storage<T>Ttrivially destructibleである場合とない場合でテンプレートの部分特殊化によって実装を切り替えます。そしてその実装では、Ttrivially destructibleである場合はデストラクタはトリビアルに定義され(ユーザー定義されず)、Ttrivially destructibleでない場合に引き続きユーザー定義されます。これらの選択は与えられた型Tによって自動的に行われ、my_optional<T>Ttrivially destructible性を継承します。

int main() {
  // パスする
  static_assert(std::is_trivially_destructible_v<my_optional<int>>);
  static_assert(std::is_trivially_destructible_v<my_optional<std::string>> == false);
}

簡易実装rev2は次のようになりました。

// デストラクタがトリビアルでない場合のストレージ
template<typename T, bool = std::is_trivially_destructible_v<T>>
struct optional_storage {
  bool has_value = false;
  union {
    char dummy;
    T data;
  };

  // デストラクタは常に非トリビアルでdeleteされているので定義する
  ~optional_storage() {
    if (has_value) {
      this->data.~T();
    }
  }
};

// デストラクタがトリビアルである場合のストレージ
template<typename T>
struct optional_storage<T, true> {
  bool has_value = false;
  union {
    char dummy;
    T data;
  };

  // デストラクタはトリビアルであり常にdeleteされないので、宣言すらいらない
};

template<typename T>
class my_optional : private optional_storage<T> {
public:

  // デフォルトコンストラクタ
  constexpr my_optional() 
    : has_value(false)
    , dummy{}
  {}

  // 値を受け取るコンストラクタ
  template<typename U=T>
  constexpr my_optional(U&& v)
    : has_value(true)
    , data(std::forward<U>(v))
  {}

  // コピーコンストラクタ
  my_optional(const my_optional& that)
    : has_value(that.has_value)
    , dummy{}
  {
    if (that.has_value) {
      new (&this->data) T(that.data);
    }
  }

  // ムーブコンストラクタ
  my_optional(my_optional&& that)
    : has_value(that.has_value)
    , dummy{}
  {
    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }
  }

  // コピー代入演算子
  my_optional& operator=(const my_optional& that) {
    auto copy = that;
    *this = std::move(copy);
    
    return *this;
  }

  // ムーブ代入演算子
  my_optional& operator=(my_optional&& that) {
    if (this->has_value) {
      this->data.~T();
    }

    this->has_value = that.has_value;

    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }

    return *this;
  }
};

コピー/ムーブコンストラク

コピー/ムーブコンストラクタをトリビアルに定義するとは、先程のデストラクタのようにTでのそれがトリビアルならばmy_optionalでのそれもトリビアルとなるようにすればいいのです。が、冷静に考えてみると、すでにデストラクタのトリビアル性で分岐している所にコピーコンストラクタのそれでさらに分岐し、さらにムーブコンストラクタでも・・・となって組合せ爆発のようになることがわかるでしょう。じゃあいい方法が・・・ないので愚直に書きましょう。

ただ、そのような分岐を1つのクラスにまとめようとすると組合せ爆発で死ぬのは想像が付くので、特殊メンバ関数一つに対して1つのクラスが必要で、その1つのクラスにはdefaultによるトリビアルな定義をするものと自前定義するものの2つの特殊化が必要になりそうです。
もう少しよくよく考えてみると、Tのある特殊メンバ関数トリビアルであるとき、基底となるoptional_storageでもそれはトリビアルに定義できるはずなので、そこで定義されたそれを活用すればトリビアルケースの定義を省略出来る事に気づけます(私は気づきませんでしたが)。

コピーコンストラクタだけで見てみると、次のようになります。

// デストラクタがトリビアルでない場合のストレージ
template<typename T, bool = std::is_trivially_destructible_v<T>>
struct optional_storage {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;
  
  constexpr optional_storage()
    : dummy{}
    , has_value(false)
  {}
  
  template<typename... Args>
  constexpr optional_storage(Args&&... arg)
    : data(std::forward<Args>(arg)...)
    , has_value(true)
  {}
  
  // 定義できればトリビアル、そうでないなら暗黙delete
  optional_storage(const optional_storage&) = default;
  optional_storage(optional_storage&&) = default;
  optional_storage& operator=(const optional_storage&) = default;
  optional_storage& operator=(optional_storage&&) = default;

  ~optional_storage() {
    if (has_value) {
      this->data.~T();
    }
  }
  
  template<typename... Args>
  void construct(Args&&... arg) {
    new (&this->data) T(std::forward<Args>(arg)...);
    has_value = true;
  }
  
  template<typename Self>
  void construct_from(Self&& that) {
    if (that.has_value) {
      // thatの値カテゴリを伝播する
      construct(std::forward<Self>(that).data);
    }
  }
};

// デストラクタがトリビアルである場合のストレージ
template<typename T>
struct optional_storage<T, true> {
  union {
    char dummy;
    T data;
  };
  bool has_value = false;

  constexpr optional_storage()
    : dummy{}
    , has_value(false)
  {}
  
  template<typename... Args>
  constexpr optional_storage(Args&&... arg)
    : data(std::forward<Args>(arg)...)
    , has_value(true)
  {}
  
  // 定義できればトリビアル、そうでないなら暗黙delete
  optional_storage(const optional_storage&) = default;
  optional_storage(optional_storage&&) = default;
  optional_storage& operator=(const optional_storage&) = default;
  optional_storage& operator=(optional_storage&&) = default;
  
  template<typename... Args>
  void construct(Args&&... arg) {
    new (&this->data) T(std::forward<Args>(arg)...);
    has_value = true;
  }
  
  template<typename Self>
  void construct_from(Self&& that) {
    if (that.has_value) {
      // thatの値カテゴリを伝播する
      construct(std::forward<Self>(that).data);
    }
  }
};

template<typename T>
struct enable_copy_ctor : optional_storage<T> {
  using base = optional_storage<T>;

  // ユーザー定義コピーコンストラクタ
  enable_copy_ctor(const enable_copy_ctor& that)
    : base()
  {
    this->construct_from(static_cast<const base&>(that));
  }

  // 他のは全部基底のものか上で定義されるものに頼る!
  enable_copy_ctor() = default;
  enable_copy_ctor(enable_copy_ctor&&) = default;
  enable_copy_ctor& operator=(const enable_copy_ctor&) = default;
  enable_copy_ctor& operator=(enable_copy_ctor&&) = default;

};

template<typename T>
using check_copy_ctor = std::conditional_t<
  std::is_trivially_copy_constructible_v<T>,
  optional_storage<T>,
  enable_copy_ctor<T>
>;

template<typename T>
class my_optional : private check_copy_ctor<T> {
public:
  // 他略

  // コピーコンストラクタ
  // copy_ctor_enabler<T>のコピーコンストラクタを利用する
  my_optional(const my_optional& that) = default;
};

C++11以降の共用体は内包する型の特殊メンバ関数トリビアルでないならば、対応する自身の特殊メンバ関数が暗黙deleteされます。従って、optional_storageではデストラクタ以外をとりあえず全部default定義しておけば、トリビアルの時だけは定義されていることになります。

それを利用し、Ttrivially copyableの時だけ、my_optionalからoptional_storageに至るクラス階層にコピーコンストラクタをユーザー定義するクラスを追加し、そうでなければoptional_storageを直接利用します。すると、最上位my_optionalクラスからはその基底クラスのコピーコンストラクタは常に何かしら定義されているように見えるため、my_optionalのコピーコンストラクタはdefaultで定義する事ができます。

派生クラスのコンストラクタ初期化子リストからは最基底のoptional_storageのメンバは触れませんので、optional_storageにはコンストラクタが必要です。また、フラグの管理とか構築周りのことを共通化するためにoptional_storageconstruct()/construct_from()関数を追加しておきます。

同じようにムーブコンストラクタを定義しましょう。

template<typename T>
struct enable_move_ctor : check_copy_ctor<T> {
  using base = check_copy_ctor<T>
  
  // ユーザー定義ムーブコンストラクタ
  enable_move_ctor(enable_move_ctor&& that)
    : base()
  {
    this->construct_from(static_cast<base&&>(that));
  }

  // コピーコンストラクタはenable_copy_ctorで定義されるか
  // optional_storageでトリビアルに定義される
  enable_move_ctor(const enable_move_ctor&) = default;

  enable_move_ctor() = default;
  enable_move_ctor& operator=(const enable_move_ctor&) = default;
  enable_move_ctor& operator=(enable_move_ctor&&) = default;
};

template<typename T>
using check_move_ctor = std::conditional_t<
  std::is_trivially_move_constructible_v<T>,
  check_copy_ctor<T>,
  enable_move_ctor<T>
>;

template<typename T>
class my_optional : private check_move_ctor<T> {
public:
  // 他略

  // ムーブコンストラクタ
  my_optional(my_optional&&) = default;
};

my_optionalcheck_copy_ctorの間に、さっきと同じようなものを挿入してやるだけです、簡単ですね・・・

int main() {
  // パスする
  static_assert(std::is_trivially_destructible_v<my_optional<int>>);
  static_assert(std::is_trivially_copy_constructible_v<my_optional<int>>);
  static_assert(std::is_trivially_move_constructible_v<my_optional<int>>);
  static_assert(std::is_trivially_destructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_copy_constructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_move_constructible_v<my_optional<std::string>> == false);
}

intは当然トリビアルなクラスでありstd::stringは全ての特殊メンバ関数がそうではないので、このstatic_assert群によってちゃんとトリビアル性が伝播されている事がわかります。

代入演算子

残ったのはコピー/ムーブ代入演算子です。これは特別な事をする必要はほぼなく、コンストラクタの時と同様のアプローチによって実装できます。

// デストラクタがトリビアルでない場合のストレージ
template<typename T, bool = std::is_trivially_destructible_v<T>>
struct optional_storage {

  // 中略

  template<typename Self>
  void asign_from(Self&& that) {
    if (that.has_value) {
      if (this->has_value) {
        this->data = std::forward<Self>(that).data;
      } else {
        this->construct(std::forward<Self>(that).data);
      }
    } else {
      this->reset();
    }
  }
  
  void reset() {
    if (this->has_value) {
      this->data.~T();
      this->has_value = false;
    }
  }
};

// デストラクタがトリビアルである場合のストレージ
template<typename T>
struct optional_storage<T, true> {

  // 中略

  template<typename Self>
  void asign_from(Self&& that) {
    if (that.has_value) {
      if (this->has_value) {
        this->data = std::forward<Self>(that).data;
      } else {
        this->construct(std::forward<Self>(that).data);
      }
    } else {
      this->reset();
    }
  }
  
  void reset() {
    this->has_value = false;
  }
};

// 中略

template<typename T>
struct enable_copy_asign : check_move_ctor<T> {
  using base = check_move_ctor<T>;
  
  // ユーザー定義コピー代入演算子
  enable_copy_asign& operator=(const enable_copy_asign& that) {
    this->asign_from(static_cast<const base&>(that));
  }

  enable_copy_asign() = default;
  enable_copy_asign(const enable_copy_asign&) = default;
  enable_copy_asign(enable_copy_asign&&) = default;
  enable_copy_asign& operator=(enable_copy_asign&&) = default;
};

template<typename T>
using check_copy_asign = std::conditional_t<
  std::is_trivially_copy_assignable_v<T>,
  check_move_ctor<T>,
  enable_copy_asign<T>
>;

template<typename T>
class my_optional : private check_copy_asign<T> {
public:
  // 他略

  // コピー代入演算子
  my_optional& operator=(const my_optional&) = default;
};

代入演算子では自身の状態を一度無効化する必要がありますが、その処理はTのデストラクタがトリビアルであるかによって変化しますので、代入に伴うあれこれと共にoptional_storageに実装しておきます(asign_from()/reset())。

それを用いてenable_~_asignクラスで代入演算子を実装します。まあ、難しいところはないですね(とてもめんどくさいですね・・・)。

ムーブ代入演算子も同じように実装できます。

template<typename T>
struct enable_move_asign : check_copy_asign<T> {
  using base = check_copy_asign<T>;
  
  // ユーザー定義ムーブ代入演算子
  enable_move_asign& operator=(enable_move_asign&& that) {
    this->asign_from(static_cast<base&&>(that));
  }

  enable_move_asign() = default;
  enable_move_asign(const enable_move_asign&) = default;
  enable_move_asign(enable_move_asign&&) = default;
  enable_move_asign& operator=(const enable_move_asign&) = default;
};

template<typename T>
using check_move_asign = std::conditional_t<
  std::is_trivially_move_assignable_v<T>,
  check_copy_asign<T>,
  enable_move_asign<T>
>;

template<typename T>
class my_optional : private check_move_asign<T> {
public:
  // 他略

  // ムーブ代入演算子
  my_optional& operator=(my_optional&&) = default;
};

やることは同じです。これによってほぼ全ての特殊メンバ関数トリビアル性継承を実装することができました・・・

int main() {
  // 全てトリビアル
  static_assert(std::is_trivially_destructible_v<my_optional<int>>);
  static_assert(std::is_trivially_copy_constructible_v<my_optional<int>>);
  static_assert(std::is_trivially_move_constructible_v<my_optional<int>>);
  static_assert(std::is_trivially_copy_assignable_v<my_optional<int>>);
  static_assert(std::is_trivially_move_assignable_v<my_optional<int>>);
    
  // 全て非トリビアル
  static_assert(std::is_trivially_destructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_copy_constructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_move_constructible_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_copy_assignable_v<my_optional<std::string>> == false);
  static_assert(std::is_trivially_move_assignable_v<my_optional<std::string>> == false);

  // しかしユーザー定義されている
  static_assert(std::is_destructible_v<my_optional<std::string>>);
  static_assert(std::is_copy_constructible_v<my_optional<std::string>>);
  static_assert(std::is_move_constructible_v<my_optional<std::string>>);
  static_assert(std::is_copy_assignable_v<my_optional<std::string>>);
  static_assert(std::is_move_assignable_v<my_optional<std::string>>);
}

確かに、トリビアル性を継承しつつそうでない場合はユーザー定義、というようになっています。

実はもう少し厳密にやると、そもそもTがコピー可能でない場合に適切にdeleteするとかのハンドルが必要となりますが、主題ではないのでここではやりません。

このような複雑怪奇なテクニックはしかし、std::optionalstd::variantの実装で実際に使用されています。少なくともGCC/MSVCの実装はこうなっているはずです(MSVCは将来的に変更するかもしれませんが)。そして、std::expectedなど類似のクラスでも同じ事をする必要が出てくるでしょう。

※ このあたりを書くにあたってはMSVCの実装(<optional>と、xsmf_control.h)を大変参考にしています。特に、xsmf_control.hにはこのテクニックが一般化されまとまっていて、MSVCのoptional/variantはどちらも同じものを使用しています。これはある程度TMPがわかればなんとか読めるので、気になった人はそちらを参照してください。

階層構造

check_xxxxxみたいなエイリアステンプレートは、xxxxxに対応する特殊メンバ関数トリビアルでない場合にユーザー定義する層を挿入し、そうでないならスキップします。したがって、intのような全トリビアルなクラスでは階層は最小になります。

  • my_optional<int>
    • optional_storage<int>

一方、std::stringのように全部トリビアルではないクラスではフルで挿入されることになります。

  • my_optional<std::string>
    • enable_move_asign<std::string>
      • enable_copy_asign<std::string>
        • enable_move_ctor<std::string>
          • enable_copy_ctor<std::string>
            • optional_storage<std::string>

例えばムーブだけトリビアルでないような型(move_non_trivial)なら

  • my_optional<move_non_trivial>
    • enable_move_asign<move_non_trivial>
      • enable_move_ctor<move_non_trivial>
        • optional_storage<move_non_trivial>

のようなクラス階層になります。

一部のデバッガでは、このようなクラス階層を直接観測することができます(VSのデバッガだと多分途中が省略されるので見られない気がします)。あるいは観測してなんだこれ?と思ったことがあるかもしれません。

デフォルトコンストラク

optionalはその実装の都合上、デフォルトコンストラクタをトリビアルにすることができません。そのためoptional以外を例にすると、次のように書くことで要素型のtrivially default constructible性を継承できます。

template<typename T>
class wrap {
  T t;  // 初期化しない

public:
  wrap() = default;
};

他のコンストラクタが存在するとデフォルトコンストラクタは暗黙deleteされるため、defaultで書いておきます。この時、メンバに持っているTのオブジェクトに対してデフォルトメンバ初期化してしまうとトリビアルにならないので注意が必要です。

int main() {
  static_assert(std::is_trivially_default_constructible_v<wrap<int>>);  // パスする
}

またおそらく、このような単純な型ではその他の部分のトリビアル性継承時にも先程までのような謎のテクニックを駆使する必要はないはずです。

C++20 Conditionally Trivial Special Member Functions

C++20ではコンセプトが導入され、それを利用したConditionally Trivial Special Member Functionsという機能が追加されました。これはまさに、ここまで見てきた事をコンセプトによって簡易に実現するための機能です。

これによって、my_optional実装は次のようになります。

template<typename T>
class my_optional {
  bool has_value = false;
  union {
    char dummy;
    T data;
  };

public:

  // デフォルトコンストラクタ
  constexpr my_optional() 
    : has_value(false)
    , dummy{}
  {}

  // 値を受け取るコンストラクタ
  template<typename U=T>
  constexpr my_optional(U&& v)
    : has_value(true)
    , data(std::forward<U>(v))
  {}

  // トリビアルに定義できるならそうする
  my_optional(const my_optional& that) requires std::is_trivially_copyable_v<T> = default;
  my_optional(my_optional&& that) requires std::is_trivially_movable_v<T> = default;
  my_optional& operator=(const my_optional& that) requires std::is_trivially_copy_assignable_v<T> = default;
  my_optional& operator=(my_optional&& that) requires std::is_trivially_move_assignable<T> = default;
  ~my_optional() requires std::is_trivially_destructible_v<T> = default;


  // そうでない場合はユーザー定義する

  my_optional(const my_optional& that)
    : has_value(that.has_value)
    , dummy{}
  {
    if (that.has_value) {
      new (&this->data) T(that.data);
    }
  }

  my_optional(my_optional&& that)
    : has_value(that.has_value)
    , dummy{}
  {
    if (that.has_value) {
      new (&this->data) T(std::move(that.data));
    }
  }

  my_optional& operator=(const my_optional& that) {
    auto copy = that;
    *this = std::move(copy);
    
    return *this;
  }

  my_optional& operator=(my_optional&& that) {
    if (that.has_value) {
      if (this->has_value) {
        this->data = std::move(that.data);
      } else {
        new (&this->data) T(std::move(that.data));
      }
    } else {
      this->reset();
    }

    return *this;
  }

  ~my_optional() {
    this->reset();
  }
  
  // reset()の定義も同様の記法で分岐できる

  void reset() requires std::is_trivially_destructible_v<T> {
    this->has_value = false;
  }

  void reset() {
    if (this->has_value) {
      this->data.~T();
    }
    this->has_value = false;
  }
};

defaultな特殊メンバ関数に対してrequiresによる制約を付加する事で、テンプレートパラメータの性質によって定義するしないを分岐することができ、100行以上も謎のコードを削減することができました・・・

ここでは、オーバーロード解決時の制約式による半順序に基づいて、特殊メンバ関数定義にも制約によって順序が付けられ、最も制約されている(かつそれを満たしている)1つだけが資格のある(eligible)特殊メンバ関数として定義され、それ以外はdeleteされます。

この場合、my_optionaldefaultな特殊メンバ関数定義はis_trivially_~によって制約されており、Tの対応する特殊メンバ関数トリビアルである時my_optionalの対応する特殊メンバ関数トリビアルな方が選択され、ユーザー定義のものは無制約なのでdeleteされます。逆に、Tの対応する特殊メンバ関数トリビアルではない時、制約を満たさないことからdefaultのものがdeleteされ、結果的に適切な一つだけが定義されています。

先ほどまで書いていたものすごく労力のかかった意味のわからないコードはこれによって不要になります。このConditionally Trivial Special Member Functionsという機能がいかに強力で素晴らしく、どれほどマイナーなのかがわかるでしょう!

そしてC++20以降、あのようなテクニックは忘れ去られていく事でしょう。この記事は、失われいく謎のテクニックを後世に伝えるとともに、理解しづらいConditionally Trivial Special Member Functionsという機能の解説を試みるものでした・・・

なぜにトリビアル

長いので分けました。そもそもなんでそこまでしてトリビアル性にこだわるのか?という事を書いています。

参考文献

この記事のMarkdownソース

[C++]トリビアルってトリビアル?

別のことを調べていたらなぜか出来上がっていたメモです・・・

ABIとtriviality

型(の特殊メンバ関数)がトリビアルであることは、ABIにとって重要です。

例えば、型Ttrivially default constructibleであればT t;のような変数宣言時に初期化処理を省略することができ、trivially destructibleであればtの破棄時(スコープ外になる時)にデストラクタ呼び出しを省略できます。この2つのトリビアル性はstd::vectorなどコンテナに格納した際にも活用されます。そして、型Tのコピー/ムーブコンストラクタがトリビアルであれば、Tのコピーはmemcpy相当の簡易な方法によってコピーすることができ、それは代入演算子でも同様です。

もしそれらがトリビアルでは無い時、コンパイラはそれらの呼び出しが必要になる所で必ずユーザー定義の関数を呼び出すようにしておく必要があります。それが実質的にトリビアル相当のことをしていたとしても、トリビアルでない限りは何らかの関数呼び出しが必要になります。もっともそのような場合、インライン展開をはじめとする最適化によってそのような呼び出しは実質的に省略されるでしょう。

より重要なのは(あるいは問題となるのは)、トリビアルでない型のオブジェクトが関数引数として値渡しされる時、あるいは戻り値として直接返される時、静かなオーバーヘッドを埋め込んでしまうことです。

どういうことかというと、Tのオブジェクトを値渡しした時に、Tトリビアル型であればレジスタに配置されて渡される(可能性がある)のに対し、Tが非トリビアル型であるとスタック上に配置したオブジェクトのポインタ渡しになり、戻り値型についてもほぼ同様のことが起こります。これはC++コード上からは観測できず、出力されたアセンブラを確認して初めて観測できます。

// トリビアルな型
struct trivial {
  int n;
};

// 非トリビアルな型
struct non_trivial {
  int n;

  ~non_trivial() {}
};

// 引数渡し
int f(trivial t);
int f(non_trivial t);

// 戻り値で返す
trivial g1() {
  return {20};
}
non_trivial g2() {
  return {20};
}

void h(int);

int main() {
  int n1 = f(trivial{10});
  int n2 = f(non_trivial{10});
}

GCCのものをコピペすると、次のようなコードが生成されています。

g1():
        mov     eax, 20
        ret
g2():
        mov     DWORD PTR [rdi], 20
        mov     rax, rdi
        ret
main:
        sub     rsp, 24
        # f(trivial)の呼び出し
        mov     edi, 10
        call    f(trivial)
        # f(not_trivial)の呼び出し
        lea     rdi, [rsp+12]
        mov     DWORD PTR [rsp+12], 10
        call    f(non_trivial)
        # main()の終了
        xor     eax, eax
        add     rsp, 24
        ret

godbolt上で見ると対応がより分かりやすいかと思います。

f(trivial)の呼び出し時はediレジスタ(32bit)に即値10を配置して(trivial型を構築して)呼び出しているのに対し、f(not_trivial)の呼び出し時は、rdiレジスタ(64bit)にrsp(スタックポインタ)の値に12を足したアドレスをロードし、その領域に即値10を配置して(non_trivial型を構築して)から呼び出しを行なっています。
rdiレジスタはx64の呼び出し規約において整数/ポインタ引数に対して最初に使用されるレジスタであり、ediレジスタrdiの下位32bitの部分で役割は同様です。したがって、f(trivial)の呼び出しではtrivial型をレジスタに構築して渡しているのに対して、f(not_trivial)の呼び出し時はnon_trivial型をスタック上に配置してそのポインタを渡しています。

今度は、g1(), g2()の定義について生成されたコードを見てみると、trivial型を返すg1()eaxレジスタ(32bit)に即値20を配置して(trivial型を構築して)returnしているのに対し、non_trivial型を返すg2()rdiレジスタ(64bit)の値をポインタとして読みその領域に即値20を配置し(non_trivial型を構築し)、raxレジスタ(64bit)にrdiの値をコピーしてからreturnしています。
raxレジスタはx64の呼び出し規約において戻り値を返すのに使用されるレジスタであり、eaxはその下位32bit部分で役割は同様です。したがって、g1()returnではtrivial型をレジスタに構築して返しているのに対して、g2()returnではnon_trivial型をスタック上に配置してそのポインタを渡しています。

MSVCはf()の呼び出しがどちらも同じコードを生成していますが、g1(), g2()GCC/clangと同じことをしているのが分かります。

このトリビアル型と非トリビアル型の扱いの差異は、ABIによって規定され要求されている事です(MSVCとGCC/clangの差異も使用しているABIの違いによります)。そしておそらく、C++における各種トリビアル性はこうしたABIからの要請によって生まれた規定でしょう。

有名な所では、std::unique_ptrトリビアル型ではないために生ポインタと比較した時にこの種のオーバーヘッドを発生させてしまっています。このことによるオーバーヘッドは微々たるものですが、例えばそれがヘビーループの中で起こっていると問題となるかもしれません。しかもこの事は非常に認識しづらく、よく知られてはいません。このため、std::optional/std::variantに見られるように、近年(C++17以降くらい)の標準ライブラリのクラス型はトリビアル性に注意を払って設計されるようになりました。

とはいえ、MSVC ABIにおけるstd::spanのように(std::spanは常にトリビアル型)、ABIの別の事情によってこの種のオーバーヘッドが発生してしまっていたりと、ABIにまつわる問題は複雑で把握しづらいものがあります・・・

各種ABIでのトリビアル性と引数渡し、戻り値返し

Itanium C++ ABI

1.1 Definitions non-trivial for the purposes of callsで定義されています。

A type is considered non-trivial for the purposes of calls if:

  • it has a non-trivial copy constructor, move constructor, or destructor, or
  • all of its copy and move constructors are deleted.

これはItanium C++ ABIの定める非トリビアルな型の定義で、以下のどちらかの時にクラス型は非トリビアルとして扱われます

  • コピー/ムーブコンストラクタおよびデストラクタのいずれか一つでも非トリビアルである
  • 全てのコピー/ムーブコンストラクタがdeleteされている

さらにすぐ下にはこう書かれています。

This definition, as applied to class types, is intended to be the complement of the definition in [class.temporary]p3 of types for which an extra temporary is allowed when passing or returning a type. A type which is trivial for the purposes of the ABI will be passed and returned according to the rules of the base C ABI, e.g. in registers; often this has the effect of performing a trivial copy of the type.

この定義に該当する非トリビアルな型は、引数として渡すときと戻り値として返す時に一時オブジェクトを作成して返すことが許容され、そうでない型はレジスタ等で受け渡される、みたいな事を言っています。これがまさに先ほどの生成コードに現れている静かなオーバーヘッドの正体であり根拠です。
「non-trivial for the purposes of calls」という用語からもトリビアルという性質がABI(特に関数呼び出しの都合)からきている事が窺えます。

そして、この定義を用いて、関数呼び出し時の非トリビアル型引数について次のように規定されています(3.1.2.3 Non-Trivial Parameters

If a parameter type is a class type that is non-trivial for the purposes of calls, the caller must allocate space for a temporary and pass that temporary by reference. Specifically:

  • Space is allocated by the caller in the usual manner for a temporary, typically on the stack.
  • The caller evaluates the argument in the space provided.
  • The function is called, passing the address of the temporary as the appropriate argument. In the callee, the address passed is used as the address of the parameter variable.
  • If the type has a non-trivial destructor, the caller calls that destructor after control returns to it (including when the caller throws an exception).
  • If necessary (e.g. if the temporary was allocated on the heap), the caller deallocates space after return and destruction.

意訳

トリビアルな型のオブジェクトを関数引数として渡す時、呼び出し元が一時オブジェクトを作成しその参照を渡さなければならない。具体的には

  • 呼び出し元は、一時オブジェクトを作成する通常の方法で、一般的にはスタック上に領域を確保し構築する
  • 呼び出された側(関数内)は、その提供された領域で引数を評価する
  • 関数は、その一時オブジェクトのアドレスを適正な引数として受け取って呼び出される。呼び出された側では渡されたアドレスが引数変数のアドレスとして使用される
  • 型が非トリビアルデストラクタを持つ場合、呼び出し側は関数がリターンした後(制御を戻した後)にデストラクタを呼び出す(関数が例外を投げた場合も同様)
  • 関数のリターンとデストラクタ呼び出しの後、呼び出し側は必要に応じて一時オブジェクトに割り当てられていた領域を解放する(一時オブジェクトがヒープに構築されていた場合など)

戻り値の非トリビアル型引数について次のように規定されています(3.1.3.1 Non-trivial Return Values

If the return type is a class type that is non-trivial for the purposes of calls, the caller passes an address as an implicit parameter. The callee then constructs the return value into this address. If the return type has a non-trivial destructor, the caller is responsible for destroying the temporary when control is returned to it normally. If an exception is thrown out of the callee after the return value is constructed but before control returns to the caller, e.g. by a throwing destructor, it is the callee's responsibility to destroy the return value before propagating the exception to the caller. Thus, in general, the caller is responsible for destroying the return value after, and only after, the callee returns control to the caller normally.

The address passed need not be of temporary memory; copy elision may cause it to point anywhere, including to global or heap-allocated memory.

意訳

戻り値の型が非トリビアル型である場合、呼び出し側は暗黙のパラメータとしてアドレスを渡す。呼び出された側は、そのアドレスに戻り値を構築する。戻り値型が非トリビアルデストラクタを持つ場合、呼び出し側には制御が戻った後でこの一時オブジェクトを破棄する責任が発生する。
戻り値が構築された後呼び出し元に制御が戻る前に、呼び出された関数から例外が送出された場合(ローカル変数のデストラクタからの例外送出など)、呼び出し元に例外を送出する前に戻り値オブジェクトを破棄する(デストラクタを呼び出す)のは呼び出された側(関数内)の責任である。
したがって、一般的には、呼び出し側は呼び出した関数が正常にリターンした場合にのみ戻り値を破棄する責任を負う。

この暗黙に渡される戻り値格納用領域のアドレスは、スタックなどの一時領域のものである必要はなく、コピー省略などによってグローバル領域やヒープ領域のアドレスなど、どこを指していても構わない。

先ほどのサンプルコードを改めて見てみると、まさにこのあたりに書かれている通りになっている事がわかります。

ところで、非トリビアル型戻り値に関する規定の最後の一文は少し驚きです。

// Tは何かしら非トリビアル型とする
T f();

int main() {
  T* p = new T(f());
}

C++17以降コピー省略が保証されているため、このような場合にf()の戻り値はpの領域に直接構築されることになり、先ほどの規定によると、new式によるメモリの確保->f()の評価->Tの構築、のような順番で処理が実行されることが示唆されます。すなわち、new式が行なう2つのこと(メモリの確保とオブジェクトの構築)の間にf()の評価が挟まる事になり、この評価順序はかなり非自明です。

System V AMD64 ABI

System V AMD64 ABIはItanium C++ ABIを参照しており、「non-trivial for the purposes of calls」という言葉とその定義をそのまま使用しています。したがって、System V AMD64 ABIにおけるトリビアルな型とは先ほどのItanium C++ ABIにおけるそれと同様という事になります。

その扱いについて、「3.2.3 Parameter Passing」のクラス型の引数渡しについての欄外に次のようにあります

An object whose type is non-trivial for the purpose of calls cannot be passed by value because such objects must have the same address in the caller and the callee. Similar issues apply when returning an object from a function. See C++17 [class.temporary] paragraph 3.

トリビアルな型のオブジェクトは、呼び出し元と呼び出された側で同じアドレスを持っている必要があるため、値で渡す事ができず、関数からオブジェクトを返す場合も同様の問題がある。のように書かれています。

この一文は非トリビアル型がなぜ特別扱いされるのか?という疑問の回答となるものです。非トリビアル型のオブジェクトが関数の呼び出し元と呼び出された側で同じアドレスを持っている必要がある、というのは非トリビアルなコピー/ムーブコンストラクタおよび非トリビアルデストラクタの呼び出しを避けるためでしょう。関数の呼び出しに伴って実装の予測できないユーザー定義の関数(コピー/ムーブコンストラクタ等)を何度も呼び出す可能性(レジスタとスタックやメモリとの間のコピー)が生じるというのはとてつもないオーバーヘッドになります。あるいは、コピーやムーブがトリビアルでない型のオブジェクトのビット表現(バイト列)をただコピーすることには意味が無いか有害ですらある可能性があります。その様な事を避けるために、レジスタの外、関数の呼び出し前後で消えたりしない領域に一時オブジェクトを作成してその領域を共有していると考えられます。

逆に、トリビアルな型ではコピー/ムーブコンストラクタはmemcpy相当(CPUにとっては普通のコピー)、トリビアルデストラクタは省略可能であるので、レジスタにコピーして渡したり、レジスタからコピーして受け取ったりと言ったことを何の問題もなく行う事ができます。

また、System V AMD64 ABIの規定として、トリビアル型であっても64 [byte]を超える場合はスタック領域上のポインタ渡し(非トリビアル型と同様)となるようです。

ARM64 C++ ABI

ARM64のC++ABIはItanium C++ ABIを参照しており、トリビアルの定義もそのまま使用しています。したがって、ARM64 C++ ABIにおけるトリビアルな型とはItanium C++ ABIにおけるそれと同様という事になります。

ただし、関数の戻り値型で非トリビアルな型を返すときの指定が変更されています。4.1 Summary of differences from and additions to the generic C++ ABI より

When a return value has a non-trivial copy constructor or destructor, the address of the caller-allocated temporary is passed in the Indirect Result Location Register (r8) rather than as an implicit first parameter before the this parameter and user parameters.

意訳

戻り値型に非トリビアルなコピーコンストラクタかデストラクタがある場合、呼び出し側が割り当てた戻り値用一時領域へのアドレスは、暗黙の第一引数ではなくIndirect Result Location Register(x8)に渡される。

トリビアル型を返す関数を呼び出すときに呼び出し側が用意する領域へのアドレスを特定のレジスタを経由して渡す、という事を言っているだけでやることは変化していません。用は、ARM64では非トリビアル型を返すときに専用のレジスタを使用するわけです。

呼び出し時のことについては特に変更がないため、Itanium C++ ABIの時と同様となります。

Procedure Call Standard for the Arm 64-bit Architecture6.4.2 Parameter Passing Rulesには関数呼び出し時の引数の渡し方について規定されており、それによると、トリビアルな型であっても16 [byte]を超える型はレジスタ渡しではなくスタック領域へのポインタ渡しとなるようです。

Windows x64 呼び出し規約 (Windwos x64 ABI)

Windwos x64 ABIは当然?ながらItanium C++ ABIを参照せず、独自定義しています。

引数渡しでは、特にトリビアル型とそうではないクラス型の区別がなく、次のように規定されています

サイズが 8、16、32、または 64 ビットの構造体と共用体、および __m64 型は、同じサイズの整数であるかのように渡されます。他のサイズの構造体または共用体は、呼び出し元によって割り当てられたメモリへのポインターとして渡されます。

つまりWindows x64 ABIでは、関数引数に渡す分にはクラス型のサイズが8 [byte]以下であることしか求められていません。先ほどのサンプルのgodboltの出力も確かにそうなっていました。

なお、この規定のせいで、std::spanstd:string_view(どちらもポインタ1つとstd::size_t1つの16 [byte])と言った軽量なはずのクラスオブジェクトはレジスタ渡しされません。System V AMD64 ABIでは64 [byte]が閾値だったので、できないわけでは無いはずですが・・・

戻り値ではクラス型について要件が次のように課されています

ユーザー定義型は、グローバル関数や静的メンバー関数からの値で返すことができます。 RAX の値によってユーザー定義型を返すには、その長さが 1、2、4、8、16、32、または 64 ビットである必要があります。 また、ユーザー定義のコンストラクター、デストラクター、またはコピー代入演算子は含まれません。 プライベートまたは保護された非静的データ メンバー、および参照型の非静的データ メンバーは含まれません。 基底クラスまたは仮想関数は含まれません。 これらの要件を満たすデータ メンバーのみが含まれます。 (この定義は、実質的には C++03 POD 型と同じです。 C++11 標準ではこの定義は変更されているため、このテストに std::is_pod を使うことはお勧めしません。)それ以外の場合、呼び出し元で、戻り値のメモリを割り当て、最初の引数として、その戻り値用のポインターを渡す必要があります。 残りの引数は、引数 1 つ分だけ右にシフトされます。 RAX 内の呼び出し先は同じポインターを返す必要があります。

まず引数の時と同様に8 [byte]のサイズ要件があり、トリビアルに近い事が要求されます。要件を抜き出して並べると次のようになります

  • ユーザー定義コンストラクタを持たない
  • ユーザー定義コピー代入演算子を持たない
  • ユーザー定義デストラクタを持たない
  • public以外の非静的メンバ変数を持たない
  • 参照型の非静的メンバ変数を持たない
  • 基底クラスを持たない
  • 仮想関数を持たない
  • メンバ変数型もこれらの要件を満たす

これは明らかにItanium C++ ABIのトリビアルよりも厳しい要件です。また、これはC++03のPOD型の要件であるようですが、C++11 std::is_podとは異なるようです。

しかし、この要件に沿わない型は呼び出し元が用意した領域へのポインタを受け取り、そこに構築して返される、というところはItanium C++ ABIと共通しています。

Windows ARM64 ABI

ARM64 ABI 規則の概要には次のようにあるので、x64の時と異なりWindows ARM64 ABIは先ほど見たARM64 ABIに準ずるようです。

Windows 用の基本的なアプリケーション バイナリ インターフェイス (ABI) を、ARM プロセッサ上で 64 ビット モード (ARMv8 以降のアーキテクチャ) でコンパイルして実行するとき、ほとんどの部分は ARM の標準 AArch64 EABI に従います。

ただ、大元のARM64 ABIではC++ ABIについてはItanium C++ ABIを参照していましたが、ここ(Windows ARM64 ABI)ではItanium C++ ABIを参照しておらず、独自に規定しているようです。

引数渡しでは、C++のクラス型は複合型(Composite Type)として次のように規定されています

引数の型が 16 バイトより大きい複合型である場合、引数は呼び出し元によって割り当てられたメモリにコピーされ、その引数がそのコピーへのポインターによって置き換えられます。

クラス型のサイズが16 [byte]以下であれば、レジスタ渡しになります。特にトリビアルであるかどうかは指定がありません。Windowsは引数渡しに関してはサイズしか気にしないようです。

戻り値では戻り値型について規定していますが、型の分類によって分岐し少し複雑です(以下適宜英語版を参照し訳を修正しています)

4 つ以下の要素を持つ HFA と HVA の値が、s0-s3、d0-d3、または v0-v3 で適宜返されます。

HFA/HVAとは次の要件を満たすクラス型です

HFA型はfloatdouble型のメンバで構成された集成体のような型の事で、HVA型はショートベクタ型のメンバで構成された集成体のような型の事です。そして、4要素以下のHFA/HVA型の場合はレジスタ返しになるようです。

それ以外のクラス型については次のようにあります

値で返される型は、特定のプロパティがあるかどうか、および関数が静的でないメンバー関数であるかどうかによって、異なる方法で処理されます。 型に次のプロパティがすべて含まれる場合:

トリビアルという言葉が出てきましたが、C++14集成体型であることを指定されているため、x64の時と同じくらいに厳しいものです。

そして、この要件を満たし16 [byte]以下の型の場合はレジスタ返しされます。16 [byte]を超える型については

16 バイトを超える型の場合、呼び出し元は十分なサイズのメモリ ブロックと、結果を保持するためのアラインメントを予約する必要があります。 メモリ ブロックのアドレスは、x8 内の関数に追加の引数として渡す必要があります。 呼び出し先は、サブルーチンの実行中の任意の時点で、結果のメモリ ブロックを変更する場合があります。 呼び出し先は、x8 に格納されている値を保持する必要はありません。

とあり、それ以外の(そもそも要件を満たさない)型については

呼び出し元は、十分なサイズのメモリ ブロックと、結果を保持するためのアラインメントを予約する必要があります。 メモリ ブロックのアドレスは、x0 内の関数に追加の引数として渡す必要があります。また、$thisx0 に渡される場合は、x1 に渡す必要があります。 呼び出し先は、サブルーチンの実行中の任意の時点で、結果のメモリ ブロックを変更する場合があります。 呼び出し先は、x0 内のメモリ ブロックのアドレスを返します。

結局はどちらも、呼び出し側が戻り値用領域を確保して暗黙の引数としてそのアドレスを渡し、呼び出された側はそこに戻り値を構築してリターンする、という何度か見たことをしています。しかし、その際に使用されるレジスタが異なり(x8x0orx1)そのレジスタの値を保持すべきかも逆で、戻り値としてそのアドレスを返すかが異なっています。なして・・・?

引数が多い時

全てのABIで共通することですが、引数が多くあり後ろの方の引数がレジスタに配置できない場合、本来レジスタ渡しできる型の値であってもスタック上に配置して渡されます。

C++ ABI?

Itanium C++ ABIは元々intelのItanium系CPUの上で動作するC++プログラムのために定められたABIでしたが、その後一般化され、より広範な64bitプロセッサのためのC++ABIへと進化しました。その結果、Windows環境以外を対象とする多くのC++コンパイラで採用され、実質的なデファクトスタンダードとなっています。

Itanium C++ ABIはその名の通りC++のためのABIです。名前空間やテンプレートのマングリングや例外、オーバーロード、クラス、(仮想)メンバ関数などC++特有の事情をどうやってハードウェアにマップするかなどを定めたものです。そのため特にプロセッサに依存するより基礎的な部分のABIについては指定されておらず、あくまでC++の部分のABIについてのみ規定したものです。そのため、特定のプロセッサに依存せずに書かれており、それによって多くのコンパイラで採用されるに至りました(多分)。

Itanium C++ ABIではより基礎的な部分のABIのことをC ABIと呼んでいます。Itanium C++ ABIは任意のC ABIの上に重ねることによって特定のプラットフォームにおけるC++ABIとして機能します。例えばx86-64ならばSystem V AMD64 ABIが、ARM64ならばApplication Binary Interface for the Arm® ArchitectureがC ABIに該当しています。

前述のように、x86-64もARM64もC++ABIについてはItanium C++ ABIを参照しているため、64bit(非Windows)環境のC++ABIとはイコールItanium C++ ABIの事です。ただし、プラットフォーム固有のABIの指定する所によって、変更されている部分はあります。

参考文献

この記事のMarkdownソース

[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に転送される予定でした。しかし、著者の方のレスポンスが無くなり進行していなかったため、この提案が新しい提案として再提出されました。

おわり