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

文書の一覧

採択されたものはありません、全部で32本あります。

N4878 Working Draft, Standard for Programming Language C++

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

N4879 Editors' Report - Programming Languages - C++

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

前回会議で採択された提案文書とコア言語/ライブラリのIssue解決が適用されているようです。

P0401R5 Providing size feedback in the Allocator interface

アロケータが実際に確保したメモリのサイズをフィードバックすることのできるメモリ確保インターフェースを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを受けて提案する文言を改善したことです。

P0561R5 An RAII Interface for Deferred Reclamation

deferred reclamationを実現するためのより高レベルAPIを標準ライブラリに追加する提案。

deferred reclamationは並行処理における複数スレッド間のデータ共有におけるパターンで、この提案では次のように説明されています。

ある1つのデータ(変数)に対してreaderupdaterの2種類のコンポーネントを考えます。readerupdaterも複数存在し、各々別々のスレッドからデータにアクセスします。
readerは読み取りロックを取得してデータを読み取ります。ここでは、そのロックが保持されている間そのデータの生存が保証されます。一方、updaterはデータを新しく確保された値によって置き換えることでデータを更新します。更新以降にデータを読みだしたreaderは新しい値を読み取りますが、置換前の古いデータを読み取った全てのreaderがロックを解除するまでは古いデータは破棄されずに生存します。
readerの読み取り操作は他のreaderupdaterをブロックせず、updaterreaderをブロックしません。データの更新はメモリ確保を必要とするため高コストですが、読み取りと比較すると非常に稀であることが想定されます。

このdeferred reclamationの実装には参照カウントやread-copy update(RCU)、ハザードポインタなどの方法があり、そのうちのいくつかは過去にC++に対して提案されています。しかし、それらはより実装そのものに近い低レベルなAPIを提供するものであり、それらの利用例の一つとしてdeferred reclamationが実現できるものでしかありませんでした。

この提案は、そのような低レベルなプリミティブによるものよりも安全性と使いやすさを重視し、かつ効率的な実装を可能とするdeferred reclamationだけのための高レベルなAPIを提供するものです。

#include <snapshot> // 新ヘッダ

// Configクラスによる設定を用いてリクエストを処理するServerクラス
class Server {
public:

  // 設定は随時変更可能
  // 設定変更を調べる別のスレッドから更新される
  void SetConfig(Config new_config) {
    config_.update(std::make_unique<const Config>(std::move(new_config)));
  }

  // リクエストはその時点の設定を使用して処理する
  // 設定の更新タイミングを考慮する必要はない
  void HandleRequest() {
    // リクエスト処理開始時点での設定データの取得
    std::snapshot_ptr<const Config> config = config_.get_snapshot();
    // configはunique_ptr<const Config>のように使用可能
    // configの生存期間内に設定データが更新されたとしても、configが参照するデータに影響はない
  }

private:
  // 共有される設定データ
  // 読み取り、更新、いずれに際しても同期を必要としない(ロックフリー)
  std::snapshot_source<Config> config_;
};

この提案のAPIGoogle社内で実装され使用されているものをベースにしており、そこでは高レベルAPIとRCUによる実装の低レベルなAPIの両方が提供されているようですが、高レベルAPIの利用者が低レベルAPIに比べて多く、その経験こそがdeferred reclamationのための高レベルAPIを提供する価値を実証していると主張しています。

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

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

以前の記事を参照

このリビジョンでの変更は、EWGでのレビュー中に明らかになったいくつかの問題と、ライブラリの文言変更の意味合いを明確にする表を追記したことです。

この提案は現在LEWGにおけるライブラリパートのレビューを待っており、それが終了次第CWGに送られる予定です。

P0901R8 Size feedback in operator new

::operator newが実際に確保したメモリのサイズを知ることができるオーバーロードを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LEWGでのレビューを受けて引数名を変更したことと、提案する文言を洗練させたことです。

この提案はこのリビジョンをもってLEWGでの投票にかけ、コンセンサスが得られればCWGに送付される予定です。

P1030R4 std::filesystem::path_view

パス文字列を所有せず参照するstd::filesystem::path_viewの提案。

std::filesystem::pathクラスはパスを表現するクラスですが、その実態はパス文字列であり、std::stringと同様にパス文字列を所有しています。したがって、構築や連結などの操作において動的メモリ確保が発生します。
std::filesystem::pathに対する操作のいくつかは新しいpathオブジェクトを返します。そこではメモリ確保と文字列のコピーが発生します。

例えば、ディレクトリを列挙する様な場合には1つのディレクトリの列挙毎にpathオブジェクトの構築コストがかかる事になり、ディレクトリの数が多い場合にはボトルネックとなります。また、pathオブジェクトの構築に伴う新規メモリ確保と文字列のコピーはCPUのキャッシュにも優しくありません。Windowsのパス文字列制限260文字に遭遇したことのある人が多くいる様に、パス文字列は数百バイトに達することもあり、パス文字列のコピーの度にキャッシュから有用なデータを削除する事になります。

std::filesystem::path_viewstd::filesystem::pathを参照する軽量なViewです。std::filesystem::pathと同様のインターフェースを提供し、ローカルプラットフォームのパス文字列に対してconst/constexprな参照であり、std::filesystem::pathとほぼ同様に振舞います。これによって、現在std::filesystem::pathを受け入れている所をリファクタリングをほぼ必要とせずにstd::filesystem::path_viewを受け入れられる様にすることができます。

また、std::filesystem::path_viewに対するイテレーションpath_viewを返さない様にするために、std::filesystem::path_view_componentも追加されます。これはpath_viewとほぼ同じものですが、パス要素のイテレーションや抽出のための一部のメンバ関数を提供していません。
path_viewに対するイテレーションで得られた各パス要素をさらにパスとして扱う事は意味がなく、またそれを行う事はバグの可能性が高いため、パス要素である事を表現するための別の型が必要とされたのだと思われます。

この提案はC++23入りを念頭に作業が進められているようです。

P1072R6 basic_string::resize_and_overwrite

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

例えばパフォーマンスに敏感なところで、std::stringに文字を流し込んでいく処理を書くとき、おおよそ次の3つのいずれかを選択することになります。

  1. 追加の初期化のコストを捧げる : resize() ゼロ初期化してから元の文字列をコピー
  2. 追加のコピーコストを捧げる : 一時バッファに文字列をためておき、最後にまとめてコピー
  3. 追加の簿記コストを捧げる : reserve() その後文字列が追加されるたびに、残りの長さが調べられ、null終端される

ここでやりたいことは、断続的に取得される文字列を随時追記していき最後にまとめて1つの文字列として扱う事です。しかし、いずれの方法も何かしら余分なコストがかかってしまい、最適な方法はありませんでした。

問題なのは、この様な場合にstd::stringをバッファとして使おうとしても、その領域をある程度の長さで確保しつつそのままアクセス可能にする、という操作が欠けていることです。

resize_and_overwrite()はまさにそのためのもので、指定された長さに領域を拡張しつつ、増やした領域はデフォルト初期化するだけに留める関数です。

namespace std {

  template <class charT, class traits = char_traits<charT>, class Allocator = allocator<charT> >
  class string {

    template<typename Operation>
    void resize_and_overwrite(size_type n, Operation op);

  };
}

resize_and_overwrite()は1つ目の引数に変更したい長さをとり、現在の長さがそれよりも短い場合は追加された領域はデフォルト初期化されています。また、2つ目の引数に変更後の領域に対する初期化処理を書く事ができ、変更後の領域の先頭ポインタと1つ目の引数nを取る任意の関数を指定できます。operase(begin() + op(data(), n), end())の様に呼び出されるため、opは処理後に残しておきたい領域のサイズを返す必要があります。

P1102R2 Down with ()!

引き数なしのラムダ式の宣言時に()をより省略できるようにする提案。

以前の記事を参照

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

この提案はコア言語に対するIssue報告から始まっており解決のための議論はすでに済んでいるようです。この提案は解決のための文言を確認・議論するためのもので、初めからCWGで議論されており現在はレビュー待ちです。そのため、特に問題が無ければ早めに採択されそうです。

P1315R6 secure_clear (update to N2599)

特定のメモリ領域の値を確実に消去するための関数secure_clear()の提案。

以前の記事を参照

このリビジョンでの大きな変更は、関数名がsecure_clearからmemset_explicitに変更されたことです。他の変更はC言語に向けた文言の表現の選択肢の追加や文言表現の修正、提案文書全体のマイナーな調整などです。

P1478R6 Byte-wise atomic memcpy

アトミックにメモリのコピーを行うためのstd::atomic_load_per_byte_memcpy()/std::atomic_store_per_byte_memcpy()の提案。

以前の記事を参照

このリビジョンでの変更はLEWGなどからのフィードバックを受けて提案する文言を修正した事とそれに伴って文言に関する未解決の質問のセクションを追記した事が主です。

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

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

前回の記事を参照

このリビジョンでの変更はstd::quick_exitstd::_Exitに依存しているため、_Exitも対象に追加した事。および一部のエンティティ名の(提案文書としての)参照方法を変更した事です。

P1689R3 Format for describing dependencies of source files

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

モジュール導入以前は、各翻訳単位のコンパイルは独立して行うことができ、翻訳単位の依存関係はビルドしながら把握すれば十分でした。しかしモジュールを使用すると、ある翻訳単位をコンパイルするためにはそこでインポートされているモジュールのコンパイル(少なくともインターフェースの抽出)が必要となります。
すなわち、モジュールを利用したプログラムでは各翻訳単位のコンパイルに順序付けが必要となります。

このために、ビルドツールはコンパイルする前にこの順序関係を(C++コードとしてではなく)ソースコードから抽出できる必要があります。

このフォーマットは次のような情報を含みます。

  • 依存関係スキャンツールそのものの依存関係
  • スキャンされた翻訳単位がコンパイルされる時に必要となるリソース
  • スキャンされた翻訳単位がコンパイルされた時に提供されるリソース

このフォーマットはその表現としてJSONを使用しその規格を参照しています。そのエンコーディングユニコードであり、特にファイルパスはUTF-8の有効な文字列であることがさらに要求されます。

例えば次のようなソースコードに対しては

export module my.module;

import other.module;
import <header>;

#include "config.h"

次のようになります。

{
  "version": 1,
  "revision": 0,
  "rules": [
    "work-directory": "/scanner/working/dir",
    "inputs": [
      "my.module.cpp"
    ],
    "outputs": [
      "depinfo.json"
    ],
    "depends": [
      "/system/include/path/header",
      "include/path/config.h"
    ],
    "future-compile": {
      "outputs": [
        "my.module.cpp.o",
        "my_module.bmi"
      ],
      "provides": [
        {
          "logical-name": "my.module",
          "source-path": "my.module.cpp",
          "compiled-module-path": "my_module.bmi"
        }
      ],
      "requires": [
        {
          "logical-name": "other.module"
        }
        {
          "logical-name": "<header>",
          "source-path": "/system/include/path/header",
        }
      ]
    }
  ]
}

なお、この動機となったモジュールのコンパイル順の問題はFortranのモジュールが長年抱えている問題と同じものであり、このフォーマットはC++だけではなくFortranでも使用することを想定しているようです。

P2077R2 Heterogeneous erasure overloads for associative containers

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

以前の記事を参照

このリビジョンでの大きな変更は、ヘテロジニアスerase/extractのキーを受け取る引数型をconst K&からK&&へ変更したことです。

当初のヘテロジニアスerase()の宣言は次のように提案されていました。

template <class K>
size_type erase( const K& x );

加えて、このキーに変換可能な型Kiterator/const_iteratorへ変換可能ではない事が要求されています。そのため、Kの全ての値カテゴリからiterator/const_iteratorへの変換可能性を調査している最中に次のような問題が発見されました。

// Compare::is_transparentが有効で型を示すstd::map
using map_type = std::map<...>;

struct HeterogeneousKey {
  HeterogeneousKey() { /*...*/ }

  // 右辺値参照修飾された変換演算子
  operator map_type::iterator() && { /*変換処理*/ }

  // map_type::key_typeとの比較演算子
  // ...
};

void foo() {
  map_type m;
  HeterogeneousKey key;

  m.erase(key); // コンパイルエラー
}
  1. keyの型からテンプレートパラメータKHeterogeneousKeyと推論される
  2. std::is_convertible_v<HeterogeneousKey&&, iterator>trueとなる(テンプレートパラメータ制約を満たしていないと判定される)
  3. ヘテロジニアスerase()オーバーロード候補から外れ、最適な関数は見つからない・・・

ここでeraseに渡っているkeyは左辺値であるため、map_type::iteratorへは変換できないはずで、コンパイルエラーにはならないはずです。しかし、const K&という引数型になっていることで、テンプレートパラメータKには実際の引数の値カテゴリの情報が正しく伝播していません。そのため、何が渡されてもKprvalueにしかなりません。

この問題を解決するには正しく引数の値カテゴリの情報をテンプレートパラメータに伝播させればよく、それはforwarding referenceによって実現できます。つまりK&&となります。

template <class K>
size_type erase(K&& x);

この変更によって先程のコードはコンパイルが通るようになり、期待通りヘテロジニアスerase()を呼び出します。また、右辺値を渡したときはテンプレートパラメータ制約を満たせなくなり、コンパイルエラーとなります。

void foo() {
  map_type m;

  m.erase(HeterogeneousKey{...}); // コンパイルエラー
}

この問題及び解決策はstd::mapに限定されるものではなく、erase()だけではなくextract()でも同様です。

これ以外の変更は、標準の文書に合わせて文字のフォントや修飾を調整した事です。

この提案はこのリビジョンをもってLEWGでの投票にかけられ、問題が無ければC++23導入を目標としてLWGに送られる予定です。

P2136R2 invoke_r

戻り値型を指定するstd::invokeであるinvoke_rの提案。

以前の記事を参照

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

この提案は(R1が)LEWGでの投票にかけられ、問題が無ければC++23導入を目標としてLWGに送られる予定です。

P2175R0 Composable cancellation for sender-based async operations

Executor提案(P0443R14)に対して、非同期処理のキャンセル操作を追加する提案。

現在のExecutor提案には、実行コンテキストに投入した非同期処理がキャンセルされたときに処理が空の結果で完了しそれを通知する機能を提供していますが、非同期処理を呼び出し元がキャンセルするための機能は提供されていません。

非同期処理のキャンセルという操作は並行プログラムにおける基礎的なプリミティブであり、それによって並行処理を構造的に記述できるようにするために、非同期処理のキャンセル操作と、それを伝達するためのメカニズムをExecutorに導入しようとする提案です。

このためには、個々の非同期処理でアドホックなメカニズムによって呼び出し元がキャンセル要求を伝達できるようにする必要があります。例えば、std::chrono::durationを渡すことでタイムアウトによるキャンセルを行う、std::stop_tokenを非同期関数/コルーチンに渡す、又は非同期処理を表現する呼び出し可能な型のメンバとして.cancel()を実装する、などです。

キャンセルを適切に処理するためには、全てのレイヤーがキャンセルをサポートし、そのキャンセル要求は全てのレイヤーに適切に伝播しなければなりません。例えば、タイマーやI/O、ループなど、高レベルの処理の完了が依存しているより低レベルな処理に要求を伝達しなければなりません。

しかし、アドホックカニズムを使用すると、キャンセル可能な処理を構成してそのキャンセル要求を中間層を介して伝播することが困難になります。これは特にwhen_all()アルゴリズムなど、構築済みのsenderによって構成されるアルゴリズムに当てはまり、入力となる元のsenderを作成した非同期処理に渡されるパラメータを制御する方法がありません。

この様な事を考慮したうえで、この提案の目指すキャンセル操作は次のような設計に基づきます。

  • 汎用的に構成可能なキャンセルのためのメカニズムを用意する。これによって、キャンセルに対して透過的であるか、新しいキャンセルスコープを導入して、キャンセル操作を処理のチェーンに挿入できるアルゴリズムを構築できる。
  • キャンセル要求が行われないことがコンパイル時に分かる場合、キャンセルのためのオーバーヘッドがかからないようにする。
  • 非同期処理の呼び出し元と呼び出された側のいずれに対しても、キャンセルのサポートを強制しない。
    • キャンセルのサポートはオプトイン(デフォルトは非サポート)
    • 呼び出し元がキャンセルを要求しない場合は、キャンセルをオプトアウトするために何もする必要が無い

そして、このために次のような変更を提案しています。

  • std::stop_tokenと同様に扱えるクラスを表すための、std::stoppable_tokenコンセプトを追加する
    • 特定のユースケースにおいてより効率的なstop_token-likeな同期プリミティブをstd::stop_tokenの代わりに使用できるようにする
  • std::stoppable_tokenを改善版である2つのコンセプトを追加する。
    • std::stoppable_token_for<CB, Initializer> : stoppable_tokenであることに加えて、stop_tokenインスタンスInitializerの値からT::callback_type<CB>が構築可能であることを表す。
    • std::unstoppable_token : stop_possible()メンバ関数constexprであり常にfalseを返すstop_tokenを表す。
  • その操作に使用するstop_tokenconnectされたrecieverが取得できるようにするためにget_stop_token()CPOを追加する。
  • recieverに関連付けられたstop_tokenの型を求めるための型特性を追加する。
    • std::stop_token_type_t<T>は、型Tを引数として呼び出されたget_stop_token()の戻り値型をdecayして取得する
  • std::stoppable_tokenコンセプトを満たす2つの新しいstop_tokenを追加する
    • std::never_stop_token : キャンセルが不要な場合に使用されるstop_token
    • std::in_place_stop_token : stop_sourcemovable/copyableである必要が無く、stop_tokenの生存期間が対応するstop_sourceの生存期間内に厳密に収まっている場合に使用できるstop_token
  • std::stop_tokenにメンバ型::callback_typeを追加する(std::stoppable_tokenコンセプトのために必要)

提案されているものは、facebookによるC++ Executorの実装であるlibunifexにて既に実装されているようです。

P2186R1 Removing Garbage Collection Support

ガベージコレクタサポートのために追加された言語とライブラリ機能を削除する提案。

以前の記事を参照

このリビジョンでは、EWGおよびLEWGでの投票の結果が追記されています。概ね、これらのものを削除することに異論はないようです。

P2195R1 Electronic Straw Polls

委員会での投票が必要となる際に、メールまたは電子投票システムを用いて投票できるようにする。

以前の記事を参照

このリビジョンでの変更はよく分かりません。LEWGではこれに従った運用が始まっているようです。

P2213R1 Executors Naming

Executor提案(P0443R13)で提案されているエンティティの名前に関する報告書。

以前の記事を参照

このリビジョンでの変更はLEWGのレビューを受けて現在の名前がなぜその名前なのかの根拠を追記した事とP1897関連の名前についてをP2252(未発行)に移動した事です。また、次のものは指示を得られなかったため削除されたようです。

  • connect -> pipe
  • submit -> start
  • sender, receiver -> producer, consumer

また、LEWGはoperation_state(コンセプト)とset_done(CPO)の2つについては変更の必要性を強く認めているようです。

P2216R1 std::format improvements

std::formatの機能改善の提案。

以前の記事を参照

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

  • LEWGのフィードバックを受けて、パラメータパックに非フォーマット文字列を渡したときに診断不用のill-formedとなる動作を削除
  • コンパイル時のチェックについて、診断(コンパイルエラー)を保証し、C++20機能と説明専用クラスだけを用いて実装できるように文言を調整
  • コンパイル時のフォーマットチェックについて実装例を追記
  • コードサイズの肥大化の問題について、実装品質の問題と同様に解決不可能であることを明確化
  • コードサイズの肥大化が実際に起こる例を追記

また、R0のLEWGでの投票の結果も追記されています。この提案にある2つの問題についての議論に時間を割くこと、フォーマット文字列のコンパイル時チェックに失敗したらコンパイルエラーとなる事が望ましいなどのコンセンサスが得られています。

P2233R3 2020 Fall Library Evolution Polls

LEWGが2020年秋に行った投票の対象となった提案文書の一覧。

前回との差分はよく分かりません・・・

P2238R0 Core Language Working Group "tentatively ready" issues for the November, 2020 meeting

11月に行われた全体会議で採択された、6つのコア言語のIssue報告とその解決の一覧。

概要だけを記載しておくと

  1. 構造体(集成体)に対する構造化束縛がmutableメンバを考慮するようになった
  2. 制約によるオーバーロード候補からの除外を、テンプレートパラメータの置換よりも先に行うようにする
  3. “flowing off the end of a coroutine”という用語の意味を明確にする
  4. 展開されていないパラメータパックが、その外側の関数型に依存しないようにする
  5. C言語リンケージを持ち、制約されているfriend関数の複数の宣言が、同じ関数を参照するようにする
  6. requires節にboolにならない(atomic constraintではない)有効な式を指定することがill-formedである事を規定

P2247R1 2020 Library Evolution Report

LEWG(Library Evolution Working Group)の今年2月以降の活動についてまとめた文書。

前回との差分はよく分かりません・・・

P2248R1 Enabling list-initialization for algorithms

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

以前の記事を参照

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

  • R0では議論されていなかったABI互換性についての問題を追記
  • P1997R1への参照と議論を追加
  • [algorithms.requirements]への参照を追加
  • スペルミスやフォーマットの修正

この提案の変更は関数テンプレートのテンプレートパラメータについてのもので、ABIを破壊するものではありません。しかし、std::ranges名前空間の下にあるrangeベースのアルゴリズムでは、テンプレートパラメータの並べ替えが必要になり、それによってマングル名が変化するためABI破壊の可能性があります。

とはいえ、並べ替えられる前と後で対応するテンプレートパラメータに同じ型を指定してインスタンス化する時にのみマングル名の衝突が発生するため、それが起こる可能性は非常に低いはずです(コンセプトのチェックによってコンパイルエラーとなる場合がほとんどのはず)。この様な衝突が発生しないとすれば、ABI非互換による問題は回避されます。

ただ、それら関数テンプレートの明示的インスタンス化が使用されている場合、ABI破壊が発生する可能性があります。ただ、そのような行為はどうやら許可されていないようでもあります。

これらの事を問題となるか、またするかどうかは今後議論されるようです。

P2262R0 2020 Fall Library Evolution Poll Outcomes

2020年11月に行われた全体会議におけるLEWGでの投票の結果。

投票にかけられる提案の一覧はP2233R3 2020 Fall Library Evolution Pollsにあります。

ここでは、投票の結果及び投票者のコメントが記載されています。

P2263R0 A call for a WG21 managed chat service

WG21が管理するチャットサービスを確立するための提案。

現在WG21は、freenodeのIRCチャネルとSlackのcpplangワークスペースの2つのチャットサービスを利用しているようですが、これらはWG21のメンバによって維持・管理されているものではなく、WG21行動規範やISO行動規範に従って管理されているものでもありません。

WG21の慣行及び手順に基づいて管理されるWG21が後援・維持するチャットサービスが必要とされたため、この提案はその検討と議論のためのものです。主に、チャットサービスに求める要件が列挙されています。

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

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

assertマクロは一引数の関数マクロとして定義されており、プリプロセッサは丸括弧のペアのみを考慮します。したがって、次のようなコードはコンパイルエラーとなります。

#include <cassert>
#include <type_traits>

using Int=int;

void f() {
  assert(std::is_same<int,Int>::value); // コンパイルエラー
}

関数マクロの呼び出しにおいては、丸括弧で囲まれていないカンマの出現は引数の区切りとみなされます。その結果、このコードはassertマクロを2引数で呼び出そうとすることになり、コンパイルエラーとなっています。
この事は、巧妙なコードを書く必要があるとはいえCでも同様です。

#include <assert.h>

void f() {
  assert((int[2]){1,2}[0]);   // コンパイルエラー
  struct A {int x,y;};
  assert((struct A){1,2}.x);  // コンパイルエラー
}

Cでは、NDEBUGが定義されていない時にassertの引数に指定された式がwell-definedならばそのままコンパイルされる必要があり、C++もその点に関してC標準を参照しています。

これらのように、現在のassertマクロの定義は適切ではなく初学者にも優しく無いため、このような事が起きないように定義し直そうという提案です。

例えば、可変長引数を利用して次のように定義する事を提案しています。

#define assert(...) ((__VA_ARGS__)?(void)0:std::abort())

ただ、これだと0引数で呼び出すことができてしまい、その場合に分かりづらいコンパイルエラーが発生する可能性があります。しかし、1つの引数+可変長引数にすると実装が複雑となることからそのままにしています。

そして、この変更はおそらく既存のコードを壊しません。これまでのassertはそのまま動作し、一部コンパイルエラーを起こしていたものがコンパイル可能となるだけのはずです。

この提案は同時にC標準に対してのものも含んでおり(文書を共有している)、CとC++両方で同時に解決する事を目指しています。

P2265R0 Renaming any_invocable

提案中のany_invocableの名前を変更する提案。

any_invocablestd::functionの制約付きのサブセットであり、最大の特徴はムーブオンリーであることです。しかし、その名前はその特徴を表しておらず、一般的なC++開発者はムーブオンリーstd::functionの名前としてany_invocableを期待することは無いといっても過言ではありません。

また、any_invocableは進行中のfunction_refCallableオブジェクトを所有せず参照するだけの軽量なstd::function)及びstd::functionの補完を行い標準ライブラリの関数ラッパ機能をより完全にするためのものです。しかし、それらとの名前の一貫性が無く、function_refに比べると何をするものなのか分からない命名になっています。

これらの理由により、any_invocableという名前は適切ではないため、変更を推奨する提案です。

この提案ではエンティティの名前に求めるもの、求められるものを6項目上げて説明したうえで、それに従った名前として、名前にfunctionを含めたうえでmovablemove_onlyなどのプリフィックスを付けることが望ましいと述べています(筆者の方はmovable_functionを推しています)。

LEWGのオンライン会議と投票の結果(2021/01/05)、変更後の名前としてmove_only_functionが採用されました。それに伴って、any_invocable(move_only_function)<functional>ヘッダに導入されるように変更されました。P0228はすでにLWGに送られていましたが、これらの変更を反映した上で再送されることになります。

P2268R0 Freestanding Roadmap

C++の機能のフリースタンディング化について、方向性の概説と協力者を募る文書。

検討されフリースタンディングとなるには問題がある機能、有用性があるが未検討で実装経験が必要な機能、現在進行中の機能などについて解説されています。

筆者の方は多くのフリースタンディング関連提案を出していますが、ここに上げられているものを全て実装・テストし提案するにはリソースが足りないため、これらのトピックの実装と提案文書の共著に興味のある人は連絡してほしいとの事です。

P2272R0 Safety & Security Review Board

C++標準化委員会に、安全性とセキュリティのための新しいReview Groupを設立する提案。

モダンC++safety criticalなアプリケーション及びセキュアなアプリケーションの分野での優位性を脅かされつつあります。C++が安全でもなくセキュアでもないという認識は、RustやGoなどの他の言語に相対的な優位性をもたらしています。C++11以降、セロオーバーヘッド原則に基づいたC++の進化はC++の競争力の確立に貢献しましたが、C++には安全性とセキュリティに関して行うべきことがまだ残っています。

安全性とセキュリティが強く求められる分野には例えば、航空宇宙、自動運転、ネットワーク、医療デバイスなどがありますが、これらの分野の全てでC++は安全でもセキュアでもないという認識があり、すべての分野でC++と比較してより安全・セキュアなプログラミング言語が存在しています。

このような認識に対処するために、C++標準化委員会は言語標準自体でこれらの問題に対処するために協調して努力する必要があり、そのために専門のReview Groupを設立する、という提案です。

このグループの活動は主に次のようなものです。

  • ほかのグループ(EWGやLEWGなど)から受け取った安全性とセキュリティのための新しい機能の評価を行う。
  • C++の将来の方向性について、安全性とセキュリティの観点からDirection Groupなどのほかグループに助言を行う。
  • 安全性とセキュリティの実際に認識された問題に対処するために、既存の仕様の変更を推奨する。

P2273R0 Making std::unique_ptr constexpr

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

C++20で定数式でのnew/deleteが許可され、コンパイル時に動的メモリ確保ができるようになりました。しかし、そこにはstd::unique_ptrはなく、C++11以前の時代の手動メモリ管理を強いられることになります。

std::unique_ptrを定数式で使えるようにすることで、メモリ管理を自動化し、実行時と定数式で同じコード共有できるようになります。

例えば、次のようなコードがコンパイルできるようにするものです。

#include <memory>

constexpr auto fun() {
  auto p = std::make_unique <int>(4); // 今は出来ない

  return *p;
}

int main () {
  constexpr auto i = fun();
  static_assert(4 == i);
}

筆者の方はフォークしたlibc++でこれを実装し、ポインタ比較以外の所では問題が無いことを確認しているそうです。また、std::shared_ptrconstexpr提案も予定しているようです。

P2274R0 C and C++ Compatibility Study Group

前回の会議で決定された、CとC++の共通する部分の相互互換性についてのStudy Group(SG22)を設立するにあたって、WG21とWG14の文化の違いの説明やポリシーを記述した文書。

WG21から見たWG14

  • WG14はWG21と比較して小さな委員会で、会議への出席者は20人程。全ての作業はWorking Groupに分割されず、全体会議で行われる。
  • Study Groupの数も少なめで、SGの活動は常に委員会の会議の外で行われている。
  • WG14での提案の採択は、まず全体会議で提出された提案にフィードバックを行い、著者はそれを受けて再提出する。最終的に、提案は会議で採択されるか、WG14委員会に対する変更への動機付けに失敗するかのどちらか。
  • WG14では、小さなデバイスや特殊なハードウェアにおける実装などニッチなものも含めた実装全体を重視する。
    • WG21では主要な3実装を重視する傾向にある。
  • WG14には委員会の運営原則を記した憲章がある(N2086)。提案には憲章の期待することにどのように応えているかの情報が付加されていることが望ましい。
    • もし提案が憲章に反している場合、なぜその提案には憲章が適用されないのかを論理的根拠とともに述べる必要がある。
  • WG14では実装が少なくとも2つ無いような独創的な提案を採択しない。通常、あるコンパイラのフォークやあまり使用されていないものを複数の実装としてカウントしない。
    • ただし、C++による標準化を1つの実装としてカウントする事を検討中
  • WG21では後方互換性を意図的に壊すことがあるが、WG14では既存のコードが壊れないためにあらゆる手を尽くす。
    • 機能の追加時には予約済みの識別子を用いるように特に注意し、(影響がC++に及び)WG21内で問題を引き起こさない(とみなされるような)場合でも、存在しうる後方互換性の懸念について呼びかけを行う。
  • WG14には標準文書作成を支援するWGはなく、Wordingの議論はオフラインまたはメーリングリストでよく行われる(本会議ではあまり行われない)。
  • 追加のレビューを行うために会議中に提案文書を書き直す事は一般的では無い。WG14委員会は会議のメーリングリストに提出されない提案の議論を行わない事が多いため。
  • C言語の次のリリーススケジュールは2023年になる予定。

WG14から見たWG21

  • WG21は大きな委員会で、本会議には通常250人以上が参加する。出席者の数が多いため本会議ではほとんど作業は行われず、代わりに同じ週に同時に実行される各WGとSGそれぞれで分割して作業される。
    • 4つのWG(EWGとLEWGの2つはC++の進化に焦点を当て、CWGとLWGの2つは標準の表現に焦点を当てている)と多数のSGがある。
  • WG21での提案採択のプロセスは基本的にパイプラインで行われている。提案は多くの場合最初にIncubatorグループ(EWGI,LEWGI)か適切なSGのどちらかで議論が開始される。そこのグループが提案に満足すれば、最も関連性の高いSGかEvolution WG(EWG,LEWG)に移される。そこを通過すると、標準化のWording作成を支援するグループ(CWG/LWG)でチェックされ、最終的に本会議での全体投票にかけられる。
  • WG21には提案の著者が標準に対する文言を作成することを支援するWGがあるため、SG22で見る提案には標準に対する文言が欠けている場合がある。
  • WG21には憲章はないが、委員会の方針と手続き、及びDirection Groupが設定した野心的な目標をまとめた文章がある。これらは有用な背景情報を提供するかもしれない。
  • WG21では、提案の実装経験を非常に価値のあるものだと考えているが、提案を採択するにあたって実装経験に関する要件はない。
  • WG21では、あまり人気の無い実装が最終的にはそれに続くものとして、最も人気のある一部のC++実装における実装経験を重視する傾向にある。
    • WG21が重視する実装には例えば、Clang, GCC, MSVC, EDGが含まれる。
  • WG21は、新機能を追加しようとする際に破壊的変更を制限しようとしているが、ユーザーコードを壊すことが許容される場合にはそれを許可するいくつかの(文書化されていない)ルールがある。これはWG14から来た人にはなじみの無い方法ではあるが、WG21が後方互換性について考慮していることを意味している。
  • C++言語の次のリリーススケジュールは2023年になる予定。現在のWG21のスケジュールはP1000にある。

数日後かもしれない・・・

この記事のMarkdownソース

[C++] C++17イテレータ <=> C++20イテレータ != 0

これはC++ Advent Calendar 2020の24日めの記事です(大遅刻です、すいません)。

前回前々回C++20のイテレータは一味も二味も違うぜ!という事を語ったわけですが、具体的にどう違うのかを見てみようと思います。

以下、特に断りが無ければIイテレータ型、iイテレータのオブジェクトだと思ってください。

iterator

イテレータとは何か?という事は、C++20ではstd::input_or_output_iteratorコンセプト、C++17ではCpp17Iterator要件がそれを定義しています。

C++17

まず、次の要件が要求されています

  • iterator_­traits<I>インスタンスが存在する
  • コピー構築可能
  • コピー代入可能
  • デストラクト可能
  • lvalueについて、スワップ可能
  • iterator_­traits<I>​::​difference_­typeは符号付き整数型もしくはvoid

これらの~可能というのはそれはそれで一つの名前付き要件になっているのですがここでは深堀しません。おそらく言葉から分かるのとそう異なる意味ではないはずです。

そして、次の式が可能であることが要求されます

戻り値
*i 未規定
++i I&

間接参照と前置インクリメントによる進行が可能であれ、という事です。

C++20

C++20は言葉で長々語ったりしません。コンセプトで語ります。

template<class I>
concept input_or_output_iterator =
  requires(I i) {
    { *i } -> can-reference;
  } &&
  weakly_incrementable<I>;

can-referenceは戻り値型がvoidではない事を表すコンセプトです。
ここで直接見ることの出来るのは間接参照の要求です。これはC++17の要求と同じことを意味しています。

std::weakly_incrementable++によってインクリメント可能であることを表すコンセプトです。

template<class I>
concept weakly_incrementable =
  default_initializable<I> && movable<I> &&
  requires(I i) {
    typename iter_difference_t<I>;
    requires is-signed-integer-like<iter_difference_t<I>>;
    { ++i } -> same_as<I&>;
    i++;
  };

コンセプトを深さ優先探索していくとスタックオーバーフローで脳内コンパイラがしぬので適宜cpprefjpを参照してください。

requires式の中では、difference_­typeが取得可能であることと、前置インクリメントに対しては先ほどのC++17と同じことが要求されています。

大きく異なる点は、デフォルト構築可能であることと、コピー可能ではなくムーブ可能であることです。また、difference_­typevoidが認められない代わりに符号付き整数型と同等な任意の型が許可されており、後置インクリメントが要求されています。

C++20イテレータiterator_traitsで使用可能である事を要求されていませんが、C++20のiterator_traitsC++17互換窓口としてなんとかして情報をかき集めてきてくれるのでほぼ自動で使用可能となるはずです。また、前々回に説明したiterator_traitsに代わるイテレータ情報取得手段はイテレータの性質からその情報を取ってくるのでiterator_traitsのようなものはほぼ必要なくなっています。したがって、iterator_traitsで使用可能かどうかは差異とはみなさないことにします。

差異

結局、C++20イテレータC++17イテレータの差異は次のようになります。

要求 C++20 C++17
デフォルト構築可能性 要求される 不要
ムーブ可能性 要求される 要求される
コピー可能性 不要 要求される
difference_­type 符号付整数型 or それと同等な型 符号付整数型 or void
後置インクリメント 要求される 不要

コピー可能 -> ムーブ可能ですが、ムーブ可能 -> コピー可能ではありません。difference_­typeを除いて、C++20のイテレータC++17から制約が厳しくなっています。

殆どの要件が厳しくなっていることからC++17イテレータでしかないものをC++20イテレータとして扱うことは出来ませんが、difference_­typeの差異を無視すればC++20イテレータC++17イテレータとして扱うことは出来そうです。

is-integer-likeが求めているものは整数型とほぼ同様にふるまうクラス型であり、通常の演算や組み込み整数型との相互の変換が可能である必要があります。すなわちジェネリックなコードにおいては何かケアの必要なく整数型として動作するものなので、このdifference_­typeの差異はほとんど気にする必要は無いでしょう。

input iteratorr

入力イテレータとは何ぞ?という事は、C++20ではstd::input_iteratorコンセプト、C++17ではCpp17InputIterator要件がそれを定義しています。

C++17

まず、次の要件が要求されています

  • Cpp17Iterator要件を満たす
  • 同値比較可能

そして、次の式が可能であることが要求されます(ここではCpp17Iterator要件で要求されていたものを上書きする形で含んでいます)

戻り値
i1 != i2 contextually convertible to bool
*i referencce、要素型Tに変換可能であること
i->m
++i I&
(void)i++
*i++ 要素型Tに変換可能であること

== !=による同値比較と->、後置++が使用可能である事が追加されました。

C++20

C++20はコンセプトで(ry

template<class I>
concept input_iterator =
  input_or_output_iterator<I> &&
  indirectly_readable<I> &&
  requires { typename ITER_CONCEPT(I); } &&
  derived_from<ITER_CONCEPT(I), input_iterator_tag>;

ITER_CONCEPTについては前回の記事をご覧ください。要はiterator_categoryを取得してくるものです。

std::indirectly_readableは少し複雑ですが定義そのものは次のようなものです。

template<class In>
concept indirectly-readable-impl =
  requires(const In in) {
    typename iter_value_t<In>;
    typename iter_reference_t<In>;
    typename iter_rvalue_reference_t<In>;
    { *in } -> same_as<iter_reference_t<In>>;
    { ranges::iter_move(in) } -> same_as<iter_rvalue_reference_t<In>>;
  } &&
  common_reference_with<iter_reference_t<In>&&, iter_value_t<In>&> &&
  common_reference_with<iter_reference_t<In>&&, iter_rvalue_reference_t<In>&&> &&
  common_reference_with<iter_rvalue_reference_t<In>&&, const iter_value_t<In>&>;

iter_value_tなどが使用可能であると言うことは窓口が違うだけで、iterator_traitsで取得可能と言う要件と同じ意味です。また、std::iter_rvalue_reference_tstd::ranges::iter_moveを使用して右辺値参照型を取得しており、std::ranges::iter_moveはカスタマイぜーションポイントとしてC++17イテレータに対しても作用します。したがって、これらは差異とはみなさないことにします。

間接参照の戻り値型に対する要件は同じです。大きく違うのはイテレータreferencevalue_typeの間にcommon referenceが要求されている事です。また、後置インクリメントに関してはinput_or_output_iteratorから引き継いでいるのみで、C++17イテレータが戻り値型に要求があるのに対してC++20イテレータにはそれがありません。

差異

結局、C++20入力イテレータC++17入力イテレータの差異は次のようになります(iteratorでの差異を含めています、追加されたものは先頭に+で表示)。

要求 C++20 C++17
デフォルト構築可能性 要求される 不要
ムーブ可能性 要求される 要求される
コピー可能性 不要 要求される
difference_­type 符号付整数型 or それと同等な型 符号付整数型 or void
+ == !=による同値比較 不要 要求される
+ -> 不要 要求される
+ 後置インクリメントの戻り値型 任意(voidも可) value_typeに変換可能な型
+ referencevalue_typeとのcommon reference 要求される 不要

後置インクリメントができる事、と言う点においては一致した代わりに差異が増えました。C++17 -> C++20で緩和されたものもあれば厳しくなったものもあり、C++20入力イテレータC++17入力イテレータの間には相互に互換性がありません。

ouptut iterator

出力イテレータとは一体?という事は、C++20ではstd::output_iteratorコンセプト、C++17ではCpp17OutputIterator要件がそれを定義しています。

C++17

まず、次の要件が要求されています

  • Cpp17Iterator要件を満たす

そして、次の式が可能であることが要求されます(ここではCpp17Iterator要件で要求されていたものを上書きする形で含んでいます)

戻り値
*i = o 結果は使用されない
++i I&
i++ const I&に変換可能であること
*i++ = o 結果は使用されない

後で関係してくる事として、このiは左辺値(I&)です。

4つ全ての操作において、それぞれの操作の後でイテレータiが間接参照可能であることは要求されません。

C++20

コンセプトによって次のように定義されます。

template<class I, class T>
concept output_iterator =
  input_or_output_iterator<I> &&
  indirectly_writable<I, T> &&
  requires(I i, T&& t) {
    *i++ = std::forward<T>(t);
  };

ここを見るぶんには違いがなさそうですね。

std::indirectly_writableコンセプトは次のように定義されます

template<class Out, class T>
concept indirectly_writable = 
  requires(Out&& o, T&& t) {
    *o = std::forward<T>(t);
    *std::forward<Out>(o) = std::forward<T>(t);
    const_cast<const iter_reference_t<Out>&&>(*o) = std::forward<T>(t);
    const_cast<const iter_reference_t<Out>&&>(*std::forward<Out>(o)) = std::forward<T>(t);
  };

*iによる出力が可能であることが求められているのですが、制約式がやたら複雑です。上2つは左辺値からでも右辺値からでも出力可能である事と言う要件でしょう。C++17までは右辺値イテレータからの出力は要求されていません。
下二つのconst_castをしている制約式は、規格書の言によれば間接参照がprvalueを返すようなプロクシイテレータを弾くためにあるらしいです。よくわかんない・・・

また、std::indirectly_writableコンセプトの意味論的な制約に目を向けると、出力操作後の値の同一性が要求されています。またその一部として、出力操作の後でiが間接参照可能であることは要求されていません。

差異

結局、C++20出力イテレータC++17出力イテレータの差異は次のようになります(iteratorでの差異を含めています、追加されたものは先頭に+で表示)。

要求 C++20 C++17
デフォルト構築可能性 要求される 不要
ムーブ可能性 要求される 要求される
コピー可能性 不要 要求される
difference_­type 符号付整数型 or それと同等な型 符号付整数型 or void
+ 右辺値イテレータからの出力可能性 要求される 不要
+ prvalueへの出力の禁止 要求される 不要

追加された二つはC++17 -> C++20で制約が厳しくなっています。したがって、C++17出力イテレータC++20出力イテレータに対して互換性がありません。一方、C++20出力イテレータC++17出力イテレータに対してdifference_­type以外の所では互換性があります。

特に、C++20出力イテレータiterator_traitsを介して性質を取得される時、特に特殊化がなければdifference_­typevoidになります。その場合、C++20出力イテレータC++17出力イテレータとして完璧に振る舞うことができます。

これを利用して、出力イテレータより強いC++20イテレータに対するiterator_traitsからの問い合わせに対して常にoutput iteratorとして応答することで後方互換性を確保する、と言うアイデアがあるそうです。無論、これに意味があるのかはイテレータによるでしょう。

forward iterator

前方向イテレータって何?という事は、C++20ではstd::forward_iteratorコンセプト、C++17ではCpp17ForwardIterator要件がそれを定義しています。

C++17

まず、次の要件が要求されています

  • Cpp17InputIterator要件を満たす
  • デフォルト構築可能
  • Imutable iteratorならば、referenceTの参照(TIの要素型)
  • Iconstant iteratorならば、referenceconst Tの参照
  • マルチパス保証

そして、次の式が可能であることが要求されます(ここではCpp17InputIterator要件で要求されていたものを上書きする形で含んでいます)

戻り値
i1 != i2 contextually convertible to bool
*i referencce、要素型Tに変換可能であること
i->m
++i I&
i++ const I&に変換可能であること
*i++ referencce

C++20

コンセプトによって次のように定義されます。

template<class I>
concept forward_iterator =
  input_iterator<I> &&
  derived_from<ITER_CONCEPT(I), forward_iterator_tag> &&
  incrementable<I> &&
  sentinel_for<I, I>;

std::incrementablestd::weakly_incrementableを少し強くしたものです。

template<class I>
concept incrementable =
  regular<I> &&
  weakly_incrementable<I> &&
  requires(I i) {
    { i++ } -> same_as<I>;
  };

ここで重要なのは、後置インクリメントの戻り値型が自分自身であることが要求された事です。

もう一つ、std::sentinel_forイテレータ自身が終端を示しうる事を表すコンセプトで、次のようなものです。

template<class S, class I>
concept sentinel_for =
  semiregular<S> &&
  input_or_output_iterator<I> &&
  weakly-equality-comparable-with<S, I>;

std::regularstd::semiregularを包含しており、等値比較可能である事とコピー可能であることを要求しています。結局、std::sentinel_for<I, I>は自分自身との== !=による比較が可能である事を表します。

マルチパス保証はstd::forward_iteratorの意味論的な要件によって要求されています。

差異

結局、C++20前方向イテレータC++17前方向イテレータの差異は次のようになります(input iteratorでの差異を含めています、追加されたものは先頭に+で表示)。

要求 C++20 C++17
difference_­type 符号付整数型 or それと同等な型 符号付整数型 or void
-> 不要 要求される
後置インクリメントの戻り値型 I const I&に変換可能な型
referencevalue_typeとのcommon reference 要求される 不要

デフォルト構築、コピー可能、等値比較可能、などが共通の性質となりました。残ったもので変わったのは後置インクリメントの戻り値型ですが、これはC++20イテレータの方がC++17イテレータに比べて厳しく指定されています。

ここでもC++20前方向イテレータC++17前方向イテレータには相互に互換性はありませんが、difference_type->の差を無視すれば、C++20前方向イテレータC++17前方向イテレータとして使用することができます。

bidirectional iterator

双方向イテレータとは?という事は、C++20ではstd::bidirectional_iteratorコンセプト、C++17ではCpp17BidirectionalIterator要件がそれを定義しています。

C++17

まず、次の要件が要求されています

  • Cpp17ForwardIterator要件を満たす

そして、次の式が可能であることが要求されます(ここではCpp17ForwardIterator要件で要求されていたものを含んでいます)

戻り値
i1 != i2 contextually convertible to bool
*i referencce、要素型Tに変換可能であること
i->m
++i I&
i++ const I&に変換可能であること
*i++ referencce
--I I&
i-- const I&に変換可能であること
*i-- referencce

C++20

コンセプトによって次のように定義されます。

template<class I>
concept bidirectional_iterator =
  forward_iterator<I> &&
  derived_from<ITER_CONCEPT(I), bidirectional_iterator_tag> &&
  requires(I i) {
    { --i } -> same_as<I&>;
    { i-- } -> same_as<I>;
  };

ここは深掘りする必要がないですね、C++17要件とほとんど同じ事を言っています。

差異

結局、C++20双方向イテレータC++17双方向イテレータの差異は次のようになります(forward iteratorでの差異を含めています、追加されたものは先頭に+で表示)。

要求 C++20 C++17
difference_­type 符号付整数型 or それと同等な型 符号付整数型 or void
-> 不要 要求される
後置インクリメントの戻り値型 I const I&に変換可能な型
referencevalue_typeとのcommon reference 要求される 不要
+ 後置デクリメントの戻り値型 I const I&に変換可能な型

追加されたのは後置デクリメントの戻り値型ですが、インクリメントと同様にC++20イテレータの方がC++17イテレータに比べて厳しく指定されています。

互換性に関しては前方向イテレータと同様です。difference_type->の差を無視すれば、C++20双方向イテレータC++17双方向イテレータとして使用することができます。

random access iterator

ランダムアクセスイテレータって・・・?という事は、C++20ではstd::random_access_iteratorコンセプト、C++17ではCpp17RandomAccessIterator要件がそれを定義しています。

C++17

まず、次の要件が要求されています

  • Cpp17BidirectionalIterator要件を満たす

そして、次の式が可能であることが要求されます(ここではCpp17ForwardIterator要件で要求されていたものを含んでいます)

戻り値
i1 != i2 contextually convertible to bool
*i referencce、要素型Tに変換可能であること
i->m
++i I&
i++ const I&に変換可能であること
*i++ referencce
--I I&
i-- const I&に変換可能であること
*i-- referencce
i += n I&
i + n
n + i
I
i -= n I&
i - n I
i1 - i2 deference_type
i[n] referenceに変換可能であること
i1 < i2 contextually convertible to bool
i1 > i2 contextually convertible to bool
i1 <= i2 contextually convertible to bool
i1 >= i2 contextually convertible to bool

出てくるnIdeference_typeの値です。つまり、deference_typeは符号付整数型である事を暗に要求しています。また、4つの順序付け比較< > <= >=は全順序の上での比較であることが要求されています。

C++20

コンセプトによって次のように定義されます。

template<class I>
concept random_access_iterator =
  bidirectional_iterator<I> &&
  derived_from<ITER_CONCEPT(I), random_access_iterator_tag> &&
  totally_ordered<I> &&
  sized_sentinel_for<I, I> &&
  requires(I i, const I j, const iter_difference_t<I> n) {
    { i += n } -> same_as<I&>;
    { j +  n } -> same_as<I>;
    { n +  j } -> same_as<I>;
    { i -= n } -> same_as<I&>;
    { j -  n } -> same_as<I>;
    {  j[n]  } -> same_as<iter_reference_t<I>>;
  };

std::totally_ordered<I>は全順序の上での4つの順序付け比較が可能である事を表し、std::sized_sentinel_for<I, I>は2項-によって距離が求められるイテレータである事を表しています。

その後に並べられているものも含めて、ほぼほぼC++17イテレータに対するものと同じ要求がなされています。

差異

結局、C++20ランダムアクセスイテレータC++17ランダムアクセスイテレータの差異は次のようになります(forward iteratorでの差異を含めています、追加されたものは先頭に+で表示)。

要求 C++20 C++17
difference_­type 符号付整数型 or それと同等な型 符号付整数型
-> 不要 要求される
後置インクリメントの戻り値型 I const I&に変換可能な型
referencevalue_typeとのcommon reference 要求される 不要
後置デクリメントの戻り値型 I const I&に変換可能な型
+ i[n]の戻り値型 reference referenceに変換可能な型

添字演算子の戻り値型に関してC++20イテレータはより厳しく指定されています。

結局互換性に関しては双方向・前方向イテレータと同様です。difference_type->の差を無視すれば、C++20ランダムアクセスイテレータC++17ランダムアクセスイテレータとして使用することができます。

contiguous iterator

隣接イテレータ🤔という事は、C++20ではstd::contiguous_iteratorコンセプトがそれを定義しています。
C++17では文章でひっそりと指定されていたのみで、名前付き要件になっておらずC++20にも対応する要件はありません(cppreference.comにはLegacyContiguousIteratorとして記述があります)。

C++17

C++17でひっそりと指定されていた文章を読み解くと、次のような要件です

  • Cpp17RandomAccessIterator要件を満たす
  • 整数値n、間接参照可能なイテレータi(i + n)について
    • *(i + n)*(addresof(*i) + n)と等価(equivalent

要はイテレータn進めても、要素のポインタをn進めても、同じ要素を指してね?っていうことです。なるほど確かにcontiguous

C++17ではcontiguous iteratorという分類を導入し、std::arraystd::vectorなどのイテレータcontiguous iteratorであると規定はしましたが、イテレータカテゴリとして正式にライブラリに取り入れたわけではありませんでした。

そのため、contiguous iteratorであると規定したイテレータさえも、ジェネリックコード上ではランダムアクセスイテレータとしてしか扱えませんでした。C++17隣接イテレータという種類のイテレータは実質的に存在していないのです。

C++20

C++20では正式にライブラリに取り入れられ、コンセプトによって定義されています。

template<class I>
concept contiguous_iterator =
  random_access_iterator<I> &&
  derived_from<ITER_CONCEPT(I), contiguous_iterator_tag> &&
  is_lvalue_reference_v<iter_reference_t<I>> &&
  same_as<iter_value_t<I>, remove_cvref_t<iter_reference_t<I>>> &&
  requires(const I& i) {
    { to_address(i) } -> same_as<add_pointer_t<iter_reference_t<I>>>;
  };

3つ目の制約式は間接参照の結果がlvalueとなることを要求しており、4つ目の制約式はIreferenceからCV修飾と参照修飾を取り除いたものが要素型になることを要求しています。

最後のrequires式にあるstd::to_addressというのはC++20から追加されたもので、イテレータを含めたポインタ的な型の値からそのアドレスを取得するものです。その経路はstd::pointer_traitsが利用可能ならそこから、そうでないならoperator->()の戻り値を再びstd::to_addressにかけることによってアドレスを取得します(つまり、operator->()がスマートポインタを返していてもいいわけです・・・)。

イテレータstd::pointer_traitsを特殊化することを求められていないため、イテレータ型に対してのstd::to_addressは実質的にイテレータoperator->()を利用することになります。

そして、std::add_pointerは参照型に対しては参照を除去したうえでポインタを足します。

最後の制約式は全体として、operator->が利用可能であり、その戻り値から最終的に得られる生のポインタ型は、間接参照の結果から取得したアドレスのポインタ型と同じ、であることを要求しています。

そして、std::contiguous_iteratorコンセプトの意味論的な制約として、std::to_addressによって得られるポインタと、間接参照の結果値を指すポインタが一致すること、及び2つのイテレータの間の距離とその要素を指すポインタ間距離が等しくなることを要求しています。

わかりにくい制約ですが、contiguous iteratorというのが実質的にポインタ型を指していることを考えると少し見えてくるものがあるでしょうか。

ポインタではない隣接イテレータは存在意義が良く分かりませんが、これらの制約は直接ポインタ型を要求しておらず、メモリ上の連続領域をラップした形のポインタではない隣接イテレータというのを作ろうと思えば作れることを示しています。

差異

C++17隣接イテレータは居ないので、差異はあっても気にする必要はありません。C++20隣接イテレータC++17コードからはC++17ランダムアクセスイテレータとしてしか扱われることはないでしょう。

C++20隣接イテレータは実質的に->が要求されるようになったため、C++17ランダムアクセスイテレータとして扱う時の非互換な部分はdeference_typeだけとなります。とはいえ、ジェネリックなコードにおいてはそこはあまり気にする必要はなさそうですので、実質的にはC++20隣接イテレータC++17ランダムアクセスイテレータに対して後方互換性があるとみなして良いでしょう。

まとめ

振り返ると結局、大きな差異というのは次のものでした

この->が抜け落ちているのは忘れているわけではなく、意図的なものの様です。なぜかは知りません。

->を無視すると、C++20前方向イテレータ以上の強さのイテレータは同じカテゴリのC++17イテレータに対して後方互換性があり、必然的にC++17入力イテレータに対して後方互換性があります。また、C++20隣接イテレータは全てのC++17イテレータに対して実質的に後方互換性を持っています。

一方全てのカテゴリで、C++17イテレータC++20イテレータに対する前方互換性はありません。要件が厳しくなっているためで、中には使用できるものもないではないかもしれませんが、多くの場合はC++20イテレータコンセプトによって弾かれるでしょう。

<ranges>の各種viewに代表されるC++20イテレータでは、メンバ型としてiterator_conceptiterator_categoryを二つ備えることでC++17互換イテレータとしての性質を表明しています(その詳細は前回参照)。そこでは、iterator_conceptがランダムアクセスイテレータ等であっても、iterator_categoryは常に入力イテレータとする運用が良く行われているように見えます。
これを見るに、標準化委員会的にはC++20イテレータ->の欠如は対C++17互換にとって重要な事とはみなされてはいないようです。

この記事のMarkdownソース

[C++]C++20からのiterator_traits事情

これはC++ Advent Calendar 2020の14日めの記事です。

C++20のiterator_traitsには、C++17以前のコードに対する互換レイヤとしての複雑な役割が与えられるようになります。従って、C++20からのイテレータ情報の問い合わせには前回説明したものを利用するようにする必要があります。

結論だけ先に書いておくと

前回も合わせてお読みいただくと理解が深まるかもしれません。

目次

以下、特に断りが無ければIイテレータ型だと思ってください。

C++20におけるiterator_traitsの役割

前回見たように、C++20以降はイテレータ利用にあたってはiterator_traitsを利用する必要は全く無くなっています。それに伴ってiterator_traitsにはC++20イテレータC++17互換イテレータとして利用するための互換レイヤとしての役割が新たに与えられています。

どういう事かというと、C++20のイテレータC++17イテレータから求められることが変化しており、C++20イテレータにはC++17イテレータに対する後方互換性がありません。そのために、C++17のコードからC++20以降のイテレータを利用しようとすると謎のコンパイルエラーが多発する事になるでしょう。
そんな時でも、iterator_traitsC++17以前のコードからは利用されているはずで、イテレータを利用する際は何かしらそれを介しているはずです。そこで、iterator_traitsイテレータ互換性のチェックとC++17イテレータへの変換を行う事にしたようです。

iterator_traitsによるイテレータカテゴリの取得経路

C++20のiterator_traits<I>は大まかには次のようにIiterator_categoryを取得しようとします。

  1. Iのメンバ型
    • iterator_categoryI::iterator_categoryから取得
  2. IC++17入力イテレータに準するのであれば、可能な操作から
    • iterator_categoryIが4つのC++17イテレータコンセプトのどれに準ずるかによって決定
  3. IC++17出力イテレータに準ずるのであれば、そう扱う
    • iterator_categoryoutput_iterator_tag
  4. 上記いずれでも取得できなかった場合、iterator_traits<I>は空

この手順内でC++17入力イテレータとか言っているものはコンセプトです。ただし、C++20から使用可能となっている各種イテレータコンセプトではなく、C++17までのイテレータで要求されていたことを構文的に列挙しただけのとても簡易なものです。
これらのC++17イテレータコンセプトはC++20イテレータコンセプトほど厳密ではなく、同じカテゴリでも要件が異なるため互換性がありません。

結果、2番目の手順で取得されるiterator_categoryC++17基準の判定によって決定されます。

イテレータコンセプトによるイテレータカテゴリの取得経路

C++20で提供される、std::input_iteratorをはじめとする各イテレータカテゴリを定義するコンセプトの事をまとめて イテレータコンセプト と呼びます。

std::output_iteratorを除く5つのイテレータコンセプトは、ITER_CONCEPTという操作(説明専用のエイリアステンプレート)によってイテレータカテゴリを取得します。

そして、取得されたカテゴリタグ型がどのカテゴリタグ型から派生しているかを調べて、型Iがどのイテレータカテゴリを表明しているのかを取得します。たとえば、std::random_access_iteratorならば、std::derived_from<ITER_CONCEPT(I), std::random_access_iterator_tag>のようにチェックします。継承関係もチェックすることで、より強いイテレータも含めて判定でき、将来的なイテレータカテゴリの増加にも備えています。
もちろんこれだけではなく、Iが備えているインターフェースがそのカテゴリのイテレータとして適格であるかかもチェックされます。

ITER_CONCEPT(I)

ITER_CONCEPT(I)は次のようにIからイテレータカテゴリを取得します。

  1. iterator_traits<I>の明示的特殊化が無い場合(iterator_traitsのプライマリテンプレートが使用される場合)
    1. I::iterator_conceptから取得
    2. I::iterator_categoryから取得
    3. random_access_iterator_tagを取得
  2. iterator_traits<I>の明示的特殊化がある場合
    1. iterator_traits<I>::iterator_conceptから取得
    2. iterator_traits<I>::iterator_categoryから取得
    3. ITER_CONCEPT(I)は型名を示さない

1-3による手順はフォールバックです。イテレータカテゴリが取得できない場合はとりあえずランダムアクセスイテレータとして扱っておいて、イテレータコンセプトの他の部分で判定するようにするためのものです。

iterator_traitsによるカテゴリの取得時と大きく異なるところは、iterator_conceptがあればそれを優先する点にあります。

itereator_conceptiterator_category

iterator_conceptC++20からのイテレータカテゴリ表明のためのメンバ型です。やっていることはiterator_categoryと同じです。

これは主にポインタ型の互換性を取るために導入されたもので(C++20からポインタ型のイテレータカテゴリはcontiguous iteratorとなる)、それまで使用されていたiterator_categoryを変更しないようにカテゴリを更新しようとするものです。
それはiterator_traitsのポインタ型に対する特殊化に見ることができます。

namespace std {
  template<class T>
    requires is_object_v<T>
  struct iterator_traits<T*> {
    // C++20からのカテゴリ
    using iterator_concept  = contiguous_iterator_tag;
    // C++17までのカテゴリ
    using iterator_category = random_access_iterator_tag;
  
    using value_type        = remove_cv_t<T>;
    using difference_type   = ptrdiff_t;
    using pointer           = T*;
    using reference         = T&;
  };
}

これによって、Iiterator_categoryを見ればC++17までのイテレータとしてのカテゴリが、iterator_conceptを見ればC++20からのイテレータとしてのカテゴリがそれぞれ取得できることになります。

つまりは、C++20イテレータ型に対してそこからiterator_categoryを取得できる場合、そのC++20イテレータC++17イテレータとして扱うことができる事を意味しています。

また、iterator_conceptメンバ型を定義するということは、イテレータコンセプト、ひいてはC++20イテレータへの準拠を表明することでもあります。

iterator_traitsを通して見るC++20イテレータ

iterator_traitsを介してイテレータカテゴリを取得する時、そのイテレータiterator_categoryメンバ型を取得することになります。
iterator_categoryメンバ型はC++17以前のコードに対する互換性のために、C++17イテレータとしてのカテゴリを表明しています。

もしiterator_categoryメンバ型が無い場合、そのイテレータの可能な操作に基づくC++17の要件によって、適切なイテレータカテゴリが取得されます。

従って、iterator_traitsを通してC++20イテレータを見てみると、C++17互換イテレータとして見えることになります。

逆に、iterator_traitsを通してC++17イテレータを見た時はC++17までの振る舞いとほぼ変わりません。C++17イテレータはそのままC++17イテレータとして見えます。

標準のC++20イテレータ型(たとえば、<ranges>の各種viewイテレータ)には、そのメンバ型としてiterator_conceptiterator_categoryを両方同時に提供して、iterator_categoryの方を例えばinput_iterator_tagなど弱めることで安全にC++17イテレータとして利用できるようになっているものがあります。

イテレータコンセプトを通して見るC++17イテレータ

C++17のイテレータイテレータコンセプトに渡したときは、ITER_CONCEPTを通してイテレータカテゴリがI::iterator_cateogryから取得され、コンセプトによってそのインターフェースがチェックされます。C++17イテレータとして定義されていて、C++20での要件も満たしている場合は問題なくそのカテゴリのC++20イテレータとして判定されるでしょう。
多くの場合はC++17イテレータでは各カテゴリにおいての要件がC++20のものよりも厳しいはずなので、正しくC++17イテレータとして定義されていれば問題なくC++20イテレータとなれるはずです。

もちろん、C++17まではそれを判定する仕組みはなかったので思わぬところで要件を満たしていない可能性はあります。その時は、君がイテレータだと思ってるそれ、イテレータじゃないよ?ってコンパイラくんが教えてくれます。親切ですね・・・

逆に、イテレータコンセプトを通してC++20イテレータを見た時、ITER_CONCEPTを通してiterator_conceptが取得され、あるいはなかったとしても、最終的にコンセプトによって定義されたC++20イテレータとしての性質を満たしているかによって判定されます。
C++20のイテレータC++20のイテレータとして見ることができるのはこの経路だけです。

まとめると、それぞれからイテレータを見た時にどう見えるかは、次のようになります。

窓口 \ イテレータ C++17イテレータ C++20イテレータ
iterator_traits C++17イテレータ C++17イテレータ
イテレータコンセプト C++20イテレータ C++20イテレータ

こうしてみると古いものからは古いものとして、新しいものからは新しいものとして見える、というなんだか当たり前の話に見えてきます。

iterator_traitsの明示的特殊化

ここまであえて深く触れていませんでしたが、iterator_traitsを使おうとイテレータコンセプトを使おうと、必ず考慮しなればならない経路がもう一つあります。それはiterator_traitsIについて明示的に特殊化されていた場合です。典型的な例はポインタ型です。

その場合、iterator_traitsにせよITER_CONCEPTにせよ、特殊化iterator_traits<I>を最優先で使用するようになります。

iterator_traitsを直接使うのはC++17以前のコードがメインだと思われるので、そこにiterator_conceptメンバが生えていても触られることはないでしょう。

ITER_CONCEPT(I)では、I::iterator_conceptは無視されiterator_traits<I>::iterator_conceptがあればそれを、なければiterator_traits<I>::iterator_categoryを取得します。

どちらにせよ重要なことは、iterator_traitsIについて明示的に特殊化されている場合は、Iのメンバや実際の性質は無視して特殊化に定義されているものが取得されるという事です。

これは、元のイテレータIに対して非侵入的なカスタマイゼーションポイントになっています。最も重要なのはその特殊化にiterator_conceptメンバがあるかないかで、ある場合は元のイテレータが何であれC++20イテレータコンセプト準拠を表明することになり、ない場合は元のイテレータC++20イテレータであってもC++17イテレータとして扱われる、ということになります。

特殊化されたiterator_traits<I>::iterator_conceptメンバを触るのはITER_CONCEPT(I)だけですが、この場合であってもiterator_categoryの役割はC++17イテレータとして使用可能なカテゴリを表明する事なのが分かります。

iterator_traitsはプライマリテンプレートにせよ特殊化にせよC++17コードから利用されるものなので、iterator_categoryを含めた5つのメンバ型は必ず定義しておく必要があります。オプショナルなのはiterator_conceptのみです。

iterator_traitsの2つの役割

結局、C++20iterator_traitsには大きく次の二つの役割がある事が分かります。

  1. プライマリテンプレートが使用される場合、C++17互換レイヤとして機能する。
    この時、iterator_conceptメンバは定義されず、iterator_categoryのみがイテレータ型のC++17互換の性質によって定義される。
  2. 明示的に特殊化されている場合、イテレータのメンバ型よりも優先されるカスタマイゼーションポイントとして機能する。
    この時、iterator_conceptメンバが定義されないならば、iterator_categoryを使用してイテレータ型のC++20的性質を制限する。

C++20以降のイテレータ情報の問い合わせ、まとめ

(本当は前回の最後に載せるべきでしたが忘れていたのでここに置いておきます・・・)

情報 C++17 iterator_traits<I> C++20からの窓口
距離型 difference_type std::iter_difference_t<I>
要素の型 value_type std::iter_value_t<I>
要素の参照型 reference std::iter_reference_t<I>
要素のポインタ型 pointer なし
イテレータのカテゴリ iterator_category 各種イテレータコンセプト
要素の右辺値参照型 なし std::iter_rvalue_reference_t
イテレータcommon reference なし std::iter_common_reference_t

参考文献

この記事のMarkdownソース

[C++] C++20からのイテレータの素行調査方法

これはC++ Advent Calendar 2020の11日めの記事です。

これまでiterator_traitsを介して取得していたイテレータ情報は、C++20からはより簡易な手段を介して取得できるようになります。

特に、C++20以降はiterator_traitsを使わずにこれらのものを利用することが推奨されます。

以下、特に断りが無ければIイテレータ型、iイテレータのオブジェクトだと思ってください。

difference_type

difference_typeイテレータの距離を表す型で、イテレータの差分操作(operator-)の戻り値型でもあります。

従来はstd::iterator_traits<I>::difference_typeから取得していましたが、C++20からはstd::iter_difference_t<I>を用いる事で同じものを取得できます。

#include <iterator>
#include <vector>
#include <ranges>

int main() {
  using iota_view_iter = std::ranges::iterator_t<std::ranges::iota_view<int>>;

  static_assert(std::same_as<std::iter_difference_t<std::vector<int>::iterator>, std::ptrdiff_t>);
  static_assert(std::same_as<std::iter_difference_t<int*>, std::ptrdiff_t>);
  static_assert(std::same_as<std::iter_difference_t<iota_view_iter>, std::ptrdiff_t>);
}

std::incrementable_traits

std::iter_difference_t<I>は基本的にはC++20で追加されたstd::incrementable_traitsを用いてdifference_typeを取得し、std::incrementable_traitsはいくつかの経路を使ってdifference_typeを探してくれます。

イテレータ型をIとして

  • I::difference_type
  • Ioeprator-(2項演算)の戻り値型
  • std::incrementable_traits<I>の明示的/部分特殊化

C++20からのイテレータ型は上記いずれかで取得できるようにしておけばいいわけです。

なお、difference_typeは意外に多くの場所で使用されているので必ず定義しておいたほうがいいでしょう。おそらくの多くの場合は入れ子difference_typeを定義するのが簡単でしょう(つまり今まで通り)。

value_type

value_typeイテレータの指す要素の型を表す型です。大抵はイテレータの間接参照の戻り値型から参照を除去した型になることでしょう。

従来はstd::iterator_traits<I>::value_typeから取得していましたが、C++20からはstd::iter_value_t<I>を用いる事で同じものを取得できます。

#include <iterator>
#include <vector>
#include <ranges>

int main() {
  using iota_view_iter = std::ranges::iterator_t<std::ranges::iota_view<unsigned int>>;

  static_assert(std::same_as<std::iter_value_t<std::vector<int>::iterator>, int>);
  static_assert(std::same_as<std::iter_value_t<double*>, double>);
  static_assert(std::same_as<std::iter_value_t<iota_view_iter>, unsigned int>);
}

std::indirectly_readable_traits

std::iter_value_t<I>は基本的にはC++20で追加されたstd::indirectly_readable_traitsを用いてvalue_typeを取得し、std::indirectly_readable_traitsはいくつかの経路を使ってvalue_typeを探してくれます。

イテレータ型をIとして

  • I::value_type
  • I::element_type
  • std::incrementable_traits<I>の明示的/部分特殊化

このvalue_typeも他の場所で使用されていることがあるので必ず定義しておいたほうがいいでしょう。おそらく入れ子value_typeを定義するのが簡単でしょう(これも今まで通り)。

reference

referenceイテレータの指す要素を参照する参照型で、これはイテレータの間接参照の戻り値型です。

従来はstd::iterator_traits<I>::referenceから取得していましたが、C++20からはstd::iter_reference_t<I>を用いる事で同じものを取得できます。

#include <iterator>
#include <vector>
#include <ranges>

int main() {
  using iota_view_iter = std::ranges::iterator_t<std::ranges::iota_view<unsigned int>>;

  static_assert(std::same_as<std::iter_reference_t<std::vector<int>::iterator>, int&>);
  static_assert(std::same_as<std::iter_reference_t<double*>, double&>);
  static_assert(std::same_as<std::iter_reference_t<iota_view_iter>, unsigned int>);
}

referenceというのは歴史的経緯から来る名前で、イテレータの間接参照の戻り値型は必ずしも参照型でなくてもいいのです。

std::iter_reference_tは次のように定義されています。

namespace std {
  template<dereferenceable I>
  using iter_reference_t = decltype(*declval<I&>());
}

そのまま間接参照の戻り値型ですね、つまり我々は何もする必要がありません。普通のイテレータ型なら常にこれを利用できます。

std::iter_rvalue_reference_t

std::iter_rvalue_reference_titerator_traitsにはなかったもので、イテレータの要素を指す右辺値参照型を表すものです。

#include <iterator>
#include <vector>
#include <ranges>

int main() {
  using iota_view_iter = std::ranges::iterator_t<std::ranges::iota_view<unsigned int>>;
  
  static_assert(std::same_as<std::iter_rvalue_reference_t<std::vector<int>::iterator>, int&&>);
  static_assert(std::same_as<std::iter_rvalue_reference_t<double*>, double&&>);
  static_assert(std::same_as<std::iter_rvalue_reference_t<iota_view_iter>, unsigned int>);
}

イテレータがprvalueを返す場合はこれも素の型を示します。

これは少し複雑な定義をされていますが、大抵の場合はdecltype(std::move(*i))の型を取得することになります。つまりこれも我々は何もしなくても使用できます。

std::iter_common_reference_t

std::iter_common_reference_titerator_traitsにはなかったもので、std::iter_value_t<I>&std::iter_reference_t<I>の両方を束縛することのできるような共通の参照型を表すものです。

#include <iterator>
#include <vector>
#include <ranges>

int main() {
  using iota_view_iter = std::ranges::iterator_t<std::ranges::iota_view<unsigned int>>;

  static_assert(std::same_as<std::iter_common_reference_t<std::vector<int>::iterator>, int&>);
  static_assert(std::same_as<std::iter_common_reference_t<double*>, double&>);
  static_assert(std::same_as<std::iter_common_reference_t<iota_view_iter>, unsigned int>);
}

これもreferenceといいつつ、必ずしも参照型であるとは限りません。

std::iter_common_reference_tは次のように定義されています。

namespace std {
  template<indirectly_readable I>
  using iter_common_reference_t = common_reference_t<iter_reference_t<I>, iter_value_t<I>&>;
}

要はstd::common_referenceに投げているのですが、これは組み込み型であれば何もしなくても取得できます。ユーザー定義型ではstd::basic_common_referenceを通してcommon referenceを定義してやる必要があります。

pointer

その要素のポインタ型を取得する口は用意されていません。おそらくvalue_type*で十分という事でしょう。

iterator_category

C++20以降、イテレータカテゴリの判定に各イテレータのタグ型を調べてどうこうするのは完全にナンセンスです。コンセプトを使いましょう。

そのために、<iterator>ヘッダにはあらかじめいくつかのイテレータコンセプトが用意されています。特に、基本的なinput iteratorとかforward iteratorといったものにはそのままの名前でコンセプトが定義されています。

// forward_iteratorコンセプトの定義の例
namespace std {
  template<class I>
  concept forward_iterator =
    input_iterator<I> &&
    derived_from<ITER_CONCEPT(I), forward_iterator_tag> &&
    incrementable<I> &&
    sentinel_for<I, I>;
}

これらのコンセプトの間にはその性質の関係に応じた包含関係があり、コンセプトの半順序上でもそれに応じた順序付けがなされます。

#include <iostream>
#include <iterator>
#include <ranges>
#include <vector>
#include <list>
#include <forward_list>

template<std::forward_iterator I>
void iter_print(I) {
  std::cout << "forward iterator!" << std::endl;
}

template<typename I>
  requires std::bidirectional_iterator<I>
void iter_print(I) {
  std::cout << "bidirectional iterator!!" << std::endl;
}

void iter_print(std::random_access_iterator auto) {
  std::cout << "random access iterator!!" << std::endl;
}


int main() {
  iter_print(std::forward_list<int>::iterator{});
  iter_print(std::list<int>::iterator{});
  iter_print(std::vector<int>::iterator{});
}

iterator_concept

従来はイテレータを定義する時にiterator_categoryを用意してイテレータのカテゴリを表明していましたが、C++20からはそれをiterator_conceptで行います。

iterator_conceptが用意されたのはC++17以前との互換性を取るためで、特にポインタ型のカテゴリがcontiguous_iteratorに変更された事に対処する面が大きいと思われます。

C++17までのコードではイテレータのタグ型を判定する時にその継承関係まで調べない事が多く、特にrandom_access_iteratorの場合はイコール(is_same)で判定される事がほとんどでした。そのため、ポインタ型のiterator_categorycontiguous_iterator_tagに変えてしまうとそのようなコードがコンパイルエラーを起こすようになってしまいます。

C++20以降のイテレータではiterator_conceptからカテゴリを取得するようにし、iterator_categoryC++17以前のコードの互換のためにそのままにしておくことにしました。
また、C++20の各種イテレータコンセプトでは、iterator_conceptがあればそこから、なければiterator_categoryからイテレータのタグ型を取得し、その継承関係も含めて判定を行う事で新しいカテゴリに対応しつつ将来的な変更に備えています。

そして、ユーザーコードではコンセプトを用いることでイテレータのカテゴリタグ型からそのカテゴリを問い合わせる必要はなくなります。

詳細は次回説明しますが、これらの事情より、C++20以降でしか利用できないイテレータを定義する場合は、常にiterator_conceptイテレータカテゴリを宣言しiterator_categoryは定義しないようにしておきます。

// C++20以降しか考慮しないイテレータ
struct newer_iterator {
  using iterator_concept = std::forward_iterator_tag;
  // 他のメンバは省略
};

// C++17以前との互換性を確保するC++20仕様イテレータ
struct cpp17_compatible_iterator {
  using iterator_concept = std::random_access_iterator_tag; // C++20コードから使用されたときのイテレータカテゴリ
  using iterator_category = std::input_iterator_tag;        // C++17コードから使用されたときのイテレータカテゴリ
  // 他のメンバは省略
};

C::iterator

これはiterator_traitsを使用する前段階の話ですが、任意のrangeからそのイテレータ型を取得するのに、これまでは::iteratorという入れ子型に頼っていました。C++20からはstd::ranges::iterator_tによってこれをより確実かつ簡易に取得できるようになります。

#include <iterator>
#include <vector>
#include <ranges>

int main() {
  using vector_iter = std::ranges::iterator_t<std::vector<int>>;
  using array_iter = std::ranges::iterator_t<double[]>;
  using iota_view_iter = std::ranges::iterator_t<std::ranges::iota_view<unsigned int>>;

  static_assert(std::same_as<vector_iter, std::vector<int>::iterator>);
  static_assert(std::same_as<array_iter, double*>);
  //static_assert(std::same_as<iota_view_iter, std::ranges::iota_view<unsigned int>::iterator>);
}

とくに、このiota_viewイテレータ型のように、<ranges>の多くのView型のイテレータ型は種々の条件で変化する複雑な型で、入れ子::iteratorからはその型を取得できません。

また、C++20からは終端イテレータの事をsentinel(番兵)と呼んで区別して、イテレータと番兵の型は異なっていても良くなりました。そのため、任意のrangeからその番兵型を取得するstd::ranges::sentinel_tも用意されています。

#include <iterator>
#include <vector>
#include <ranges>

int main() {
  using vector_se = std::ranges::sentinel_t<std::vector<int>>;
  using array_se = std::ranges::sentinel_t<double[1]>;
  using iota_view_se = std::ranges::sentinel_t<std::ranges::iota_view<unsigned int>>;

  static_assert(std::same_as<vector_se, std::vector<int>::iterator>);
  static_assert(std::same_as<array_se, double*>);
}

このiterator_t/sentinel_tは実はとても単純に定義されています。

namespace std::ranges {
  template<class T>
  using iterator_t = decltype(ranges::begin(declval<T&>()));

  template<range R>
  using sentinel_t = decltype(ranges::end(declval<R&>()));
}

ranges::begin/ranges::endは従来のstd::begin/endをよりジェネリックかつ安全に定義しなおしたカスタマイゼーションポイントオブジェクトです。要はイテレータを取得するbegin()/end()の戻り値型を直接求めているだけで、我々は何もせずともこれを利用できます。

C++20のイテレータに必要なもの

これらの事によって、C++20からのイテレータは少し記述を削減することができるようになりました。

// C++17のイテレータ定義例
template<typename T>
struct cpp17_iter {
  using difference_type = std::ptrdiff_t;
  using value_type = T;
  using reference = T&;
  using pointer = T*;
  using iterator_category = std::bidirectional_iterator_tag;

  cpp17_iter& operator++();

  reference operator*();

  difference_type operator-(const cpp17_iter&) const;

  // 以下略
};

// C++20のイテレータ定義例
template<typename T>
struct cpp20_iter {
  using value_type = T;
  using iterator_concept = std::bidirectional_iterator_tag;

  cpp20_iter& operator++();

  T& operator*();

  std::ptrdiff_t operator-(const cpp20_iter&) const;

  // 以下略
};

さらに、比較演算子の自動導出もあるのでoperator!=の定義も省略できるようになっています。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で42本あります。

採択された文書

この2つの提案はC++23入りが確定したものです。

P0943R6 Support C atomics in C++

Cとの相互運用性を高めるために、Cのアトミック操作に関するヘッダ(<stdatomic.h>)をC++としてもサポートする提案。

C11で_Atomicと言うキーワードを用いてアトミック型が定義できる様になり、<stdatomic.h>には組み込み型に対するアトミック型のエイリアスやアトミック操作のための関数などが用意されています。その名前はおそらく意図的にC++<atomic>にあるものと同じ名前になっており、少し手間をかけると一応はコードの共通化を図れます。

#ifdef __cplusplus
  #include <atomic>
  using std::atomic_int;
  using std::memory_order;
  using std::memory_order_acquire;
  ...
#else /* not __cplusplus */
  #include <stdatomic.h>
#endif /* __cplusplus */

しかし、この様なコードはCとC++のアトミックなオブジェクトの表現や保証について互換性があることを前提としていますが、その様な保証はありません。

この提案は、C++でも<stdatomic.h>をインクルードできる様にし、そこで提供されるものについてCとC++で同じ保証が得られることを規定するものです。

このヘッダの実装は<atomic>で定義されているものをグローバル名前空間へ展開することで行われます(ただし、<atomic>をインクルードするかは未規定です)。また、Cの_Atomicは関数マクロとして提供されます。
ヘッダ名が<cstdatomic>ではないのは、このヘッダの目的が<stdatomic.h>の中身をstd名前空間に導入する事ではなく、アトミック周りのC/C++の相互運用性向上のためにCとC++で同じヘッダを共有できる様にするためのものだからです。

P1787R6 Declarations and where to find them

規格内でのscopename lookupという言葉の意味と使い方を改善する提案。

このリビジョンでの変更点は多岐に渡っているので文書を参照してください。

その他文書

N4869 WG21 Pre-Autumn 2020 telecon minutes

N4871 WG21 Pre-Autumn 2020 telecon minutes

先月初めに行われたC++標準化委員会の全体会議(テレカンファレンス)の議事録。

一部の発言記録と各SGがどの様な提案について議論をしたかなどが記されていて、特筆する所では、Cとの相互運用性について議論するためにC標準化委員会(SC22/WG14)との共同作業グループを設立することや、Networking TSに向けて2つの提案が採択されたことなどが書かれています。

N4869とN4871の違いは不明。

N4870 WG21 2020-02 Prague Minutes of Meeting

今年2月に行われたプラハでのC++標準化委員会の全体会議の議事録。 先月初めに行われたC++標準化委員会の全体会議(テレカンファレンス)の議事録。

N4873 Working Draft, C++ Extensions for Library Fundamentals, Version 3

Library Fundamentals TSの最新の仕様書。

ここでは、将来の標準ライブラリの拡張のうち、広く基礎的なものとして使用されるうる物をまとめて、慎重に検討しています。例えば、scope_exitobserver_ptrなどが含まれています。

N4874 Editor's Report: C++ Extensions for Library Fundamentals, Version 3

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

変更点はLWG Issueへの対応と、Editorialなものだけの様です。

N4875 WG21 admin telecon meeting: Winter 2021

2021年02月08日 08:00 (北米時間)に行われるWG21本会議のアジェンダ

これはC++23のための2回目の会議です。

N4876 WG21 virtual meeting: Winter 2021

↑のWG21本会議周知のための文章?

中身は日付とzoomのURLがあるだけです。

N4877 WG21 2020-11 Virtual Meeting Minutes of Meeting

先月初めに行われたC++標準化委員会の全体会議(テレカンファレンス)の議事録。

ここでは採択された提案についての投票の結果が書かれています。

P0447R11 Introduction of std::colony to the standard library

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

std::colonybucket arrayと呼ばれるデータ構造を改良したもので、いくつかのブロック(配列)の集まりとしてデータを保持します。一つのブロックにはメモリ上で連続して要素が並んでおり、要素はその先頭に削除済みかどうかのフラグを持っています。イテレーションの際は削除済みの要素はスキップされ、すべての要素が削除されたブロックはイテレーション対象としてあがらなくなります。

主に次のような特性があります。

  • メモリ位置が安定(要素の追加・挿入・削除で変化しない)
  • 削除された要素の位置を再利用する
  • 一つのブロック(配列)はメモリ上で連続している
  • ブロックサイズは可変
  • 一つの要素あたりのイテレーションにかかる時間は償却定数
  • 非順序、ソート可能
  • bidirectional range
    • 添え字アクセス([])は提供されない

std::colonyの外観イメージ(引用元 : https://www.lotteria.jp/menu/001701/

colonyのイメージ図

std::vetorはその要素がメモリ上で連続しており、何も考えずに使っても良いパフォーマンスを得ることができます。しかし、そのシーケンス中にある要素を削除したり、要素を追加したりしようとすると話は変わってきます。
std::vetorは常にその要素がメモリ上で連続しているので、削除された部分は詰めようとし、追加されたときにメモリの再確保が発生すると、すべての要素を新しいメモリ領域に移動させます。この動作はパフォーマンスを大きく損ねます。

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

vec.erase(vec.begin()); // 削除した分詰められる

vec.push_back(6); // メモリ再確保が発生すると、すべての要素の移動が行われる

この事が問題になる場合、標準ライブラリ内で代替となるものにstd::listがあります。しかし、std::listはメモリの局所性が低く(要素はメモリ上でばらばらに存在している)、イテレーション中のキャッシュパフォーマンスで劣ります。

std::colonyは要素が削除された部分は単に歯抜けの様な状態になるだけでその他の操作は行われず、追加の際も歯抜け部分を再利用するか、新しいブロックに追加するために要素の大移動も発生しません。
ブロック内要素はメモリ上で連続しており、歯抜けとなっている部分があるので参照局所性が若干低下しますが、std::vector/dequeに次いでイテレーションを高速に行うことができます。

std::colonyは要素の順序が重要ではなく、要素が外部から参照されていて挿入や削除が頻繁に行われるようなシーンで有効です。筆者の方は、特にゲーム開発や数値シミュレーションの世界で頻繁に利用されていると述べています。

#include <colony>

int main() {
  std::colony<int> col = {1, 3, 3, 5};

  // 要素の削除(他の要素は移動されない)
  col.erase(col.begin() + 1);

  // 要素の追加(歯抜け部分があれば再利用される)
  col.insert(7);

  for (int n : col) {
    std::cout << n << '\n'; // 要素の順序は実装定義(この場合は多分 1, 7, 3, 5)
  }
}

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

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

以前の記事を参照

このリビジョンでの変更は、auto(x)によるdecay-copy構文によって標準ライブラリの規定の書き換え作業を完了した事です。

P0401R4 Providing size feedback in the Allocator interface

アロケータが実際に確保したメモリのサイズをフィードバックすることのできるメモリ確保インターフェースを追加する提案。

説明は次の項で。

P0901R7 Size feedback in operator new

::operator newが実際に確保したメモリのサイズを知ることができるオーバーロードを追加する提案。

例えば次のようなstd::vector::reserve()の呼び出し(一度目)では、使用されるoperator newが実際に37バイト丁度を確保する、という事はほぼありません(アライメントの制約やパフォーマンスの向上など、実装の都合による)。

std::vector<char> v;
v.reserve(37);
// ...
v.reserve(38);

しかし、それを知る方法は無いため、2回目のreserve(38)無くして38バイト目を安全に使用する方法はありません。

std::vector::reserve()は典型的には次のような実装になります。

void vector::reserve(size_t new_cap) {
  if (capacity_ >= new_cap) return;
  const size_t bytes = new_cap;
  void *newp = ::operator new(new_cap);
  memcpy(newp, ptr_, capacity_);
  ptr_ = newp;
  capacity_ = bytes;
}

capacity_というのが使用可能なメモリ量を記録しているstd::vectorのメンバとなりますが、これはあくまでユーザーが指定した値new_capで更新されます。3行目の::operator newが実際に確保しているnew_capを超える部分の領域サイズを知る方法はありません。

僅かではあるのでしょうが、この余剰部分の量を知ることができればメモリ確保を行う回数を削減することができる可能性があります。

この提案は::operator new()オーバーロードを追加し、戻り値としてその実際に確保した領域サイズとポインタを受け取れるようにするものです。

namespace std {
  struct return_size_t {
    explicit return_size_t() = default;
  };

  inline constexpr return_size_t return_size{};

  template<typename T = void>
  struct sized_allocation_t {
    T *p;
    size_t n;
  };

  [[nodiscard]]
  std::sized_allocation_t ::operator new(size_t size, std::return_size_t);
  // その他オーバーロード省略
}

std::return_size_tというのは単なるタグ型で、std::return_sizeはそのオブジェクトです。

これによって、先ほどのreserve()実装は次のように改善できます。

void vector::reserve(size_t new_cap) {
  if (capacity_ >= new_cap) return;
  const size_t bytes = new_cap;
  auto [newp, new_size] = ::operator new(new_cap, return_size);  // 実際の確保サイズを受け取る
  memcpy(newp, ptr_, capacity_);
  ptr_ = newp;
  capacity_ = new_size; // 実際に使用可能なサイズでキャパシティを更新
}

P0401R4は同じものをアロケータに対しても導入するものです。こちらはstd::allocate_at_leastという関数にアロケータとサイズを渡すことでnewの時と同じことをします。

namespace std {
  template<typename Pointer>
  struct allocation_result {
    Pointer ptr;
    size_t count;
  };

  template<typename Allocator>
  constexpr allocation_result<typename Allocator::pointer> allocate_at_least(
    Allocator& a, size_t n);
}

allocate_at_least関数は、std::allocator_traits及びstd::allocatorにもメンバとして追加されます。

P1012R1 Ternary Right Fold Expression

条件演算子三項演算子)で右畳み込み式を利用できるようにする提案。

例えば次のように利用できます。

#include <functional>
#include <stdexcept>

// なんか処理
template<std::size_t i>
int f();

template <std::size_t... is>
int test_impl(std::size_t j, std::index_sequence<is...>) {
  // j >= n の時は例外を投げたいとする

  // この提案による条件演算子に対する右畳み込み式の利用
  return ( (j == is) ? f<is>() : ... : throw std::range_error("Out of range") );
}

template <std::size_t n>
int test(std::size_t j) {
  // 実行時の値jによってf<j>()を呼び出す
  return test_impl(j, std::make_index_sequence<n>());
}

これは次のような展開を行うものです。

// 展開前
(C ? E : ... : D)

// 展開後
(C(arg1) ? E(arg1)
         : ( C(arg2) ? E(arg2)
                     : (...( C(argN-1) ? E(argN-1)
                                       : D )...)))

Cは条件式、Eは格段のCがtrueの場合に実行される式、Dはすべての条件がfalseの時に実行される式になり、CとEがパラメータパックを含むことができます。

またこの提案では同時に、条件演算子の2番目か3番目のオペランド[[noreturn]]な関数を指定したときにthrow式と同等の扱いを受けるように変更することも提案しています。

P1018R7 C++ Language Evolution status - pandemic edition - 2020/03–2020/10

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

以下の提案がEWGでの議論を終えてCWGに転送され、投票待ちをしているようです。

これらのものはC++23に入る可能性が高そうです。

P1102R1 Down with ()!

引き数なしのラムダ式の宣言時に()をより省略できるようにする提案。

ラムダ式は引数を取らなければ引数リストを記述するための()を省略することができます。

std::string s2 = "abc";
auto noSean = [s2 = std::move(s2)] {
  std::cout << s2 << '\n'; 
};

しかし、例えばこの時にキャプチャしたs2を変更したくなって、mutableを追加してみるとたちまちエラーになります。

std::string s2 = "abc";
auto noSean = [s2 = std::move(s2)] mutable {  // error!()が必要
  s2 += "d";
  std::cout << s2 << '\n'; 
};

規格では、(...)が無い時は空の()があるように扱うとあるのでこのことは矛盾しています。

mutableだけなら影響はそこまででもなかったかもしれませんが、次のものがあるときもやはり()を省略できません

  • テンプレートパラメータ指定(C++20から)
  • constexpr
  • mutable
  • noexcept
  • 属性
  • 後置戻り値型
  • requires

この提案はこのいずれの場合にも引数が無い場合は()を省略できるようにするものです。

P1206R3 ranges::to: A function to convert any range to a container

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

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

  • ネストしたコンテナへの変換のサポート
  • ()なしで使用する構文の削除
  • 既存のコンテナに対してrangeコンストラクタを呼び出すためのタグであるfrom_rangeの追加

既存のコンテナに直接任意のrangeからのコンストラクタを追加すると、自身のコピー/ムーブコンストラクタとの衝突によって暗黙にコピー/ムーブになってしまうなどの問題があります。

そこで、タグを指定することによってそれをサポートするコンストラクタを追加し、それをサポートすることを提案しています。

std::vector<int> foo = ....;
std::vector a{std::from_range, foo}; // std:vector<int>をrangeコンストラクタから構築

std::from_rangeはそのタグ型の値です。ただ、この導入そのものは別の提案によって行われるようです。

P1478R5 Byte-wise atomic memcpy

アトミックにメモリのコピーを行うためのstd::atomic_load_per_byte_memcpy()/std::atomic_store_per_byte_memcpy()の提案。

このリビジョンでの変更は、想定される疑問点のリストを追記したことです。

P1885R4 Naming Text Encodings to Demystify Them

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

以前の記事を参照

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

  • id::otherの比較を変更
  • 文言の改善と、フリースタンディング処理系でもサポートされることと、サポートしなくてもよいものを指定
  • エンコーディング文字列のエイリアス比較をunicode TR22に従うように修正

 

P1950R1 An indirect value-type for C++

フリーストア(ヒープ領域)に確保したメモリ領域上に構築されたオブジェクトに対して値のセマンティクス(Value Semantics)を与えて扱う事の出来るクラステンプレートstd::indirect_value<T>の提案。

クラスのデータメンバの一部がクラスのレイアウトの外にあるような場合、その実体を参照するためには通常ポインタをメンバとして持つことで実装します。
しかしこの時、そのクラスの特殊メンバ関数(特にコピー)の処理とconst性の伝播がそのポインタで切られることになり、それらに関しては注意深い手書き実装を必要とします。

典型的な例はPImplと呼ばれるイディオムで見ることができます。

/// ヘッダファイル

class widget {
public:
  widget();
  ~widget();

private:
  class impl; // 実装クラスの前方宣言
  std::unique_ptr<impl> pimpl_; // 実装クラスの実体はヒープ上にある
};
/// ソースファイル

// 実装クラスの定義
class widget::impl {
  // :::
};

// widgetクラスの特殊関数の定義
widget::widget() : pimpl_{ std::make_unique<impl>(/*...*/)} {}
widget::~widget() = default;

ここではPImplの共通する部分だけに注目しています。
ここでは実装クラスのオブジェクトを保持するのにstd::unique_ptrを使用していますが、ここにポインタを使っても以降の議論に影響はありません。

const性伝播の遮断

widgetクラスのconstメンバ関数の中でも、実装クラス(widget::impl)のオブジェクトを変更することができます。これは、widgetクラスのconst性はそのメンバであるstd::unique_ptr<impl>そのものに対しては作用しますが、その先にある実体オブジェクトには伝播しないためです。

これはポインタであっても同じことで、メンバのポインタ(std::unique_ptr)によってconst性の伝播が遮断されてしまっています。

コピーの問題

std::unique_ptrはムーブオンリーなので、std::unique_ptrをメンバにもつすべてのクラスはコピーコンストラクタ/代入演算子を自前実装しなければなりません。default実装に頼ることはできず、バグが混入するポイントになりがちです。

これがポインタであった場合はコピーも含めた特殊メンバ関数は全てdefault実装可能ですが、コピーはポインタそのものしかコピーしません。ともすればこれによるバグはさらに厄介かもしれません。

std::indirect_value<T>

std::indirect_value<T>は上記のような問題をすべて解決するためのクラスです。先程のPImpl実装は次のように書き換えられます。

/// ヘッダファイル

class widget {
public:
  widget();
  widget(widget&& rhs) noexcept;
  widget(const widget& rhs);
  widget& operator=(widget&& rhs) noexcept;
  widget& operator=(const widget& rhs);
  ~widget();
private:
  class impl;

  // unique_ptrに代わって利用する
  std::indirect_value<impl> pimpl;
};
/// ソースファイル

class widget::impl {
  // :::
};

// widgetクラスの特殊メンバ関数はすべてdefault定義可能
widget::widget(widget&& rhs) noexcept = default;
widget::widget(const widget& rhs) = default;
widget& widget::operator=(widget&& rhs) noexcept = default;
widget& widget::operator=(const widget& rhs) = default;
widget::~widget() = default;

std::indirect_value<T>のオブジェクトを介して実装クラスの実体にアクセスすると、そのconst性が正しく伝播されます。また、std::indirect_value<T>は値のセマンティクスを持つかのようにコピーを実装しており、コピーコンストラクタ/代入演算子はそれを使用する形でdefault定義可能です。

これはスマートポインタ(std::unique_ptr)を深いコピーを行うようにしたうえでconst性の伝播という性質を追加したものです。
このようなものは既に在野にあふれており、その実装は微妙に異なって(深いコピーを行うかどうかやconst性の伝播をするかどうかなど)いくつも存在しています。筆者の方は、このような多様な実装があふれていることこそが、単一の標準化されたソリューションが利益をもたらすことの証拠であると述べています。

提案されているstd::indirect_value<T>の宣言は次のようになります。

namespace std {
  template <class T>
  struct default_copy {
    T* operator()(const T& t) const;  // return new T(t);
  };

  template <class T, class C = std::default_copy<T>, class D =  std::default_delete<T>>
  class indirect_value; 
}

std::indirect_value<T>はテンプレートパラメータでコピーをどうするか(copierと呼ばれている)とカスタムデリータを指定することができます。デフォルトのコピーは新しいメモリ領域にコピー構築を行い、そのポインタを保持するものです。ムーブも定義されており、それはunique_ptr同様所有権を移動するものです。

また、std::indirect_value<T>には空のステートがあり、それはデフォルトコンストラクト時あるいはムーブ後の状態として導入されます。

P2012R0 Fix the range-based for loop, Rev0ix the range-based for loop

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

例えば次のような場合に、少し書き方を変えただけで未定義動作の世界に突入します。

// prvalueを返す関数
std::vector<std::string> createStrings();

for (std::string s : createStrings()) // ok
{
  // 略
}

for (char c : createStrings().at(0))  // Undefined Behavior
{
  // 略
}

for (char c : createStrings()[0])       // Undefined Behavior
for (char c : createStrings().front())  // Undefined Behavior

範囲for文は組み込みの制御構文ではありますが、その実体は通常のfor文に書き換えることによって定義されています。

for ( init-statement(opt) for-range-declaration : for-range-initializer ) statement

これは、次のように展開されて実行されます。

{
    init-statement(opt)

    auto &&range = for-range-initializer ;  // イテレート対象オブジェクトの保持
    auto begin = begin-expr ; // std::begin(range)相当
    auto end = end-expr ;     // std::end(range)相当
    for ( ; begin != end; ++begin ) {
        for-range-declaration = * begin ;
        statement
    }
}

この4行目のauto &&range = ...の所で、範囲for文の:の右側にある式(for-range-initializer)の結果を受けています。auto&&で受けているので、for-range-initializerの結果オブジェクトが右辺値の場合でも範囲for文全体まで寿命が延長されます。そのため最初の例のfor (std::string s : createStrings())は問題ないわけです。

しかし、それ以外の例は全てcreateStrings()の返す一時オブジェクトからさらにその内部にあるオブジェクトを引き出しています。しかもワンライナーで書くことになるので、展開後の範囲for文のrange変数に束縛されるのは一時オブジェクトそのものではなくそのメンバの一部です。従って、展開後4行目のauto &&range = ...;セミコロンをもってcreateStrings()の返した一時オブジェクトの寿命が尽き、デストラクタが呼ばれ、rangeの参照先へのアクセスは全て未定義動作となります。

これを回避するには次のようにcreateStrings()の結果をどこかで受けておく必要があります。

auto tmp = createStrings();
for (char c : tmp.at(0))  // OK

for (auto tmp = createStrings(); char c : tmp[0]) // OK

もう少し変なコードを書くと、さらに複雑怪奇な闇を垣間見ることができます。

struct Person {
  std::vector<int> values;

  const auto& getValues() const {
    return values;
  }
};

// prvalueを返す
Person createPerson();

for (auto elem : createPerson().values)       // OK
for (auto elem : createPerson().getValues())  // Undefined Behavior

関数を介さずに、そのデータメンバを直接auto&&で束縛した時にはその親のオブジェクトの寿命も延長されるため、このような差異が生まれます。

これらの一番大きな問題は、範囲for文というシンタックスシュガーによってこの問題が覆い隠されてしまっている所にあります。
通常の構文であれば、;で明示的に一時オブジェクトの寿命が終了するため、上記のような一時オブジェクトから何か引き出すような事をしていたとしても;をマーカーとして気づくことができます。しかし、範囲for文の場合は一時オブジェクトの寿命終了を示すような;は基本的に見えていません。

この文章を読んでいる人には当たり前のことかもしれませんが、多くのC++プログラマーは範囲for文がどう定義されているかなど知らず、このような問題があることなど思いもしないでしょう。また、知っている人から見ても、この問題は発見しづらいものです。
これらのことによって、範囲for文は安全ではなく利用を推奨できるものではなくなってしまっています。

この提案はこれらの問題の解決のために、範囲for文でUBを起こしていた上記のコード全てで適切な寿命延長を行うように規定しようとするものです。

まだ提案の初期段階なので方針は定まっておらず、次のような提案をしています。

1. 範囲for文の:の右側の式に現れる全ての一時オブジェクトを受けておくように定義を変更する

例えば次のような範囲for文を

for (auto elem : foo().bar().getValues())

次のように展開するように定義を書き換えてしまう事です。

{
  auto&& tmp1 = foo();      // 一時オブジェクトの寿命が延長される
  auto&& tmp2 = tmp1.bar(); // 一時オブジェクトの寿命が延長される
  auto&& rg = tmp2.getValues();
  auto pos = rg.begin();
  auto end = rg.end();
  for ( ; pos != end; ++pos ) {
    auto elem = *pos;
    …
  }
}

とはいえ、これは既存のコードによるものでは表現することが難しそうです。

2. 範囲for文の定義をラムダ式を用いて変更する

少し技巧的ですが、ラムダ式を用いて範囲for文の定義を書き換えてしまう事で問題に対処するものです。

[&](auto&& rg) {
  auto pos = rg.begin();
  auto end = rg.end();
  for ( ; pos != end; ++pos ) {
  auto elem = *pos;
  … // return, goto, co_yield, co_returnでは特殊対応が必要
  }
}(foo().bar().getValues()); // 全ての一時オブジェクトはラムダ式の実行完了まで有効

特に追加のルールも必要なく良さげに見えますが、この場合はループのスコープを抜けるような構文(return, goto, co_yield, co_return)に関して特殊対応が必要となります。

3. 文章で規定する

例えば、「for-range-initializer内の全てのデストラクタの呼び出しはループの終了まで遅延する」のように文書で寿命延長を指定するものです。筆者の方はこのアプローチを推しています。

是非修正されてほしいところですが、これらのアプローチが可能なのか、どれが選択されるのか、はこれからの議論次第です・・・

P2160R1 Locks lock lockables (wording for LWG 2363)

現在のMutexライブラリ周りの文言にはいくつか問題があるのでそれを修正するための提案。

以前の記事を参照

このリビジョンでの変更は、ベースとなる規格書をN4868へ更新したことと、Open Issueセクションを削除したことです。

P2164R3 views::enumerate

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

以前の記事を参照

このリビジョンでの変更は、typoと提案文言の修正だけのようです。

P2181R1 Correcting the Design of Bulk Execution

進行中のExecutor(P0443)提案中のbulk_executeのインターフェースを改善する提案。

以前の記事を参照

このリビジョンでの変更は次のようなものです。

  • P2224で示された設計変更の適用
  • ↑にともなって不要となったmany_receiver_ofコンセプトの削除
  • bulk_scheduleで起動された各エージェント(作業)それぞれでconnectが呼び出されることを規定
  • executorschedulerを慎重に区別し、相互の暗黙変換を仮定しないようにした
  • デフォルトのbulk_executeは実行に際してexecuteを一度だけ呼ぶようになった
  • 実行作業の形状を指定する型executor_shape_t<E>executor_index_t<E>executor_coordinate_t<E>に変更
  • bulk_scheduleが二つの操作on(prologue, scheduler)bulk(prologue, shape, sender_factory)に分割される可能性についての議論の追加

一番最後のものは、bulk_scheduleの実装が次のように簡素化できるかもしれないという話です。

template<typed_sender P, scheduler S, invocable SF>
typed_sender auto bulk_schedule(P&& prologue,
                                S scheduler,
                                scheduler_coordinate_t<S> shape,
                                SF sender_factory)
{
  return on(prologue, scheduler) | bulk(shape, sender_factory);
}

onprologueの実行コンテキストをschedulerの指す実行コンテキストへ遷移させるものです。これによって、実装の複雑さが軽減されるとともに、onという有用な汎用操作を導入することができます。

P2182R1 Contract Support: Defining the Minimum Viable Feature Set

C++20で全面的に削除されたContractsのうち、議論の余地がなく有用であった部分と削除の原因となった論争を引き起こした部分とに仕分けし、有用であった部分だけを最初のC++ Contractsとして導入する事を目指す提案。

以前の記事を参照

このリビジョンでの変更は次のようなものです。

  • Design Objectives and Programming Model(設計目標とプログラミングモデル)セクションの追加
  • MVP(Minimum Viable Product)に含まれているものの例を追加
  • C++20ContractにあってMVPにないものの説明の追加
  • Other Use-Cases Compatibe with Our Modelセクションの追加
  • Use-Cases Incompatible with Our Modelセクションの追加

結構しっかりとした説明が追加されており、C++23のContractはこの方向性で行くのかもしれません。

P2211R0 Exhaustiveness Checking for Pattern Matching

提案中のパターンマッチングに対して、パターンの網羅性チェックを規定する提案。

P1371で提案中のパターンマッチングでは、パターンの網羅性のチェックを規定していません。この提案はパターンが不足している場合にコンパイルエラーにする事を提案すると共に、それぞれのケースについてどのように判定するかを示したものです。

簡単には、次のような場合にコンパイルエラーにしようとするものです。

enum Color { Red, Green, Blue };
//...
Color c = /*...*/;

// Blueに対応するパターンが無いためエラー
vec3 v = inspect(c) {
  case Red   => vec3(1.0, 0.0, 0.0);
  case Green => vec3(0.0, 1.0, 0.0);
};

// OK
vec3 v2 = inspect(c) {
  case Red   => vec3(1.0, 0.0, 0.0);
  case Green => vec3(0.0, 1.0, 0.0);
  case Blue  => vec3(0.0, 0.0, 1.0);
};

多くの他の言語では、パターンが網羅されているかのチェックはあくまで警告にとどめており、それが推奨されるアプローチとなっていたようです。C++でもコンパイラフラグで有効にするタイプの警告でもこの提案の大部分の利益を享受することができますが、概してそのような警告を扱えるのはそれを知っている一部の人達だけで、それが本当に必要な初学者や多くのプログラマーがその恩恵に預かる事はできません。

この提案では、パターン網羅性の徹底的なチェックを規定しコンパイルエラーとして報告することで、C++を安全かつ高パフォーマンスな言語として印象付けることができると述べられています。

P2212R2 Relax Requirements for time_point::clock

std::chrono::time_pointClockテンプレートパラメータに対する要件を弱める提案。

以前の記事を参照

このリビジョンでの変更は、<thread>関連の文書で同様の意味でClockテンプレートパラメータを規定していたところを、Cpp17Clock要件を参照するように変更したことです。

P2233R1 2020 Fall Library Evolution Polls

P2233R2 2020 Fall Library Evolution Polls

LEWGが2020年秋に行う投票の対象となった提案文書の一覧。

これはP2195R0で示された方向性に従った、4半期毎に行われる電子投票の第一回です。

Executor関連の事がメインで、他のところではP2212R1P2166R1をLWGへ進めるものがあります。

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

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

template<typename T> constexpr bool f() {
  if (std::is_constant_evaluated()) {
    // ...
    return true;
  } else {
    T t;  // コンパイル時に評価されない
    // ...
    return true;
  }
}
struct nonliteral { nonliteral(); };

static_assert(f<nonliteral>()); // ?

これは現在の規格では禁止されていますが、実装には微妙な相違があるようです。

C++20からは、定数式で評価されない限りconstexpr関数にthrow式やインラインアセンブリを含めておくことができます。std::is_constant_evaluatedの導入によって、コンパイル時に評価されなければ定数実行不可能なものも書くことを許可するという方向性が示されており、これを許可することはその方向性に沿っています。

この事は規格上では、constexpr関数の定義に制限を設けている条項を、定数式で現れることのできる式の制限の所へ移動することで許可することができます。その際、ちょうど近くにgotoとラベルについての制限もあったことからついでに緩和することにしたようです。

P2246R0 Character encoding of diagnostic text

コンパイル時にメッセージを出力するものについて、ソースコードエンコーディングが実行時エンコーディング(出力先のエンコーディング)で表現できない場合にどうするかの規定を修正する提案。

提案されているのは、static_assertのメッセージ内の基本ソース文字集合に含まれない文字を出力する必要はない、という規定を削除することです。

同様の提案がC標準ではすでに採択されていて、そちらではこの提案と同様の修正に加えて[[nodiscard]][[deprecated]]に指定されている文字列を必ず出力する事を規定しています(C++は既にそうなっている)。この提案はそれを受けてCとC++の間で仕様の統一を図るものです。

P2247R0 2020 Library Evolution Report

LEWG(Library Evolution Working Group)の今年2月以降の活動についてまとめた文書。

2月以降のオンラインミーティングの実績やどのように活動していたかなどと、レビューと投票を行った提案文書の一覧が書かれています。

P2248R0 Enabling list-initialization for algorithms

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

std::findなどのイテレータに対するアルゴリズムでは、引数に値を渡してその値との比較を行って何かするタイプのものがいくつかあります。基本型なら推論してくれるのですが、クラス型の場合に{}だけで初期化しようとするとinitializer_listに推論されるためエラーになります。

struct point { int x; int y; };

std::vector<point> v;

v.push_back({3, 4}); // OK、point型の指定は不要

// 全てNG
std::find(v.begin(), v.end(), {3, 4});
std::ranges::find(v.begin(), v.end(), {3, 4});
erase(v, {3, 4});

// OK、最後の値指定にpoint型を指定しなければならない
std::find(v.begin(), v.end(), point{3, 4});
std::ranges::find(v.begin(), v.end(), point{3, 4});
erase(v, point{3, 4});

この提案は、このような場合にも型の指定(point)を省略し、{}だけで渡すことができるようにするものです。

struct point { int x; int y; };

std::vector<point> v;

v.push_back({3, 4}); // point型の指定は不要

// 全てで省略可能にする
std::find(v.begin(), v.end(), {3, 4});
std::ranges::find(v.begin(), v.end(), {3, 4});
erase(v, {3, 4});

なぜこの問題が起きているのかと言うと、値を受け取る部分のテンプレートパラメータがイテレータの値型とは無関係に宣言されているためです。

// TはInputIteratorの値型と無関係
template <class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& value);

// UとTは無関係
template <class T, class Allocator, class U>
typename vector<T, Allocator>::size_type erase(vector<T, Allocator>& c, const U& value);

これはデフォルトテンプレートパラメータを適切に指定してやれば解決できます。

// TはInputIteratorの値型
template <class InputIterator, class T = typename iterator_traits<InputIterator>::value_type>
InputIterator find(InputIterator first, InputIterator last, const T& value);

// UのデフォルトとしてTを使用
template <class T, class Allocator, class U = T>
typename vector<T, Allocator>::size_type erase(vector<T, Allocator>& c, const U& value);

たったこれだけの変更で、API互換性を維持しながら上記の問題を解決できます。また、関数テンプレートの宣言のみの変更であるため、おそらくABI互換性も維持されます。

P2250R0 Scheduler vs Executor

executorschedulerの違いについてを説明したスライド。

誰に向けたものなのかは分かりませんが、LEWGでの議論のために使われたものかと思われます。

ここで説明されていることは、変更して分離しようとする提案(P2235)が出ています。

P2251R0 Require span & basic_string_view to be Trivially Copyable

std::spanstd::string_viewtrivially copyableである、と規定する提案。

std::spanstd::string_viewは想定される定義及び特殊メンバ関数の規定からしtrivially copyableであることが期待されます。しかし、標準はその実装について何も規定しておらず、trivially copyableであるかどうかも触れられていません。

std::spanstd::string_viewはどちらも次のような特徴があります。

  • デフォルトコピーコンストラク
  • デフォルトコピー代入演算子
  • デフォルトデストラク
  • 生ポインタとstd::size_tによるサイズを持つ型として説明される
  • 多くのメンバはconstexprであり、ともにtrivial destructibleな型(これはC++17の要件)

この様に共通する性質がありその実装もほぼ同じで、これらの事を考えると必然的にtrivially copyableであるはずです。実際、clangとGCCの実装はtrivially copyableとなっています。

この提案は、この2つの型がtrivially copyableであることを規定しようとするものです。

P2253R0 SG16: Unicode meeting summaries 2020-09-09 through 2020-11-11

2020年9月9日から同年11月11日までのSG16の活動記録。

主にオンラインミーティングでの議事録や投票結果などが記録されています。

P2254R0 Executors Beyond Invocables

executorschedulerの間にある意味論の非一貫性を正す提案。

これはExecutor提案(P0443R14)に対してのものです。

executorschedulerはどちらも、作業を実行するための(基盤となるコンテキストの)インターフェースという点で関連しており、それに対する操作にも関連性があることが期待されます。

例えば、start(connect(schedule(a), b))execute(a, b)をカリー化した形式ととらえることができます。aにはexecutorschedulerが、bにはinvocablereceiverが入り、それぞれどちらが渡されたとしても同じ効果が得られることが期待されます。
しかし、現在のExecutorライブラリはそれを保証せず、実装がそれを保証しようとする際の簡単な方法もありません。

まず1つめの不一致はエラーハンドリングに関してです。

executeはエラーハンドリングを個々のexecutor固有の方法で行います。対してstartconnectされたrecieverを介してエラーを伝達します。特に、executeでは作業がexecutorに投入されてから実際に実行されるまでのエラーをハンドルできません。

2つ目の不一致はその実行に際する保証に関してです。

executorExecutor Propertyによって、投入された処理がどこでどのように実行されるかを保証しており、プログラマはその実行に際して内部で行われていることやforward progressに関する懸念事項を推論することができます。対して、startを介して投入される処理(sender)にはそれがありません。

これらの不一致によってプログラマstart(connect(schedule(a), b))execute(a, b)が意味論的に等価である事を期待できません。

この提案では、この様な不一致を取り払いexecutorschedulerを一貫させるために次の様な変更を提案しています。

  • execution::execute(ex, f)finvocableであることを要求されているが、これを無くしてより広い実行可能な型を受け入れられるようにする。
  • それに伴い、executor_ofコンセプトの定義を修正する
    • これらの事により、executerecieverを渡せるようにする
  • receiver_archetypeを導入し、execution::executor<E>コンセプトをexecution::executor_of<E, receiver_archetype>エイリアスとして定義する
    • receiver_archetypeは典型的なrecieverを表す実装定義の型
  • execution::get_executor CPOによってsenderからexecutorを取り出せるようにする
    • startで呼び出されても、そのsenderget_executorして得られるexecutorの実行コンテキストで実行されることが保証されるようにする

これらの変更によって、start(connect(schedule(a), b))execute(a, b)は意味論的にかなり近づくことになり、executorは更なる柔軟さを、start(とsender)はより強い保証を手に入れることになります。

提案文書ではこの変更でexecutorschedulerの実装がどのように変化するか、またどのような実装が可能になるかの例を豊富に掲載しています。気になる方は見てみると良いでしょう。

P2255R0 A type trait to detect reference binding to temporary

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

標準ライブラリを始め、ジェネリックなライブラリでは、ある型Tを別の型の値から変換して初期化する事がよく必要になります。この時、Tが参照型だと容易にダングリング参照が作成されます。

using namespace std::string_literals;

std::tuple<const std::string&> x("hello");  // 危険!
std::tuple<const std::string&> x("hello"s); // 安全

例えば上の例は常にダングリング参照を生成します。std::stringの一時オブジェクトはstd::tupleのコンストラクタの内側で作成され、内部でconst std::string&を初期化した後、コンストラクタの完了と共に天寿を全うします。一方、コンストラクタの外側でstd::stringの一時オブジェクトが作成されていればconst参照に束縛される事で寿命が延長されます。

また、別の例として参照を返すstd::functionがあります。

std::function<const std::string&()> f = [] { return ""; };

auto& str = f();  // ダングリング参照を返す

このように、参照を返すstd::functionは実際に入れる関数の戻り値型によっては暗黙変換によってダングリング参照を生成してしまいます。

この提案ではまず、これらのダングリング参照を生成するタイプの参照の初期化や変換を検出する型特性(メタ関数)、std::reference_constructs_from_temporary<To, From>std::reference_converts_from_temporary<To, From>を追加することを提案しています。この実装はコンパイラマジックで行われます。

namespace std {
  template<class T, class U> struct reference_constructs_from_temporary;

  template<class T, class U> struct reference_converts_from_temporary;

  template<class T, class U>
  inline constexpr bool reference_constructs_from_temporary_v
    = reference_constructs_from_temporary<T, U>::value;

  template<class T, class U>
  inline constexpr bool reference_converts_from_temporary_v
    = reference_converts_from_temporary<T, U>::value;
}

constructsconvertsは検出する対象の、T t(u);T t = u;の違いです。この2つのメタ関数は、型UからTの構築(変換)時に一時オブジェクトの寿命延長が発生する場合にtrueを返すものです。

そして、これを用いてstd::pairstd::tupleのコンストラクタの制約を変更し、構築に伴う変換で一時オブジェクトの寿命延長が発生する場合にコンパイルエラーとなるように規定します。

また、INVOKE<R>の定義を修正し呼び出し結果のRへの変換で一時オブジェクトの寿命延長が発生する場合はill-formedとなるように規定します。

これによって、先程のサンプルコードの2例はともにコンパイルエラーとなるようになります。

// この提案の下では両方ともコンパイルエラー
std::tuple<const std::string&> x("hello");
std::function<const std::string&()> f = [] { return ""; };  // 実際に格納される関数によらず、関数の戻り値型で判断される

P2257R0 Blocking is an insufficient description for senders and receivers

executorに対するブロッキングプロパティ指定を再定義し、senderに対して拡張する提案。

P2220R0でExecutorライブラリのプロパティ指定の方法の変更が提案され、それによってプロパティ指定はexecutor以外のものにも拡張されることになります。

現在、executorに対するブロッキングプロパティ(その実行が現在のスレッドをブロックするか否か)はexecutor自身が保証するように規定されており、現在のままではsenderに対するブロッキングプロパティ指定はできません。また、現在のブロッキングの規定ではsenderに対してブロッキングを規定することは不可能となっているようです。

この提案は、ブロッキングプロパティとその規定を再定義し、senderに対するブロッキング指定を可能にするものです。
senderブロッキングプロパティを取れるようになることによって、submitの実行において動的確保を避けることができるなどの恩恵があります。

P2259R0 Repairing input range adaptors and counted_iterator

iterator_categoryが取得できないことから一部のrange adoptorのチェーンが機能しない問題と、counted_iteratorの問題を修正する提案。

iterator_categoryが取得できない問題

次のコードはコンパイルエラーを起こします。

#include <vector>
#include <ranges>

int main() {
  std::vector<int> vec = {42};

  auto r = vec | std::views::transform([](int c) { return std::views::single(c);})
               | std::views::join
               | std::views::filter([](int c) { return c > 0; });

  auto it = r.begin();
}

join_viewviews::join)の提供するイテレータC++17のイテレータとの互換性が無く(後置++の戻り値型がvoid)、join_viewイテレータJIとするとiterator_traits<JI>は空(すべてのメンバが定義されない状態)となります。ところが、filter_viewviews::filter)はメンバ型iterator_categoryを定義するために、入力のイテレータIに対してiterator_traits<I>::iterator_categoryがある事を前提にしており、それによってエラーが起きています。

このような問題は、C++20から追加された他のrange adopterや入力イテレータに対しても、あるいはユーザー定義イテレータに対しても共通するものです。

C++20からのイテレータC++17のイテレータと比べると制限が緩和されている部分があり、互換性がありません。したがって、iterator_traits<I>イテレータIC++17イテレータの要件を満たさない場合にいかなるメンバも定義しません。言い換えると、iterator_taraitsはあくまでC++17以前のイテレータを期待する場合に使用するものであって、C++20以降は積極的に使用するものではありません。
C++20イテレータC++17イテレータ互換にしようとする場合は、そのiterator_traitsが有効になるようにする必要があります。

C+;20のinput_iteratorC++17のものとは互換性が無く、どのように取り繕ったとしても異なるものです。従って、後方互換性のないC++20イテレータについてiterator_taraits(特にiterator_category)を有効化することに意味はありません。また、それを表すタグ型を導入することも、後方互換性を確保するという意味合いから無意味です。

結局、range adopterイテレータC++20のforward_iterator以上の強さの時にのみC++17のinput_iteratorとの互換性があります。そのためこの問題の解決としては、range adopterイテレータiterator_categoryを提供するのは入力されたイテレータC++20のforward_iterator以上の強さの時のみ、というように規定することが最善です。
そして、そうする場合はそのイテレータ自身もC++20のforward_iteratorでなければならず、その場合はiterator_traitsに適切なiterator_categoryが定義されている必要があります。

これらのことからこの提案では、次のようにこの問題を解決します。

counted_iteratorの問題

iota_viewは整数列として使う分にはrandom_access_rangeとなります。しかし、std::counted_iteratorを通すとそうはなりません。

auto v = std::views::iota(0);
auto i = std::counted_iterator{v.begin(), 5};

// アサーションは失敗する
static_assert(std::random_access_iterator<decltype(i)>);

GCCはこれを修正済みのようです)

この問題の原因は、std::cunted_iteratoriterator_traitsを特殊化することによって元のイテレータをエミュレートしようとすることにあります。

// counted_iteratorに対するiterator_traits特殊化
template<input_iterator I>
struct iterator_traits<counted_iterator<I>> : iterator_traits<I> {
  using pointer = void;
};

iterator_traitsC++20のイテレータデザインにおいて2つの重要な役割を果たします。

  1. プライマリテンプレートが使用される場合、C++17互換レイヤーとして機能する。
    この時、iterator_conceptメンバは定義されずiterator_categoryのみがイテレータ型のC++17互換の性質によって定義される。
  2. 明示的/部分的に特殊化されている場合、イテレータ型のメンバ型よりも優先されるカスタマイゼーションポイントとして機能する。
    この時、iterator_conceptメンバが定義されない場合、iterator_categoryを使用してイテレータ型のC++20的性質を制限する。

iterator_conceptメンバとはC++20からのiterator_category指定方法です。C++20からはcontiguous_iteratorというカテゴリが追加されたため、互換性を確保しつつcontiguous_iteratorを定義するため(主にポインタ型のため)に導入されました。

std::counted_iteratoriterator_traits特殊化の問題は、上記1で得られたiterator_traitsを2で使用してしまっていることにあります。

std::random_access_iteratorコンセプトはITER_CONCEPTという操作によってC++20とC++17のイテレータ両方から適切なカテゴリを取得しようとします。

ITER_CONCEPTは入力のイテレータIiterator_traitsを特殊化していればそれを使用して、iterator_conceptあるいはiterator_categoryを取得します。
counted_iteratorの場合はiterator_traitsを定義しているためそちらを使用して元のイテレータiterator_categoryを取得しに行きます。

iota_viewイテレータiterator_conceptiterator_categoryの両方を定義していますが、iterator_categoryC++17互換イテレータとして使用される場合のカテゴリの表明なので、常にinput_iteratorとなります。C++20移行のイテレータは基本的にiterator_traitsにアダプトしておらず、iota_viewイテレータに対するiterator_traitsもプライマリテンプレートが使用されることになります。プライマリテンプレートのiterator_traits<I>は任意のイテレータIC++17イテレータとして見せる振る舞いをし、そのiteretor_categoryI::iteretor_categoryから取得します。

その結果、input_iteratorが取得されるため、ITER_CONCEPT(counted_iterator<iota_view::iterator>)の結果はinput_iteratorとなり、random_access_iteratorとはなりません。

つまりは、counted_iteratorを通すことによってC++20イテレータC++17イテレータとしてしか扱われなくなってしまっています。counted_iterator自身はC++20イテレータであるにもかかわらず・・・

また、counted_iteratorは元のイテレータcontiguous_iterator性を正しく受け継ぐことができません。operator->std::pointer_traits::to_address()も定義しないためstd::to_addressが使用できず、std::to_addressは戻り値型を推論しているためハードエラーを起こします。結果、問い合わせる事さえハードエラーとなります。

// ハードエラーとなる
static_assert(std::contiguous_iterator<std::counted_iterator<int*>> || true);

これらの問題の解決のため、counted_iteratorを次のように変更します。

  • iterator_traits<counted_iterator<I>>の特殊化はiterator_traits<I>の特殊化が存在する場合にのみ使用するようにする
  • counted_iteratorのメンバとしてvalue_type/difference_typeを定義する
  • ↑の変更によって必要なくなるので、std::incrementable_traitsの特殊化を削除する
  • iterator_concept/iterator_categoryを元のイテレータが定義している場合、それを使用してcounted_iteratorのメンバとしてiterator_concept/iterator_categoryをそれぞれ定義する
  • contiguous_iteratorをラップする場合、->を提供してstd::to_addressが動作するようにし、iterator_traits特殊化のpointerを適切に定義するようにして、counted_iterator自身もcontiguous_iteratorとなるようにする

この問題は両方とも、C++20イテレータに対してC++17イテレータとの後方互換性を頑張って考えた結果起きているようです・・・

P2260R0 WG21 2020-11 Virtual Meeting Record of Discussion

先月初めに行われたC++標準化委員会の全体会議(テレカンファレンス)の議事録。

N4871よりもだれがどんな発言をしたのかが詳細に記録されています。

多分2週間後くらい

この記事のMarkdownソース

[C++]inline名前空間の使途

inline名前空間C++11から追加された機能で、その中にあるものは透過的に(名前空間がないかのように)アクセスすることができます。一見使いどころがなく見られがちですが、うまく使えばとても便利に活用することができます。

1. using namespaceの範囲を限定する

これは標準ライブラリではユーザー定義リテラルの定義と利用でよく利用されます。

例えば、std::string_viewを簡易利用するためのsvリテラルは次のように宣言されています。

namespace std {

  // string_view本体
  template <class CharT, class Traits = char_traits<CharT>>
  class basic_string_view;

  inline namespace literals {
    inline namespace string_view_literals {

      // svリテラル
      constexpr std::string_view operator""sv(const char* str,   std::size_t len) noexcept;
    }
  }
}

これによって、using namespace std;という広すぎる範囲をusingすることなく、次のいずれかによってこのsvリテラルを使用することができます。

#include <string_view>

int main() {
  {
    // 1、標準ライブラリの定義する全リテラルだけが使用可能になる
    using namespace std::literals;

    auto str = "literals"sv;
  }
  {
    // 2、string_viewのsvリテラルだけが使用可能になる
    using namespace std::string_view_literals;

    auto str = "string_view_literals"sv;
  }
  {
    // 2、std名前空間の神羅万象が利用可能になる
    using namespace std;

    auto str = "std"sv;
  }
}

std::literals名前空間は標準ライブラリの定義するすべてのユーザー定義リテラルs, h, m, s, iなどなど)が定義されているinline名前空間であり、それをusing namespaceするとそれらのリテラルの全てがそのスコープで見えるようになります。
std::string_view_literalssvリテラルだけが定義されているinline名前空間であり、using namespaceしてもsvリテラル以外のものは見えません。
そして、これらのユーザー定義リテラルstd名前空間直下からも参照できます。

このようにライブラリの提供するものをinline名前空間である程度グループ化しておくことで、使う側は適宜必要な範囲だけをusing namespaceすることができるようになります。

2. APIのバージョニング

これは多分最もポピュラーなinline名前空間の使い方でしょうか。

例えば、既に利用している関数があり、その関数のAPI(インターフェース)は変えないけれども内部実装を変更したい時を考えます。

namespace mylib {
  int f(int a) {
    return a;
  }
}

この処理を、2倍してから返すようにしたいとします。

namespace mylib {
  int f(int a) {
    return 2 * a; // 2倍して返すようにしたい
  }
}

しかし、この関数は既に至るところで使われており、変更するならしっかりテストしてからにしたいし、使われているとこを書き換えて回るのは嫌です。そんな時、変更前のバージョンをinline名前空間で、変更後のバージョンを名前空間で囲っておきます。

namespace mylib::inline v1 {

  // 現在の処理
  int f(int a) {
    return a;
  }
}

namespace mylib::v2 {

  // 変更後の処理
  int f(int a) {
    return 2 * a; // 2倍して返すようにしたい
  }
}

そして、新しい関数mylib::v2::f()の実装とテストが完了してすべてのf()を新バージョンへ切り替えることができるようになったら、それぞれの名前空間inline指定を逆にします。

// 古いバージョンを非inlineに
namespace mylib::v1 {

  // 現在の処理
  int f(int a) {
    return a;
  }
}

// 新しいバージョンをinlineに
namespace mylib::inline v2 {

  // 変更後の処理
  int f(int a) {
    return 2 * a; // 2倍して返すようにしたい
  }
}

inline名前空間は透過的です。そのため、これだけで、f()を使用しているところを一切書き換えることなくその処理内容をアップデートできます。もし古いバージョンを使いたい場合はmylib::v1::f()のように名前空間を明示的に指定してやればよく、古いバージョンを使用していることも分かりやすくなります。

3. ABIのバージョニング

inline名前空間APIでは省略可能ですが、ABI(マングル名)では通常の名前空間と同じ扱いをされ、常にマングル名に表示されていますし、参照するときも省略できません。inline名前空間の効果は純粋にC++の意味論上だけのものです。

この性質によって、APIは変わらないけれどABIを破壊するような変更がなされたときにその影響を軽減することができます。

/// header.h

namespace mylib {
  class S {
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

namespace mylib {

  int S::get_m() const {
    return this->m;
  }
}
/// main.cpp

#include <iostream>

#include "header.h"

int main() {
  mylib::S s{};
  
  std::cout << s.get_m();
}

例えばこんなクラスとそのメンバ関数がありヘッダと実装が分かれているとき、これを利用するmain.cppが同じコンパイラを使ってsource.cppコンパイルしている間は何も問題はありません。

しかし、source.cppを静的ライブラリや動的ライブラリの形であらかじめコンパイルしてから利用しているときに、このmylib::SにABIを破壊する変更がなされてしまったとします。

/// header.h

namespace mylib {
  class S {
    float f = 1.0;  // メンバを追加した
    int m = 10;
  public:

    int get_m() const;
  };
}

このような変更はAPIに何も影響を及ぼしませんが、クラスのレイアウトが変更されているのでABIから見ると重大な変更です。ABIを破壊しています。

このとき、source.cppコンパイルした静的or動的ライブラリを再コンパイルせずに使い続けて(リンクして)いたとしてもコンパイラは何も言わないでしょう。この場合の変更によってはマングル名は変化しておらず、コンパイルもリンクもつつがなく完了します。

/// main.cpp

#include <iostream>

// これは最新のものを参照しているとする
#include "header.h"
// source.cppは10年前にコンパイルしたものをリンクして使い続けているとする

int main() {
  mylib::S s{};
  
  std::cout << s.get_m(); // 未定義動作!
}

古いS::get_m()関数の定義はsource.cppコンパイルした外部ライブラリにあり、新しいmylib::Sのレイアウトを知りません。したがって、レイアウト変更後のクラスのどこかの領域をint型のメンバS::mとして読みだした値を返してくれるでしょう(たぶん実行時エラーも起きないのではないかと思われます)。これは紛う事なき未定義動作です・・・

これは稀によくあるビルド済みバイナリをリンクして利用する時の問題で、C++でヘッダオンリーライブラリが好まれる傾向にある事の一つの理由でもあります。

こんな時、inline名前空間を利用することでこの問題の軽減を図れます。

リンクエラーにする

解決策の一つ目は、変更後のコードをそれを表すinline名前空間で囲ってしまう事です。

/// header.h

// inline名前空間を追加する
namespace mylib::inline v2 {
  class S {
    float f = 1.0;  // メンバを追加した
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

// inline名前空間を追加する
namespace mylib::inline v2 {

  int S::get_m() const {
    return this->m;
  }
}

inline名前空間APIC++コード上)からは透過的ですがABI(マングル名)には表示されます。従って、この変更後のmylib::Sおよびそのメンバ関数を利用するコードに変更は必要ありませんが、そのマングル名は変更前のものと異なっています。
結果、再コンパイルしないで用いている変更前ソースによるビルド済みバイナリからはそのようなシンボルが見つからずリンクエラーによってコンパイル時に気づくことができます。

/// main.cpp

#include <iostream>

// これは最新のものを参照しているとする
#include "header.h"
// source.cppは10年前にコンパイルしたものをリンクして使い続けているとする

int main() {
  mylib::S s{};
  
  std::cout << s.get_m(); // リンクエラー、シンボルが見つからない
}

ABI互換性を確保する

もう一つの方法は初めからinline名前空間を利用していた場合にのみ利用可能となります。例えば先程のサンプルは初めから次のようにinline名前空間に囲まれていたとします。

/// header.h

// inline名前空間に包まれている
namespace mylib::inline v1 {

  class S {
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

// inline名前空間に包まれている
namespace mylib::inline v1 {

  int S::get_m() const {
    return this->m;
  }
}

このコードに対して先程の変更がなされたとしましょう。その際、古いバージョンのinline名前空間を非inlineにし、新しいバージョンのinline名前空間名を変更しておきます。

/// header.h

// 古いバージョン、inline名前空間ではなくする
namespace mylib::v1 {

  class S {
    int m = 10;
  public:

    int get_m() const;
  };
}

// 最新版
namespace mylib::inline v2 {

  class S {
    float f = 1.0;  // メンバを追加した
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

// 古いバージョン、inline名前空間ではなくする
namespace mylib::v1 {

  int S::get_m() const {
    return this->m;
  }
}

// 最新版
namespace mylib::inline v2 {

  int S::get_m() const {
    return this->m;
  }
}

こうしておいた状態で、さっきまでのように使用しようとすれば正しくリンクエラーになります。

今回のようにした場合は、逆に変更前のバージョンのものを参照し続けているようなプログラムに最新のsource.cppをビルドした(変更が反映された)バイナリをリンクした時でも、リンクエラーも未定義動作も起こさずに使用することができます。

/// main.cpp
// 10年前のコードを参照ヘッダも含めて変更せずに使い続けているとする

#include <iostream>

// 変更前のものを参照している
#include "header.h"
// source.cppはさっきコンパイルした最新のものをリンクしているとする

int main() {
  // mylib::v1::Sを参照している
  mylib::S s{};

  // mylib::v1::S::get_m()を参照している
  std::cout << s.get_m(); // 10
}

inline名前空間はマングル名レベルでは名前空間と区別なく扱われます。すなわち、ABIからは名前空間名がinlineであるかどうかは分かりません。したがって、このように変更を追記し古いバージョンを維持しておけば、古いバージョンを利用しているプログラムに対しても古いバージョンを提供し続けることができます。これによって、ABI互換性を維持し、ABIを保護することができます。

GCCやclangの最近の標準ライブラリの実装では、ABI保護のためにinline名前空間が多用されています。

4. 名前の衝突を回避する

これは特にCPO(Customization Point Object)の定義で利用されています。

CPOはその呼び出しに当たって自身と同名の非メンバ関数をADLで探索するようになっていることがよくあります。これはあるクラスに対してHidden friendsと呼ばれるfriend関数を探し出すものです。

一方で、標準ライブラリにあるCPOは一部のものを除いてstd名前空間のすぐ下に定義されることになります。

すると、標準ライブラリにあるクラスに対するHidden friends関数とCPOとで名前が衝突してしまいます。

namespace mystd {
  
  namespace cpo_impl {
    
    // swap CPOの実装クラス
    struct swap_cpo {
      
      template<typename T, typename U>
      void operator()(T&, U&) const;
    };
  }

  // swap CPO #1
  inline constexpr cpo_impl::swap_cpo swap{};
  
  struct S {
    
    // Hidden friendsなswap関数 #2
    friend void swap(S& lhs, S& rhs);
  };
}

この例では#1と#2の異なる宣言が同じ名前空間にあるためにコンパイルエラーになっています。

このように、CPOと同じ名前空間にあるものがそのCPOにアダプトしようとすると名前衝突してしまうわけです。

この場合にCPOの定義をinline名前空間で囲ってやることでこの問題を解決できます。しかも、呼び出す際に名前空間名が増えることもありません。

namespace mystd {
  
  namespace cpo_impl {
    
    // swap CPOの実装クラス
    struct swap_cpo {
      
      template<typename T, typename U>
      void operator()(T&, U&) const;
    };
  }

  // CPO定義をinline名前空間で囲う
  inline namespace cpo {
    // swap CPO #1
    inline constexpr cpo_impl::swap_cpo swap{};
  }
  
  struct S {
    
    // Hidden friendsなswap関数 #2
    friend void swap(S& lhs, S& rhs);
  };
}

こうしてもmystd::swapという名前でswapCPOを参照できますし、#1と#2のswapは別の名前空間にいるために名前は衝突していません。
このため、標準ライブラリにあるCPOはほとんどのものがinline名前空間に包まれています。

ここでは説明のために変な名前空間と適当なクラスを用意しましたが、mystdstdmystd::Sstd::vectorとかに読み替えるとつかみやすいかもしれません。

参考文献

この記事のMarkdownソース

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

文書の一覧 www.open-std.org

提案文書で採択されたものはありません。全部で35本あります。

N4863 Agenda for Fall Virtual WG21/PL22.16 Meeting

2020年11月9日 08:00 (北米時間)に行われるWG21本会議のアジェンダです。

C++23への機能追加のための投票も行われると思われるので、ようやくC++23入りするものが出てきそうです。

N4864 WG21 virtual meeting: Autumn 2020

↑のWG21本会議周知のための文章?

中身は日付とzoomのURLがあるだけです。

N4865 Response to Editorial Comments: ISO/IEC DIS 14882, Programming Language C++

C++20のDIS(Draft international standard)に対して寄せられた各国の委員会からのコメントのまとめ。

N4866 WG21 admin telecon meeting: Pre-Autumn 2020

2020年11月9日に行われるWG21本会議のスケジュール表。先程のN4863よりも少し詳しく書かれています。

N4867 Editors' Report - Programming Languages - C++

↓の更新されたWorking Draftの差分をまとめたもの。

今回は新しい機能の追加はありません。

N4868 Working Draft, Standard for Programming Language C++

C++23のWorking Draft第二弾。↑のEditors' Reportにあるように、新規追加された機能はなく、文言の調整などのみの変更です。

P0847R5 Deducing this

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

現在のC++では、メンバ関数のCV修飾と参照修飾によって暗黙のthisパラメータのconst/volatile性と値カテゴリを指定したオーバロードを行うことができます。それはクラスのオブジェクトの実際の状態に応じて処理内容を切り替えるのに必要ではありますが、ほぼ同じ処理をいくつも(大抵は2×2)書くことになります。おおよそ次の3つ方法のどれかによって実装されます。

  1. 4つのメンバ関数それぞれに処理を記述する
  2. どれか1つに委譲するようにする
  3. 4つ全てで別の実装関数に委譲する

例えばstd::optionalvalue()関数はまさにconst有無と参照修飾で4つのオーバーロードを提供しています。おおよそ1つ目の方法で実装されており、次のようになります。

template <typename T>
class optional {
  // ...
  constexpr T& value() & {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  constexpr T const& value() const& {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  constexpr T&& value() && {
    if (has_value()) {
      return move(this->m_value);
    }
    throw bad_optional_access();
  }

  constexpr T const&& value() const&& {
    if (has_value()) {
      return move(this->m_value);
    }
    throw bad_optional_access();
  }
  // ...
};

この様にほぼ同じ実装を微妙に異なって複数書かなければいけない事はバグを誘発しやすく、また保守性も低下します。

一方でこれがもしメンバ関数ではなかったとしたら、次のように簡潔な実装を選択できます。

template <typename T>
class optional {
  // ...
  template <typename Opt>
  friend decltype(auto) value(Opt&& o) {
      if (o.has_value()) {
          return forward<Opt>(o).m_value;
      }
      throw bad_optional_access();
  }
  // ...
};

この1つの関数テンプレートでさきほどの4つのメンバ関数と全く同じ動作をさせることができます。ただ、これはメンバ関数ではないのでopt.value()のように呼び出すことは出来ません。

この2種の関数の差は、thisに相当する引数を明示的に書けるかどうかという事から来ています。明示的に書くことができれば、フォワーディングリファレンスと完全転送によって4つの実装を1つに圧縮できます。

この提案は、このような問題を解決するためにメンバ関数でも非メンバ関数のようにthisに相当する引数を明示的に取れるようにしつつ、呼び出し側は従来通りに呼び出せるようにするものです。

非静的メンバ関数の第一引数にthisによって注釈をつけておく事でそれ以外のものと区別します。その場合はCV/参照修飾を行えなくなります。

struct X {
  // void foo(int i) const & 相当の宣言
  void foo(this X const& self, int i);

  // フォワーディングリファレンスによる宣言
  template <typename Self>
  void bar(this Self&& self);
};

struct D : X { };

void ex(X& x, D const& d) {
  x.foo(42);      // selfはxを束縛し、iに42が渡される
  x.bar();        // SelfはX&に推論され、X::bar<X&>が呼ばれる
  move(x).bar();  // SelfはXに推論され、X::bar<X>が呼ばれる

  d.foo(17);      // selfはdを束縛する
  d.bar();        // SelfはD const&に推論され、X::bar<D const&>が呼ばれる
}

この引数のことをexplicit object parameterと呼びます。

.によってメンバ関数呼び出しされた時、explicit object parameterには呼び出したオブジェクトが渡されます。それ以降は通常の関数引数と同じ扱いとなり、テンプレートの恩恵を受けることができます。

これによって、先ほどのstd::optional<T>::value()の実装は次のように改善されます。

template <typename T>
class optional {
  // ...
  template <typename Self>
  constexpr auto&& value(this Self&& self) {
    if (!self.has_value()) {
      throw bad_optional_access();
    }

    return forward<Self>(self).m_value;
  }
  // ...
};

また、これはラムダ式においても使用する事ができます。

std::vector captured = {1, 2, 3, 4};
[captured](this auto&& self) -> decltype(auto) {
  // forward_like<T>(U u)はTのCV修飾と値カテゴリをUにコピーした上でuを転送するもの
  return forward_like<decltype(self)>(captured);
}

[captured]<class Self>(this Self&& self) -> decltype(auto) {
  return forward_like<Self>(captured);
}

これが可能になる事によって例えばCR抜きのCRTPができるようになります。

CRTP この提案
template <typename Derived>
struct add_postfix_increment {
  Derived operator++(int) {
    auto& self = static_cast<Derived&>(*this);

    Derived tmp(self);
    ++self;
    return tmp;
  }
};

struct some_type
  : add_postfix_increment<some_type> {
    some_type& operator++() { ... }
};
struct add_postfix_increment {
  template <typename Self>
  auto operator++(this Self&& self, int) {
      auto tmp = self;
      ++self;
      return tmp;
  }
};

struct some_type : add_postfix_increment {
    some_type& operator++() { ... }
};

他にも、再帰ラムダ、値によるメンバ関数、SFINAE-friendlyで完全なCall wrapperなど、新しいイディオムへの道が開けるようです。

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

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

decay-copyというのは関数テンプレートに引数を渡すときに行われる変換のことです。関数テンプレートのテンプレートパラメータによる引数に値を渡すとき、左辺値は右辺値に、配列はポインタに、CV修飾を除去しつつ変換されます。

template<typename T>
void f(T t);

std::vector<int> vec{};
const std::vector<int> cvec{};

// 全て T = std::vector<int>
f(vec);                 // コピーされる
f(cvec);                // コピーされる
f(std::vector<int>{});  // ムーブされる

int arr[] = {1, 2, 3};

// T = int*
f(arr);

std::decaydecay-copyの型の変換をシミュレートするものです。このような振る舞いはauto copy = value;のように書くことで再現できますが、この提案はその意図を明確にするためにもワンライナーで書くことができるようにするものです。

auto(value)という構文でvalueをコピーしたprvalueを生成するもので、例えば次のように利用できます。

現在 この提案
// Containerはコンセプトとする
void pop_front_alike(Container auto& x) {
  auto a = x.front();
  std::erase(x.begin(), x.end(), a);
}
void pop_front_alike(Container auto& x) {
  std::erase(x.begin(), x.end(), auto(x.front()));
}

std::eraseは指定されたイテレータ範囲から、第3引数で渡された値と同じものを削除する関数です。イテレータ範囲に含まれている要素を削除するときは、その操作の最中でダングリング参照とならないようにあらかじめコピーする必要があります。その際に、auto()によるdecay-copy構文を使用できます。

現在 この提案
struct S {
  S(const S&) {
    /**/
  }
  S& operator=(S&& other) {
    /**/
  }

  // コピー構築とムーブ代入を利用した簡略化
  S& operator=(const S& other) {
    if (this != &other) {
      auto copy = other;
      *this = std::move(copy);
    }

    return *this;
  }
}
struct S {
  S(const S&) {
    /**/
  }
  S& operator=(S&& other) {
    /**/
  }


  S& operator=(const S& other) {
    if (this != &other) {
      *this = auto(other);
    }

    return *this;
  }
}

auto a = x.front();によるコピーは変数宣言構文であり、ここでの主目的であるコピーは変数宣言の持つプロパティの一つでしかありません。一方、auto(x.front())は明確にコピーという操作を表しています。

関数キャストT(x)Tautoに置き換えることによって、auto(x)の構文は関数キャストの亜種であると見ることができます。クラステンプレートの引数推論を考慮すれば、この構文には次のような直交・一貫性があります。

変数定義 関数キャスト new
auto v(x) auto(x) new auto(x)
auto v{x} auto{x} new auto{x}
ClassTemplate v(x) ClassTemplate(x) new ClassTemplate(x)
ClassTemplate v{x} ClassTemplate{x} new ClassTemplate{x}

ライブラリサポートではなく言語サポートすることによって、このように変数宣言や関数スタイルキャストなどの構文との一貫性を向上することができます。

P0870R4 A proposal for a type trait to detect narrowing conversions

Tが別の型Uへ縮小変換(narrowing conversion)によって変換可能かを調べるメタ関数is_convertible_without_narrowing<T, U>を追加する提案。

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、名前がis_narrowing_convertible<T, U>からis_convertible_without_narrowing<T, U>に変更されたことと、使用例のサンプルコードが追加されたことです。

P1048R1 A proposal for a type trait to detect scoped enumerations

scoped enumenum class)を識別するためのメタ関数であるstd::is_scoped_enum<T>の提案。

SFINAEによって、型がenumなのかenum classなのかで処理を分けたいときにあると有用なので追加しようというものです。
筆者の方は、古いC++ライブラリをアップデートする際にそこに含まれるenumからenum classへの移行を追跡するためのテストにおいて活用したそうです。

#include <type_traits>

enum E1{};

enum class E2{};

int main() {
  bool b1 = std::is_enum_v<E1>; // false
  bool b2 = std::is_enum_v<E2>; // true
}

これは例えば次のように実装できます。

template<class T, bool = is_enum_v<T>>
struct is_scoped_enum_helper : false_type {};

template<class T>
struct is_scoped_enum_helper<T, true> : public bool_constant<!is_convertible_v<T, underlying_type_t<T>>> {};

template<class T>
struct is_scoped_enum : public is_scoped_enum_helper<T> {};

この提案は次の本会議での投票にかけられる予定で、C++23入りがほぼ確実そうです。

P1206R2 ranges::to: A function to convert any range to a container

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

現在 この提案
std::list<int> list = {...};

std::vector<int> vec(list.begin(), list.end());
std::list<int> list = {...};

auto vec = list
  | std::ranges::to<std::vector<int>>(list);

このようなコンテナの変換はとても基本的な操作ですがこの提案のメインはこれではなく、Viewの実体化を簡易化することにあります。

標準コンテナは上記のようにイテレータペアを受け取るrangeコンストラクタを持っていますがそのイテレータペアは同じ型となる事を前提としています。ところが、<ranges>Viewは多くがそのようなcommon_rangebegin()/end()イテレータ型が同じrange)ではありませんので、少し遠回りをしなければなりません。そこにranges::toを用いると簡潔に書くことができるようになります。

現在 この提案
std::iota_view v{0, 1024};
std::vector<int> vec;
std::copy(v, std::back_inserter(vec));

// あるいは
auto iota = std::views::iota(0, 1024)
  | std::views::common;

std::vector<int> vec(iota.begin(), iota.end());
auto vec = std::views::iota(0, 1024)
  | std::ranges::to<std::vector<int>>(list);

これによって、range adoptorのチェーンから任意のコンテナへの変換が簡単に行えるようになります。

std::ranges::toによる変換は指定するコンテナによって最も効率的な方法で実装されます。例えば、reserve可能な標準コンテナに関しては、変換元のrangeがその距離を効率的に求められればreserveしてから代入されます(ただし、この振る舞いはとりあえず標準コンテナのみとなるようです)。

また、std::ranges::toにはクラステンプレートの実引数推定によって値型を推定してもらうことのできるオーバーロードが提供されています。

std::list<int> list = {...};

// std::vector<int>を推論してくれる
auto vec = list
  | std::ranges::to<std::vector>(list);

P1401R4 Narrowing contextual conversions to bool

constexpr ifstatic_assertの引数でのみ、整数型からbool型への暗黙の縮小変換を定数式で許可する提案。

onihusube.hatenablog.com

このリビジョンでの変更は、EWGでの指摘を受けてサンプルをいくつか追加した事と、提案する文言を調整した事です。

P1525R1 One-Way execute is a Poor Basis Operation

Executor提案(P0443R14)におけるstd::execution::execute及びstd::execution::executorコンセプトは、Executorライブラリにおける基本的なものとしては不適格であるという報告書。

std::execution::executeは任意のexecutorと引き数なしで呼び出し可能な処理を受け取って、そのexecutorの実行コンテキストで処理を即座に実行します。その戻り値はvoidであり、処理の結果やキャンセル、エラーを受け取ったり、処理をチェーンする方法も提供しません。つまりは処理を投げたらその処理について何かする方法が一切ありません。

int main() {
  std::execution::executor auto ex = ...; // 任意のexecutor
  std::invocable auto f = []() { /*何か処理*/ };

  // 処理fをexの実行コンテキストで即座に実行し、何も返さない
  std::execution::execute(ex, f);
}

このために、execute()による実行は処理の発行時、発行と実行の間、実行中のそれぞれで発生するあらゆるエラーをハンドリングする方法を提供せず、それは実装定義となりexecutorによって異なる事になります。
そのため、ジェネリックなコードでは非同期に発生するエラーに対応するポータブルな方法が無く、柔軟なエラー処理を必要とする高レベルな非同期アルゴリズムexecute()上で構築する事を妨げています。

さらに、execute()は処理の実行そのものが何らかの理由でキャンセルされた事を伝達するためのチャネルも持たず、execute()による非同期タスクの実行ではその状態のための動的なアロケーションが必要ですが、そのアロケーションを制御する方法もありません。

一方で、schedule()およびsender/receiverによる設計ではそれらの問題は全て解決されています。

int main() {
  std::execution::executor auto ex = ...; // 任意のexecutor
  std::invocable auto f = []() -> int { /*何か処理*/ };

  // 実行のスケジューリング、senderを返す
  std::execution::sender auto s1 = std::execution::schedule(ex);
  // 処理の登録
  std::execution::sender auto s2 = std::execution::then(s1, f);
  // 処理をチェーン
  std::execution::sender auto s3 = std::execution::transform(s2, [](int n) { return std::to_string(n); });

  // receiverは単なるコールバック
  // 処理の結果、エラー、完了(キャンセル)を受ける3つのチャネルを持つ
  std::execution::receiver auto r = ...;  // 任意のreceiver

  // senderにreceiverを接続する
  std::execution::operation_state auto state = std::execution::connect(s3, r);

  // 処理の実行
  // senderとreceiverの実装によって、実行中のアロケーションを制御できる
  std::execution::start(state);
}

この文書では、execute()及びexecutorコンセプトよりもschedule()及びschedulerコンセプトの方が、Executorライブラリの基本的な操作とコンセプトとして相応しいと述べています。

P1759R3 Native handles and file streams

標準ファイルストリームに、OSやプラットフォームネイティブのファイルを示すものを取得する方法およびその型エイリアスを追加する提案。

この提案の対象の標準ファイルストリームとは以下のものです。

  • basic_filebuf
  • basic_ifstream
  • basic_ofstream
  • basic_fstream

例えば開いているファイルの最終更新日を取得したい場合に、ファイルストリームからそれを取得する手段はありません。標準ライブラリでそれを行うには、例えばstd::filesystem::last_write_timeを利用しますが、これは引数としてstd::filesystem::pathをとります。そのため、どうしてもファイルオープンと最終更新日取得のタイミングは開いてしまう事になり、同じpathが同じファイルを指していなかったり、そもそもファイルがない可能性があります。
また、標準ライブラリにはないファイル操作を行いたい場合は、ファイルストリームを必要になるタイミングで再構築するかプラットフォーム依存のコードを書くかの選択になります。

// 最終更新日を取得する
std::chrono::sys_seconds last_modified(int fd) {
  ::stat s{};
  int err = ::fstat(fd, &s);
  return std::chrono::seconds(s.st_mtime.tv_sec);
}

int main() {
  // ファイルストリームの再オープン
  {
    // 最終更新日をまず取得
    int fd = ::open("~/foo.txt", O_RDONLY); // CreateFile on Windows
    auto lm = last_modified(fd);
    ::close(fd); // CloseFile on Windows

    // このパスは本当に同じファイルを指している?
    std::ofstream of("~/foo.txt");
    of << std::chrono::format("%c", lm) << '\n';
  }

  // プラットフォーム固有APIを常に使用
  {
    int fd = ::open("~/foo.txt", O_RDWR);
    auto lm = last_modified(fd);
  
    auto str = std::chrono::format("%c\n", lm);
    ::write(fd, str.data(), str.size());
  
    // 閉じるのを忘れずに!
    ::close(fd);
  }
}

この提案は、このような場合のためにOSネイティブのファイルハンドル(POSIXならファイルディスクリプタWindowsならファイルハンドル)を取得できるようにし、標準ファイルストリームを使用しつつ、必要な時にプラットフォーム固有のファイル操作を行えるようにするものです。

int main() {
  std::ofstream of("~/foo.txt");
  // ネイティブファイルハンドルの取得
  auto lm = last_modified(of.native_handle());
  of << std::chrono::format("%c", lm) << '\n';
}

この例の他にも、ファイルロックやステータスフラグの取得、Vectored I/Onon-blocking I/Oなどのユースケースがあります。

これはstd::threadstd::mutexなどがすでに持っているnative_handle()と同じものです。同じように、ネイティブファイルハンドルの型を示すエイリアスnative_handle_typeがファイルストリームのクラスに入れ子型として追加されます(POSIXならintWindowsならHANDLEvoid*))。

P1938R2 if consteval

constevalstd::is_constant_evaluated()にある分かりづらい問題点を解決するためのconsteval ifステートメントの提案

constevalstd::is_constant_evaluated()を組み合わせた時、あるいはstd::is_constant_evaluated()そのものの用法について、次の2つの問題があります。

constexpr関数でのconsteval関数の条件付き呼び出し

consteval関数は即時関数と呼ばれ、その呼び出しは必ずコンパイル時に完了しなければならず、コンパイル時に実行できないような呼び出しはコンパイルエラーとなります。

consteval int f(int i) { return i; }

constexpr int g(int i) {
  if (std::is_constant_evaluated()) {
      return f(i) + 1; // ng
  } else {
      return 42;
  }
}

consteval int h(int i) {
  return f(i) + 1;  // ok
}

g()h()を実行時にも呼び出し可能なように拡張したものです。一見、このコードは何の問題もなく意図通りに動作しそうに思えます。しかし、h()は問題ありませんがg()コンパイルエラーが発生します。

f()consteval関数でありその引数は定数式でなければなりません。g()で呼ばれるf()の引数iは単にconstexpr関数の引数であり定数式ではありません。従って、このf()呼び出しはstd::is_constant_evaluated()の結果に関わらず常に失敗します。
一方、h()で呼ばれるf()h()consteval関数であるのでこの制約を受けません。

しかし、g()内のf()の呼び出しが例えばf(42)の様になっているとその呼び出しは成功し、コンパイルエラーは起きません。

この問題は即時関数が呼ばれるコンテキストの問題ですが、constexpr ifの特性を知っている人はif (std::is_constant_evaluated())のようにすればg()が実行時評価されたときにはf(i)の呼び出しはコンパイルされないので行ける!と思うかもしれません・・・

if constexpr (std::is_constant_evaluated())

std::is_constant_evaluated()コンパイル時に呼び出されたときにtrueを返し、実行時に呼ばれるとfalseを返す関数、と単純に説明されることが多いです。するとおそらく誰もが考えるでしょう、実行時にまでifを残したくないのでif constexprを使おう!と。

#include <type_traits>

constexpr int f() {
  if constexpr (std::is_constant_evaluated()) {
    return 20;
  } else {
    return 0;
  }
}

int main() {
  // コンパイル
  constexpr int n = f();
  // 実行時
  int m = f();
  
  std::cout << n << '\n' << m << std::endl;
  // 20
  // 20
}

std::is_constant_evaluated()の正確な効果は、コンパイル時実行されることが確実な特定のコンテキストで呼び出されたときにのみtrueを返し、それ以外の場合はfalseを返す、というものです。
特に、if constexprの条件式で呼び出されたときは常にtrueを返します。

std::is_constant_evaluated()if文と組み合わせて使うのが正しい用法です。

この関数の呼び出しはおそらく常にコンパイル時に行われます。その際、特定のコンテキストにある呼び出しのみがtrueを返しそれ以外はfalseとなります。実際の所、普通のifと共に使ったとしてもその条件分岐が実行時まで残ることは無いでしょう。

if consteval

とはいえ、この2つの振る舞いは非直感的であり、特に2つ目の方は罠になり得ます。この提案は新しくif constevalという条件分岐構文を追加することでこの解消を図る物です。

constexpr int f() {
  if consteval {
    return 20;  // コンパイル時の処理
  } else {
    return 0;   // 実行時の処理
  }
}

if constevalは次の事を除くと、殆どif (std::is_constant_evaluated())シンタックスシュガーです。

  • <type_traits>のインクルードが必要ない
  • 構文が異なるため、誤用や誤解のしようがない
    • コンパイル時に評価されているかをチェックする適切な方法についての混乱を完全に解消できる
  • if constevalを使用してconsteval関数を呼び出すことができる
consteval int f(int i) { return i; }

constexpr int g(int i) {
    if consteval {
        return f(i) + 1; // ok!
    } else {
        return 42;
    }
}

consteval int h(int i) {
    return f(i) + 1;  // ok
}

if constevalコンパイル時評価ブロック内では、consteval関数の呼び出しが特別扱いされて、定数式ではない引数を受けていても呼び出すことができるようになります。

このように、if constevalの導入によってC++20で導入されてしまった2つの非自明な点を解消できます。

この提案はEWGでの議論をほぼ終えていて、CWGへ転送するための投票待ちをしています。CWGでの議論次第ではありますがC++23に入る可能性は高そうです。

P2029R4 Proposed resolution for core issues 411, 1656, and 2333; escapes in character and string literals)

文字(列)リテラル中での数値エスケープ文字('\xc0')やユニバーサル文字名("\u000A")の扱いに関するC++字句規則の規定を明確にする提案。

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンの変更点は、提案している文言を調整したことです。

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

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

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンの変更点は、atomicブロックでのthrow式が未定義動作であると変更されたことです(以前は実装定義)。

P2093R2 Formatted output

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

前回の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、std::printlnFILE*std::ostream&を取るオーバーロードを追加した事とstd::ostream&を取るオーバーロードについて<ostream>へ移動したことで<format><ostream>に依存しなくなった事、vprint_*関数の有用性を明確にしたこと及び提案する文言の調整です。

std::printが可変長テンプレートで任意個数の引数を受け取り出力を行うのに対して、std::vprint_unicode()/std::vprint_nonunicode()は型消去された引数参照の配列であるformat_argsオブジェクトを引数に取る非テンプレートの関数です。
std::print等他のものは内部でこれらに委譲して実装することで、余分なテンプレートのインスタンス化を減らしてバイナリサイズを削減することができます。

P2148R0 Library Evolution Design Guidelines

C++に新しいライブラリ機能を提案する際の設計のガイドラインの提案。

型やコンセプトなどの命名、関数オーバーロードの追加方法、クラスにおける特定のメンバ関数や変換、例外についてが簡単にまとめられています。自分でライブラリを書く際にも参考にできそうな内容です。

P2171R1 Rebasing the Networking TS on C++20 (revision 1)

P2171R2 Rebasing the Networking TS on C++20 (revision 2)

現在のNetworking TS(N4771)のベースとなっている規格はC++14なので、C++20ベースに更新する提案。

以前の記事(参照するほどの事は書いてない) onihusube.hatenablog.com

P2187R5 std::swap_if, std::predictable

より効率的な条件付きswapを行うためのstd::swap_ifと、その使用を制御するstd::predictableの提案。

前回の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

onihusube.hatenablog.com

前回からの変更は、標準への影響を説明するセクションと報告された既知の問題点についてのセクションが追加されたことと、機能テストマクロが追加されたことです。

P2192R3 std::valstat - Returns Handling

関数の戻り値としてエラー報告を行うための包括的な仕組みであるvalstatの提案。

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、サンプルコードを明確にしたことです。

P2198R1 Freestanding Feature-Test Macros and Implementation-Defined Extensions

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

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、どのワーキングドラフトをベースとするか明示されたこと、P1642が採択されることに依存している部分があることを明示したこと、P2013R3に関する機能テストマクロを追加した事です。

P2214R0 A Plan for C++23 Ranges

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

コロナウィルスの流行によって対面のミーティングが行えなくなったため、委員会のメンバーにrangeライブラリ周りでC++23に向けて何をすべきかを共有するために書かれた文書のようです。

この文書では、機能に三段階の優先度を設けた上で、機能をView adjunctsViewsAlgorithmActionsに分けてそれぞれについて解説しています。

かなり膨大な数の新規機能がリストアップされていますが、最優先のものだけを列挙してみます。

  • ranges::to
  • std::formatによるviewのフォーマット
  • range adopter
    • views::cache_latest
    • views::cartesian_product
    • views::chunk
    • views::group_by
    • views::iter-zip-transform<V> (exposition-only)
    • views::iter-adjacent-transform<V> (exposition-only)
    • views::index-view<S, D> (exposition-only)
    • views::join_with
    • views::slide
    • views::stride
    • views::transform_maybe
    • views::enumerate
    • views::flat_map (renamed to… something)
    • views::zip
    • views::zip_transform
    • views::adjacent
    • views::adjacent_transform
  • range algorithm
    • ranges::iota
    • ranges::fold

使い道や必要性はわかりますが、これだけでもかなり巨大です。この優先度最高のものはC++23を目指して議論されるようです。

P2223R1 Trimming whitespaces before line splicing

バックスラッシュ+改行による行継続構文において、バックスラッシュと改行との間にホワイトスペースの存在を認める提案。

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、この変更が生文字列リテラルに影響を与えないことを明記した事と、CWG Issue 1698の修正をここではしない事にしたことです。

P2226R0 A function template to move from an object and reset it to its default constructed state

std::exchangeによるmoveしてリセットするイディオムを行う新しいCPO、taketake_assignの提案。

std::exchangeによるmoveしてリセットするイディオムとは次のようなものです。

// old_objの値をムーブしてnewobjを構築し、old_objをデフォルト状態にリセット
T new_obj = std::exchange(old_obj, {});

// old_objの値をnewobjにムーブ代入して、old_objをデフォルト状態にリセット
new_obj = std::exchange(old_obj, {});

このイディオムの利点の1つは複数の操作をひとまとめにする事でエラーが起きにくくすることにあります。std::unique_ptrの様にポインタを所有するようなクラスのムーブコンストラクタとreset()で次のようにコードを改善できます。

struct MyPtr {
  Data *d;

  // BAD, ポインタのコピーとnullptr代入が分割されているため、nullptr代入が忘れられうる
  MyPtr(MyPtr&& other) : d(other.d) { other.d = nullptr; }

  // BETTER, use std::exchange
  MyPtr(MyPtr&& other) : d(std::exchange(other.d, nullptr)) {}

  // GOOD, std::exchangeによる一般化されたイディオム(知っていれば意図が明確)
  MyPtr(MyPtr&& other) : d(std::exchange(other.d, {})) {}


  void reset(Data *newData = nullptr)
  {
    // BAD, 読みづらい
    swap(d, newData);
    if (newData) {
      dispose(newData);
    }

    // BETTER, 読みやすい
    Data *old = d;
    d = newData;
    if (old) {
      dispose(old);
    }

    // GOOD, 合理的
    if (Data *old = std::exchange(d, newData)) {
      dispose(old);
    }
  }
};

もう一つの利点は、move後の抜け殻となっているオブジェクトの状態を確定できる事です。

f(std::move(obj));          // objの状態は良く分からない・・・

f(std::exchange(obj, {}));  // objはデフォルト構築状態にリセットされる

例えば標準ライブラリのものであれば、moveした後の状態は「有効だが未規定な状態」と規定されています。とはいえ結局どういう状態なのか分からず、より一般のライブラリ型などではドキュメント化されていることの方が稀です。
このイディオムを用いることによって、moveとその後のオブジェクトの状態の確定を1行で簡潔に書くことができます。

このイディオムは名前がついていたわけではありませんが、既存の大規模なC++コードベース(Boost, Qt, firfox, Chromium等)で広く使われており、かつ有用性も明らかです。これらのパターンに名前を付けてイディオムとして広めることはC++コミュニティ全体にとって有益であり、その名前が明確かつ簡潔であれば、std::exchangeによる物よりもイディオムの意図が明快になります(std::exchange(old, {})というのは一見すると分かり辛いです)。そのような理由からtake/take_assignCPOとして提案に至ったようです。

現在 この提案
class C {
  Data *data;
public:
  // idiomatic, C++14
  C(C&& other) noexcept
    : data(std::exchange(other.data, {}))
  {}
};
void Engine::maybeRunOnce() {
  if (std::exchange(m_shouldRun, false)) {
    run();
  }
}
template <
  typename K, typename V,
  template <class...> class C = std::vector
>
class flat_map {
  C<K> m_keys;
  C<V> m_values;

public:

  flat_map(flat_map&& other) noexcept(/**/)
    : m_keys(
        std::exchange(other.m_keys, {})),
      m_values(
        std::exchange(other.m_values, {}))
  {}

  flat_map &operator=(flat_map&& other)
    noexcept(/**/)
  {
    m_keys
      = std::exchange(other.m_keys, {});
    m_values
      = std::exchange(other.m_values, {});
    return *this;
  }
};
class C {
  Data *data;
public:
  // idiomatic, C++2?
  C(C&& other) noexcept
    : data(std::take(other.data))
  {}
};
void Engine::maybeRunOnce() {
  if (std::take(m_shouldRun, false)) {
    run();
  }
}
template <
  typename K, typename V,
  template <class...> class C = std::vector
>
class flat_map {
  C<K> m_keys;
  C<V> m_values;

public:

  flat_map(flat_map&& other) noexcept(/**/)
    : m_keys(std::take(other.m_keys)),
      m_values(std::take(other.m_values))
  {}

  flat_map &operator=(flat_map&& other)
    noexcept(/**/)
  {
    std::take_assign(m_keys, other.m_keys);
    std::take_assign(m_values, other.m_values);
    return *this;
  }
};

P2227R0 Update normative reference to POSIX

現在のC++標準規格が参照しているPOSIX規格への参照を更新する提案。

現在のC++規格は「ISO/IEC 9945:2003 (POSIX.1-2001 または、The Single UNIX Specification, version 3)」を主に標準ライブラリの定義中で現れるPOSIX関数のために参照しています。
ただ、これは古い規格であり、現在のC++標準ではそこに載っていない関数を参照していることがあるようです。

そのため、POSIX規格の参照を最新の「ISO/IEC/IEEE 9945:2009 (POSIX.1-2008 aka SUSv4)」に更新しようとするものです。

P2228R0 Slide Deck for P1949 EWG Presentation 20200924

P1948R6 C++ Identifier Syntax using Unicode Standard Annex 31のプレゼンの際に使われたスライド。

EWGで行われたP1948の内容を解説するプレゼンの際に使用された資料のようです。

P2231R0 Add further constexpr support for optional/variant

std::optionalstd::variantをさらにconstexpr対応させる提案。

C++20では共用体のアクティブメンバの切り替えplacement new(std::construct_atが定数式で可能となりました。std::optionalstd::variantはこれらを実装に利用しているため、いくつかの関数をさらにconstexpr対応させることができるようになっています。
この提案はそれに従ってconstexprを付加するだけで対応可能なものにconstexprを追加するものです。

どちらに対しても、コピー/ムーブコンストラクタや代入演算子emplace(), swap()constexpr対応が提案されています。

P2233R0 2020 Fall Library Evolution Polls

LEWGが2020年秋に投票を行うことが予定されている提案についてのリスト。

Executor提案の調整や、いくつかの提案をLWGに転送することを決める投票がメインです。C++23に何かを導入するものではありません。

P2234R0 Consider a UB and IF-NDR Audit

C++標準のUB(undefined behavior)とIF-NDRill-formed no diagnostic required)について、委員会の小さなチームによって監査されるプロセスの提案。

UBとIF-NDRC++の多くの所に潜んでおり、特に文書化されておらず、出会ってしまうとプログラムのデバッグをより困難にしてしまいます。このことは、C++に深く精通していないプログラマC++プログラムについて推論することを妨げています。

この提案の目的は、多くのUBとIF-NDRの全てについて専門家の小さなグループによって監査し、より良い振る舞いを規定できるものを特定し、その変更の方法や影響範囲を見積もることを継続的に行っていくことです。

この提案ではUBを改善可能なものとして、nullptrや使用できないポインタに対するサイズ0のmemcpyの動作や副作用のない無限ループを挙げています。

P2235R0 Disentangling schedulers and executors

現在のExecutor提案(P0443R14)について、schedulerexecutorの設計を簡素化し、schedulerexecutorの絡み合いをほどく提案。

P0443のexecutorコンセプトによって定義されるexecutorは引数も戻り値もないCallableオブジェクトを受け取って即座に実行する能力しかありません。schedulerコンセプトによって定義されるschedulerはそれに加えて実行の遅延と、sender/receiverと組み合わせた結果の受け取りやエラーハンドリング、そして処理のチェーンをサポートします。
schedulersender/receiverと共に、C++ Executorライブラリ上での多彩なジェネリックアルゴリズムの実装をサポートします。

schedulerexecutorの持つ能力を包含していますが、executorはそうではありません。schedulerからexecutorへの変換は縮小変換の様なもので、ソースコード上の見えないところで変換が起きた場合静かなバグの源となり得ます。にもかかわらず、現在のP0443は相互の暗黙変換をサポートしています。

一方、C++ Executorライブラリが非同期並行処理のための基盤となるものであることを考えると、どこかから渡されてきたschedulerexecutorとして扱うことも避けるべきです。これは不可逆変換ではありませんが、広い契約を持つ関数が中でより狭い契約を持つ関数に丸投げしているようなもので、広い契約を期待する呼び出し元の期待は満たされません。
schedulerexecutorとして扱ってexecuteCPOに投入してしまうと、まず処理のスケジューリングの機会がありません。そして、スケジューリングエラー(この場合、executeCPOが処理を受け取り実行環境に投入してから実際に実行されるまでの間のエラー)をハンドルする機会もありません。ユーザーがschedulerをカスタマイズしてスケジューリングエラーをハンドルする仕組みを備えていたとしても、schedulerexecutorとして扱ってしまえばそれが活かされる機会はありません。

このように、この2つのものは混ざり合いません。一方を他方として扱うとすれば、それは目に見える形で明確に細心の注意を払って行われるべきです。

この提案では次の変更によってこの絡み合いを解消し、問題の解決を図ります。

  • scheduleCPOはschedulerのみを受け付ける
  • executeCPOはexecutorのみを受け付ける
  • connectCPOなどのsenderreceiverに対する操作はsenderreceiverのみを受け付ける
  • executorからexecutorへの一方向の明示的な変換を追加する
    • 双方向の暗黙変換を削除する
  • schedulesenderの単純なfire-and-forget実行(executeの行うような実行)を可能にする個別のアルゴリズムは、execute以外の名前を使用するようにする

これは既に次のLEWGの投票にかけられることが決まっていて、そこでコンセンサスを得られればすぐにP0443に適用されることになります。

P2236R0 C++ Standard Library Issues to be moved in Virtual Plenary, Nov. 2020

標準ライブラリのIsuueのうち2020年11月のオンライン投票にかけられるもののリスト。

そらくここにあるものは投票でコンセンサスが得られればLWG Isuueとして規格に反映されることになります。

P2237R0 Metaprogramming

C++23以降に予定されている、あるいは現在提案中のメタプログラミングサポートに関連する機能に関するサーベイ論文。

リフレクション、メタクラスexpansion statementstemplate引数、コンパイル時I/Oなどコンパイル時にあれこれするための機能についてどう使うかや何に役立つかなど多岐にわたって述べられています。
なお、ここに上がっているものはまだ提案中のものばかりです。

11月半ばごろ?

この記事のMarkdownソース