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

文書の一覧

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

が、SG22(C/C++相互互換性に関する研究グループ)経由でCの提案が含まれているので、そこからC++に対してのものでないものを除くと48本になります。

N4880 PL22.16/WG21 agenda: 22 February 2021, Virtual Meeting

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

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

N4881 WG21 virtual meetings: 2021-02, -06, and -10

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

↑の2月のものを除くと、6月と10月に予定されています。どちらもオンラインで行われることが決定しています。

N4882 WG21 admin telecon meetings: 2021-02, -05, and -09

全体会議に共通する大まかな進行表。

N4883 WG21 February 2021 admin telecon minutes

2021年2月8日に行われた、WG21各SG等の責任者によるミーティングの議事録。

P0401R6 Providing size feedback in the Allocator interface

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

以前の記事を参照

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

この提案はすでにLWGでのレビューが完了しており、そこではこの提案はC++23に導入する事に全会一致でのコンセンサスが取れています。そのため、次の全体会議(2021年6月)で全体投票にかけられ、問題がなければC++23に入る見込みです。

P0448R3 A strstream replacement using span as buffer

長い間非推奨のまま代替手段のなかったstd::strstreamの代替となるstd::spanによるspanstreamを追加する提案。

std::strstreamは事前に確保された固定長のバッファを受け取りそれを利用したストリームを構築できるものでしたが、同時に可変長の内部バッファを扱う機能も持っており(コンストラクタでスイッチする)、その結果.str()から返されるchar*の指すメモリの管理責任が不明瞭になっていました。また、可変長バッファを使用する場合は.freez(false)をしてからデストラクタを呼び出す必要があるのですがわかりづらく、忘れられることが多かったようです。

このように、使いづらくメモリリークの危険性を備えていることからC++98でstd::strstreamは非推奨とされました。ただし、固定長バッファからストリームを構築し、そのバッファを文字列として参照する、という機能は有用で完全に代替するものが無かったことから削除されずに今日まで残っています。

文字列ベースのストリームという機能はstd::stringstreamが代替として利用できますが、固定長バッファによるストリームを代替する機能はありませんでした。

この提案はstd::strstreamの機能の一つだった、事前に確保された固定サイズのバッファを用いたストリームをstd::spanを利用して実現するものです。

ヘッダ<spanstrem>に以下のものが追加されます。

  • std::basic_spanbuf
    • std::spanbuf
    • std::wspanbuf
  • std::basic_ispanstream
    • std::ispanstream
    • std::wispanstream
  • std::basic_ospanstream
    • std::ospanstream
    • std::wospanstream
  • std::basic_spanstream
    • std::spanstream
    • std::wspanstream
// 入力ストリームのサンプル

char input[] = "10 20 30";

std::ispanstream is{std::span<char>{input}};

int i;
is >> i;
ASSERT_EQUAL(10,i);

is >> i;
ASSERT_EQUAL(20,i);

is >> i;
ASSERT_EQUAL(30,i);

is >>i;
ASSERT(!is);
// 出力ストリームのサンプル

char output[30]{}; // zero-initialize array

ospanstream os{span<char>{output}};
os << 10 << 20 << 30;

auto const sp = os.span();

ASSERT_EQUAL(6, sp.size());
ASSERT_EQUAL("102030", std::string(sp.data(),sp.size()));
ASSERT_EQUAL(static_cast<void*>(output), sp.data()); // ポインタの比較
ASSERT_EQUAL("102030", output);

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

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

以前の記事を参照

このリビジョンでの変更は、decltype(auto(...))decay_tとの構文比較表の追加とLWG Issue 3491に関する文言を別のところで議論することにしたこと、および関連しそうな機能(P2255R0 A type trait to detect reference binding to temporaryP0847R6 Deducing this)との相互作用についての追記です。

この提案は、ライブラリパートについてLEWGからLWGへ転送され、そこでのレビューが完了次第CWGに送られ最後のレビューを迎えます。

P1018R8 C++ Language Evolution status 🦠 pandemic edition 🦠 2020/11–2021/01

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

前回と比べると、多くのIssueに関して議論をしていたようです。

P1072R7 basic_string::resize_and_overwrite

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

以前の記事を参照

このリビジョンでの変更は、記述の修正がメインの様です。

P1322R3 Networking TS enhancement to enable custom I/O executors

Networking TSのI/Oオブジェクトをio_contextだけではなく、任意のExecutorによって構築できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、AcceptableProtocol要件に入れ子::socket_for<Executor>を追加し、ip::tcp,ip::udpクラスがそれを備えるようにしたことなどの、提案する文言の調整です。

P1425R3 Iterators pair constructors for stack and queue

std::stackstd::queueに、イテレータペアを受け取るコンストラクタを追加する提案。

以前の記事を参照

このリビジョンでの変更は、アロケータ型も含めてテンプレートパラメータを推論するための推論補助を追加したことです。

この提案はLEWGからLWGへ送られ、LWGでのレビューの結果、まだ未公開のR4がC++23入りのコンセンサスを得たようです。おそらく次の全体会議(2021年6月)でC++23に採択されます。

P1659R2 starts_with and ends_with

P1659R3 starts_with and ends_with

任意の範囲に対して動作するstd::ranges::starts_with/std::ranges::ends_withの提案。

以前の記事を参照

R2およびR3での変更は、提案する文言の修正がメインです。

この提案はすでにLWGでのレビューを終えており、LWGでのC++23入りのコンセンサスを得ています。おそらく次の全体会議(2021年6月)でC++23に採択されます。

P1682R3 std::to_underlying

列挙型の値からその基底の整数型への変換を行うstd::to_underlyingの提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロの修正など提案する文言の調整のみです。

この提案は2021年2月22日(米国時間)に行われた全体会議でC++23入りが承認されています。次のワーキングドラフトからC++23に反映されます。

P1885R5 Naming Text Encodings to Demystify Them

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

以前の記事を参照

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

  • エンコーディング名としてchar*を返す動機の説明の追記
  • 文言の改善と、フリースタンディング関連の文言の削除
  • systemという言葉のenvironmentへの置き換え
  • 名前の照合に問題のある古いエンコーディングを除外

LEWGでの最初のレビューの結果、提案の方向性への支持とこの提案のために作業時間をかけることへのコンセンサスが得られています。とはいえ、まだLEWGでの議論は続きます。

P1951R1 Default Arguments for pair's Forwarding Constructor

std::pair<T1, T2>forwarding constructor(要素型に変換可能な値を転送して構築するコンストラクタ)のテンプレートパラメータにデフォルト引数としてT1, T2を指定する提案。

現在のstd::pairでは次のようなコードを書くと、見えない所にオーバーヘッドを埋め込むことになります。

// std::stringとstd::vector<std::string>の一時オブジェクトが作られ、コピーされる
std::pair<std::string, std::vector<std::string>> p("hello", {});

std::pair<T1, T2>について、上記コードのコンストラクタのオーバーロード解決では、次の2つのコンストラクタが考慮されることになります。

explicit(see below) constexpr pair(const T1& x, const T2& y);

template <class U, class V>
explicit(...) constexpr pair(U&& x, V&& y);

しかし、2つ目の候補は、第二引数が{}のみであることからVの推定ができないため候補から外されます。結果1つ目の候補が選ばれ、一時オブジェクトの作成とそこからのコピー構築が行われます。

この場合に2つ目のコンストラクタを選択しようとする場合、次のように書く必要があります。

std::pair<std::string, std::vector<std::string>> p("hello", std::vector<std::string>{});

しかしこの構文は冗長でユーザーフレンドリーではありません。

この提案は2つ目のコンストラクタ(forwarding constructor)のデフォルトテンプレートパラメータとしてT1, T2を入れておくことで、最初の例のように書いた場合でも常にforwarding constructorが選択されるようにするものです。

2つ目の形式のコンストラクタは次のように変更します。

template <class U = T1, class V = T2>
explicit(...) constexpr pair(U&& x, V&& y);

たったこれだけの事で、{}の初期化子の使用が意図通りになるようになります。std::pairの構築で{}を使用している既存のコードの振る舞いが変更され、一時オブジェクトのコピーからムーブまたは完全転送による構築に切り替わります。これは明らかに望まれる動作であり、それによって壊れるコードはないものと思われます。

この提案の内容とC++23導入を目指すことはLEWGにおいて合意が取れており、LWGに転送するためにLEWGでの投票を待っている所です。

P2047R1 An allocator-aware optional type

Allocator Awarestd::optionalである、std::pmr::optionalを追加する提案。

Allocator Awareなオブジェクトとは、次の要件を満たすものです。

  • 一度構築されると、そのアロケータは変更されない。
  • オブジェクトのコンストラクタはuses-allocator構築で指定される2つの方法のいずれかでアロケータを受け取る。
    • これによって、コンテナの要素となるときにコンテナのアロケータが伝播される。
  • Allocator Awareな型を要素としうるオブジェクトはそれ自体がAllocator Awareであり、受け取ったアロケータを要素に伝播させなければならない。

現在のstd::optionalAllocator Awareではなく、上記ルールのいずれも満たしません。有効値を破棄して無効状態となるとそこで使用されていたアロケータを忘れてしまい、uses-allocator構築のプロトコルに従ったアロケータを受け取るコンストラクタを持っていないため、コンテナで使用されたときに要素のオブジェクトに適切にアロケータが伝播しません。

結果、現在のstd::optionalはコンテナで使用するときにアロケータを適切に管理できず、例えばpmrコンテナの不変条件である全ての要素が同じアロケータを使用する、という条件を満たすことができません(これは未定義動作につながります)。

この提案のstd::pmr::optionalAllocator Awareな要素型を持つときに上記3つのルールに従うようにすることで、自身もAllocator Awareとなるものです。そのために、構築に使用されたアロケータを内部で保持しています。

std::pmr::optionalの基本設計は単純で、全てのコンストラクタでアロケータを受け取るようにした上で、受けたアロケータを使用して要素を構築するようにします。また、そのように渡されたアロケータを内部で保持し、有効値の再構築時に使用します。

ただし、対象とするアロケータはstd::pmr::polymorphic_allocatorのみです。これによってアロケータの型がstd::optionalの型に現れることを防止します(polymorphic_allocatorC++20での改修によってこのような型に依存しない運用が可能になっています)。

これは従来のstd::optionalを置き換えるものではなく、std::optionalの振る舞いを変更するものでもありません。また、std::pmr::optionalAllocator Awareでない型を保持する場合は従来のstd::optionalに切り替わります。すなわち、std::pmr::optionalAllocator Awareな型を要素とする場合にのみ、std::optionalと異なる振る舞いをします。

std::pmr::optionalジェネリックなコードで(特にコンテナの要素型として)使用して、Allocator Awareな型を要素とする場合に適切にアロケータが伝播されるようにするものです。そして、std::pmr::optionalstd::optionalの単なるエイリアスではなく別の型として実装されることになります。

なお、std::pmr::optionalAllocator Awareな型を要素とする場合でも、そのアロケータを使用して自身のためのストレージを確保するものではありません。要素のストレージは従来通り自身の内部にあります。

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

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

以前の記事を参照

このリビジョンの変更点は、atomicブロックでのthrow式のうち、ブロック内で処理される例外は未定義動作ではないとされたこと、ほぼ全ての標準ライブラリ関数はatomicブロック内で使用可能とされたことです。

P2093R4 Formatted output

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

前回の記事を参照

このリビジョンでの変更は、_isattyを使って処理の例示を行なっていた所をGetConsoleModeに置き換えた事です。

この提案はSG16での議論とレビューを終えて、LEWGに送られたようです。

P2128R3 Multidimensional subscript operator

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

前回の記事を参照

このリビジョンでの変更は、t[a][b]のような構文をt[a, b]と書き換えてoperator[]に委譲する機能についての議論を追加した事です。ただし、これはここでは提案されていません。

P2162R2 Inheriting from std::variant (resolving LWG3052)

std::variantを公開継承している型に対してもstd::visit()できるようにする提案。

以前の記事を参照 - P2162R0 Inheriting from std::variant (resolving LWG3052) - [C++]WG21月次提案文書を眺める(2020年5月) - P2162R1 Inheriting from std::variant (resolving LWG3052) - [C++]WG21月次提案文書を眺める(2020年8月)

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

この提案は2021年2月22日(米国時間)に行われた全体会議でC++23入りが承認されています。次のワーキングドラフトからC++23に反映されます。

P2164R4 views::enumerate

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

以前の記事を参照

このリビジョンでの変更は、views::enumrateの間接参照結果の型がindex/valueという名前のメンバを持つようにするために必要な事について追記された事です。

以前の提案では結果の型はシンプルな集成体で、views::enumrateイテレータvalue_typereferenceは同じ型とされていました。

struct result {
  count index;
  T value;
};

しかし、indirectly_readableコンセプトを満たすためにはvalue_typereferenceの間のcommon_referenceが必要であり、そのためにはそこそこ大きな追加の実装が必要となります。

このリビジョンではその実装の一部を示すとともに、std::pair/tupleを再現せずまた利用せず、かつ名前のついた(説明専用でない)新しい型を導入する方向性を提案しています。

また、以前の提案では上記result型のメンバはconstメンバでしたが、LEWGでのレビューで否定されたため、非constに修正されました。

P2195R2 Electronic Straw Polls

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

以前の記事を参照

このリビジョンでの変更はよく分かりません。

P2216R3 std::format improvements

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

以前の記事を参照

このリビジョンでの変更は、提案している文言の調整がメインです。

この提案はすでにC++20に逆適用されることがほぼ決まっています。

P2231R1 Missing constexpr in std::optional and std::variant

std::optionalstd::variantをさらにconstexpr対応させる提案。

以前の記事を参照

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

この提案はLEWGでのレビューが終わる前にLWGでのレビューが完了しており、C++23に導入するコンセンサスが得られています。LEWGのレビュー完了を待って、全体会議での投票にかけられる予定です。

P2234R1 Consider a UB and IF-NDR Audit

C++標準のUB(undefined behavior)とIF-NDRill-formed no diagnostic required)について、委員会の小さなチームによって監査されるプロセスの提案。

以前の記事を参照

このリビジョンでの変更は、R0が何を目的としているか混乱を招いたようで、文書を全体的に再構成した事です。

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

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

以前の記事を参照

このリビジョンでの変更は、この提案は何かを新しくconstexpr関数で実行可能にしようとするものではなく、そのことを明記した事です。

この提案はEWGでのレビューでは反対意見なくCWGへ転送されるコンセンサスが得られています。このリビジョンのEWGでの投票を待って、CWGに送られる予定です。

P2249R0 Mixed comparisons for smart pointers

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

スマートポインタ型はリソースの所有権を表現する方法として広く認識されており、対して生ポインタはリソースにアクセスするために用いる所有権を保有しないものとして認識されつつあります。

そして、どちらも任意のリソース(オブジェクト)のアドレスを表現するという意味論を共有しています。

その一方で、現在の標準ライブラリにあるスマートポインタ型の比較演算子は自身と同じ型(テンプレートパラメータを除いて)との間の比較演算子しか備えていません。そのため、生ポインタとスマートポインタの間で比較をするためには、一旦スマートポインタから生ポインタを取り出さなければなりません。

std::shared_ptr<object> sptr1, sptr2;
object* rawptr;

// 2つのポインタが同じオブジェクトを指しているかを調べる。
if (sptr1 == sptr2) { ... }        // OK
if (sptr1 == rawptr) { ... }       // Error
if (sptr1.get() == rawptr) { ... } // OK

この事は同値比較演算子だけではなく、大小比較を行う関係演算子でも同様です。

スマートポインタと生ポインタの比較を行うケースは一般的に発生しているため、スマートポインタに生ポインタとの比較演算子を追加する事でこのような不整合を修正しようとする提案です。

この提案では、std::unique_ptrstd::shared_ptrに対して保持するポインタと比較可能な任意の型との比較を行う==<=>を追加する事で、生ポインタとの比較を実装しています。

// ManagerはObjectを所有し、利用者にはそのポインタを貸し与える
// クライアントは特定のObjectについて作業をしてもらうために、借りたポインタをManagerに渡す事で作業を依頼する
class Manager {

  std::vector<std::unique_ptr<Object>> objects;

public:
  // Objectのポインタを取得
  Object* get_object(std::size_t index) const {
    return objects[index].get();
  }

  // 指定したObjectを削除する
  void drop_object(Object* input) {
    // 直接比較できないので述語オブジェクトを作成しなければならない
    auto isEqual = [input](const std::unique_ptr<Object>& o) {
        return o.get() == input;
    };
    erase_if(objects, input);

    // この提案の後では、次の1行で事足りる
    erase(objects, input);
  }

  // Objectのインデックスを得る
  ssize_t index_for_object(Object* input) const {
    // 先ほどと同じ理由
    // このような述語オブジェクトは様々なところで重複して頻出する可能性がある
    // 一元化すればいいのだが、多くの場合その場で書いてそのままにしがち・・・
    auto isEqual = [input](const std::unique_ptr<Object>& o) {
        return o.get() == input;
    };
    auto it = std::ranges::find_if(objects, isEqual);
    // etc.

    // この提案の後では、次の1行で事足りる
    auto it = std::ranges::find(objects, input);
  }
};

P2280R1 Using unknown references in constant expressions

定数式での参照のコピーを許可する提案。

以前の記事を参照

このリビジョンでの変更は、EWGのレビューと投票の結果を記載した事と、提案する文言を改善した事(特にthisポインタの定数式での利用を明示的に許可したこと)です。

P2281R1 Clarifying range adaptor objects

range adaptor objectがその引数を安全に束縛し、自身の値カテゴリに応じて内部状態を適切にコピー/ムーブする事を明確化する提案。

以前の記事を参照

このリビジョンでの変更は、LWGからのフィードバックを提案する文言に反映した事です。

この提案はライブラリのIssue解決の多ものものであるのでLWGからレビューが開始されています。そこではC++23に向けて導入するコンセンサスが得られており、次の全体会議(2021年6月)で採択のための投票にかけられる予定です。

P2286R1 Formatting Ranges

任意の範囲を手軽に出力できる機能を追加する提案。

以前の記事を参照

このリビジョンでの変更は、実装定義としている範囲の出力フォーマットのオプションを提示した事です。何人かの方が筆者の方にフォーマットを実装定義としていることは受け入れられないと伝えているようです。

P2287R1 Designated-initializers for base classes

基底クラスに対して指示付初期化できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、R0で提案していた基底クラスを指定する構文を拡張して、{}を取り除いた形の基底クラスのメンバを直接指定した初期化を許可するようにしたことです。

struct A {
  int a;
};

struct B : A {
  int b;
};

int main() {
  // R0で提案した構文
  B b1{:A = {.a = 1}, b = 2};
  B b2{:A{.a = 1}, b = 2};
  B b3{:A{1}, .b{2}};

  // R1で追加された形式
  B b4{.a = 1, .b = 2};
  B b5{.a{1}, .b{2}};
}

P2289R0 2021 Winter Library Evolution Polls

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

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

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

P2290R0 Delimited escape sequences

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

現在文字列中のエスケープシーケンスには、ユニバーサル文字名(\uxx... or \Uxx...)、8進エスケープシーケンス(\ooo)、16進エスケープシーケンス(\xnn...)の3種類があります。8進エスケープシーケンスは3文字制限がありますが、16進エスケープシーケンスには長さの制限はありません。そして、どちらもエスケープシーケンス中に受け付けられない文字が出てきたらそこでエスケープシーケンスを終了するようになっています。

これによって、次のような問題が発生します。

"\17";      // 8進エスケープシーケンス、"0x0f"と等価
"\18";      // 8進エスケープシーケンスと文字、"0x01 8"の2文字
"\xabc";    // 1文字
"\xab" "c"; // 2文字

つまりどれも、エスケープシーケンスの終端(あるいは区切り)が明確ではありません。一番最後の例の様な回避策はありますが分かりづらく、この問題をよく知らない人から見ると余計なことをしているようにしか見えません。

また、ユニバーサル文字名は16進数字4桁もしくは8桁のどちらかになりますが、ユニコードのコードポイントの範囲が[0, 0x10FFFF]に制限されているため、有効なコードポイントは5桁以下の16進数字列によって書くことができます。そして、5桁のユニコード文字を書く場合は\U0001F1F8のように冗長な0が必要になってしまいます。

この提案ではこれらの問題の解決のために、\u{}, \o{}, \x{}の形の新しいエスケープシーケンス構文を提案しています。それぞれ{}の中に任意の長さの数字列(8 or 16進)を書けること以外は既存の対応するエスケープシーケンスと同様の扱いとなります。そして、この形式のエスケープシーケンスは現在コンパイルエラーとなる(\oの形式は予約されている)ために既存のコードを壊すこともありません。

なお、エスケープシーケンスの置換は文字列リテラルの連結の前に行われるため、"\x{4" "2}"のような形での使用はコンパイルエラーとすることを提案しています。

この提案の後では、先程の問題のエスケープシーケンスを次のように書くことができるようになります。

"\o{18}";     // 8進エスケープシーケンスに8は現れてはいけないのでコンパイルエラー
"\x{ab}c";    // 2文字
"\u{1F1F8}";  // 5桁のユニバーサル文字名 

この提案はC++に対してのものですがCに対しても同じ問題がある事から、WG14(C標準化委員会)に周知のためにSG22にも提出されています。

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

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

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

現在のC++にはコンパイル時に使用可能な文字列と数値の相互変換のためのものはありません。そのため、コンパイル時に文字列変換を行うには在野のライブラリに頼るほかありませんでした。

標準ライブラリには様々な実行時文字列変換法があり、中でもstd::to_chars, std::from_charsロケールに依存せず、例外を投げず、動的メモリ確保も行わないなど、コンパイル時の文字列と数値の相互変換に最適な候補です。そのため、この2つの関数を整数変換に限ってconstexpr対応することを目指す提案です。

constexpr std::optional<int> to_int(std::string_view s) {
  int value;
  
  if (auto [p, err] = std::from_chars(s.begin(), s.end(), value); err == std::errc{}) {
    return value;
  } else {
    return std::nullopt;
  }
}

static_assert(to_int("42") == 42);
static_assert(to_int("foo") == std::nullopt);

C++20でstd::stringコンパイル時に構築できるようになったため、この提案と合わせるとstd::formatcosntexprにすることができるようになります。

筆者の方は、MSVC STLの実装をconstexpr対応させることで実装テストを行い、その結果大きな変更や問題がなかったことを確認しています。

浮動小数点数変換のconstexpr対応を提案していないのは、実装が難しいためにサポートが進んでおらず(MSVCとGCC11が実装完了)、提案するのは時期尚早という判断のようです。

P2295R0 Correct UTF-8 handling during phase 1 of translation

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

現在のC++コンパイラが入力ソースファイルのエンコードとしてサポートしている文字コード(ソースファイル文字集合)は実装定義であり、必ずしもUTF-8をサポートしていません。それによって、UTF=8ソースファイルで書かれたmain関数だけのシンプルなプログラムですら、移植性を保証できません。

C++コンパイラがどのような文字コードを入力としてサポートするにせよUTF-8はサポートすることを義務付けることで、ソースコード移植性の問題を改善し、またユニコード関連の機能やその処理方法を(規格中でも)適切に指定し、幅広く使用できるようにしようとする提案です。

現在の主要な実装は全て、細部の差異はあれどUTF-8をサポートしているため、この提案の実装に必要なのはそれをデフォルトにすることだけです。この提案は、ユーザーのUTF-8ソースコードが適切にでコードされていることを保証する一方で、実装者はより寛大なオプションを提供できるようにするものです。例えば、UTF-8ソースファイルであることをどう決定するか(BOMの考慮)や、UTF-8として無効なコードポイントのシーケンスをどう扱うかなどは実装定義となります。

P2297R0 Wording improvements for encodings and character sets

規格の文章の~ encoding~ character setという言葉の使い方を改善し、何が何を指しているのかを一貫させる提案。

これによって、ユニバーサル文字名の扱いに関する3件のIssueが解決されます。

P2301R0 Add a pmr alias for std::stacktrace

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

std::basic_stacktraceはテンプレートパラメータにアロケータ型を取り、std::allocatorがあてがわれたエイリアスstd::stacktraceとして定義されています。そして、std::stacktraceAllocatorAwareContainerの要件を満たしており、std::polymorphic_allocatorを用いることに障害はありません。

他のコンテナなど、アロケータモデルをサポートしデフォルトでstd::allocatorを使用するものについてstd::pmrエイリアスが用意されているように、std::basic_stacktraceにもstd::pmr::stacktraceを用意すべき、という提案です。

P2302R0 Prefer std::ranges::contains over std::basic_string_view::contains

新しいアルゴリズムとしてstd::ranges::containsを追加して、std::basic_string_view/std::basic_string.contains()メンバ関数を削除する提案。

C++23に向けたP1679R3の採択によって、std::string/string_viewにはある文字列が含まれているかを判定するための.contains()メンバ関数を備えています。

筆者の方は、このような操作の必要性は同意するがこの操作はより一般の範囲(range)に適用可能なアルゴリズムであり、メンバ関数ではなく<algorithm>に配置される非メンバアルゴリズムとして追加すべき、と述べています。

それによって、ある範囲に別の範囲が含まれているかどうか、あるいは述語を満たす部分が含まれているかどうかを調べる形に一般化でき、また他のrangeアルゴリズムに従った形で幅広い型について利用できるようになります.

std::ranges::containsの使用イメージ。

// 文字の範囲と文字を受ける
if (std::ranges::contains(haystack, 'o')) {
  // meow
}

// 文字範囲のイテレータペアと文字を受ける
if (std::ranges::contains(haystack.begin(), haystack.end(), 'c')) {
  // purr
}

// 探索する範囲のイテレータペアと探索したい範囲のイテレータペアを受ける
if (std::ranges::contains(haystack.begin(), haystack.end(), long_needle.begin(), long_needle.end())) {
  // hiss
}

// 探索する範囲と探索したい範囲を受ける
if (std::ranges::contains(haystack, long_needle)) {
  // hiss again
}

// 探索する範囲と述語を受ける
if (std::ranges::contains(haystack, long_needle, bind_back(std::modulo(), 4))) {
  // double purr
}

P2314R0 Character sets and encodings

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

そしてこれらの用語を用いて文言を書き直すことで、翻訳フェーズ1でユニバーサル文字名が形成されなくなり、全てのユニコード文字入力はコンパイル全体で保持されるようになります。それにより、プリプロセッサにおける文字列化演算子#)の動作が変更されます。

C++20 この提案
#define S(x) # x
const char * s1 = S(Kテカppe);      // "K\\u00f6ppe"
const char * s2 = S(K\u00f6ppe); // "K\\u00f6ppe"
#define S(x) # x
const char * s1 = S(Kテカppe);     // "Kテカppe"
const char * s2 = S(K\u00f6ppe); // "Kテカppe"

ただし、既存の実装は全てこれを実装している(エスケープされたユニバーサル文字名を返す実装はない)ために問題にはならないようです。

また、これらの変更によって現在使用されているbasic / extended source character setという言葉は使われなくなります。

P2315R0 C++ Standard Library Issues to be moved in Virtual Plenary, Feb. 2021

標準ライブラリのIsuueのうち2021年02月のオンライン全体会議の際に投票にかけられるもののリスト。

ここにあるものは投票でコンセンサスが得られればLWG Isuueとして規格に反映されることになります。

これを書いている時点で投票は完了しており、その結果ここに記載されているIssueは全て標準に適用されることになりました。

P2316R0 Consistent character literal encoding

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

#if 'A' == '\x41'
//...
#endif

if ('A' == 0x41){}

現在の仕様では、この2つの条件式は同じ結果となる保証がありません。

#ifの条件式において文字リテラルは対応する数値に変換され処理されますが、文字リテラルをどのように解釈するか(どのエンコーディングで読み取るか)は実装定義であり、C++の式上でのそれと一致するかどうかも実装定義とされます。

筆者の方がvcpkgを用いてコードベースを調査したところ、このような#ifでの文字リテラルの比較はその環境でのナロー文字エンコーディングを取得するために使用されているようです。

sqliteより

#if 'A' == '\301'
# define SQLITE_EBCDIC 1
#else
# define SQLITE_ASCII 1
#endif

主要なコンパイラは全て期待通りに動作し、#ifでの文字リテラルをナロー文字エンコーディングにおいて対応する数値として扱うようです。

C++で文字エンコーディングを検出するより良い方法は提案中ですが現状では手段がなく、この振る舞いを標準化して上記の様なコードがユーザーの期待通りに振舞うことを保証しようとする提案です。

P2317R0 C++ - An Invisible foundation of everything

C++とは何か?(原則と理想や使用についてなど)というよく聞かれる質問に対する返答をまとめた文書。

Bjarne Stroustrup先生がC++とは何か?とかC++はまだどこでも使われてるの?などよく聞かれるらしく、その簡単な答えとその詳細を知ることのできる文書へのリンクをまとめた文書です。WG21メンバーに向けて書かれたものではないですが、WG21メンバにも役立つ可能性があるためWG21に提出された様です。

目次

  1. 目的と手段
    • C++の設計の高レベルでの目的とシステムにおけるその役割
  2. 使用
    • 基礎的な使用法に焦点を当てた、C++のいくつかの用例
  3. 進化
    • フィードバックに基づくC++開発のための進化戦略
  4. 保証、言語、ガイドライン
    • 進化、安定性、表現力、型・リソース安全を同時に達成するための戦略
    • ソフトウェア開発における人の役割の再認識
  5. 参考文献とリソース
    • C++のより深い理解につながる可能性のある参考文献への注釈付きリスト
  6. 付録
    • C++の主要なプロパティと機能の非常に簡単な概説

C++ヲタク必見です!

P2320R0 The Syntax of Static Reflection

静的リフレクションのための新しい構文の提案。

表現力・読みやすさ・柔軟さ・曖昧さがない・実装可能、の観点から提案中のものも含めた現在のリフレクションの構文を見直したものの提案です。

リフレクション

現在のリフレクションTSでは、名前からメタ情報を取得するのにreflexpr()という構文を使用しています。この提案では代わりに^を使用します。

// この提案
meta::info r1 = ˆint;   // reflects the type-id int
meta::info r2 = ˆx;     // reflects the id-expression x
meta::info r3 = ˆf(x);  // reflects the call f(x)

// N4856現在
meta::info r1 = reflexpr(int);   // reflects the type-id int
meta::info r2 = reflexpr(x);     // reflects the id-expression x
meta::info r3 = reflexpr(f(x));  // reflects the call f(x)

スプライシング

リフレクションによって取得したメタ情報から型名を取り出して利用することです。この提案では[: R :]の様な構文によって行います。

struct S { struct Inner { }; };
template<int N> struct X;
auto refl = ˆS;
auto tmpl = ˆX;

void f() {
  typename [:refl:] * x;  // OK: xはS*
  [:refl:] * x;           // error: typename必須
  [:refl:]::Inner i;      // OK
  typename [:refl:]{};    // OK: Sの一時オブジェクトをデフォルト構築
  using T = [:refl:];     // OK
  struct C : [:refl:] {}; // OK
  template [:tmpl:]<0>;   // OK
  [:tmpl:] < 0 > x;       // error: xと0の比較になる

  // N4856現在
  namespace reflect = std::experimental::reflect;
  using refl_t = reflexpr(S);
  using tmpl_t = reflexpr(X);

  reflect::get_reflected_type_t<refl_t> * x;
  typename reflect::get_reflected_type_t<refl_t>::Inner i;
  reflect::get_reflected_type_t<refl_t>{};
  using T = reflect::get_reflected_type_t<refl_t>;
  struct C : reflect::get_reflected_type_t<refl_t> {};
  // 残りの2例は対応するものがない(おそらく)
}

パックのスプライシング

リフレクションによって取得したメタ情報から型名のリストを取り出して利用することです。この提案では...[: R :]...の様な構文によって行います。

// 型エイリアス
using T = std::tuple<int, ...[:range_of_types:]..., bool>;
// 関数宣言
void f(... [:range_of_types:] ...args);

// 関数呼び出し
fn(0, 1, ...[:range:]...);  // OK: 通常引数(0, 1)の後に展開
fn(...[:range:]..., 0, 1);  // OK: 通常引数(0, 1)の前に展開
fn(...[:range:] * 2...);    // OK: rangeの要素に2をかけながら展開
fn(...[:r1:] * [:r2:]...);  // OK: ただし、r1とr2の長さは同じでなければならない

P2321R0 zip

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

これら4つのziplikeなviewは基本的には複数の範囲を一つの範囲に変換する様に振る舞うものです。そのため、元のそれぞれの範囲の要素型(value_type)をEnとすると、間接参照の結果型(reference)はstd::pair<E1&, E2&>std::tuple<E1&, ..., En&>の様になるでしょう。

このstd::pairstd::tupleイテレータの間接参照の結果として返されるプロクシオブジェクトとして使用されるのに必要な性質を備えておらず、その議論が間に合わなかったためにC++20ではこれらのviewは採択されませんでした。

この提案は、std::pairstd::tupleの各種の問題を解決した上で、zip_view, adjacent_view, zip_transform_view, adjacent_transform_viewを追加するものです。

std:tupleへの出力

std::indirectly_writableコンセプトは、const_castを用いた制約式によって、イテレータの間接参照がprvalueを返すようなイテレータを弾く一方で、それが単にプロクシ参照である場合は許容する、という事をしています。これはstd::output_iteratorコンセプトを構成するものの一つです。

ziplikeなviewイテレータはその間接参照の結果としてstd::pair/tupleprvalueを返します。output_iteratorとして有効であるためには、それをconst化したもの(例えば、const std::tuple<...>&&なオブジェクト)に対して代入できなければなりません。

当然ながら、現在のstd::pair/tupleはそうなっておらず、プロクシイテレータの間接参照の結果型としては不足しています。

この提案では、std::pair/tupleに代入演算子constオーバーロードを追加し、std::pair/tupleconstでもその要素に代入が可能であるならばできるように変更します。

同時に、std::vector<bool>::referenceにも同様の理由から同じように代入演算子constオーバーロードを追加します。

std:tupleの読み取り

std::indirectly_readableコンセプトは、イテレータ型のvalue_type&referenceの間にcommon_referenceが存在していることを求めています。これはstd::input_iteratorコンセプトを構成するものの一つです。

ziplikeなviewイテレータにおけるvalue_type&referenceの間のcommon_referenceとは何でしょうか?

std::vector<int> vi = /* ... */;
std::vector<std::string> vs = /* ... */;

ranges::sort(views::zip(vi, vs));

例えばこの場合のzip_viewイテレータvalue_typestd::pair<int, std::string>referencestd::pair<int&, std::string&>となります。

std::pair<int&, std::string&>std::pair<int, std::string>へは変換できるため、common_referencestd::pair<int, std::string>となるでしょうか。参照ではありませんが、common_referenceに求められることは果たします。

しかし、zip対象のイテレータの要素型がコピー不可能な型になった場合、value_typereferenceはどちらをどちらにも変換できなくなるため、common_referenceは機能しなくなります。それによって、input_iteratorのモデルとなれなくなります。

この事はstd::tupleにも同じことが言えます。

この問題の解決のために、std::pair<T1, T2>に対してstd::pair<U1, U2>&, const std::pair<U1, U2>&&から構築するコンストラクタ、およびstd::tuple<Ts...>に対してstd::tuple<Us...>&, const std::pair<Us...>&&から構築するコンストラクタを追加します。

この2つのコンストラクタを追加するだけで、std::pair<std::unique_ptr<int>, std::string>からstd::pair<std::unique_ptr<int>&, std::string&>が構築できるようになります。そして、この型は参照のセマンティクスを持っています(std::tupleでも同様)。

またcommon_referenceに適合するために、std::tuplestd::pairに対してstd::basic_common_referencestd::common_typeの特殊化を追加します

これは、common_referenceとしてよりふさわしい型となります。

zip, zip_tranformvalue_type

zip_transformは与えられた範囲をどのように一つの範囲にzipするかを指定することのできるviewです。zipはデフォルトでstd::tuple/pairを用いてまとめ、その振る舞いを変更できません。対して、zip_transformはその部分を任意の関数を指定することでカスタマイズできます。

ここで問題になるのが、zip_transformvalue_typeがどうなるかという点です。

std::vector<int> vi = /* ... */;
std::vector<std::string> vs = /* ... */;

auto b = views::zip_transform([](auto&... r){
    return std::tie(r...);
  }, vi, vs);

このzip_transformzipと同じことをします。breferencestd::tuple<int&, std::string&>となり、これはzipのふるまいと一貫しています。しかし、この場合のvalue_typestd::tuple<int, std::string>としてしまう事は適切でしょうか?

zipの行う事と異なり、この場合にtupleの各要素が参照となっているのはユーザーが指定した変換の結果であり、別の変換ではこれは参照とならないかもしれません。従って、zip_transformvalue_typeは単にstd::remove_cvref_t<std::invoke_result_t<F&, std::range_reference_t<R>...>>のような型とするのが適切です。上記の例ではstd::tuple<int&, std::string&>となります。ただ、そうしてしまうとzipとの振る舞いの一貫性がなくなってしまいます。

この提案ではこの方針を採用し、zipzip_transformreference/value_typeの間の一貫性を失う事を許容することにしたようです。

これらの様な設計を選択し、C++23への導入に向けてziplikeな4つのviewの作業が開始されます。

なお、adjacent_viewとは1つの範囲のある要素についてそれに続くN - 1要素をひとまとめにしたものを要素とする範囲を生成するviewで、入力の範囲に対してその範囲の1つ目の要素を飛ばした範囲を生成して、その二つの範囲をzipしたようなviewです。adjacent_transform_viewadjacentする部分をカスタムできるものです。

std::vector<int> vi = {1, 2, 3, 4};
std::vector<std::string> vs = { "one", "two", "three", "four" };

for (auto& [n, s] : vi | std::views::zip(vs)) {
  std::cout << n << " : " << s << std::endl;
}
// 1 : one
// 2 : two
// 3 : three
// 4 : four

for (auto [n, m] : vi | std::views::adjacent<2>) {
  std::cout << n << " : " << m << std::endl;
}
// 1 : 2
// 2 : 3
// 3 : 4

P2322R0 ranges::fold

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

このranges::foldというのは関数型言語foldlと呼ばれているものに対応し、現在のC++標準ライブラリには数値に特化したイテレータ版として<numeric>std::accumulateとして存在しています。

std::ranges::accumlateP1813R0で提案されており、検討中のstd::ranges::accumlateは以下のようになっています。

template <input_range R, movable T, class Proj = identity,
          indirect_magma<const T*, projected<iterator_t<R>, Proj>, T*> BOp = ranges::plus>
constexpr accumulate_result<safe_iterator_t<R>, T>
    accumulate(R&& r, T init, BOp bop = {}, Proj proj = {});

これを良しとしないのは、特定の2項演算をデフォルトにするべきではない、戻り値型が複雑(単に結果だけが欲しい)、制約がきつすぎる(indirect_magmaコンセプトは大きすぎる)、等の理由です。

この操作は数値に限定されたものではないためより一般的な名前を付けて、より汎用的であるために過度な制約を課さないようにしたものがranges::foldであり、もはや数値のためのものではないため<algorithm>に追加することを提案しています。

提案ではさらに、ranges::foldのファミリとして、foldrに対応するranges::fold_rightと、範囲の最初の要素を初項として使用するranges::foldであるranges::fold_first、最後の要素を初項として使用するranges::fold_rightであるranges::fold_right_lastを同時に提案しています。

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

int sum1 = std::ranges::fold(vec, 0, std::ranges::plus{});
int sum2 = std::ranges::fold_first(vec, std::ranges::plus{});
// sum1 == sum2 == 15

std::vector<std::string> vec2 = {"aaa", "bbb", "ccc"};
std::string concat1 = std::ranges::fold_first(vec2, std::ranges::plus{});
// concat1 == "aaabbbccc"

std::string concat2 = std::ranges::fold_right(vec2, std::string{}, std::ranges::plus{});
std::string concat3 = std::ranges::fold_right_last(vec2, std::ranges::plus{});
// concat2 == concat3 == "cccbbbaaa"

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

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

Cではこれが可能になっていますがC++では可能になっておらず、Cとの互換性向上のためにできるようにしようという提案です。この提案はSG22に提出されたものです。

複合ステートメントとはブロック({...})のことで、その末尾とは例えば関数の末尾の事です。

void foo(void)
{
first:  // C/C++共にok
  int x;

second: // C/C++共にok
  x = 1;

last:   // Cはok、C++はng
}

この提案はこの例のlastの様なラベルを置けるようにするものです。

P2325R0 Views should not be required to be default constructible

Viewとみなされる型にデフォルト構築可能性を要求しない様にする提案。

Viewを定義しているstd::ranges::viewコンセプトは、現在次の様に定義されています。

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

viewrangeであって少なくともムーブ可能である必要があり、viewは構文的な部分よりも意味論の部分でrangeと大きく異なるため、構文的にviewであるかどうかは明示的なオプトイン(enable_view)が必要です。

ただ、残ったdefault_initializableに関しては、C++20の<ranges>に至る議論からはviewがデフォルト構築可能である事のモチベーションは見つからず、range-v3のものを含めても本質的にデフォルトコンストラクタを必要とするviewはむしろ少数であり、default_initializableの要求はviewには不要のものに思えます。

また、デフォルト構築を要求されていることによって、関数を受け取るタイプのviewrange adaptor)は受け取った関数オブジェクトをstd::optionalsemiregular-box)に格納する必要があるため、実装を複雑化しviewのサイズを増加させています。

さらに、型がデフォルト構築可能であるという性質を持つことによって、そのような型をメンバとして持つクラスには、そのメンバが初期化されていないという追加の状態が発生します。それによって、ユーザーはそれをチェックする必要があり、またドキュメントなどにはその旨を記載する必要が出てきます。これは、現在のviewに当てはまっていることです。

デフォルト構築可能という要求がメリットをもたらさずむしろ害(型に無効状態を追加し、全てのrange adaptorにそのサイズ増加をもたらし、これが原因でviewコンセプトを満たせない場合のユーザの混乱など)をもたらすのであれば、それは取り除くべき、という提案です。

この提案では、viewコンセプトからdefault_initializableを取り除くと共に、weakly_incrementableコンセプトからも取り除くことで、input iterator/output iteratorへのデフォルト構築可能という要求を取り除くことを提案しています。

そして、一部のviewおよびイテレータ型からデフォルトコンストラクタを削除し、std::spanが常にviewとなるようにします。

多分2週間後くらい

この記事のMarkdownソース

[C++]indirectly_writableコンセプトの謎の制約式の謎

std::indirectly_writableコンセプトはイテレータによる出力操作を定義するコンセプトで、std::output_iteratorコンセプトの中核部分を成しています。

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);
  };

定義を見てみると、見慣れない構文を用いた良く分からない制約式が入ってるのが分かります。

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

常人ならばおおよそ使うことの無いであろうconst_castをあろうことかC++20のコンセプト定義で見ることになろうとは・・・

cpprefjpには

const_castを用いる制約式は、右辺値に対しても代入できるがconstな右辺値では代入できなくなる非プロキシイテレータprvalue(例えばstd::stringそのものなど)を返すイテレータを弾くためにある。これによって、間接参照がprvalueを返すようなイテレータ型はindirectly_writableのモデルとならないが、出力可能なプロキシオブジェクトを返すイテレータindirectly_writableのモデルとなる事ができる。

とあり、規格書にも似たようなことが書いてありますが、なんだかわかったような分からないような・・・

これは一体何を表しているのでしょうか、またどういう意図を持っているのでしょう?

prvalueを返すようなイテレータ

どうやらこれはrange-v3において発見された問題に端を発するようです。

struct C {
  explicit C(std::string a) : bar(a) {}

  std::string bar;
};

int main() {
  std::vector<C> cs = { C("z"), C("d"), C("b"), C("c") };

  ranges::sort(cs | ranges::view::transform([](const C& x) {return x.bar;}));

  for (const auto& c : cs) {
    std::cout << c.bar << std::endl;
  }
}

クラスCstd::vectorCの要素のstd::stringの順序によって並び変えたいコードです。コンパイルは通りますし実行もできますが、順番が並び変わることはありません。

なぜかといえば、sortに渡しているvectortransformしているラムダ式の戻り値型がstd::stringの参照ではなくprvalueを返しているからです。

割とよくありがちなバグで、戻り値型をきちんと指定してあげれば意図通りになります。

ranges::sort(cs | ranges::view::transform([](const C& x) -> std::string& {return x.bar;}));

しかし、ranges::sortはrange-v3にあるindirectly_writableコンセプトで制約されているはずで、この様なものは出力可能とは言えず、indirectly_writableを満たしてほしくは無いしコンパイルエラーになってほしいものです。

prvalueの区別

この問題は突き詰めると

std::string() = std::string();

の様な代入が可能となっているという点に行きつきます。

この様な代入操作は代入演算子の左辺値修飾で禁止できるのですが、標準ライブラリの多くの型の代入演算子は左辺値修飾された代入演算子を持っていません。メンバ関数の参照修飾はC++11からの仕様であり、C++11以前から存在する型に対して追加することは出来ず、それらの型に倣う形で他の型でも参照修飾されてはいません。

これを禁止する為の方法は、単純には間接参照の結果が常に真に参照を返すことを要求することです。

その時に問題となるのが、イテレータの間接参照でプロキシオブジェクトが得られるようなイテレータです。当然そのようなプロキシオブジェクトはprvlaueなので、出力可能であるはずでもindirectly_writableを満たさなくなってしまいます。

そうなると、プロキシオブジェクトを識別してそのprvalueへの出力は許可する必要があります。

プロキシオブジェクトはその内部に要素への参照を秘めているオブジェクトであって、自身のconst性と参照先のconst性は無関係です。従って、constであるときでも出力(代入)が可能となります。

一方、std::string等の型は当然constであるときに代入可能ではありません。

そして、イテレータoについて、decltype(*o)が真に参照を返すとき、そこにconstを追加しても効果はありません。

これらの事から、間接参照がprvalueを返すときにプロキシオブジェクト以外の出力操作を弾くためには、const_castdecltype(*o)に対して適用してconstを付加してから、出力操作をテストすれば良いでしょう。

この結果得られたのが、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);
  };

std::forward<Out>の差で制約式が2本あるのは、Outに対する出力操作がその値カテゴリによらない事を示すためです。つまり、lvalueは当然として、イテレータそのものがprvalueであっても出力操作は可能であり、そうでなければなりません。これは今回の事とはあまり関係ありません。

iter_reference_tOutからその間接参照の直接の結果型(reference)を取得します。

それが真に参照ならば(その型をT&あるいはT&&とすれば)、そこにconstを追加しても何も起こらず、型はT&あるいはT&&のままとなります。しかし、iter_reference_tprvalueならば(Tとすれば)素直に追加されてconst Tとなります。

ここで起きていることはusing U = T&に対するconst Uのようなことで、これはT& const(参照そのものに対するconst修飾)となって、これは参照型には意味を持たないのでスルーされています。

最後にそこに&&を付加するわけですが、参照が得られているときはT&&& -> T&T&&&& -> T&&となります。*oprvalueを返すときはconst T&&となり、const右辺値参照が生成されます。

最後にこの得られた型を用いて*oconst_castしそこに対する代入をテストするわけですが、この過程をよく見てみれば*oが参照を返している場合は実質的に何もしておらず、すぐ上にある制約式と等価となっています。

つまり、このconst_castを用いる制約式は*oprvalueを返しているときにしか意味を持ちません。そして、const T&&なオブジェクトへの出力(代入)ができるのはTがプロキシオブジェクト型の時だけとみなすことができます。

この様にして、冒頭のコード例の様に意図せずprvalueを返すケースをコンパイルエラーにしつつ、意図してプロキシオブジェクトのprvalueを返す場合は許可するという、絶妙に難しい識別を可能にしています。

そして、これこそが問題の制約式の存在意義です。

std::vector<bool>::reference

イテレータの間接参照がプロキシオブジェクトを返すようなイテレータには、std::vector<bool>イテレータがあります。そのreferenceは1ビットで保存されたbool値への参照となるプロキシオブジェクトのprvlaueであり、まさに先ほどの議論で保護すべき対象としてあげられていたものです。

が、実際にはstd::vector<bool>イテレータstd::indirectly_writableコンセプトを構文的にすら満たしません。まさにこのconst_castを用いる制約式に引っかかります。

int main() {
  // 失敗する・・・
  static_assert(std::indirectly_writable<std::vector<bool>::iterator, bool>);
}

エラーメッセージを見ると、まさにそこを満たしていないと指摘されているのが分かります。

なぜかというと、std::vector<bool>::referenceのプロキシオブジェクトには代入演算子はあってもconst修飾されていないためです。const化してしまうと代入できなくなってしまいます。自身のconst性と参照先のそれとは無関係のはずなのに・・・

C++23に向けてここを修正する動きはあるようですが、この様なプロキシオブジェクトを用いるイテレータを作成するときは、プロキシオブジェクトの代入演算子const修飾に思いを馳せる必要があります。

参考文献

謝辞

この記事の99割は以下の方々のご指摘によって成り立っています

この記事のMarkdownソース

[C++]ラムダキャプチャの記述順序

ラムダ式を雰囲気で書いているので、キャプチャの正しい順序が分かりません。そのため、コンパイラに怒られて直したり怒られなかったからヨシ!をしています。

正しい順序とは一体何なのでしょうか・・・?

正しいキャプチャ順序

C++20現在、キャプチャには3種類の方法があります。明確に書かれているわけではありませんがそこには確かに順序があり、次のようになります。

  1. デフォルトキャプチャ
    • &
    • =
  2. それ以外
    • 簡易キャプチャ
      • &x
      • x
      • this
      • *this
    • 初期化キャプチャ

はい、これだけです。悩む事も無いですね・・・

// 正しい順番
[&]{};
[=]{};
[=, x]{};
[=, &x]{};
[=, x = 0]{};
[=, this]{};
[=, *this]{};
[=, x, &y, z = 0, this]{};
[=, this, &x, y, z = 0]{};

// 間違った順番
[x, =]{};
[x, &]{};
[&x, &]{};
[&x, =]{};
[x = 0, =]{};
[this, &]{};
[*this, =]{};

= &によるデフォルトキャプチャが先頭にきてさえいれば、後はどういう順番でも構わないという事です。

詳細

ラムダ式の文法定義の中で、ラムダ導入子([])の中のキャプチャ(lambda-capture)は次のように構文定義されています。

lambda-capture:
  capture-default
  capture-list
  capture-default , capture-list

capture-default:
  &
  =

capture-list:
  capture
  capture-list , capture

capture:
  simple-capture
  init-capture

simple-capture:
  identifier ...(opt)
  & identifier ...(opt)
  this
  * this

init-capture:
  ...(opt) identifier initializer
  & ...(opt) identifier initializer

まず最初のlambda-captureを見てみると、capture-defaultcapture-listそれぞれ単体あるいはcapture-default , capture-listの形の列のいずれかとして定義されています。capture-defaultはその次で定義されており、= &のどちらかです。そして、capture-defaultはここ以外では出現しません。

従ってまず、capture-defaultcapture-listよりも前に来なければならない事が分かります。

ではcapture-listとは何なのかと見に行けば、captureあるいはcapture-list , captureのどちらかとして定義されています。この書き方はEBNFにおいて繰り返しを表現する定番の書き方であり、capture-listとは1つ以上のcaptureの列として定義されています。

captureはさらにsimple-captureinit-captureのどちらかとして定義され、ここには順序がありません。

simple-captureは4つのキャプチャが定義されており、上からコピーキャプチャ、参照キャプチャ、thisのコピーキャプチャ、*thisのコピーキャプチャ、が定義されています。ここにもその出現順を制約するものはありません。

init-captureはその名の通り初期化キャプチャを定義しており、コピーキャプチャと参照キャプチャの2種類が定義されています。そしてここにも順序付けはありません。

結局、lambda-captureの中で出現順が定義されているのはcapture-default , capture-listという形式だけであり、これがデフォルトキャプチャ(= &)が先頭に来て後は順不同という事を意味しています。

なお、...(opt)はパラメータパック展開のことで、これはC++20で許可されたものです。これも= &が先頭にきてさえいればどういう順番で現れても構いません。

参考文献

この記事のMarkdownソース

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

文書の一覧

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

SD-1 2021 PL22.16/WG21 document list

2016年〜2021年(1月)までの提案文書の一覧。

P0447R12 Introduction of std::colony to the standard library

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

以前の記事を参照

このリビジョンでの変更は、範囲やinitializer_listinsert()の戻り値型がvoidに変更されたこと、要素を指定した値で初期化しておくタイプのコンストラクタが非explicitに変更されたこと、reserve()の文言など標準のための文言の調整などです。

P0847R6 Deducing this

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

以前の記事を参照

このリビジョンでの変更は、検討した他の構文の記録と、リフレクション・explicit staticvirtual・コルーチンの議論を含むセクションを再度追加した事、及び*this引数を明示した関数が非静的メンバ関数になるように規格の文書の文言を調整した事です。

P1121R2 Hazard Pointers: Proposed Interface and Wording for Concurrency TS 2

標準ライブラリにハザードポインタを導入する提案。

ハザードポインタは並行処理におけるデータ共有のための仕組みで、ABA問題を回避するためのdeferred reclamationを実装する方法の一つです。

deferred reclamationに関しては以前の記事を参照。

複数のスレッドによって共有されるデータがあり(ヒープ上にあるとします)、あるスレッドがそれを読み取っており他のスレッドも同様に読み書きができる時、読んでいるスレッドは勝手にデータを変更されたり削除されたりされたくはありません。とはいえ、高コストなロックを使いたくもありません。

そこで、全スレッドが読み取る事の出来るところにポインタを用意しておいて、読み取り中のデータのアドレスをそこに入れておきます。そのポインタに登録されたデータは誰かが見ている途中なので変更しない事、というルールを課します。このポインタのことをハザードポインタと呼びます。
ハザードポインタはある瞬間には最大1つのスレッドによって書き込みが可能とされます。読み取りは全てのスレッドから行えます。

他のスレッドが共有データを変更しようとする時、まずハザードポインタを見に行きます。何も登録されていなければ現在のデータを消すのも書き換えるのも自由です。ハザードポインタに登録がある時(そして変更しようとするデータが登録されている時)、現在のデータを維持したまま新しいデータで置き換えることでデータを更新します。維持されたデータは削除待ちとしてマークして、ハザードポインタからの登録が解除された段階で削除されます。

ハザードポインタのイメージ(P0233R6より)

ハザードポインタは単一のものを全スレッドで共有するというよりは、それぞれのスレッドがそれぞれハザードポインタを所有し、変更の際は全てのスレッドのハザードポインタをチェックする、というような実装になるようです。また、ロックフリーデータ構造の実装に使用される場合はハザードポインタは2要素程度のリストになることがあります。

この提案は、ハザードポインタを中心としたこの様な仕組みをサポートし、安全かつ簡単に利用できるようにするためのライブラリを導入しようとするものです。

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

struct Name : public hazard_pointer_obj_base<Name> {
  /* details */
};

std::atomic<Name*> name;

// 頻繁に複数スレッドから呼ばれる
void print_name() {
  // ハザードポインタを取得する
  hazard_pointer h = make_hazard_pointer();
  // nameをハザードポインタへ登録
  Name* ptr = h.protect(name);
  // 以降、*ptrには安全にアクセスできる(勝手に消えたり変更されたりしない)
}

// あんまり呼ばれない
void update_name(Name* new_name) {
  // nameを更新する
  Name* ptr = name.exchange(new_name);
  // 削除待ち登録、全てのスレッドが必要としなくなった時に削除される
  ptr->retire();
}

コメントにあるように、ハザードポインタはデータの読み取りに比べてデータの更新が稀である場合に威力を発揮するものです。更新が頻繁に起こるような場合に適した方法ではありません。

この提案はConcurrency TS v2に向けて議論が進んでいます。現在はLWGで議論中なのでそこには入りそうです。標準ライブラリに入るとしてももう少し先になりそうです。

P1425R2 Iterators pair constructors for stack and queue

std::stackstd::queueに、イテレータペアを受け取るコンストラクタを追加する提案。

std::stackstd::queueイテレータペアを受け取るコンストラクタがなく他のコンテナとの一貫性を欠いており、それによってranges::toの実装では特別扱いするかサポートしない選択を迫られていました。

この提案はこれらのコンテナアダプタと他のコンテナの間の一貫性を改善し、統一的な扱いができるようにするものです。

#include <array>
#include <stack>
#include <queue>

int main() {
  std::array<int, 4> arr = {1, 2, 3, 4};

  // C++20まで、こう書けばできた
  std::stack<int> st{{arr.begin(), arr.end()}};
  std::queue<int> qu{{arr.begin(), arr.end()}};

  // この提案
  std::stack<int> st{arr.begin(), arr.end()};
  std::queue<int> qu{arr.begin(), arr.end()};
}

この提案はLWGでのレビューをほぼ終えていますが、最近提出されたIssue(LWG 3506)との兼ね合いを調査するためのLEWGでのレビューを待っている状態です。問題がなければC++23に入るものと思われます。

P1682R2 std::to_underlying

列挙型の値からその基底の整数型への変換を行うstd::to_underlyingの提案。

多くのコードベースで、列挙値をその基底型に変換する小さな関数を見ることができます。この様な関数がよく見られる理由は単純で、static_cast<int>のように書くと列挙型から基底型に変換しているという事を見失いやすくなるためです。

この様な関数はEffective Modern C++においてもtoUtype()として紹介されており、2019年6月17日時点で、Githubなどでのto_underlying/to_underlying_type/toUtypeのヒット数は(重複を除いても)1000件を超えているようです。

この関数の使用量の増加はScott Meyersの先見性とアドバイスがあらゆる層のC++プログラマーに受け入れられていることを示しており、この様に頻繁に使用されるユーティリティに標準での名前と意味を与えることには価値があります。

また、列挙値の変換という処理は簡単でありながらも正しく書くことが難しいものでもあります

#include <cstdint>

// 基底型を後から変更した(明示的に指定した)
enum class ABCD : uint32_t {
  A = 0x1012,
  B = 0x405324,
  C = A & B,
  D = 0xFFFFFFFF // uint32_t最大値
};

// from before:

void do_work(ABCD some_value) {
  // static_castを使用していることで、コンパイラはこのキャストは意図的なものだと認識
  // 警告は発せられない
  // ABCD::Dが渡ってきた時に間違ったキャストをすることになる
  internal_untyped_api(static_cast<int>(some_value));
}

do_work(ABCD::D);と呼び出されると間違ったキャストが行われ、internal_untyped_api()には意図しないビットパターン渡されることになります。static_cast<int>はそのキャストを意図的に行っていることを宣言するものでもあるため、コンパイラはエラーも警告も発しません。

do_work()内のキャストは、正しくは次のように書く必要があります。

void do_work(ABCD some_value) {
  internal_untyped_api(static_cast<std::underlying_type_t<ABCD>>(some_value));
}

しかし、この関数の引数型を整数に変換可能な型に変更してしまった時の事を考えるとまだ問題があります。static_castを適切に修正するかABCDという型を削除しない限りこのコードはコンパイル可能であり続けます。

この提案は、頻繁に使用される列挙値から整数への変換の意図を明確にしその正しい実装を提供するために、std::to_underlying関数を追加しようとする提案です。

先ほどのdo_work()は次のように書き換えられます。

void do_work(ABCD some_value) {
  internal_untyped_api(std::to_underlying(some_value));
}

std::to_underlyingの引数の型情報は引数型としてコンパイラから渡され、列挙値以外のものが渡されるとコンパイルエラーとなります。これによって先ほどの問題を解決する事が出来ます。また、static_castではなく関数の戻り値として整数型が得られている事によって、戻り値を別の関数に直接渡す場合などにビット幅や符号のミスマッチを警告として得ることができる場合があります。

この提案はすでにLWGでのレビューを終えており、2月初め頃にある次の全体会議で投票にかけられる予定です。何事もなければそこでC++23入りが決定されます。

P1708R3 Simple Statistical Functions

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

提案されている関数は以下のものです。

  • 平均(mean
    • 算術平均
    • 幾何平均
    • 調和平均
  • 分位数(quantile
    • 分位数
    • 中央値
  • 最頻値(mode
  • 歪度(skewness
  • 尖度(kurtosis
  • 分散(variance
  • 標準偏差standard deviations
  • 重み付きの各種統計量

この提案はBoost Accumulators Libraryを参考にしており、これらの統計処理関数はstd名前空間のグローバル関数としても、Accumulator Objectという複数の統計量の同時計算用クラスとしても提供されます。

基本統計量のサンプル

#include <stats>
#include <vector>

int main() {
  std::vector vec = { 2, 3, 5, 7, 7, 11, 13, 17, 19};

  // 算術平均
  auto mean = std::mean(vec);
  
  // 中央値
  auto [m1, m2] = std::sorted_median(vec);
  
  // 最頻値
  std::vector<int> modes{};
  std::mode(vec, std::back_inserter(modes));

  // 標本分散
  auto sample_var = std::var(vec, std::sample_t);

  // 標本の標準偏差
  auto stddev = std::stddev(vec, std::sample_t);
}

このように、これらの関数はstd::rangesアルゴリズム関数と共通したインターフェースを持っており、入力としてrangeオブジェクトを受け取ります。また、射影やExecutionPolicyを取るオーバーロードも用意されています。

Accumulator Objectのサンプル

#include <stats>

int main() {
  std::vector vec = { 2, 3, 5, 7, 7, 11, 13, 17, 19};

  std::mean_accum<int> am{};
  std::geometric_mean_accum<int> gm{};
  std::harmonic_mean_accum<int> hm{};

  // シングルパスの操作によって複数の統計量をまとめて計算
  accum(vec, am, gm, hm);
}

Accumulator Objectはこのように複数の統計量を同時に計算したいときに使用でき、渡された範囲を一回だけ走査します。

P1989R1 Range constructor for std::string_view 2: Constrain Harder

std::string_viewのコンストラクタにrangeオブジェクトから構築するコンストラクタを追加する提案。

この提案の元となった提案(P1391R3)がC++20にて採択されており、std::string_viewイテレータペアを受け取るrangeコンストラクタを獲得しました。P1391R3ではrangeオブジェクトから構築するコンストラクタも提案されていたのですが、それは見送られこの提案に分離されました。

自作の型からstd::string_viewを構築するには、std::string_viewへの変換演算子を用意することで行われます。そのような型は多くの場合文字列のrangeとしてrangeインターフェースを備えていることが多く、単純にstd::string_viewrangeコンストラクタを追加してしまうと、一見どちらのコンストラクタが選択されているのか分からなくなります。

struct buffer {
  buffer() {};

  // rangeインターフェース
  char const* begin() const { return data; }
  char const* end() const { return data + 42; }

  // string_viewへの変換演算子
  operator basic_string_view<char, s>() const{
    return basic_string_view<char, s>(data, data +2);
  }

private:
  char data[42];
};

std::string_view f(const buffer& buf) {
  // string_viewにrangeコンストラクタがある時、どっちが使われる??
  std::string_view strview{buf};

  return strview;
}

このように、場合によっては既存のコードの振る舞いを変えてしまうことになります。

この事に対する検討のために、P1391R3からはrangeオブジェクトを取るコンストラクタは分離され、この提案に引き継がれました。

この提案では、rangeオブジェクトを取るコンストラクタを次のように定義する事でこれらの問題を回避しています。

namespace std {

  template <typename T, typename Traits>
  concept has_compatible_traits = !requires { typename T::traits_type; }
    || ranges::same_as<typename T::traits_type, Traits>;

  template<typename charT, typename traits = std::char_traits<char>>
  struct basic_string_view {

    //...

    template <ranges::contiguous_range R>
      requires ranges::sized_range<R>
        && (!std::is_convertible_v<R, const charT*>)
        && std::is_same_v<std::remove_cvref_t<ranges::range_reference_t<R>>, charT>
        && has_compatible_traits<R, traits>
        && (!requires (std::remove_cvref_t<R> & d)
          {
            d.operator ::std::basic_string_view<charT, traits>();
          })
    basic_string_view(R&&);
  }
}

まず、rangeオブジェクトを取るコンストラクタは型remove_cvref_t<R>が自分と同じstd::basic_string_viewへの変換演算子を持っている場合は選択されないようにしています(一番最後のrequires式)。

次に考慮されているのは、型remove_cvref_t<R>Traits型だけが異なるstd::basic_string_viewへの変換演算子を持っている場合です。その場合今までは(型変換演算子による構築では)コンパイルエラーとなっていました。この提案によるrangeオブジェクトを取るコンストラクタはそのような場合でも、remove_cvref_t<R>::traits_typeを持っていないなら呼び出されるようになっています。

この提案はP1391R3の議論の過程でこの問題以外の部分のレビューをほぼ終えているため、現在はLWGでの最後のレビューを待っている状態です。

P2036R1 Changing scope for lambda trailing-return-type

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

現在の仕様の下では、次のコードはコンパイルできません。

auto counter1 = [j=0]() mutable -> decltype(j) {
  return j++;
};

ラムダ本体のjは初期化キャプチャした変数jを参照しますが、後置戻り値型のdecltype(j)にあるjはキャプチャしたものではなく外のスコープの名前を探しに行きます。これは、初期化キャプチャした変数名はラムダ式の本体内部でしか変数名として参照できないためです。

このコードはコンパイルエラーとなるのでまだいいですが、もしjが存在していたらどうなるでしょう・・・

int j = 0;

auto counter1 = [j=0.0]() mutable -> decltype(j) {
  return j++;
};

この場合コンパイルは恙なく完了し、このラムダの戻り値型はdoubleではなくintになります。この様な暗黙変換が静かに起こっていると思わぬバグとなる可能性があります。

この問題は初期化キャプチャで最も顕著になりますが、通常のコピーキャプチャでも問題になる可能性があります。

template <typename T>
int bar(int&, T&&);        // #1

template <typename T>
void bar(int const&, T&&); // #2


int i;

auto f = [=](auto&& x) -> decltype(bar(i, x)) {
  return bar(i, x);
}

f(42); // コンパイルエラー

ラムダの後置戻り値型指定ではiは外で宣言されたint型の変数iを参照し、bar()は#1が選択され戻り値型はintと推論されます。しかし、ラムダ式内部でのiはコピーキャプチャしたconst int型の変数iを参照し、return文のbar()は#2が選択され戻り値型はvoidと推論されます。これは当然コンパイルエラーとなります。

とはいえ、この種の問題は非常に出会いにくいものであり、サンプルコードを思いつくことは出来ても実際に見かけることはなく、筆者の方によるコードベースの調査でも見つけることは出来なかったようです。

ただ、このラムダ式の本体と後置戻り値型における同じ名前の異なる解釈は、ラムダ式の簡易構文の提案(P0573R2)が拒否された理由の一つでした。この様な同じ名前の非常に近しいコンテキストでの異なる解釈はバグであると思われ、C++の将来の進化を妨げていることからこの様なコーナーケースは排除すべき、という提案です。

この事を修正するにしても少し問題があります。

int i;
[=]() -> decltype(f(i))
{/* ... */}

この様な場合、iがキャプチャされるかどうかはラムダ式の本体を全て見ないと分かりません。現在は外側の変数iを見に行きますが、先程の問題の解決のためにはキャプチャされているならラムダ内部の変数を見に行く必要があります。

この提案では、この様な場合は常にその名前はキャプチャされた変数であるとして扱って推論を行う事を提案しています。

そして、この変更は初期化キャプチャかコピーキャプチャをしていて、後置戻り値型にキャプチャした変数名を使用している場合に既存のコードを壊す可能性があります。特に、次の様なコードのコンパイル結果が変わります。

auto f(int&)       -> int;
auto f(int const&) -> double;

int i;

auto should_capture = [=]() -> decltype(f(i)) {
    return f(i);
};

auto should_not_capture = [=]() -> decltype(f(i)) {
    return 42;
};

現在、この二つのラムダ式の戻り値型はintとなりますが、この提案以降では両方共doubleとなります。

筆者の方の調査でもこの変更で壊れるコードは見つからなかったことから、既存のコードを壊す可能性は極低いものと思われます。EWGにおけるレビューでは、この問題をC++の欠陥として扱うことに合意が取れているようです。

P2072R1 Differentiable programming for C++

C++微分可能プログラミング(Differentiable Programming)サポートを追加するための検討の文書。

微分可能プログラミングとは、従来の計算に微分可能という性質を加えた形で処理を記述するプログラミングスタイル・言語・DSLを指し、その実態は自動微分であるようです。
微分可能プログラミングは機械学習ディープラーニング)の分野で興った概念で、ニューラルネットワークの出力部分の処理を微分可能にすることで、ニューラルネットの学習時にその出力処理も含めて学習を行うものです。それによって、ニューラルネットに出力処理を組み込むことが可能になります。例えば、微分可能レンダリングというものがあります。

微分可能プログラミングサポートを追加するというのは、C++で自動微分サポートを追加するという意味なので、その恩恵は機械学習だけではなく数値最適化や物理シミュレーションなど様々な分野に及びます。

この文書はC++に最も適した形での自動微分サポートの議論のために、微分を計算するための方法や自動微分についてを解説し、ライブラリ・言語サポート・将来の言語機能によるサポートなど、自動微分を実装するために可能なアプローチについてを概説したものです。

この文書では、ライブラリサポートよりも言語サポートが望ましく、浮動小数点数型に適用可能でさえあればテンプレートである必要もなく、特に既存のコードに自動微分を適用可能であることが望ましいと述べています。ようするにコードをコンパイルする過程で処理のグラフを解析し、自動でその勾配を算出していくものです。

そのような既存実装の一つであるEnzymeというLLVM IRを解析してリバースモード自動微分を実現するコードトランスパイラによるサンプルです。

// 行列の差の二乗和(自動微分を意識していない普通のコード)
__attribute__((noinline))
static double matvec(const MatrixXd* __restrict W, const MatrixXd*
__restrict M) {
  MatrixXd diff = *W-*M;
  return (diff*diff).sum();
}

int main(int argc, char** argv) {
  // 行列の初期化
  MatrixXd W = Eigen::MatrixXd::Constant(IN, OUT, 1.0);
  MatrixXd M = Eigen::MatrixXd::Constant(IN, OUT, 2.0);
  MatrixXd Wp = Eigen::MatrixXd::Constant(IN, OUT, 0.0);
  MatrixXd Mp = Eigen::MatrixXd::Constant(IN, OUT, 0.0);

  // matvecによる処理の実行と自動微分の計算
  // EnzymeがLLVM IRを解析することでこの関数の導関数を求め、計算するコードを出力する
  __enzyme_autodiff((void*)matvec, &W, &Wp, &M, &Mp);
  
  // ...
}

このコードは、ClangによってLLVM IRに変換されたあとでEnzymeによって導関数LLVM IRとして求められ、その結果をClangによって実行ファイルへとコンパイルすることで実行可能なプログラムを得ます。この文書の示すC++自動微分サポートの方向性は、__enzyme_autodiffの部分を言語サポートによって簡易な構文に置き換えつつ、このコンパイル過程を通常のコンパイルで行おうとするものです。

P2093R3 Formatted output

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

前回の記事を参照

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

  • std::printという名称が他言語の出力機能との互換性の点で有利であることの説明を追加
  • P1885を使用してリテラルエンコーディングを取得することで実装を簡素化
  • 様々な言語でのユニコード処理の比較結果を追記
  • 提案文書の文言と、規格のための文言の調整

などです。

P2168R1 generator: A Synchronous Coroutine Generator Compatible With Ranges

Rangeライブラリと連携可能なT型の要素列を生成するコルーチンジェネレータstd::generator<T>の提案。

前回の記事を参照

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

  • コンパイラでのベンチマーク結果を追加
  • 再帰したジェネレータにおける変換の曖昧さの解消のためにelements_of()を追加
  • アロケータサポートを追加
  • Symmetric transferが様々なアロケータ/値のジェネレータで機能することを追記
  • イテレータ->を削除
  • 提案しているstd::generatorを新ヘッダ<generator>に配置するように変更
  • Valueテンプレートパラメータの利点を強調するために例を追加

などです。

P2210R1 Superior String Splitting

現状のviews::splitの非自明で使いにくい部分を再設計する提案。

前回の記事を参照

このリビジョンでの変更は、const-iterationの説明を修正し現在のviews::splitの上により適切なsplitを構成できるようにしたこと、現在のviews::splitの機能を別の意味論に基づく名前で維持するようにしたことです。

この提案では、現在の超汎用splitstd::ranges::lazy_split_view/std::views::lazy_splitと名前を変更し、新しいより直感的なsplitstd::ranges::split_view/std::views::splitとすることを提案しています。

P2216R2 std::format improvements

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

以前の記事を参照

このリビジョンでの変更は、提案している機能の効果についての調整がメインです。

この提案はLEWGでの投票において 提案する2つの事項(コンパイルフォーマットチェック、バイナリサイズ削減)についてC++20へのDefact Reportとすることで合意が取れ、C++20への逆適用のためにLWGで先行してレビューされました。そこでもC++20へのDRとすることで合意が取れ、LEWGでのレビューと投票を待ってどうやら2021年の夏ごろにはC++23に採択されそうです。その場合この変更はC++20にさかのぼって適用されます。

P2232R0 Zero-Overhead Deterministic Exceptions: Catching Values

任意の値を投げることのできる静的例外機能の提案。

P0709R4にて議論されているstd::errorによる静的例外では、例外オブジェクトとして特定のものだけをthrowすることができます。しかし、既存の例外を使用するコードにおいては独自の例外型が使用されていることがあり、その場合はstd::errorに移行することはできず、結果としてC++におけるエラーハンドリングの複雑さを増加させることになってしまいます。

この提案はそれを防止するため、値によるキャッチを使用する場合に任意の型の例外オブジェクトを効率的に転送するためのP0709R4とは別のアプローチを提案するものです。

まずこの提案による任意の値のthrowでは、次の2つの仮定を置きます

  • 値のcatchのみを使用する
    • catch(E& e)ではなく、catch(E e)
  • catch(E e)のセマンティクスを変更する
    • 動的な型ではなく、静的な型によって例外オブジェクトとマッチさせる

この過程を置くと、例外オブジェクトはtry-catchの範囲内でスタック領域を使用できるようになります。

try
{
    f(); // Throws
}
catch( E1 e1 )
{
    // Use e1
}
catch( E2 e2 )
{
    // Use e2
}

今、E1, E2のそれぞれの例外オブジェクトの型とそのサイズは静的に決定できるため、スタック上の領域を予約しておくことができます。すると、キャッチした側ではE1, E2のどちらの値が投げられてきたのかを知る仕組みが必要となります。この判定のために、std::optional<E1>, std::optional<E2>の領域をスタック上に確保しておきます。

次に、f()内のthrow E1{}/throw E2{}がそのスタック領域を使用できるようにする効率的な仕組みが必要となります。これには、スレッドローカルストレージを使用します。

f()を囲うtryブロックでは、f()を呼び出す前にe1, e2用に予約されたsスタック上のstd::optional<E1>, std::optional<E2>の領域を指すようにスレッドローカルストレージにポインタpE1, pE2を初期化します。現在の例外処理の実装もスレッドローカルストレージを使用しているので、それを利用することに問題はありません。

f()が例えばをthrow E1{}したとき、スレッドローカルポインタpE1にアクセスし、次のどちらかを実行します

  • pE1nullであれば、catch(E1 e1)ステートメントが現在のコールスタックで使用できないことを意味しており、従来の例外機構による処理に切り替える
  • それ以外の場合、pE1の指す領域にE1のオブジェクトを構築する

次に、スタックを最上位の例外スコープ(最も内側にあるtry-catchブロックの位置)まで巻き戻します。この実装はP0709にあるものと同じものが利用でき(詳細は不明・・・)、代替戻り値としては、例外オブジェクトの代わりに失敗か成功かを表す1ビットのフラグを返します。

try-catchブロックに到着する(巻き戻る)と、あらかじめ確保しておいたstd::optional<E1>, std::optional<E2>(スタック上)を調べます。catchブロックの順番にstd::optionalオブジェクトがチェックされ、空でない最初のstd::optionalに対応するcatchブロックが実行されます。ここでの説明の例では、e1 -> e2の順でチェックされ、catch(E1 e1)スコープが選択されます。適切なブロックが見つからなければスタック巻き戻しを続行し、最終的には適切なcatchで捕捉されるか従来の例外機構にスイッチするかのどちらかで完了します。

これによって、スタックの巻き戻しが高速になりcatchが動的型ではなく静的な型でチェックされるようになるため、P0709と比較しても効率がさらに向上しているとのことです。

P2244R0 SG14: Low Latency/Games/Embedded/Finance/Simulation Meeting Minutes

SG14のミーティングの議事録。

SG14はゲームハードや組み込みシステムなどのリソースが制限されている環境や金融やシミュレーションなど低遅延が重視される環境などにおけるC++についてを議論・研究するグループです。

P2245R0 SG19: Machine Learning Meeting Minutes

SG19のミーティングの議事録。

SG19はC++における機械学習サポートについて議論・研究するグループです。

P2246R1 Character encoding of diagnostic text

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整(shallshouldへ変更)がメインです。

この提案はSG16で議論されていましたが、この提案の方向性についての合意が取れたたためEWGへ転送されました。

P2259R1 Repairing input range adaptors and counted_iterator

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

以前の記事を参照

このリビジョンでの変更は、elements_viewiterator_categoryを修正しiterator_conceptを定義した事です。そのiterator_conceptは受けているrangeの満たすC++20的性質を、iterator_categoryは受けているイテレータC++17的性質を受け継いで決定されます。これはこの提案で変更されている他のViewも同様の方向性で調整されています。

この提案はIssue解決のためのものだったこともありLWGで議論されており、LWGでは全会一致でアクセプトされました。次の全体会議で全体投票にかけられ問題が無ければC++23に採択される予定です。

P2266R0 Simpler implicit move

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

C++20での欠陥改善(P1825R0)により、関数のreturnにおいては右辺値参照型のローカル変数からでも暗黙的にムーブ(implicitly move)を行うことができるようになります。しかし、この改善されたはずの規格の文書にはまだ欠陥があり、関数が参照を返す場合に暗黙ムーブが行われないようです。

struct Widget {
  Widget(Widget&&);
};

struct RRefTaker {
  RRefTaker(Widget&&);
};

// 次の3つのケースではreturnで暗黙ムーブされる

Widget one(Widget w) {
  return w;  // OK、C++11以降
}

RRefTaker two(Widget w) {
  return w;  // OK、C++11以降(CWG1579解決後)
}

RRefTaker three(Widget&& w) {
  return w;  // OK、C++20以降(P0527による)
}

// 暗黙ムーブされてほしい、されない・・・
Widget&& four(Widget&& w) {
  return w;  // Error!
}

この様に関数が右辺値参照を返す場合には暗黙のムーブが行われず、このケース(four())では、関数ローカルの左辺値wを右辺値参照型Widget&&に暗黙変換できずにエラーとなります。

戻り値が参照である場合に、同様の事が起きます。

struct Mutt {
  operator int*() &&;
};
struct Jeff {
  operator int&() &&;
};

// 暗黙ムーブされ、Mutt&&からの暗黙変換によってreturn
int* five(Mutt x) {
  return x;  // OK、C++20以降(P0527による)
}

// 暗黙ムーブされず、int&へ変換できずエラー
int& six(Jeff x) {
  return x;  // Error!
}
template<class T>
T&& seven(T&& x) { return x; }

void test_seven(Widget w) {
  // Widget& seven(Widget&)
  Widget& r = seven(w);               // OK
  // Widget&& seven(Widget&&)
  Widget&& rr = seven(std::move(w));  // Error
}

関数の戻り値型がオブジェクト型ではないとき、暗黙ムーブが行われない事によってこれらのような問題が起きています。

この提案は、関数から返されるmove-eligibleな式は常にxvalueであると言うように指定する事でこれらの問題の解決を図るものです。

P2276R0 Fix std::cbegin(), std::ranges::cbegin, and cbegin() for span (fix of wrong fix of lwg3320)

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

std::cbegin()/std::ranges::cbegin()はともに、const引数で受け取ったオブジェクトに対して使用可能なbegin()を呼び出そうとします。標準のコンテナならばconstオブジェクトに対するbegin()メンバ関数const_iteratorを返すため、メンバcbegin()と同様の結果を得ることができます(cend()も同様)。

しかし、これはつまりメンバとしてcbegin()/cend()を用意していてもstd::cbegin()/std::ranges::cbegin()はそれを呼び出さない事になり、クラスによってはstd::cbegin()/std::ranges::cbegin()で期待されるread-onlyなイテレータアクセスを提供しない可能性があります。

C++20当初のstd::spanがまさにその問題に引っかかっておりLWG Issue 3320にて一応解決されました。しかし、この修正はstd::span::cbegin()メンバ関数std::cbegin()/std::ranges::cbegin()の戻り値型の不一致の是正に重きを置いていたため、std::spanからcbegin()/cend()メンバ関数を削除する事でその不一致を解消していました。

一方、std::spanbegin()メンバ関数constオブジェクトに対するオーバーロードを提供しておらず、std::cbegin()/std::ranges::cbegin()から呼び出された時でもmutableイテレータを返してしまいます。結局std::spanconst_iteratorを提供しないため、std::cbegin()/std::ranges::cbegin()を用いてもread-onlyなイテレータアクセスはできません。

std::vector<int> coll{1, 2, 3, 4, 5};
std::span<int> sp{coll.data(), 3};

for (auto it = std::cbegin(sp); 
          it != std::cend(sp); ++it) 
{
  *it = 42; // コンパイルエラーにならない・・・
}

for (auto it = std::ranges::cbegin(sp);
          it != std::ranges::cend(sp); ++it)
{
  *it = 42; // コンパイルエラーにならない・・・
}

for (auto it = sp.cbegin(); // コンパイルエラー!
          it != sp.cend(); ++it)
{
  // ...
}

この提案では、次の2つの変更によってこの問題の解決を図ります。

  • std::cbegin()/std::ranges::cbegin()は引数に対してそのメンバ関数.cbegin()が呼び出し可能ならばそれを使用する
  • その上で、std::spanメンバ関数cbegin()/cend()を追加し、const_iteratorサポートを復活させる

1つ目の変更はcend()/crbegin()/crend()に対しても同様の変更を提案しています。

P2277R0 Packs outside of Templates

パラメータパックをテンプレートではないところでも使えるようにする提案について、実装難度がメリットを上回っていないかどうかの検討を促す文書。

現在の進行中の提案のうち、パラメータパックをより活用しようとするものには次の4つがあります。

これらの提案では、可変長テンプレートでないところでも、あるいは非テンプレートの文脈でもパラメータパックを活用しようとしています。

template <typename... Ts>
struct simple_tuple {
  // データメンバ宣言時のパック展開(P1858)
  Ts... elems;
};

int g(int);

// 非関数テンプレート
void f(simple_tuple<int, int> xs) {
  // 構造化束縛でのパック導入(P1061)
  auto& [...a] = xs;
  int sum_squares = (0 + ... + a * a);
  
  // パラメータパックでないもののパック展開(P1858)
  int product = (1 * ... * g(xs.elems));

  // テンプレートパラメータのreflection-rangeを構築する(P1240)
  // これはint型のリフレクション2つを含むvector
  constexpr auto params = std::meta::parameters_of(reflexpr(decltype(xs)));
  
  // reflection-rangeの展開1(P1240)
  // decltype(ys) is simple_tuple<int, int>
  simple_tuple<typename(...params)> ys = xs;

  // reflection-rangeの展開2(P2236)
  // decltype(zs) is simple_tuple<int, int>
  simple_tuple<|params|...> zs = xs;
}

P1240とP2236の二つのリフレクションベースの提案におけるreflection-rangeの展開は似ていますが、P1240が単なる展開しかできないのに対して、P2236は展開しつつ変換することもできます。例えば、simple_tuple<int&, int&>構成しようとするとそれぞれ次のように書くことができます。

// P1240の方法
constexpr auto refs = params
                    | std::views::transform(std::meta::add_lvalue_reference);
simple_tuple<typename(...refs)> ys_ref{a...};

// P2237の方法
simple_tuple<|params|&...> zs_ref{a...};

これらの提案のうち一部のものについては実装の複雑さとコンパイル時間増大の懸念が示されています。

ある名前がパック展開の対象となるか否かは一連の式を最後まで見る必要があります。...は接尾辞であり、しかもパック名の直後だけではなくその後の任意の場所にあらわれる可能性があるためです。ただし、現在のところこのような配慮は可変長テンプレートの中でだけ行えばよく、他のところではこれを考慮する必要はありません。

しかし、これらの提案の機能の一部には可変長テンプレートではない場所でパック展開が発生するものがあります。これがもし導入されると、可変長テンプレートではないC++の全てのところでパック展開が出現する可能性を考慮する事になり、これはコンパイル時間を増大させます。

P1240R1はこのことを考慮して注意深く設計されているようですが、他の提案にはこの問題があります。

この問題への対処には次の3つの方法があります。

  1. テンプレートの外でのパック展開を許可し、そのために発生するコスト(コンパイル時間増大)を受け入れる
  2. テンプレート外でのパック展開には、接頭辞によって行う何かを考え出す。パック展開にはその仕様を推奨する。
  3. 可変長テンプレートの外でのパック展開は許可しない

筆者の方は1を推奨しているようです。

この文書は、EWGがこれらの提案の議論の前に上記選択肢のどれを選択するのか?あるいはテンプレートの外でのパック展開は利点がコストを上回っているのか、なるべく早期にその方向性を決定することを促すものです。

P2278R0 cbegin should always return a constant iterator

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

先程P2276の所でも言っていたように、std::ranges::cbegin/cendconstオブジェクトに対するstd::ranges::begin/endを呼び出すため、必ずしもconst_iteratorを返しません。

C++11でメンバcbegin/cendと非メンバstd::begin/endが追加され、その後std::cebgin/cendが追加された(LWG Issue 2128)ときは、コンテナオブジェクトcに対するstd::as_const(c).begin()c.cbegin()の結果が異なるコンテナは(少なくとも標準ライブラリの中には)存在しておらず、CPOの様なものも発明されていなかったので、std::cbegin()が実質的にstd::as_const(c).begin()のように定義されても問題はなく、仕方ない所がありました。

その後もC++17までは何事もありませんでしたが、C++20にてstd::spanが追加されるとstd::cbegin/endの振る舞いが問題になりました。std::spanは別の範囲を参照するものでしかなく、それ自身のconst性と参照先のconst性が同期していません。したがって、std::as_const(c).begin()c.cbegin()の結果が一致しません。例えば、std::spanでは参照先のconst性を表すのはconst span<T*>ではなくspan<const T*>です。従って、それらのメンバbegin/endconstオーバーロードconst_iteratorを返すのはセマンティクスにあっていません。

しかし一方で、std::cbegin/endspanのメンバbegin/endconstオーバーロードを呼び出してしまうので、std::cbegin/endを使って取得したイテレータconst_iteratorではありません。ただし、spanのメンバcbegin/cendはきちんとconst_iteratorを返していたため、非メンバstd::cbegin/endとメンバcbegin/cendの間で結果が異なることになります。最終的に、一貫性のためにspanのメンバcbegin/cendは削除されました(LWG Issue 3320)。

この問題は現在のところstd::spanでしか起きてないようですが、C++20で追加されたRangeライブラリの各種Viewの中にはそもそもconst-iterableではないためにメンバcbeginもメンバbeginconstオーバーロードも提供していないものがあります。また、これから追加されるであろう他のViewでも同様あるいはspanと同様の問題が発生しうるものがあるようです。

この提案ではこの問題の解決のために、イテレータ/センチネルのペアをラップしてconst_iterator化するmake_const_iteratorようなものを追加し、std::ranges::cbegin/cendでは、それを用いて引数のrangeオブジェクトから得られるイテレータ/センチネルのペアをラップして、std::ranges::cbegin/cendが常にconst_iteratorを返すように変更することを提案しています。また、それを用いてviews::const_rangeアダプタを追加することも提案しています。

ただし、std::cbegin()/cend()後方互換性維持のために手を付けていません。

P2279R0 We need a language mechanism for customization points

C++に適切なカスタマイゼーションポイントを提供するための言語サポートを追加する検討を促す提案。

現在のC++言語機能の範囲内で利用可能なカスタマイゼーションメカニズムには次のようなものがあります。

  • 仮想関数
  • クラステンプレートの特殊化
  • ADL
  • カスタマイゼーションポイントオブジェクト(CPO)
  • tag_invoke

過去に提案されていたカスタマイゼーションメカニズムには次のようなものがあります。

  • カスタマイゼーションポイント関数
  • コンセプトマップ

それらにRustのTraitを含めて比較すると、それぞれ次のような特性を持ちます

✔️は可能であること、❌は不可能であること、 🤷 は部分的には可能だが完全ではないことをそれぞれ表しています。

各行の意味はそれぞれ

  • Interface visible in code
    • カスタマイズ可能な(あるいはその必要がある)インターフェース(関数など)がコードで明確に識別できる
  • Providing default implementations
    • デフォルト実装を提供し、なおかつオーバーライド可能
  • Explicit opt-in
    • インターフェースを明示的にオプトインできる(インターフェースへのアダプトが明示的)
  • Diagnose incorrect opt-in
    • インターフェースに意図せずアダプトしない
  • Easily invoke the customization
    • カスタマイズされたものを簡単に呼び出せる
    • デフォルト実装がある場合、必ずカスタマイズされたものを呼び出す
  • Verify implementation
    • ある型がインターフェースを実装していることを簡単に確認できる(機能がある)
  • Atomic grouping of functionality
    • インターフェースにアダプトするために必要な最小の機能グループを提示でき、早期にそれを診断できる
  • Non-intrusive
    • 非侵入的(その型を所有していない人が後からカスタマイズできる)
  • Associated Types
    • 関連する型をまとめて扱える(個別の型ごとにインターフェースにアダプトする必要が無い)
    • 例えば、イテレータ型に対するカスタマイゼーションポイントを提供する時、イテレータの要素の型ごとにカスタマイズ処理を書く必要が無い。

そして追加で、Customization Forwardingという要求も検証しています。例えばCPOやtag_invokeなら、それそのものを呼び出し可能オブジェクトとして他の関数などに渡すことができます。一方、コンセプトマップは(Rustのtaritも?)それそのものは呼び出し可能ではありません。

この提案は、理想的にはこれらの要件をすべて満足するようなカスタマイズメカニズムをC++の言語機能としてサポートすることを目指して、その議論の出発点となるべく書かれたものです。

P2280R0 Using unknown references in constant expressions

定数式での参照のコピーを許可する提案。

C++では生配列のサイズを求めるのにstd::sizeを使用できます。が、constexpr関数では不可解なコンパイルエラーに遭遇することがあります。

template <typename T, size_t N>
constexpr auto array_size(T (&)[N]) -> size_t {
  return N;
}

void check(int const (&param)[3]) {
  int local[] = {1, 2, 3};

  constexpr auto s0 = array_size(local); // ok
  constexpr auto s1 = array_size(param); // error
}

この提案は、この問題を解決し二つ目の呼び出しが適格となるようにするものです。

これはルールとしては定数式で禁止されている事項に引っかかっているためにコンパイルエラーとなります。

N4861 7.7 Constant expressions [expr.const]/5.12より

  • 参照に先行して初期化されていて次のどちらかに該当するものを除いた、変数の参照または参照型データメンバであるid-expression
    • 定数式で使用可能である
    • その式の評価の中でlifetimeが開始している

[expr.const]/5では定数式で現れてはいけない式が列挙されています。これはそのうちの一つです。

定数式ではあらゆる未定義動作が許可されないため、コンパイラはすべての参照が有効であることを確認する必要があります。先程のarray_sizeが機能するためには配列の参照を定数式で読み取る(コピーする)必要がありますが、関数引数の参照は直接的には有効性が判定できないものであり、上記のルールに抵触するタイプの参照となります。ただ、ここでは単に参照のコピーが必要なだけで参照そのものに関心はないはずです。

この事はポインタを取るように変更する事でより明快になります。

template <typename T, size_t N>
constexpr size_t array_size(T (*)[N]) {
  return N;
}

void check(int const (*param)[3]) {
  constexpr auto s2 = array_size(param); // error
}

ここでのarray_sizeの呼び出しではparamのコピーが発生しています。定数式中で関数にコピー渡しする場合、コピー元も定数式でなければなりませんが、関数の引数は定数式ではありません(cosntexpr/consteval関数の中であっても)。ローカルの配列(のポインタ)の場合はその参照(ポインタ)が有効であることをコンパイラが認識できるので問題にはなりません。

ただし、この様な問題は参照(ポインタ)でのみ起きます。値を渡す場合、例えばstd::arrayを用いるとコンパイルエラーは起こりません。

void check_arr_val(std::array<int, 3> const param) {
  std::array<int, 3> local = {1, 2, 3};

  constexpr auto s3 = std::size(local); // ok
  constexpr auto s4 = std::size(param); // ok
}

提案では、この様に参照そのものと関係なく動作するケースにおいて、定数式内での参照の読み取り(すなわちそのコピー)を許可しようとするものです。参照そのものに依存するような操作は引き続き禁止されます。

EWGでのレビューでは、この問題をC++23までに解決することとC++11までの欠陥報告とする事に合意が取れており、EWGでの次の投票を受けてCWGに転送される予定です。

P2281R0 Clarifying range adaptor objects

range adaptor objectがその引数を安全に束縛し、自身の値カテゴリに応じて内部状態を適切にコピー/ムーブする事を明確化する提案。

GCCはRangeライブラリの全ての部分を、MSVCは一部を既に実装していますが、そこには次のような相違点があるようです。

template<class F>
auto filter(F f) {
  // GCC : fを参照で保持する
  // MSVC : fを常にコピーして保持する
  return std::views::filter(f);
}

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

// 状態を持つCallableオブジェクトを渡す
auto f = filter([i = std::vector{4}] (auto x) { return x == i[0]; });

// GCCの場合、fにダングリング参照が含まれている
// MSVCは無問題
auto x = v | f;

明らかにMSVCの動作が望ましいのですが、adaptor(args...)の様に呼ばれたときにadaptorargs...をどのようにキャプチャするのかは規定がありません。また、adaptor(range, args...)adaptor(args...)(range)が同じ振る舞いをするという要件は、ここでargs...をコピーするという選択を排除している可能性があります。

結果的に現在の記述では、左辺値の範囲に対するパイプラインを再利用しても安全であることを保証していません。つまり、上記におけるxの再利用が安全であるかは未規定に近く、v | fでは(fの状態がなんであれ)fからムーブされることが無いことを示唆しています。

この提案では、range adaptor objectが行う部分適用がstd::bind_frontの行う事と同等であることが明確になるように現在の規格書の記述を変更しています。

それによって、上記のMSVC実装が合法かつ推奨される実装となります。これは、range adaptor objectに渡された引数が参照によって束縛されず常に値によって束縛され、パイプラインに渡す時にはその値カテゴリに応じて束縛されたものを適切にコピー/ムーブする、という事でもあります。

疑似コードで書くと次のようになります。

auto c = /* 任意のrange */;
auto f = /* コピーコストが高い関数オブジェクト */;

c | transform(f); // fをコピーして結果のviewにムーブする

auto t = transform(f);  // fをコピーする
c | t;            // tから再度fをコピーする
c | std::move(t); // tからfをムーブする

またこれに伴って、customizetion point objectrange adaptor objectはCPOでもある)がコピーやムーブされたときにも同じ引数に対して同じ結果を返すことを保証するように文面を明確化しています。

P2283R0 constexpr for specialized memory algorithms

<memory>にある未初期化領域に対する操作を行う各関数をconstexprにする提案。

未初期化領域に対する操作を行う各関数とは、<memory>にあるstd::uninitilized_~という関数群の事です。

これらの関数はstd::vectorの内部実装に使用されており、std::vectorconstexpr化する際にこれらの関数のconstexpr化が必要となることが発覚し、提案に至ったようです。

その際に問題になるのが、std::uninitialized_default_constructの効果で、C++20で追加されたconstexprな配置newを行うstd::construct_atを使ってしまうとデフォルト初期化ではなくゼロ初期化になってしまうためstd::construct_atを利用できないのですが、現状それ以外にconstexprな配置newを行う方法がありません。

std::construct_atは未初期化領域へのポインタpだけが与えられると::new(p) T()相当の初期化を行い、これはゼロ初期化され、Tのコンストラクタが無い時その領域がゼロ埋めされます。一方std::uninitialized_default_constructは、その領域に対して::new(p) T相当の初期化を行い、これはデフォルト初期化され、Tのコンストラクタが無い時なにもしません。)

これを回避するために、この提案ではconstexprな配置newによるデフォルト初期化を行うstd::default_construct_at関数を追加し、それを利用してstd::uninitialized_default_constructconstexpr化することを提案しています。

なお、ExecutonPolicyを取るオーバーロードconstexpr対応されません。

これはMSVC STLにおいてstd::vectorconstexpr化する過程で発覚し、既に実装されているようです。

P2285R0 Are default function arguments in the immediate context?

関数テンプレートのテンプレートパラメータに依存するデフォルト引数のインスタンス化の失敗をSFINAEできるようにする提案。

現在の規格では関数テンプレートのデフォルト引数のインスタンス化に失敗した時、そこがSFINAEできる文脈(immediate context)であるかどうかが規定されていないようです。例えば、次のようなコードで多様な結果を得ることができます。

template <typename T, typename Allocator>
struct container {

  // アロケータ型をデフォルト構築しているデフォルト引数
  template <std::ranges::range Range>
  explicit container(Range r, Allocator a = Allocator()) {}

};

// デフォルト構築できないアロケータ型
struct Alloc {
  Alloc() = delete;
  // ...
};

int main() {
  // Clang, ICCはここでコンパイルエラー
  constexpr bool c = std::constructible_from<container<int, Alloc>, std::vector<int>>;

  // GCC : 0 (false), MSVC : 1 (true)
  std::cout << c << std::endl;
}

この提案はこれを明確化し、デフォルト引数のインスタンス化の失敗はSFINAEできる文脈であると規定しようとするものです。

これによって、コードを次のように改善できるようになります。

現在 この提案
template<class Hash, class Equal, class Allocator>
struct Map {

  template<class Range>
  explicit Map(Range&&, Hash, Equal, Allocator);

  template<class Range>
  explicit Map(Range&& c, Hash h, Equal e)
   requires default_initializable<Allocator>
   : Map(c, h, e, Allocator()) {}

  template<class Range>
  explicit Map(Range&& c, Hash h)
   requires default_initializable<Equal>
         && default_initializable<Allocator>
   : Map(c, h, Equal(), Allocator()) {}
  template<class Range>
  explicit Map(Range&& c)
   requires default_initializable<Hash>
         && default_initializable<Equal>
         && default_initializable<Allocator>
   : Map(c, Hash(), Equal(), Allocator()) {}
};
template<class Hash, class Equal, class Allocator>
struct Map {

  template<class Range>
  explicit Map(Range&&,
               Hash = Hash(),
               Equal = Equal(),
               Allocator = Allocator());
};

P2286R0 Formatting Ranges

任意の範囲を手軽に出力できる機能を追加する提案。

例えば文字列を分割してその結果をコンソール出力しようと思った時、C++20では次のように書くことができます。

#include <iostream>
#include <string>
#include <ranges>
#include <format>

int main() {
  // 文字列の分割(右辺値stringをsplitできない)
  std::string s = "xyx";
  auto parts = s | std::views::split('x');
  
  // これは出来ない
  std::cout << parts;
  
  // P2093のstd::print、これもできない
  std::print("{}", parts);


  std::cout << "[";
  char const* delim = "";
  for (auto part : parts) {
    std::cout << delim;
    
    // これもできない
    std::cout << part;
    
    // std::print、当然できない
    std::print("{}", part);
    
    // これはできる!
    std::ranges::copy(part, std::ostream_iterator<char>(std::cout));
    
    // これもできる!!
    for (char c : part) {
        std::cout << c;
    }
    delim = ", ";
  }
  std::cout << "]\n";
}

{fmt}ライブラリを使うと、次のように書けます。

#include <ranges>
#include <string>
#include <fmt/ranges.h>

int main() {
  std::string s = "xyx";
  auto parts = s | std::views::split('x');

  fmt::print("{}\n", parts);
  fmt::print("[{}]\n", fmt::join(parts, ","));

  // 出力
  // {{}, {'y'}}
  // [{},{'y'}]
}

しかしこれはstd::formatには含まれていません。

この提案は、{fmt}ライブラリのこの機能をstd::formatに追加しようとするものです。

範囲も含めて以下のものに対してフォーマッタの特殊化を追加することを目指しています。

  • value_typereferenceがフォーマット可能な任意の範囲
  • 2つの型が共にフォーマット可能なstd::pair<T, U>
  • 全ての型が共にフォーマット可能なstd::tuple<Ts...>
  • std::vector<bool>::referenceboolと同じようにフォーマットする

フォーマットの方法(範囲やpair/tuple[...]/{...}のどちらでフォーマットするかなど)は実装定義とし、fmt::joinに倣ったstd::format_joinの様なものを追加することも提案しています。 

P2287R0 Designated-initializers for base classes

基底クラスに対して指示付初期化できるようにする提案。

C++20にて、集成体型に対する指示付初期化が出来るようになりましたが、それは直接のメンバに対してのもので、基底クラスのメンバに対しては行えませんでした。

struct A {
  int a;
};

struct B : A {
  int b;
};

int main() {
  A a = { .a = 1 };  // ok
  B b1 = { {2}, 3 }; // ok
  B b2 = { 2, 3 };   // ok

  B b3 = { .a = 4, .b = 5 };   // ng
  B b4 = { {6} .b = 7 };       // ng
  B b5 = { { .a = 8} .b = 9 }; // ng
}

基底クラスのメンバを指定できないため、指示付初期化と通常の初期化が混在してはいけないというルールを守ることができず、結果として継承している集成体では指示付初期化できなくなっています。

この提案は、基底クラスを指定して指示付初期化できるようにしようとするものです。

その際問題になるのが、基底クラスをどうやって指定するのかという事です。現在のC++には基底クラスを明示的に指定するような構文は存在していません。

この提案では、集成体初期化子の先頭で、:に続いて基底クラス名を指定することで指示子とする事を提案しています。

template <typename T>
struct C { 
  T val;
};

struct D : C<int>, C<char> {};

int main() {
  B b1 = { :A = { .a = 1}, b = 2 };
  B b2 = { :A{ .a = 1}, b = 2 };

  D d = { :C<int>{.val = 1}, :C<char> = {.val=`x`} };
}

この提案では基底クラスを指定するための方法を追加することだけが目的で、指示付初期化の他の部分を変更していません。従って、指示子の有無が混在することやその順番が宣言順と異なることも許可されません。

2週間後くらいかな・・・

この記事のMarkdownソース

[C++]推論補助(deduction guide)にexportはいるの?

A : いりません

根拠

モジュールにおけるexport宣言は、そのモジュールのインターフェース単位でのみ行うことができます。export宣言は名前を導入するタイプの宣言の前にexportを付けることで行い、その名前に外部リンケージを与える以外は元の宣言と同じ効果を持ちます。

モジュールのimport宣言は指定したモジュールをインポートするもので、書かれている場所と異なるモジュールを指定した場合はそのモジュールのインターフェースをインポートする事になります。

モジュールのインターフェースをインポートすると、インポートされたモジュールでexportされている名前がインポートした側での名前探索において可視となり、インポートされたモジュールのインターフェースで宣言されているものが全て到達可能(reachable)となります。

推論補助そのものはテンプレートの宣言の一種であり、名前を導入するものなのでexportすることができます。

推論補助にexportがいるかどうかというのは、推論補助が名前探索を通じて発見されるのかどうか?という事でもあります。推論補助はどのようにして発見されているのでしょうか・・・?

N4861 13.7.2.3 Deduction guides [temp.deduct.guide]に明確に書いてあります。

Deduction guides are not found by name lookup. Instead, when performing class template argument deduction ([over.match.class.deduct]), all reachable deduction guides declared for the class template are considered.

推論補助は名前探索では見つからず、代わりにクラステンプレートの引数推論時にそのクラステンプレートに対して 到達可能 な全ての推論補助が考慮される。みたいに書いてあります。

ここでの到達可能(reachable)とは、import宣言によってインターフェースにあるものが到達可能になる、と言っていたところの到達可能と同じ意味です。

つまり、推論補助はモジュールのインターフェースにありさえすればそのモジュールをインポートした側で使用可能となります。exportする必要はありません。

/// mymodule.cpp
export module mymodule;

import <ranges>;

export
template<std::input_or_output_iterator I, std::sentinel_for<I> S>
class iter_pair {
  I it;
  S se;

public:

  template<typename R>
  iter_pair(R&& r); // (1)

  iter_pair(I i);   // (2)
};

// (1)に対する推論補助1
template<typename R>
iter_pair(R&&) -> iter_pair<std::ranges::iterator_t<R>, std::ranges::sentinel_t<R>>;

module : private;
// プライベートモジュールフラグメントの内側はインポートした側から到達可能とならない


// (2)に対する推論補助2
template<std::input_or_output_iterator I>
  requires std::sentinel_for<std::default_sentinel_t, I>
iter_pair(I) -> iter_pair<std::remove_cvref_t<I>, std::default_sentinel_t>;


// コンストラクタ定義、暗黙エクスポート
template<std::input_or_output_iterator I, std::sentinel_for<I> S>
template<typename R>
iter_pair<I, S>::iter_pair(R&& r)
  : it(std::ranges::begin(r))
  , se(std::ranges::end(r))
{}

template<std::input_or_output_iterator I, std::sentinel_for<I> S>
iter_pair<I, S>::iter_pair(I i)
  : it(std::move(i))
  , se{}
{}
/// main.cpp

import <iterator>;
import mymodule;

int main() {
  int ar[3] = {1, 2, 3};
  
  iter_pair ip(ar);   // ok、推論補助1は到達可能

  std::counted_iterator ci{std::ranges::begin(ar), 2};
  
  iter_pair ip2(ci);  // ng、推論補助2は到達可能ではない
}

参考文献

この記事のMarkdownソース

[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ソース