[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>;

おわり