[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に転送される予定でした。しかし、著者の方のレスポンスが無くなり進行していなかったため、この提案が新しい提案として再提出されました。

おわり

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

文書の一覧

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

N4887 PL22.16/WG21 agenda: 7 June 2021, Virtual Meeting

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

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

N4888 WG21 virtual meetings: 2021-06, and -10

今年のWG21全体会議の予定表。

次は10月に予定されています。これもオンラインで行われることが決定しています。

N4889 WG21 admin telecon meeting: 2021-09

10月の全体会議の直前に行われる管理者ミーティングの予定表。

N4890 WG21 2021-05 Admin telecon minutes

2021年5月24日に行われた、管理者ミーティングの議事録。

N4891 WG21 2021-06 Virtual Meeting Minutes of Meeting

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

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

N4892 Working Draft, Standard for Programming Language C++

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

N4893 Editors' Report - Programming Languages - C++

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

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

P0205R1 Efficient Seeding of Random Number Engines

乱数シードの生成ヘルパーであるstd::seed_adapterを追加する提案。

<random>の乱数エンジンを使用する際、次の様に初期化を行うのが非常に一般的です。

template <typename EngineT>
void seed_non_deterministically_1st(EngineT& engine) {
  std::random_device device{};
  engine.seed(device());
}

しかし、EngineTの内部状態が大きい場合、この初期化は適切ではありません。より良い乱数の生成のためには内部状態の初期値が全て偏りなく初期化されている必要がありますが、たとえばstd::mt19937の場合その内部状態は19968bitある一方で、random_deviceの戻り値型はunsigned intであり多くの環境で32bitの大きさしかなく、初期状態の選択に偏りが生じます。それによって、エンジンが生成する乱数値にも偏りが生じる事があリます。

この様な問題に対処するために、std::seed_seqを利用する事ができます。std::seed_seqはシード列を表現するための型で、イテレータ範囲などによって任意の数の整数値から初期化し、それによって生成される32bit整数によるシード列を.generate()メンバ関数から取得する事ができます。あるいは、エンジンの.seed()メンバ関数に渡すこともできます。

template <typename EngineT, std::size_t StateSize = EngineT::state_size>
void seed_non_deterministically_2nd(EngineT& engine) {
  using engine_type = typename EngineT::result_type;
  using device_type = std::random_device::result_type;
  using seedseq_type = std::seed_seq::result_type;

  constexpr auto bytes_needed = StateSize * sizeof(engine_type);
  constexpr auto numbers_needed = (sizeof(device_type) < sizeof(seedseq_type))
      ? (bytes_needed / sizeof(device_type))
      : (bytes_needed / sizeof(seedseq_type));

  // シード列のシード?を生成
  std::array<device_type, numbers_needed> numbers{};
  std::random_device device{};
  std::generate(std::begin(numbers), std::end(numbers), std::ref(device));

  // シード列によるエンジンの初期化
  std::seed_seq seedseq(std::cbegin(numbers), std::cend(numbers));
  engine.seed(seedseq);
}

このコードにはいくつか問題があります。

  • 複雑
  • 乱数エンジンはその状態サイズを公開していない(あるいは、そう規定していない)
  • 正確ではない(std::seed_seqは偏りをもたらしうる)
  • 非効率
    • std::random_device -> std::array(スタック) -> std::seed_seq(ヒープ)とコピーされる
    • std::random_deviceは必要となる乱数のサイズを取らないため、実装によっては乱数取得が非効率になる

この提案は、この様なシード列による乱数エンジンの初期化という作業を効率的かつ簡易に行うためのstd::seed_adapterを提案するものです。

template <typename EngineT>
void seed_non_deterministically_3rd(EngineT& engine) {
  std::random_device device{};
  std::seed_adapter adapter{device};
  engine.seed(adapter);
}

std::seed_adapterは余分なコピーや一時オブジェクトが不要で、動的メモリ確保を必要とせず、偏りを導入しないシード列を表現する型です。

std::seed_adapterは任意のUniform Random Bit Generatorクラス(例えばstd::random_device)の参照をラップし、その関数呼び出し演算子を呼び出す.generate()メンバ関数を提供するクラステンプレートです。

P1068R4が採択された場合、ラップしているGeneratorクラスの関数呼び出し演算子は一度だけ呼び出せば良くなります。採択されない場合でも、例えばstd::seed_adapterの実装に合わせた最適な呼び出しをサポートする事が可能です。

std::seed_adapterは例えば次の様な小さなクラス型になります。

template <uniform_random_bit_generator U>
class seed_adapter {
public:
  // types
  using result_type = typename U::result_type;

  // constructors
  explicit constexpr seed_adapter(U& gen) noexcept;

  // generating functions
  template <random_access_iterator It>
    void constexpr generate(const It f, const It l)
    requires __unsigned_integral_least32<typename iterator_traits<It>::value_type>;

private:
  U* m_gen;  // exposition only
};

この部分は以下の方によるご指摘によって成り立っています。

P0447R15 Introduction of std::hive to the standard library

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

以前の記事を参照

このリビジョンでの変更は、splice()が例外を投げる場合の詳細を追記したこと、reshape(), splice()の設計の選択についての追記、(common_rangeではない場合の)番兵を用いたassign()の追加等です。

そして、このリビジョンより、名前がstd::colonyからstd::hiveへ変更されています。これはP2332R0を受けてのものです。

P0533R7 constexpr for cmath and cstdlib

P0533R8 constexpr for cmath and cstdlib

<cmath><cstdlib>の一部の関数をconstexpr対応する提案。

以前の記事を参照

R7での変更は、ベースとなる規格ワーキングドラフトをN4878に変更した事と、std::lerpconstexprとして<cmath>に追加されたことについてのメモを追記し、LEWGのレビューを受けて文言を修正した事です。

R8での変更は、記載サンプルをいくつか改善・修正し、constexprにする関数選択基準を修正したことと、LEWGのレビューを受けて文言を修正した事です。

この提案は2021年2月の全体会議で投票にかけられましたが、反対が多く否決されました。そこでの主な反対意見は実装経験の少なさと実装可能性についてのものでした。それを受けて次の2つのガイダンスを採択しました。

  • <cmath>関数のcosntexpr評価において、math_errhandling & MATH_ERRNO == trueとなる場合にerrnoを設定するようなエラー発生した場合、コンパイルエラーとする
  • 提案している<cmath>関数のconstexpr評価を、適用可能なすべての浮動小数点数型についてCのAnnexFに従うようにする

これらの事をベースに、再びC++23導入を目指します。

P1018R11 C++ Language Evolution status 🦠 pandemic edition 🦠 2021/05

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

P1072R8 basic_string::resize_and_overwrite

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

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加し、string::resize()の効果について曖昧な点を修正した事、ベースとなる規格ワーキングドラフトをN4885にしたことなどです。

この提案はC++23導入を目指してLEWGからLWGへ転送されました。

P1132R8 out_ptr - a scalable output pointer abstraction

スマートポインタとポインタのポインタを取るタイプのC APIとの橋渡しを行う、std::out_ptrの提案。

以前の記事を参照

このリビジョンでの変更はtypo修正がメインです。

この提案は2021年6月の全体会議で投票にかけられ、C++23入りが決定しています。

P1202R3 Asymmetric Fences

非対称なフェンスの提案。

並行処理における多くの同期アルゴリズム(ハザードポインタ、RCUなどなど)では遅いが稀にしか実行されない実行パスを保護するために、通常の実行パスに対してフェンスの挿入が必要となります。そのフェンスはあまり実行されないパスが実行された際にもデータレースを起こさないようにするためにあり、それによって通常の実行パスのパフォーマンスが低下します。現在のところC++には、これを回避するための簡易な(ライブラリユーザーの手間を必要としない)代替案が用意されていません。

例えばデッカーのアルゴリズムの例で見てみると

// グローバル変数
std::atomic_int x{0}, y{0};
int r1, r2;

// thread1(いつも実行される)
x.store(1, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
r1 = y.load(std::memory_order_relaxed);

// thread2(たまにしか実行されない)
y.store(1, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst);
r2 = x.load(std::memory_order_relaxed);

// 2つのスレッドが終了した後で、このアサートが発動することはない
assert(!(r1 == 0 && r2 == 0));

この場合のスレッド1とスレッド2の実行の頻度が大きく異なる場合、頻繁に実行される方の処理だけを見ればフェンスは必要ないはずですが、あまり実行されない処理との間で同期を取るために、両方にフェンスの挿入が必要になります。これによって、頻繁に実行される方の処理のパフォーマンスが低下する可能性があります。

この場合に、頻繁に実行される処理では軽いフェンスを、あまり実行されない処理では重い(普通の)フェンスを使用して同期を取ることができれば、頻繁に実行される処理におけるパフォーマンス低下を回避することができます。

この提案は、その意味で非対称なフェンスであるstd::asymmetric_thread_fence_light()std::asymmetric_thread_fence_heavy()の導入を目指すものです。

先ほどの例は次のように書くことができます。

// グローバル変数
std::atomic_int x{0}, y{0};
int r1, r2;

// thread1(いつも実行される)
x.store(1, std::memory_order_relaxed);
std::asymmetric_thread_fence_light(std::memory_order_seq_cst);   // コンパイラによるストア/ロードの入れ替えを防止する
r1 = y.load(std::memory_order_relaxed);

// thread2(たまにしか実行されない)
y.store(1, std::memory_order_relaxed);
std::asymmetric_thread_fence_heavy(std::memory_order_seq_cst);   // 通常のフェンス
r2 =  x.load(std::memory_order_relaxed);

// 2つのスレッドが終了した後で、このアサートが発動することはない
assert(!(r1 == 0 && r2 == 0));

std::asymmetric_thread_fence_heavy()std::atomic_thread_fence()とほぼ同等で、フェンスとしてのフル機能を持ちますが、std::asymmetric_thread_fence_light()atomic変数の読み書きが前後したり統一したりする事を防止する程度の事しかしないため軽量となります。そしてこの時でも、この2つのフェンスを介してatomic変数の読み書きに順序付けを行う(strongly happens before関係を与える)ことができます。

これを用いることによって、このような問題をライブラリの内部で解決することができるようになり、ユーザーはこのことについて何も気にしなくても良くなります。

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

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

前回の記事を参照

このリビジョンでの変更は、意図せず抜け落ちていたstd::addressofを対象に追加したことです。

この提案はLEWGからLWGに転送され、議論されています。

P1664R4 reconstructible_range - a concept for putting ranges back together

viewによって別のrangeに変換されてしまった範囲を、元のrange(と同じ型)に戻す操作、std::ranges::reconstructと関連するコンセプトの提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言を修正した事と、常にADLを使用するように設計を変更した事などです。

P1675R2 rethrow_exception must be allowed to copy

std::rethrow_exceptionの同じ例外オブジェクトを再スローするという規定を変更する提案。

std::current_exceptionの規定は注意深く書かれており、例外オブジェクトをスタック上に構築する実装(MSVC ABI)とヒープ上に構築して取り回す実装(Itanium C++ ABI)の両方をサポートし、例外オブジェクトをstd::exception_ptrの保持するメモリ領域にコピーするなどの実装の自由を与えています。

一方で、std::rethrow_exceptionはそうではなく、引数のstd::exception_ptrの参照している例外オブジェクトと同じオブジェクトを再スローする、と規定しています。これは、スタック上に例外オブジェクトを構築しているMSVC ABIでは実装不可能です(例外ハンドラ毎に例外オブジェクトのデストラクタを呼ぶため)。

MSVCの現在の実装は、std::rethrow_exceptionに割り当てられたメモリ領域に引数で指定されたstd::exception_ptrの参照する例外オブジェクトをコピーし、現在のアクティブな例外を指すTLSの値をその場所に更新してからthrowすることで、あたかもそれを再スローしているかのように実装されています。これは同じオブジェクトを再スローしていないため規格違反となっています・・・

この提案は、MSVCを含めた再スロー時に例外オブジェクトを区別する必要のあるABIのためにstd::rethrow_exceptionの規定を、例外オブジェクトのコピーとそのための追加のメモリ領域の使用を許可するように変更することで、この問題を解決しようとするものです。

次のようなコードにおいて

struct X : std::exception {
  // コピーが例外を投げる可能性のあるメンバを持つとする

  X() { }

  X(X const&) { 
    if(oh_no()) throw 42;
    /* else success */
  }
};

int main() {
  try {
    std::exception_ptr eptr;

    try { 
      throw X();
    }
    catch(X& x) {
      std::cout << "caught X with address " << (void*)&x;
      eptr = std::current_exception(); 
    }

    std::rethrow_exception(eptr);
  }
  catch(X& x) {
    std::cout << " caught X with address " << (void*)&x;
  } catch(int) {
    std::cout << " caught int";
  }
}

この提案の主張は次の3つの場合のいずれも起こりうることを許可することです

  1. caught X with address xxxx caught X with address xxxx
  2. caught X with address xxxx caught X with address yyyy
  3. caught X with address caught int

MSVCは1のケースを実装できないため、2か3を選ぶことになりますが、コピーが例外を投げることが許可されないために2と3の実装は許可されていません。この提案の目的は、この場合の2,3を許可することです。

P1689R4 Format for describing dependencies of source files

C++ソースコードを読み解きその依存関係をスキャンするツールが出力する依存関係情報のフォーマットを定める提案。

以前の記事を参照

このリビジョンでの変更はいくつかのキーの追加・削除など修正と、サンプルの更新などです。

P1708R5 Simple Statistical Functions

標準ライブラリにいくつかの統計関数を追加する提案。

以前の記事を参照

このリビジョンでの変更は、分位数(中央値)・最頻値について統計的な問題が提起されており別の提案に分離されることになったこと、stats_error例外が削除されたこと、イテレータの要素(のプロジェクション)型に応じて戻り値型を選択するstats_result_tの導入、いくつかの統計式エラーの修正、関数やクラス名、引数名を意味のあるものへ変更、rangeexecution_policyにまつわるいくつかのエラーの修正、などです。

P1967R4 #embed - a simple, scannable preprocessor-based resource acquisition method

コンパイル時(プリプロセス時)にバイナリデータをインクルードするためのプリプロセッシングディレクティブ#embedの提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善、依存関係スキャナー実装者や#embed実装者からのコメントを受けての構文の変更、名前付引数の実装拡張機能のサポートの追加などです。

このリビジョンでは、#embedが追加のパラメータ(及び属性指定)を取れるようになり、長さの指定方法が変更されました。

#embedの追加のパラメータは読み込むファイル名の後に指定します。ここでは。limit, prefix, suffix,emptyの4つが提案されています。

// limit引数、読み込みサイズを指定する
const int please_dont_oom_kill_me[] = {
    #embed "/dev/urandom" limit(512)
};
// sizeof(please_dont_oom_kill_me) == 16

// prefix引数、suffix引数
// リソース(ファイル)が空でない場合に指定したプリフィックスとサフィックスを付加する
const unsigned char null_terminated_file_data[] = {
    #embed "might_be_empty.txt" \
        prefix(0xEF, 0xBB, 0xBF, ) /* UTF-8 BOM */ \
        suffix(,)
    0 // always null-terminated
};

// empty引数
// リソースが空の場合に指定されたpp-tokenのリストを展開する
constexpr const char x[] = {
#embed "empty_file.dat" \
    empty((char)-1)
};
// sizeof(x) == 1
// x[0] == -1 or 255

以前の提案では読み取るサイズをファイル名の前に指定していましたが、このリビジョンからはlimit引数によってそれを指定します。limit(n)のように指定し、nは読み取る長さの最大長(バイト数)であって、初期化しようとする配列の要素数ではありません。

prefix引数、suffix引数は読み込むリソースが空であることを検知しやすくするためのものです。リソースが空ではない時読み取ったデータの先頭にprefixに指定されたものを展開し、データの末尾にsuffixで指定されたものを展開します。
empty引数は逆に、リソースが空であるときに指定されたものを展開し代わりのデータとします。

P2164R5 views::enumerate

元のシーケンスの各要素にインデックスを紐付けた要素からなる新しいシーケンスを作成するRangeアダプタviews::enumrateの提案。

以前の記事を参照

このリビジョンでの変更は以下のものです

  • views::enumrateの間接参照結果の型である集成体enumerate_resultについて、P2165R2の変更を仮定し単純な集成体のままとするとともにタプルインターフェースを追加した
  • ↑に伴って、enumrate_viewの参照型(reference)はenumerate_result、値型(value_type)はtupleとした(zipcartesian_prodcutとの一貫性向上)
  • P2165R2によれば、タプルライク型の各要素間にcommmon_referenceが存在していれば、そのタプルライク型とstd::tupleの間にもcommon_referenceが存在することを保証している
  • メンバ型count_typeindex_typeに変更

P2165R2を前提とすることで、R3で問題となったenumrate_viewの参照型(reference)と値型(value_type)のcommon_referenceについての問題を解決しています。

P2165R2 Compatibility between tuple, pair and tuple-like objects

std::pairと2要素std::tuple及びtuple-likeな型の間の非互換を減らし比較や代入をできるようにする提案。

前回の記事を参照

R1では、提案の対象はstd::pairと2要素std::tupleの間の非互換だけを対象にしていましたが、このリビジョンからはそれらと他のtuple-likeな型との間の非互換も対象にするようになりました。

tuple-like, pair-likeという説明専用のコンセプトを導入し、std::tuple, std::pairはそのコンセプトを満たす型のオブジェクトから構築・代入ができるようにします。pair-likeは2要素のtuple-likeとして定義され、ここでstd::tuplestd::pairの互換性が表現されます。

そして、tuple-like型に対してbasic_common_referencecommon_typeの特殊化を提供しておくことで、tuple-like型はstd::tuple, std::pairも含めた別のtuple-likeな型との間にcommon_referencecommon_typeを自然に持つようになります(対応する要素型についてcommon_referencecommon_typeが定義されている必要があります)。

constexpr std::pair p {1, 3.0};
constexpr std::tuple t {p}; // OK、現在できない

std::pair<int, double> pp{t}; //OK、現在できない

static_assert(std::tuple(p) == t);

// 2要素tupleとpairの間の比較が可能になる
static_assert(p == t);
static_assert(p <=> t == 0);

std::tuple<int,int> t = std::array {1, 2};  // OK、現在できない

std::tuple<int> t = std::array {1, 2};  // NG、サイズが異なる

std::map m{t, u};

static_assert(same_as<std::tuple<int>, range_value_t<decltype(views::zip(v))>>);
static_assert(same_as<std::tuple<int,int>, range_value_t<decltype(views::zip(v, v))>>);

auto x = true ? tuple{0,0} : pair{0,0}; // NG、2つの型は非互換

これらの変更は主に、rangesライブラリの拡張においてpairあるいはpeir-likeな型のシーケンスの各要素をtupleとみなして扱うことが容易にできるようにすることを念頭に置いています。例えば先程のenumrate_viewzip_viewなどがあります。

ただし、この変更はAPIの破壊的変更を伴う部分があります。

P2290R1 Delimited escape sequences

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

前回の記事を参照

このリビジョンでの変更は、提案する文言の古い注意事項を削除した事です。

EWGの議論では、提案する文言について改善が必要であるもののC++23に向けてこの提案を採択する方向で合意が取れているようです。

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

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

以前の記事を参照

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

P2299R3 mdspans of All Dynamic Extents

提案中のstd::mdspanのCTAD対応についての問題を報告する文書。

以前の記事を参照

以前のリビジョンが問題の周知と解決案を募るものであったのに対して、このリビジョンからは提案する文言と設計や考慮事項を説明した提案になっています。

そしてこの提案では、mdspanbasic_mdspanエイリアスである現状を修正し、basic_mdspanmdspanにリネームすることを提案しています。それが分かれているのはシンプルで使いやすいインターフェースを提供するためでしたが、そもそもそれが決定されたのはC++17以前のCTADが存在していない時代でした。mdspanの各要素を指定するためのdextentの導入とCTADによって、basic_mdspanmdspanの分割は必要なくなったという主張です。

この提案による変更の例。

mdspan<T> m(data, 16, 64, 64);  // 現在
mdspan<T> m(data, 16, 64, 64);  // この提案

mdspan<T, dynamic_extent, dynamic_extent, dynamic_extent> f();  // 現在
mdspan<T, dextents<3>> f();  // この提案

mdspan<T, 3, 3> m;           // 現在
mdspan<T, extents<3, 3>> m;  // この提案

mdspan<T, 3, 3> f();           // 現在
mdspan<T, extents<3, 3>> f();  // この提案

mdspan<T, 16, dynamic_extent, 64> m;           // 現在
mdspan<T, extents<16, dynamic_extent, 64>> m;  // この提案

mdspan<T, 16, dynamic_extent, 64> f();           // 現在
mdspan<T, extents<16, dynamic_extent, 64>> f();  // この提案

P2300R0 std::execution

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

これは、Executor提案にあるscheduler, sender, recieverという3つの抽象をベースとして、そのうえで任意の非同期処理を構成することができるようにするためのライブラリの提案です。

非同期アルゴリズム自体はP0443でも紹介されており、それは別の提案(P1897R3)に委ねられていましたが、P0443R14のLEWGにおけるレビューによっていくつかの設計変更が行われており、それに伴ってschedulersenderといった抽象の役割が変化したため、それを反映した非同期アルゴリズムを提案するとともにその設計詳細を記述しています。

基本的なサンプル

using namespace std::execution;

scheduler auto sch = get_thread_pool().scheduler();                           // 1

sender auto begin = schedule(sch);                                            // 2
sender auto hi_again = then(begin, []{                                        // 3
    std::cout << "Hello world! Have an int.";                                 // 3
    return 13;                                                                // 3
});                                                                           // 3
sender auto add_42 = then(hi_again, [](int arg) { return arg + 42; });        // 4

auto [i] = std::this_thread::sync_wait(add_42).value();                       // 5
  1. (例ではスレッドプールから)schedulerを取得する。schedulerは実行リソースを表現する軽量ハンドル。
  2. あるschedulerで一連の作業を開始するにはstd::execution::schedule()を呼び出す。これによって、そのscheduler上で処理を完了するsenderが得られる。
    • senderは非同期作業を記述し、その作業完了時にreciever(複数可)にシグナル(値、エラー、キャンセル)を通知する
  3. 非同期アルゴリズムによってsenderを生成し、非同期作業を構成する。std::execution::thenは入力senderinvocablefを受け取り、入力senderからのシグナルによってfを呼び出すsenderアダプタ。返されるsenderはその呼び出しの結果を通知する。
    • 例での入力senderschedule()からの直接のものなので値はなく(戻り値void)、受け取るinvocableは引数を取らない。生成されたsenderintを返す(完了時にint値を通知する)。
  4. ここでは、作業チェーンにさらに作業を追加している。ここのinvocableには前の処理が返したint値が送信され、ここではその値に42を加えて返している。結果はまたsenderとして得られるため、さらに任意の処理を任意の個数チェーンさせることができる。
  5. 構成した非同期パイプライン(非同期作業)全体を実行リソースに送信して、作業の完了を待つ準備が整った。ここまでの全ての作業は非同期であり、作業はまだ開始されていないかもしれない。作業を開始し完了をその場で待機するために、std::this_thread::sync_wait()を使用する。その結果は、std::optional<std::tuple<...>>で得られ、最後のsenderが値を返した場合はそれを有効値として保持し、キャンセルされた場合は空になり、エラーの場合は例外を送出する。

ここの例ではsenderを関数呼び出しによってチェーンさせていますが、rangeライブラリlikeにパイプライン演算子|)によって中間オブジェクトを省略しつつ直感的にチェーンさせることもできます。

非同期inclusive_scanのサンプル。

using namespace std::execution;

sender auto async_inclusive_scan(scheduler auto sch,                          // 2
                                 std::span<const double> input,               // 1
                                 std::span<double> output,                    // 1
                                 double init,                                 // 1
                                 std::size_t tile_count)                      // 3
{
  std::size_t const tile_size = (input.size() + tile_count - 1) / tile_count;

  std::vector<double> partials(tile_count + 1);                               // 4
  partials[0] = init;                                                         // 4

  return transfer_just(sch, std::move(partials))                              // 5
       | bulk(tile_count,                                                     // 6
           [=](std::size_t i, std::vector<double>& partials) {                // 7
             auto start = i * tile_size;                                      // 8
             auto end   = std::min(input.size(), (i + 1) * tile_size);        // 8
             partials[i + 1] = *--std::inclusive_scan(begin(input) + start,   // 9
                                                      begin(input) + end,     // 9
                                                      begin(output) + start); // 9
           })                                                                 // 10
       | then(                                                                // 11
           [](std::vector<double>& partials) {
             std::inclusive_scan(begin(partials), end(partials),              // 12
                                 begin(partials));                            // 12
             return std::move(partials);                                      // 13
           })
       | bulk(tile_count,                                                     // 14
           [=](std::size_t i, std::vector<double>& partials) {                // 14
             auto start = i * tile_size;                                      // 14
             auto end   = std::min(input.size(), (i + 1) * tile_size);        // 14
             std::for_each(output + start, output + end,                      // 14
               [=] (double& e) { e = partials[i] + e; }                       // 14
             );
           })
       | then(                                                                // 15
           [=](std::vector<double>& partials) {                               // 15
             return output;                                                   // 15
           });                                                                // 15
}
  1. これは、doubleのシーケンスを入力として、結果をdoubleのシーケンスに出力するもの
  2. 実行コンテキストを指定するschedulerを受け取る。schの指す実行コンテキスト上で処理を実行する。
  3. tile_countは生成される実行エージェントの数を制御する(すなわち、並列数)。
  4. 最初にアルゴリズムの実行に必要な作業領域を確保する。1つの実行エージェント毎にdouble1つ分の領域が必要。
  5. std::execution::transfer_justによって最初のsenderを作成する。このsenderは先程確保した作業領域を後続の作業に転送し、後続の作業を受け取ったscheduler(引数で渡されたsch)の指す実行コンテキスト上で実行することを指定する。
  6. sendersenderアダプタはパイプライン演算子|)による構成をサポートする(ほとんどのsenderアダプタは|によって構成可能)。std::execution::bulkによってtile_countで指定された数の実行エージェントを生成し次の作業を接続する。
  7. それぞれの実行エージェントは1つの(2引数)invocableを実行する。1つ目の引数は実行エージェントのインデックス(例えばスレッドプール内のスレッドインデックスなど、この例では[0, tile_count)内の単一整数値)、2つ目の引数は先程確保し転送された作業領域。
  8. まず、実行エージェントのインデックスに基づいて、このエージェントが担当する入力と出力の範囲を計算する。
  9. 次に、要素に対して順次std::inclusive_scanを実行する。すべての要素の合計である最後の要素を割り当てられた作業領域に保存する。
  10. 1つ目のbulk()でのすべての計算が完了すると、生成された実行エージェントそれぞれが担当範囲の要素の合計を作業領域に保存している。
  11. 次に作業領域の範囲に対してstd::inclusive_scanを実行する。これは単一の実行エージェントによって実行され、std::execution::thenによってその実行エージェントを作成する。
  12. then()sender|に隠蔽されている)とinvocableを受け取って、そのsenderから送信された値を用いてinvocableを実行する。ここでは、前段の処理結果である作業領域を表すstd::vector<double>が送信されてくる。
  13. ここでの処理の結果として、次の作業に渡すものを返す。ここではstd::inclusive_scan実行済みの作業領域を次に渡す。
  14. 1つ目のbulk()と同じ形状(実行エージェント数)で別の処理をバルク実行する。ここでは、他のタイル(別の実行エージェント)による結果(和)を統合するために部分和の値をスキャンしinclusive_scanを完了する。
  15. 最後に、async_inclusive_scan()は出力範囲であるstd::span<double>を送信するsenderを返す。このアルゴリズムの利用者は、呼び出し側でさらに別の処理を(ここで見たのと同様の方法で)チェーンさせることができ、あるいは任意のrecieverによって処理結果を得ることもできる。この関数が帰った時、処理全体は完了していないかもしれないし、始まってすらいないかもしれない。

これらの非同期アルゴリズムは特定の型に依存するものではなくscheduler, sender, recieverといった抽象にのみ依存しており、それらはコンセプトによって定義されています。これによって、単純な1スレッドやスレッドプール、GPU等の実行リソースを表現するschedulerさえ用意すれば、これらの例のような共通の操作によって任意の実行コンテキスト上で実行可能な非同期処理を構成することができます(GPUなど外部アクセラレータ上での実行はコンパイラの特別扱いが必要ではありますが)。

P2301R1 Add a pmr alias for std::stacktrace

std::basic_stacktracestd::pmrエイリアスstd::polymorphic_allocatorを用いるエイリアス)を追加する提案。

以前の記事を参照

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

この提案は既に議論を完了しており、次の全体会議で投票にかけられることが決まっています。

P2321R2 zip

<ranges>zip_view, adjacent_view, zip_transform_view, adjacent_transform_viewを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGのフィードバックを受けて設計や文言を改善した事と、difference_type, size_typeinteger-like-typeを考慮するようにしたことなどです。

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

P2322R3 ranges::fold

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

以前の記事を参照

このリビジョンでは、LEWGのレビューを受けて関数名と戻り値型についての議論を追加しています。

関数名については以前のリビジョンがfold_left/fold_rightとしていましたが、LEWGにおける投票で合意が取れなかったようで、いくつかの候補を提示しています。筆者の方は、メインの左畳み込みと右畳み込みの関数をfoldl, foldr、それらの初期値を取る関数をfoldl1, foldr1とすることを推しています。

戻り値型について、以前のリビジョンでは計算結果の値を直接返していましたが、計算を終了した位置のイテレータが欲しい需要があったようです。提案では、そのような関数をfold_whileとして別途追加し、イテレータと計算結果の組(となる構造体)を返すようにしています。また、初期値をとらないタイプの関数では入力範囲が空の場合を考慮してstd::optionalを返すようになっています。

P2340R1 Clarifying the status of the "C headers"

現在非推奨とされているCヘッダを相互運用の目的で使用可能なようにする提案。

以前の記事を参照

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

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

P2347R0 Argument type deduction for non-trailing parameter packs

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

この提案のモチベーションはsource_locationの使い勝手を改善させることにあります。

例えば、多くの巷のログ関数はフォーマット文字列とログ出力したいものを受けて、次のような形式になっていることが多いです。

void log(string_view formatString, auto&&...args);

C++20以降、これを自然に拡張してsource_locationを取れるようにするために、次のように書きたくなります。

void log(string_view formatString, auto&&...args, source_location loc = source_location::current());

しかし、現在のC++はパラメータパックがある場合は引数リストの末尾になければならず、そうでない場合パックの型を推論できないためコンパイルエラーとなります。

これ以外の所でも、次のようなメリットが想定できます

  • ragens::transformranges::margeにN個のrangeを受け取るオーバーロードを追加して、既存の1or2個のrangeを受け取るものと引数の順序を一致させられる
  • std::visitにおいて、任意個数のvariantを受け取るのが引数の先頭であるならば、より直感的に使用できる

など、パラメータパックを置ける位置の制限を取り払うと、APIの設計と使用の柔軟性向上が期待できます。

この提案ではこのような制限を撤廃し、関数テンプレートにパラメータパックが1つ(1つだけ)ある場合にそのパックの要素数は、推論されていない関数引数の数からパックより後ろにあるデフォルト引数を持たない引数の数を引いたもの、と推論されるようにします。

void f(auto a, auto...b, auto c, auto d);
void g(auto a, auto...b, auto c, int d = 0);
void h(auto a, auto...b, int c = 0);

f(0, 0, 0, 0);    // bのサイズは1と推論
f(0, 0, 0, 0, 0); // bのサイズは2と推論
f(0, 0, 0);       // bのサイズは0と推論
g(0, 0);          // bのサイズは0と推論
g(0, 0, 0, 0);    // bのサイズは2と推論
h(0, 0);          // bのサイズは1と推論
h(0, 0, 0);       // bのサイズは2と推論

この提案によるこのような推論規則の変更は純粋に制限の解除であって、オーバーロード解決や関数テンプレートの半順序を変更するものではありません。この推論は純粋に実引数と仮引数の数だけを使って行われます。

この提案の問題点は明らかに、パックの後にあるデフォルト引数を持つ引数に対して値を提供できないことですが、そこを何とかしようとして得られるメリットはそれによって導入されるデメリット(コンパイル時間の増大、テンプレートの引数推論とオーバーロード解決の境界が曖昧になる)を下回るようです。デフォルト引数を持つ引数に値を提供したい場合は、以下のような簡易なオーバーロードを追加することによって行うことができます。

template <typename... T>
void f(T&&... args, source_location loc = {}) requires (!(std::same_as<T, source_location>||...));

void f(auto&&... args, source_location loc);

P2361R1 Unevaluated string literals

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

以前の記事を参照

このリビジョンでの変更は、実装可能性と以前の同じ目的の作業について追記した事です。

P2368R1 2021 Spring Library Evolution Polls

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

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

基本的にはLEWGでの作業を完了してLWGへ転送することを確認するための投票です。

P2370R0 Stacktrace from exception

現在投げられている例外オブジェクトに基づくスタックトレースを取得できるようにする提案。

例えば次のように、一つのtryブロックで複数の例外を投げうる関数が呼ばれていると対応するcatchブロックではどこで例外が投げられたのか通常わかりません。

#include <iostream>
#include <stdexcept>
#include <string_view>

// 例外を投げうる関数
void foo(std::string_view key);
void bar(std::string_view key);

int main() {
  try {
    // どちらかが例外を投げたとすると
    foo("test1");
    bar("test2");
  } catch (const std::exception& exc) {
    std::cerr << "Caught exception: " << exc.what() << '\n';
    // 出力例
    // Caught exception: map::at
  }
}

C++23よりスタックトレースライブラリが標準ライブラリに導入されていますが、現在位置に対応するスタックトレースを取得することはできても、例外に応じたスタックトレースを取得することができません。

この提案は、それができるようなインターフェースをstd::basic_stacktraceに追加して、現在の例外ベースのスタックトレースを取得できるようにするものです。

#include <iostream>
#include <stdexcept>
#include <string_view>
#include <stacktrace>   // <---

void foo(std::string_view key);
void bar(std::string_view key);

int main() {
  try {
    foo("test1");
    bar("test2");
  } catch (const std::exception& exc) {
    std::stacktrace trace = std::stacktrace::from_current_exception();  // <---
    std::cerr << "Caught exception: " << exc.what() << ", trace:\n" << trace;
    // 出力例
    // Caught exception: map::at, trace:
    //  0# get_data_from_config(std::string_view) at /home/axolm/basic.cpp:600
    //  1# bar(std::string_view) at /home/axolm/basic.cpp:6
    //  2# main at /home/axolm/basic.cpp:17
  }
}

std::basic_stacktracestd::stacktrace)の静的メンバ関数としてfrom_current_exception()を追加し、それによって現在投げられている例外ベースのスタックトレースを取得できるようにします。後の扱いは通常のスタックトレースと変わりありません。これによって、デバッガを起動せずともどこで落ちたかがわかるようになります。

また、別の例として、terminate()が呼ばれた際にどこで呼ばれたのかの診断を容易にすることができます。

void broken_function() noexcept {
  std::unordered_map<std::string, int> m;
  [[maybe_unused]] auto value = m.at("non-existing-key");
}

int main() {
  std::set_terminate([] {
    auto trace = std::stacktrace::from_current_exception();
    if (trace) {
        std::cerr << "Terminate was called with an active exception:\n"
                  << trace << std::endl;
    }
  });

  broken_function();
}

特に、意図せず誤って実装されたnoexcept関数で例外が投げられた時など、一見どこで投げられているか発見することが難しい場合でも、デバッガレスでそれを知ることができます。

この変更は例外オブジェクトにスタックトレースを仕込む必要があるため、ABI破壊をせねば達成できないように思われますが、どうやらABIの変更をせずとも実装できるようです。それをしているライブラリにlibsfeがあります。

ただし、この変更によって例外オブジェクトのサイズは増大するため、例外発生時のメモリ使用量が増大します。そのため、これを使用する/しないを切り替えられるようにするためにcapture_stacktraces_at_throw(bool enable = true);という関数が用意されています。

P2380R1 reference_wrapper Associations

Networking TSで用意されている、associated_allocatorassociated_executorに対して、reference_wrapper<T>の特殊化を追加する提案。

以前の記事を参照

このリビジョンでの変更は、associated_allocatorassociated_executorを指定する際の誤りを修正した事、associated_allocatorassociated_executortypeメンバを指定するためのエイリアスassociated_allocator_t, associated_executor_tを追加した事などです。

P2384R0 2021 Spring Library Evolution Poll Outcomes

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

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

P2385R0 C++ Standard Library Issues to be moved in Virtual Plenary, June 2021

6月の会議で採択されたライブラリのIssue解決の一覧。

解決されたIssueは36件です。

  1. 2774. std::function construction vs assignment
  2. 2818. "::std::" everywhere rule needs tweaking
  3. 2997. LWG 491 and the specification of {forward_,}list::unique
  4. 3410. lexicographical_compare_three_way is overspecified
  5. 3430. std::fstream & co. should be constructible from string_view
  6. 3462. §[formatter.requirements]: Formatter requirements forbid use of fc.arg()
  7. 3481. viewable_range mishandles lvalue move-only views
  8. 3506. Missing allocator-extended constructors for priority_queue
  9. 3517. join_view::iterator's iter_swap is underconstrained
  10. 3518. Exception requirements on char trait operations unclear
  11. 3519. Incomplete synopses for <random> classes
  12. 3520. iter_move and iter_swap are inconsistent for transform_view::iterator
  13. 3521. Overly strict requirements on qsort and bsearch
  14. 3522. Missing requirement on InputIterator template parameter for priority_queue constructors
  15. 3523. iota_view::sentinel is not always iota_view's sentinel
  16. 3526. Return types of uses_allocator_construction_args unspecified
  17. 3527. uses_allocator_construction_args handles rvalue pairs of rvalue references incorrectly
  18. 3528. make_from_tuple can perform (the equivalent of) a C-style cast
  19. 3529. priority_queue(first, last) should construct c with (first, last)
  20. 3530. BUILTIN-PTR-MEOW should not opt the type out of syntactic checks
  21. 3532. split_view<V, P>::inner-iterator<true>::operator++(int) should depend on Base
  22. 3533. Make base() const & consistent across iterator wrappers that supports input_iterators
  23. 3536. Should chrono::from_stream() assign zero to duration for failure?
  24. 3539. format_to must not copy models of output_iterator<const charT&>
  25. 3540. §[format.arg] There should be no const in basic_format_arg(const T* p)
  26. 3541. indirectly_readable_traits should be SFINAE-friendly for all types
  27. 3542. basic_format_arg mis-handles basic_string_view with custom traits
  28. 3543. Definition of when counted_iterators refer to the same sequence isn't quite right
  29. 3544. format-arg-store::args is unintentionally not exposition-only
  30. 3546. common_iterator's postfix-proxy is not quite right
  31. 3548. shared_ptr construction from unique_ptr should move (not copy) the deleter
  32. 3549. view_interface is overspecified to derive from view_base
  33. 3551. borrowed_{iterator,subrange}_t are overspecified
  34. 3552. Parallel specialized memory algorithms should require forward iterators
  35. 3553. Useless constraint in split_view::outer-iterator::value_type::begin()
  36. 3555. {transform,elements}_view::iterator::iterator_concept should consider const-qualification of the underlying range

P2386R0 Core Language Working Group "ready" Issues for the June, 2021 meeting

6月の会議で採択されたコア言語のIssue解決の一覧。

解決されたIssueは9件です。

  1. 2397. auto specifier for pointers and references to arrays
    • 配列のポインタ/参照の宣言において、autoによる推論が効くようにした。 cpp int a[3]; auto (*p)[3] = &a; // OK、一部の実装では実装済 auto (&r)[3] = &a; // OK、一部の実装では実装済
  2. 2448. Cv-qualification of arithmetic types and deprecation of volatile
    • 算術型(arithmetic types)という用語が、整数型や浮動小数点数型のCV修飾も含むようにした。
    • 以前は含んでいなかったため、矛盾が生じていた。
  3. 2458. Value category of expressions denoting non-static member functions
    • 非静的メンバ関数のアドレスを取得する式の結果(&X::f)が左辺値となるようにした。
    • 以前は右辺値とされていた。
  4. 2465. Coroutine parameters passed to a promise constructor
    • コルーチンに渡された引数をコルーチン内部およびPromise型の初期化で使う際、コピーされていることを明確化した
    • 以前からなっていたが、Promise型の初期化についてカバーしきれていなかったのを修正
  5. 2466. co_await should be a single evaluation
    • co_awaitの呼び出しを並べ替えたり省略したりすることを禁止した。
    • 一つのco_await式は正確に一度評価される
  6. 2474. Cv-qualification and deletion
    • delete式において、その静的型と動的型がsimilarの関係であれば未定義動作を起こさなくなった
    • 以前の記述では、CV修飾の違いも未定義となっていた。
  7. 2477. Defaulted vs deleted copy constructors/assignment operators
    • コピー/ムーブコンストラクタがdefault定義される時と、delete定義される時の規定の矛盾の解消
  8. 2479. Missing specifications for consteval and constinit
    • consteval/constinitのキーワードについて、言及されるべき所で言及されていなかったのを正した
  9. 2481. Cv-qualification of temporary to which a reference is bound
    • コピー省略のためのtemporary materialization conversionに伴って、意図せずCV修飾が落とされていたのを修正

P2387R0 Pipe support for user-defined range adaptors

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

標準のRangeアダプタは関数呼び出しによっても、パイプライン演算子によっても呼び出すことができ、その二つの呼び出しは入力となるrangeの受け取り方以外は同じ効果となることが規定されています。

// R, R1, R2を任意のRangeアダプタオブジェクトとする

// 入力のrange(viewable_range)
viewable_range auto vr = views::iota(1);

// この2つの呼び出しは同じ効果となる
view auto v1 = R(vr);
view auto v2 = vr | R ;

// Rangeアダプタが増えた時でも同様(以下3つは同じ効果)
// 適用順は右結合
view auto v3 = R2(R1(vr));
view auto v4 = vr | R1 | R2;
view auto v5 = vr | (R1 | R2);

これらのR, R1, R2のような、1引数のRangeアダプタのことをrange adaptor closure objectと呼びます。

Rangeアダプタはさらに追加の引数をとることができ、その時でも先ほどの例と同様の保証があります。この時、追加の引数だけを先に受け取って呼び出すこともでき(rangeを受け取らないで呼び出すことができ)、その結果はrange adaptor closure objectとなります。

// Rangeアダプタが追加の引数を取るときも同様に書く事ができる
// その時でも、この3つは同じ効果となる
view auto v6 = R(vr, args...);
view auto v7 = R(args...)(vr);
view auto v8 = vr | R(args...)

range adaptor closure objectとこれら追加の引数を受け取るものを合わせて、range adaptor objectと呼びます。range adaptor objectはカスタマイゼーションポイントオブジェクトであり、1引数しか取らない場合はrange adaptor closure objectでもあります。

これらの保証はパイプラインで使用する時を考えるとユーザーにとっては便利なものですが、実際に実装しようとすると大変です。特に、range adaptor closure objectが右結合する所(R1 | R2range adaptor closure objectを返す)とか、追加の引数をとるrange adaptor objectに追加の引数だけを渡してもrange adaptor closure objectを返さねばならない所などが厄介です。

実際にはこれらのことはrange adaptor object全てに共通しているため、実装の大部分を共有することができます。Range-v3をはじめとする既存実装やGCC/MSVCなどの標準実装は、多少の差はあれど内部でそのようになっています。

この提案は、そのような共通実装の仕組みを標準化しユーザーに提供することで、ユーザー定義されたRangeアダプタに対してパイプラインサポートとそれに纏わる保証を簡易に実装できるようにしようとするものです。

問題は、range adaptor closure objectを実装する部分とrange adaptor object(からrange adaptor closure objectを除いた部分)を実装する部分の2つに分割することができます。

range adaptor closure objectの実装では、rangeを受け取った時の関数呼び出しとパイプライン演算子適用が同じ効果になり、かつrange adaptor closure objectを受け取った時に、それと自身を内包したrange adaptor closure objectとなるプロクシオブジェクトを返す必要があります。

残りのrange adaptor objectの実装では、追加の引数だけを受け取った時にそれら引数と自身をラップしたrange adaptor closure objectとなるプロクシオブジェクトを返し、それに対して関数呼び出しでrangeを渡した時と、最初にrangeと追加の引数をまとめて受け取った時で同じ効果となるようにしなければなりません。

この提案ではまず、range adaptor closure objectの実装のためにstd::ranges::range_adaptor_closure<T>というクラスを用意します。これはCRTPによって利用し、range adaptor closure object型はこれを継承することで、パイプライン演算子サポートを受けることができます(関数呼び出し演算子は実装しておく必要があります)。

namespace std::ranges {
  template<class D>
    requires is_class_v<D> && same_as<D, remove_cv_t<D>>
  class range_adaptor_closure { 

    // R | C == C(R) のサポート
    template <typename Range>
      requires /**/
    friend constexpr auto operator|(Range&& r, D& self) {
      return self(std::forward(r)); // Dの関数呼び出し演算子を使って実装
    }
    
    // C | D でrange adaptor closure objectを生成するサポート
    template <typename Lhs, typename Rhs>
        requires /**/
    constexpr auto operator|(Lhs lhs, Rhs rhs) {
      // range adaptor closure objectラッパで包んで返す
      return raco_proxy<Lhs, Rhs>(std::move(lhs), std::move(rhs));
    }
  };
}

例えばこんな感じの実装になります。

次に、range adaptor objectの実装のためにstd::bind_back()を用意します。std::bind_back(f, ys...)(xs...)のような呼び出しは、f(xs..., ys...)と等価になります。

これらを使用して、自前のRangeアダプタを例えば次のように実装できます。

namespace myns {

  // オレオレjoin_View
  template<std::ranges::view V>
  class join_view;

  // オレオレtransform_View
  template<std::ranges::view V, typename F>
  class transform_view;

  // オレオレjoin_Viewに対するrange adaptor closure object
  struct join_closure : std::ranges::range_adaptor_closure<join_closure> {

    template<std::ranges::viewable_range R>
    constexpr auto operator()(R&& r) const {
      return join_view{std::forward<R>(r)};
    }
  };

  // オレオレtransform_Viewに対するrange adaptor object
  struct transform_adopter : std::ranges::range_adaptor_closure<transform_adopter> {

    template<std::ranges::viewable_range R, typename F>
      requires std::invocble<F&, std::ranges::range_reference_t<R>>
    constexpr auto operator()(R&& r, F&& f) const {
      return transform_view{std::forward<R>(r), std::forward<F>(f)};
    }

    template<typename F>
    constexpr auto operator()(F&& f) const {
      return std::bind_back(*this, std::forward<F>(f));
    }
  };

  // range adaptor object定義
  namespace views {
    inline constexpr join_closure join{};

    inline constexpr transform_adopter transform{};
  }
}

この提案では、range adaptor objectの実装を簡易化するために最大限のことをしていません。例えば、別のラッパ型を用意してさらに簡易化することはできるはずです。これは将来的に言語機能や他のアプローチによって解決できるはずなのでここで導入しても将来的に不要となる可能性が高い、という判断のようです。

P2388R0 Abort-only contract support

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

C++20に導入が決定していた契約プログラミングコントラクト)サポートは、様々な問題があったため削除されました。その後、問題点を整理し機能を絞ったMVP (P2182R1)と呼ばれる部分を先んじて導入する事が提案されていました。

現在のコントラクトサポートの議論の方向性は、論争を引き起こしている部分の多くはコントラクトフレームワークの2次的な機能の部分であり、そのような部分が1次的な機能である「プログラマがプログラムのバグと思われるものを伝える機能」の追加を妨げたり妨げられる事がないように、コントラクトの最小の機能セットから導入を始めていこうとしています。

ただ、それを選別したはずのP2182R1の部分さえ論争を引き起こし合意が取れなかったようです。

この提案は、P2182R1からさらに機能を絞り込み、契約プログラミングの最小の機能を早期にC++に導入することを目指すものです。

この提案が導入を目指している機能は次のものです。

  1. [[pre: condition]]による事前条件
  2. [[post: condition]]による事後条件
  3. [[assert: condition]]による関数内アサーション
  4. 2つのモード
    • 全無視 : コンパイラは契約の式の有効性だけを検証し、実行時に影響を与えない
    • 実行時チェックとアボート : 全ての契約を実行時にチェックする。違反した場合契約違反ハンドラが呼び出され、その後std::abort()する
    • これらを翻訳単位ごとに切り替える実装が許可される

この提案には以前にあった、assertion levelcontinuation mode、契約違反ハンドラのカスタマイズなどは含まれていません(これらがまさに物議を醸していた部分です)。

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

int select(int i, int j)   // 最初の宣言
  [[pre: i >= 0]]
  [[pre: j >= 0]]
  [[post r: r >= 0]];      // `r`は戻り値
  
int select(int i, int j);  // 再宣言では、同じ契約を指定するか、何も指定しない
                          
int select(int i, int j)   // 定義
{
  [[assert: _state >= 0]];
  
  if (_state == 0) return i;
  else             return j;
} 

P2391R0 C23 Update Report

C23の変更点(と今後採択されうる変更)をまとめた文章。

SG22が設立されるなど、CとC++の相互互換性にはWG21とWG14双方が注意を払っており、それを受けてC23の変更点をWG21で周知するための提案です。

C23の変更点がわかりやすくまとまっているので、C23に興味のある人は覗いてみると良いかもしれません。

P2392R0 Pattern matching using “is” and “as”

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

この提案はP1371R3で提案されている現在のパターンマッチングをベースに、パターンマッチング構文が局所的な言語サブセットにならないようにisas[]を用いた構文への変更とそれを他のところ(ifrequiresなど)でも使用できるようにするものです。

constexpr bool even (auto const& x) { return x%2 == 0; } // given this example predicate

// xは何でもいい
void f(auto const& x) {
  inspect (x) {
    i as int             => std::cout << "int " << i;
    is std::integral     => std::cout << "non-int integral " << x;
    [a, b] is [int, int] => std::cout << "2-int tuple " << a << " " << b;
    [_, y] is [0, even]  => std::cout << "point on x-axis and even y" << y;
    s as string          => std::cout << "string \"" + s + "\"";
    is _                 => std::cout << "((no matching value))";
  }
}

inspect式を使用しているところなど基本的なところに変更はありませんが、型に対するマッチングをis, asを用いて直感的に書く事ができる他、[]を用いて構造化束縛による分解とマッチングを同時に行う事ができます。_ワイルドカードパターンを表します。

  • x is C : 型や値を問わないパターンの表現に使用できる構文。Cは特定の型や値、型の述語(コンセプト)や値の述語を使用可能。
  • x as T : Tへのキャスト可能パターンの表現に使用できる構文。Tは特定の型か型の述語のいずれか。

そして、このis, asによるマッチング構文はinspect式の外側でも使用可能とされます。

void f(auto const& x) {
  if (auto i as int = x)                  { std::cout<<"int"<<x;}
  else if (x is std::integral)            { std::cout << "non-int integral " << x; }
  else if (auto [a, b] is [int, int] = x) { std::cout << "2-int tuple " << a << " " << b; }
  else if (auto [_, y] is [0, even] = x)  { std::cout << "point on x-axis and even y " << y; }
  else if (auto s as string = x)          { std::cout<<"string\""+s+"\"";}
  else                                    { std::cout << "((no matching value))"; }
}

あるいはrequires式(節)

void g(auto const& x) requires requires{x as int;}
                      { std::cout << "int " << x; }

void g(auto const& x) requires (x is std::integral)
                      { std::cout << "non-int integral " << x; }

void g(auto const& x) requires (x is [int, int])
                      { auto [a, b] = x; std::cout << "2-int tuple " << a << " " << b; }

void g(auto const& x) requires (x is [0, even])
                      { auto [_, y] = x; std::cout << "point on x-axis and even y " << y;

void g(auto const& x) requires requires{x as string;}
                      { auto s = x as string; std::cout << "string \"" + s + "\""; }

void g(auto const& x) { std::cout << "((no matching value))"; }

変数宣言

std::pair<int, std::pair<int,int>> data;

// 分解とマッチング
auto [a, [_, c]] = data;
if (data is [_, [1, _]] ) {...}

// 上記の複合構文
if (auto&& [a, [_, c]] is [_, [1, _]] = data) {...}
// is によるマッチがtrueとなれば、ifによる判定もtrueとなる


// C++20の変数宣言
int a  = f();
std::integral auto b = g();

// isを用いた変数宣言
auto a is int = f();  // f()の戻り値型がintである時有効
auto b is std::integral = g();


std::variant<std::pair<int,int>, std::string> v;

// vが1つ目の要素(std::pair<int,int>)を有している場合にキャスト可能
// そうでない場合、実行時例外
auto&& [a, b] as std::pair<int,int> = v;
// もう片側
auto&& s as std::string = v;

if (auto&& [a, b] as pair<int,int> = v) {...}
// as によるマッチがtrueとなれば、ifによる判定もtrue

// isを用いた変数宣言
auto a as int = f();  // f()の戻り値型がintに変換可能である時有効

// こうかくと
auto [a, b] = v as std::pair<int,int>;
// これと等価
auto [a, b] = std::get<0>(v);

この提案では、このようなネストしたパターン指定を可能にし構造化束縛と一貫させるために、同様の構文を構造化束縛においても許可することを提案しています。

このisas演算子でもあり、任意のクラス型に対して演算子オーバーロードによってその動作をカスタマイズする事ができます。

// std::variantでオーバーロードする例

template<typename... Ts>
constexpr auto operator is(std::variant<Ts...> const& x ) {
  return x.index();
}

template<size_t I, typename... Ts>
constexpr auto operator as(std::variant<Ts...> const& x ) -> auto&& {
  return std::get<I>( x );
}

これらのisasはグローバルに予約されたキーワードではなく文脈依存キーワードです。

筆者の方は既存の文法などを調査した上でこれらの構文を提案しているため、実現可能性は高そうです。

P2393R0 Cleaning up integer-class types

整数型とみなせる型を指すinteger-classの定義を修正する提案。

integer-class型は標準ライブラリのイテレータ/rangeの文脈で整数型として使用可能な型を指すものですが、その規定にはまだいくつかの問題があるようです。

  • zip_view, join_viewなど2つのrangeを合成するタイプのdifference_typeは、2つのrangedifference_typecommon_typeによって決定される、integer-class型同士はcommon_typeを持つことを要求されていない。
  • integer-class型同士は相互に変換可能ではなく、比較可能でもない。
    • ranges::equalなど、2つのrangeから取得した距離を比較する必要がある処理を実装できない
  • integer-class型の表現可能な値の範囲、および整数型への/からの変換は定義されていない
  • <ranges>/<iterator>/<algorithm>の多くの所ではrandom_access_iteratorの操作がdifference_typeでのみ動作するように規定されており、integer-class型を考慮していない
    • そのようなところでは、ranges::range_difference_t/iter_difference_tが使用されるために、integer-class型からそれらの整数型への変換が必要になる(がそれは定義されていない)。

この提案は、これらの問題の解決を図るものです。

  • integer-classは2の補数でありどの組み込み型よりも表現可能な値の幅が広い、と規定
  • integer-classは非クラス型となることを許可する
  • 2つのinteger-class型の間には常にcommon_typeが存在することを規定し、元のinteger-classが両方とも符号付整数型の場合はそのcommon_typeもまた符号付となることを規定
  • integer-class型は同じ符号性を持ちより大きな幅のinteger-class型に暗黙変換可能であり、全てのinteger-class型に明示的変換可能であることを規定する
  • 一方が他方に暗黙変換可能なinteger-class型の間での二項演算を許可する。

これらのことをベースに<ranges>/<algorithm>の規定に変更を加え、difference_typeinteger-class型が相互に変換しつつ自然に使用可能であるようにします。

これらの変更によって、問題の解決を図ります。

P2395R0 WG21 2021-06 Virtual Meeting Record of Discussion

6月の全体会議の議事録。

投票の際にどのような意見をだれが発言したかなどが記録されています。

P2396R0 Concurrency TS 2 fixes

COncurrency TSv2を目指しているいくつかの提案について、本題と関係の薄い微修正の提案。

  • 配置するヘッダの変更
  • 機能テストマクロの追加

いずれもまだexperimentalなものです。

P2397R0 SG16: Unicode meeting summaries 2021-04-14 through 2021-05-26

SG16(Unicode and text study group)のオンラインミーティングの議事録。

どのような議論においてだれがどんな発言をしたかが記録されています。

P2400R1 Library Evolution Report: 2021-02-23 to 2021-05-25

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

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

[C++] std::formatあるいは{fmt}のコンパイル時フォーマット文字列チェックの魔術

コンパイル時フォーマット文字列チェック

{fmt}ライブラリおよび<format>には、コンパイル時のフォーマット文字列チェック機能が実装されています。

#include <format>
#include <fmt/core.h>

int main() {
  // 共にコンパイルエラーを起こす
  auto str = std::format("{:d}", "I am not a number");
  fmt::print("{:d}", "I am not a number");
}

{:d}は10進整数値1つのためのフォーマット指定であるのに、引数として整数値ではなく文字列が渡っているためにエラーとなっています。

これは先に{fmt}ライブラリで実装されたものが、遅れてC++20 <format>に導入されたものです。一見すると言語やコンパイラの特別のサポート無くしてはこのようなことはできないように思われますが、これは純粋にライブラリ機能として実装されています。

{fmt}ライブラリを追うのは辛かったので、<format>がどのようにこれを達成しているかを見てみることにします。

basic-format-stringクラス

std::formatの宣言を見てみると、次のようになっています。

template<class... Args>
  string format(format-string<Args...> fmt, const Args&... args);

template<class... Args>
  wstring format(wformat-string<Args...> fmt, const Args&... args);

どちらも第一引数にフォーマット文字列を取り、第二引数以降でフォーマット対象の変数列を受け取ります。format-stringみたいなのは説明専用の型で、フォーマット文字列を構成しているものです。

template<class charT, class... Args>
  struct basic-format-string;

template<class... Args>
  using format-string = basic-format-string<char, type_identity_t<Args>...>;

template<class... Args>
  using wformat-string = basic-format-string<wchar_t, type_identity_t<Args>...>;

basic-format-stringクラスは説明専用のもので、実際の型名は実装によって異なります。その実装は次のように描かれています

template<class charT, class... Args>
struct basic-format-string {
private:
  basic_string_view<charT> str;

public:
  template<class T>
  consteval basic-format-string(const T& s);
};

そのコンストラクタの効果については次のように規定されています。

Constraints: const T& models convertible­to<basic­string_­view>.
Effects: Direct-non-list-initializes str with s.
Remarks: A call to this function is not a core constant expression ([expr.const]) unless there exist args of types Args such that str is a format string for args.

この3つめのRemarks指定がまさに、コンパイル時フォーマット文字列チェックを規定しています。

constevalコンストラク

A call to this function is not a core constant expression ([expr.const]) unless there exist args of types Args such that str is a format string for args.

を訳すと(Powerd by DeepL)

この関数の呼び出しは、strargsのフォーマット文字列であるようなArgs型のargsが存在しない限り、コア定数式ではない。

ややこしいですが、std::formatの引数として与えられたフォーマット文字列strとフォーマット対象のargsについて、strが正しくそのフォーマット文字列となっていなければこのコンストラクタの呼び出しはコア定数式でない、と言っており、コア定数式でないものは定数式で実行できません。

ところで、このコンストラクタにはconsteval指定がなされています。constevalC++20から追加された言語機能で、consteval指定された関数は必ずコンパイル時に実行されなければならず、さもなければコンパイルエラーとなります。それはconstevalコンストラクタにおいても同様です。

このRemarks指定とconstevalの効果を合わせると、フォーマット文字列strArgs...に対して正しくない場合にコンパイルエラー、となるわけです。

実装例

規定は分かりましたが、それだけでフォーマット文字列チェックができるわけではありません。結局、ユーザーランドで規定に沿うように実装することが可能なのかどうかが知りたいことです。

C++20に強い人ならここまでのことで実装イメージが浮かんでいるでしょうが、一応書いてみることにします。なお、フォーマット文字列チェック実装については主題ではないので深入りしません。

// 定数式で呼べない関数
void format_error();

// フォーマット文字列チェック処理
// 詳細は省略するが定数式で実行可能なように実装されているとする
template<typename CharT, typename... Args>
consteval void fmt_checker(std::basic_string_view<CharT> str) {

  // ...

  if (/*かっこが足りないとかの時*/) {
    format_error(); // 定数式で実行できないため、ここに来るとコンパイルエラー
  }

  // ...

  if (/*型が合わない時*/) {
    throw "invalid type specifier"; // throw式は定数式で実行不可
  }

  // ...
}

template<typename CharT, typename... Args>
struct basic_format_string {
  std::basic_string_view<CharT> str;

  template<typename T>
    requires std::convertible_­to<const T&, std::basic_­string_­view<charT>>
  consteval basic_format_string(const T& s) 
    : str(s)
  {
    fmt_checker<CharT, Args...>(str);
  }
};

template<class... Args>
using format_string = basic_format_string<char, std::type_identity_t<Args>...>;


// std::format
template<class... Args>
std::string format(format_string<Args...> fmt, const Args&... args) {
  // フォーマット済み文字列を作成する部分は省略
  return std::vformat(fmt.str, std::make_format_args(args...));
}

一応標準ライブラリのものを使っているところにはstd::を付加していますが、実際はこのような実装もstd名前空間内で実装されるので不要です。

format()の引数として構築されたbasic_format_stringformat_string)のコンストラクタ本体において、フォーマット文字列チェックを行うfmt_checker()を呼び出します。fmt_checker()が行うフォーマット文字列チェック機能は定数式で実行可能なように実装されているものとして、受け取ったフォーマット文字列basic_format_string::strをそこに渡してfmt_checker()が完了すればフォーマット文字列チェックは完了です。

basic_format_stringのコンストラクタおよびfmt_checker()consteval関数であるので、一連のフォーマット文字列チェック機能は定数式で必ず実行される事になります。

fmt_checker()においてフォーマット文字列エラーが発生した場合コンパイルエラーとしなければなりませんが、実行環境がconstevalコンテキストなので、定数式で実行できない事をしようとすればコンパイルエラーを引き起こすことができます。それは例えば、非constexpr関数の呼び出しやthrow式の実行などがあります。

fmt_checker()にはフォーマット文字列strおよび、format()に指定された残りの戻り値の型Argsが正しく伝わっています。strの内容とArgsの各型を順番にチェックしていけば、指定されたフォーマット文字列に対して正しい引数が指定されているか?という事までチェックできます。

このようなformat()には任意の文字列型を渡すことができます。basic_format_stringではなく。

int main() {
  // 文字配列 -> basic_format_stringへの暗黙変換
  // basic_format_stringのコンストラクタがconstevalのため、必ずコンパイル時に実行される
  auto str = format("{:d}", "I am not a number");
}

このforamt()の呼び出しの第一引数においては、文字配列(任意の文字列型)->basic_format_stringの一時オブジェクト->basic_format_stringの左辺値、のような変換によってforamt()第一引数のfmtが構築されています(一時オブジェクトから左辺値への変換はコピー省略によって省略されるはずです)。

basic_format_stringの宣言された唯一つのコンストラクタはexplicitされていないテンプレートコンストラクタなので、string_viewに変換可能な任意の文字列型から暗黙変換によって呼び出すことができます。そして、そのコンストラクタはconstevalなので暗黙変換からフォーマット文字列チェックまで必ずコンパイル時に実行されます。

もしこれがconstexprだと、フォーマット文字列に間違いがあった時に必ずしもコンパイルエラーにすることができません。constexpr変数の初期化式のようにどうしても定数式で実行しなければならない所以外では、constexpr関数の実行中に定数式で実行できない物に出会った場合に定数式を中断して実行時処理に切り替えることを暗黙に行うため、constevalと同様のコンパイルエラーを起こせません。特に、関数引数にはconstexprを付加できないため、暗黙変換をトリガーとしたコンパイル時フォーマット文字列チェックを強制できません。

すなわち、このコンパイル時フォーマット文字列チェックを支えているのは、constevalという機能なわけです。consteval自体は<format>と無関係に導入されており、コンパイル時フォーマット文字列チェックは何らの言語サポートを受けたものではありません。加えて、暗黙変換というのもミソなところで、暗黙変換をトリガーとする事によってコンパイル時チェックを走らせるための追加の何かをする必要がなくなっています。うーんかしこい!!

このように、コンパイル時フォーマット文字列チェックは純粋にC++20の範囲内で実装することができます。

応用例

たとえば、宇宙船演算子の戻り値型である比較カテゴリ型0リテラルのみと比較可能とされています。これはstd::nullptr_tを用いて実装することができますが、コンパイル時フォーマット文字列チェックと同様のアプローチによって実装することができそうです。

#include <iostream>
#include <ranges>

struct lzero {
  consteval lzero(int&& n) {
    if (n != 0) {
      throw "Compare with zero only!";
    }
  }
};

struct dummy_cct {
  
  friend bool operator==(const dummy_cct&, lzero) {
    return true;
  }
};

int main() {
  dummy_cct c{};
  
  std::cout << std::boolalpha;
  
  std::cout << (c == 0) << std::endl;
  std::cout << (c != 0) << std::endl;
  std::cout << (0 == c) << std::endl;
  std::cout << (0 != c) << std::endl;
}

エラーになる例

int main() {
  dummy_cct c{};
  
  std::cout << std::boolalpha;

  // 共にng
  std::cout << (c ==  1) << std::endl;
  std::cout << (c == -1) << std::endl;
}

この方法の利点としては、nullptrとの比較ができなくなる所と、0リテラル以外との比較は未定義動作と規定されている未定義動作をコンパイルエラーとして実装できることでしょうか。

なお、lzeroのコンストラクタ引数をint&&としているのは、左辺値(すなわち変数)を受けないようにするためです。

int main() {
  dummy_cct c{};
  
  std::cout << std::boolalpha;
  
  constexpr int n = 0;
  std::cout << (c == n) << std::endl; // ng
}

なるべく確実にリテラルだけを受け取るようにしたいわけです。しかしこれでも完璧ではなく、式の結果を受け取れてしまいます・・・

このように、このテクニックは色々面白い応用が効きそうな無限の可能性があります。わくわくしますね!

参考文献

この記事のMarkdownソース