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

文書の一覧

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

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

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

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

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

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

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

N4889 WG21 admin telecon meeting: 2021-09

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

N4890 WG21 2021-05 Admin telecon minutes

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

N4891 WG21 2021-06 Virtual Meeting Minutes of Meeting

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

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

N4892 Working Draft, Standard for Programming Language C++

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

N4893 Editors' Report - Programming Languages - C++

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

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

P0205R1 Efficient Seeding of Random Number Engines

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P0447R15 Introduction of std::hive to the standard library

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

以前の記事を参照

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

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

P0533R7 constexpr for cmath and cstdlib

P0533R8 constexpr for cmath and cstdlib

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

以前の記事を参照

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

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

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

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

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

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

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

P1072R8 basic_string::resize_and_overwrite

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

以前の記事を参照

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

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

P1132R8 out_ptr - a scalable output pointer abstraction

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

以前の記事を参照

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

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

P1202R3 Asymmetric Fences

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

前回の記事を参照

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

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

P1664R4 reconstructible_range - a concept for putting ranges back together

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

以前の記事を参照

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

P1675R2 rethrow_exception must be allowed to copy

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

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

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

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

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

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

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

  X() { }

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

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

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

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

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

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

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

P1689R4 Format for describing dependencies of source files

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

以前の記事を参照

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

P1708R5 Simple Statistical Functions

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

以前の記事を参照

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

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

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

以前の記事を参照

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

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

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

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

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

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

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

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

P2164R5 views::enumerate

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

以前の記事を参照

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

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

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

P2165R2 Compatibility between tuple, pair and tuple-like objects

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

前回の記事を参照

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

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

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

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

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

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

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

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

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

std::map m{t, u};

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

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

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

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

P2290R1 Delimited escape sequences

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

前回の記事を参照

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

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

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

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

以前の記事を参照

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

P2299R3 mdspans of All Dynamic Extents

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

以前の記事を参照

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

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

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

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

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

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

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

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

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

P2300R0 std::execution

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

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

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

基本的なサンプル

using namespace std::execution;

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

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

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

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

非同期inclusive_scanのサンプル。

using namespace std::execution;

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

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

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

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

P2301R1 Add a pmr alias for std::stacktrace

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

以前の記事を参照

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

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

P2321R2 zip

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

以前の記事を参照

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

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

P2322R3 ranges::fold

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

以前の記事を参照

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

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

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

P2340R1 Clarifying the status of the "C headers"

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

以前の記事を参照

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

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

P2347R0 Argument type deduction for non-trailing parameter packs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2361R1 Unevaluated string literals

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

以前の記事を参照

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

P2368R1 2021 Spring Library Evolution Polls

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

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

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

P2370R0 Stacktrace from exception

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  broken_function();
}

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

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

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

P2380R1 reference_wrapper Associations

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

以前の記事を参照

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

P2384R0 2021 Spring Library Evolution Poll Outcomes

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

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

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

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

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

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

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

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

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

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

P2387R0 Pipe support for user-defined range adaptors

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

namespace myns {

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

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

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

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

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

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

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

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

    inline constexpr transform_adopter transform{};
  }
}

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

P2388R0 Abort-only contract support

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

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

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

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

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

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

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

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

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

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

P2391R0 C23 Update Report

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

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

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

P2392R0 Pattern matching using “is” and “as”

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

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

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

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

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

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

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

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

あるいはrequires式(節)

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

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

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

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

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

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

変数宣言

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

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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

P2393R0 Cleaning up integer-class types

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

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

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

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

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

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

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

P2395R0 WG21 2021-06 Virtual Meeting Record of Discussion

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

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

P2396R0 Concurrency TS 2 fixes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

basic-format-stringクラス

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

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

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

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

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

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

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

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

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

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

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

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

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

constevalコンストラク

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

を訳すと(Powerd by DeepL)

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

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

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

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

実装例

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

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

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

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

  // ...

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

  // ...

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

  // ...
}

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

応用例

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

#include <iostream>
#include <ranges>

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

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

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

エラーになる例

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

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

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

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

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

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

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

参考文献

この記事のMarkdownソース

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

文書の一覧

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

P0009R11 MDSPAN

P0009R12 MDSPAN

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

mdspanは、multi dimensional spanの略で、連続したメモリ領域を多次元配列として参照するものです。

int* data = /* ... */

// dataをint要素4つの連続したメモリ領域として参照
auto s = std::span<int, 4>(data);

// dataをint型2x2行列の連続したメモリ領域として参照
auto ms = std::mdspan<int, 2, 2>(data);

// 要素アクセス
ms(1, 1) = 1;
ms(0, 1) = 1;

std::spanと同様に、動的な要素数を指定することもできます。

int* data = /* ... */
int size = /* ... */

auto s = std::span<int, std::dynamic_extent>(data, size);

int rows = /* ... */
int cols = /* ... */
auto ms = std::mdspan<int, std::dynamic_extent, std::dynamic_extent>(data, rows, cols);

mdspanstd::spanよりも柔軟に設計されており、レイアウトマッピングや要素へのアクセス方法などをポリシークラスによって変更することができます。

namespace std {
  template <
    class T,
    class Extents,
    class LayoutPolicy = std::layout_right,
    class Accessor = std::accessor_basic
  >
  class basic_mdspan;

  template <class T, ptrdiff_t... Extents>
  using mdspan = basic_mdspan<T, std::extents<Extents...>>;
}

LayoutPolicyは多次元インデックス(整数値の列i0, i1, ..., in)をメモリ上の一点を指す単一のインデックス(整数値i)に変換するもので、AccessorLayoutPolicyによって得られたインデックスとメモリを指すポインタを要素1つの参照へ変換するものです。

template <
    class T,
    class Extents,
    class LayoutPolicy = std::layout_right,
    class Accessor = std::accessor_basic
  >
  class basic_mdspan {
  public:
    using extents_type = Extents;
    using layout_type = LayoutPolicy;
    using accessor_type = AccessorPolicy;
    using mapping_type = typename layout_type::template mapping_type<extents_type>;

  private:
    accessor_type acc_{};
    mapping_type map_{};
    pointer ptr_{};

  public:

    template<class... SizeTypes>
    constexpr reference operator()(SizeTypes... indices) const noexcept {
      // LayoutPolicyによってインデックス列を連続領域への単一インデックスに変換し
      // Accessorによって、インデックスとポインタから要素を引き当てる
      return acc_.access(ptr_, map_(indices...));
    }
  }

例えばdoubleの2次元配列ならば、LayoutPolicyはインデックスx, yと配列の幅wを用いて、i = y * w + xを返し、Accessorはそれと領域へのポインタptr用いてptr[i]を返すものになります。

また、submdspan()という関数によって、mdspanからsliceを取得することができます。

namespace std {

  // [mdspan.submdspan], submdspan creation
  template<class ElementType, class Extents, class LayoutPolicy,
           class AccessorPolicy, class... SliceSpecifiers>
      constexpr basic_mdspan<see below>
      submdspan(const basic_mdspan<ElementType, Extents, LayoutPolicy, AccessorPolicy>& src, 
                SliceSpecifiers... slices) noexcept;
}

提案文書より、使用例

// メモリ領域へのマッピングを作成(LayoutPolicyの作成)
using Extents3D = extents<3, dynamic_extent, 7>;
layout_right::template mapping<Extents3D> map_right(10);

// メモリ領域確保
int* ptr = new int[3 * 8 * 10];

// mdspanの構築(3x10x7の三次元行列として参照)
basic_mdspan<int,Extents3D,layout_right> a(ptr, map_right);

// mdspnによる領域の初期化
for(int i0 = 0; i0 < a.extent(0); i0++) // i0 = 0 -> 2
  for(int i1 = 0; i1 < a.extent(1); i1++) // i1 = 0 -> 9
    for(int i2 = 0; i2 < a.extent(2); i2++) // i2 = 0 -> 7
      a(i0, i1, i2) = 10000 * i0 + 100 * i1 + i2;

// subspanの取得(あるいは、sliceの取得)
// [1, [4...5], [1...5]]の範囲を参照するmdspanを得る
auto a_sub = submdspan(a, 1, pair<int, int>(4, 6), pair<int, int>(1, 6));

// subspanの内容を出力
for(int i0 = 0; i0 < a_sub.extent(0); i0++) {
  for(int i1 = 0; i1 < a_sub.extent(1); i1++) {
    cout << a_sub(i0, i1) << " ";
  }
  cout << endl;
}

/* Output
10401 10402 10403 10404 10405
10501 10502 10503 10504 10505
*/

この提案は線形代数ライブラリ整備の一環として、Liblary Fundamentals v3に向けて議論されています。現在はLWGにて検討中です。

P0447R14 Introduction of std::colony to the standard library

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

以前の記事を参照

このリビジョンでの変更は、get_iterator_from_pointer()の名前を変更するなど調整したことなどです。

P0493R2 Atomic maximum/minimum

std::atomicに対して、指定した値と現在の値の大小関係によって値を書き換えるmaximum/minimum操作であるfetch_max()/fetch_min()を追加する提案。

以前の記事を参照

このリビジョンでの変更は、各関数の処理前後に値が変わらないような呼び出しをされた場合に、出力操作が行われるかを未規定にしたことです。

P0798R6 Monadic operations for std::optional

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

std::optionalvocabulary typeとして、C++プログラミングの色々なところで活用することができ、活用されています。しかし、optionalが無効値を取る可能性のある計算を連鎖させた場合、その有効性をチェックするif文が必要となるため、可読性を著しく低下させてしまいます。

// 画像に移っている猫を可愛くする関数
image get_cute_cat (const image& img) {
  return add_rainbow(
           make_smaller(
             make_eyes_sparkle(
               add_bow_tie(
                 crop_to_cat(img))));
}

そもそも画像に猫が写っていないとき、目を閉じているとき、ネクタイを付けるいい場所がない時、などそれぞれの処理は失敗する可能性があります。そこで、optionalが利用できるわけですが、現在のC++std::optionalだとifが連なる次のようなコードになります。

std::optional<image> get_cute_cat (const image& img) {
  auto cropped = crop_to_cat(img);
  if (!cropped) {
    return std::nullopt;
  }
  auto with_tie = add_bow_tie(*cropped);
  if (!with_tie) {
    return std::nullopt;
  }
  auto with_sparkles = make_eyes_sparkle(*with_tie);
  if (!with_sparkles) {
    return std::nullopt;
  }
  return add_rainbow(make_smaller(*with_sparkles));
}

このようなコードでは、ほぼ同じだけど細部が異なるボイラープレートなコードが増産され、可読性の低下を招き、記述ミスをしやすくなってしまっています。

この提案は、monadic interfacestd::optionalに追加することによって、このような処理のチェーンをより簡易に書けるようにするものです。先程までの例は次のように書き直すことができます

std::optional<image> get_cute_cat (const image& img) {
  return crop_to_cat(img)
         .and_then(add_bow_tie)
         .and_then(make_eyes_sparkle)
         .transform(make_smaller)
         .transform(add_rainbow);
}

先程まで現れていた有効性チェックのif文はand_thentransformの中に隠蔽され、それらはstd::optionalを返すことによってメソッドチェーンによる処理の連鎖が表現できるようになっています。

追加を提案しているのは次の三種類の処理で、すべてメンバ関数です

  • .transform()
  • .and_then()
  • .or_else()

.transform()std::optional<T>std::optional<U>へ変換するもので、有効値を保持している場合にのみ渡された関数を適用して有効値の変換を行います。

.and_then().transform()と同じく有効値を保持している場合にのみ渡された関数を実行してstd::optional<T>std::optional<U>へ変換するものですが、こちらは渡す関数がstd::optional<U>を直接返すことができます。それによって、有効値 → 無効値の変換が可能になります。

.or_else().and_then()と双対的な関係にあるもので、こちらはstd::optional<T>が無効値を保持してる場合にのみ渡された関数を実行するものです。無効値 → 有効値の変換が可能になります。

// 文字列を int に変換し、失敗した場合 std::nullopt を返すような関数
std::optional<int> StoI(string s);

int main() {
  std::optional<string> opts = "abc";
    
  // opts が値を持つ場合、std::optional<size_t>{ opts->size() } を返す。
  // 持たない場合、std::optional<size_t>{ std::nullopt } を返す。
  auto s = opts.transform([](auto&& s){ return s.size(); })

  // opts が値を持つ場合、StoI(*opts) を返す。
  // 持たない場合、std::optional<int>{ std::nullopt } を返す。
  // その後、結果が有効値を保持している場合にのみ出力する
  opts
    .and_then(StoI)
    .and_then([](int n) { std::cout << n << '\n'; return n; });

  // opts が値を持つ場合、自身を返す。
  // 持たない場合、与えられた関数を実行して
  // std::optional<string>{ std::nullopt } を返す。
  // その後、プログラムを終了させる
  opts
    .or_else([]{ cout << "failed.\n"; return std::nullopt; })
    .or_else([]{ std::terminate(); });
}

宣伝

この部分では「ゲーム開発者のための C++11〜C++20 技術書典 10 Ver.」という本から文章やサンプルコードを引用・改変しています。気になった方はぜひお買い求めください。

宣伝2

この記事(not 提案)を書いている人が、同じ目的でより広い対象により多様なmonadic interfaceを提供するC++20のライブラリを書いています。良かったら使ってみてください。

P1018R10 C++ Language Evolution status - pandemic edition - 2021/04

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

これらの提案はC++23入りを目指してCWGに転送されました。

P1068R5 Vector API for random number generation

<random>にある既存の分布生成器にイテレータ範囲を乱数で初期化するAPIを追加する提案。

以前の記事を参照

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

  • 提案するベクターAPIoperator()からgenerate()という名前の関数に変更
  • コンセプトを使用した制約を行うようにした
  • rangeオブジェクトを受けてその範囲を乱数で初期化するオーバーロードを追加した
  • 以前のuniform_vector_random_bit_generatorを削除し、uniform_bulk_random_bit_generator/uniform_range_random_bit_generatorの二つのコンセプトを追加した
  • パフォーマンスについて議論を追加した

などです。

P1122R4 Proposed Wording for Concurrent Data Structures: Read-Copy-Update (RCU)

標準ライブラリにRead-Copy-Update(RCU)を導入する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのレビューを受けて文言を調整したことなどです。

この提案は2021年6月の全体会議で投票にかけられ、Concurrency TS v2に導入されることが決まりました。標準ライブラリにいつ入るのかは未定です。

P1328R1 Making std::type_info::operator== constexpr

std::type_info==constexprにする提案。

この提案に先んじて、C++20ではtypeidの定数式での実行が許可されていましたが、そのメンバ関数はどれもconstexprではなかったためにstd::type_infoオブジェクトを定数式で使用することはできませんでした。

この提案は、operator==constexprを付加しておくことでstd::type_infoオブジェクトを定数式で使用できるようにするものです。

template<typename T, typename U>
constexpr bool type_eq(const T* t, const U* u) {
  return typeid(t) == typeid(u);  // 現在はここでエラー
}

int main () {
  constexpr int n = 0;
  constexpr long l = 0;
  constexpr bool b = type_eq(&n, &l);
}

この提案は当初C++20入りを目指していたのですが、LWGの議論中に実際に実装可能かどうかについて疑問が呈され、その確認に時間がかかったためC++20に間に合いませんでした。

現在はその疑問は解消されているようで、次の全体会議にてC++23入りの投票にかけられることが決まっています。

P1701R2 Inline Namespaces: Fragility Bites

inline名前空間の名前探索に関するバグを修正する提案。

以前の記事を参照

このリビジョンでの変更は、EWGでの議論で浮かび上がった選択肢について表にまとめ、標準へ提案する文言を追加した事です。

P2013R4 Freestanding Language: Optional ::operator new

フリースタンディング処理系においては、オーバーロード可能なグローバル::operator newを必須ではなくオプションにしようという提案。

以前の記事を参照

このリビジョンでの変更は、フリースタンディング処理系におけるグローバルなnew/deleteの定義が何をするかは実装定義となるようにされたことなどです。

この提案は現在CWGのレビューを終え、ライブラリの部分についてLWGのレビュー待ちをしています。それが終わったら全体投票にかけられる予定です。

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

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

以前の記事を参照

このリビジョンの変更点は、標準ライブラリの機能で同期の問題が発生しうるものをさらに除外した事です。例えば、<chrono>の時計型やロケール関連のものです。

この提案はLEWGからCWGへ転送されようとしているところで、とりあえずTransactional Memory TS v2を目指すようです。

P2093R6 Formatted output

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

前回の記事を参照

このリビジョンでの変更は、配置されるヘッダが<io>から<print>に変更されたこと、ユニコード出力時に無効なエンコーディングを置換するU+FFFDの選択を明確にしたこと、ユニコード出力時にリテラルエンコーディング(実行時エンコーディング)を使用することを明確にしたこと、文字集合を指定するためのANSIエスケープコードはこの提案を実装するためのネイティブシステムAPIとはみなされない事を明確にした、ことなどです。

P2136R3 invoke_r

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

以前の記事を参照

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

この提案は2021年6月の全体会議で承認され、C++23入りが決定しました。

P2138R4 Rules of Design<=>Specification engagement

CWGとEWGの間で使用されているwording reviewに関するルールの修正と、それをLWGとLEWGの間でも使用するようにする提案。

以前の記事を参照

このリビジョンでの変更は、Tentatively Readyという言葉を明確にしたことなどです。

P2168R3 generator: A Synchronous Coroutine Generator Compatible With Ranges

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

前回の記事を参照

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

P2280R2 Using unknown references in constant expressions

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

以前の記事を参照

このリビジョンでの変更は、参照と同じことをポインタにも拡張したことです。

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

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

void check(int const (&param)[3]) {
  constexpr auto s1 = array_size(param);    // ok、R0
  constexpr auto s2 = array_size_p(param);  // ok、R2
}

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

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

以前の記事を参照

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

P2299R1 mdspan and CTAD

P2299R2 mdspan and CTAD

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

以前の記事を参照

このリビジョンでの変更は、推論補助を追加することでこの問題に対処することにしたことです。

以前の検討では、推論補助の追加によってこの問題に対処することはできなかったのですが、どうやらそれはエイリアステンプレートでの実引数推論を実装していたGCC/MSVCのバグであったようです。

namespace std {

  template<class ElementType, class Extents, class LayoutPolicy = layout_right,
           class AccessorPolicy = default_accessor<ElementType>>
    class basic_mdspan;

  // この推論補助を追加する
  template <class ElementType, class... IndexTypes>
  explicit basic_mdspan(ElementType*, IndexTypes...)
    -> basic_mdspan<ElementType, extents<[] (auto) constexpr
                                         { return dynamic_extent; }
                                         (identity<IndexTypes>{})...>>;

  template<class T, size_t... Extents>
    using mdspan = basic_mdspan<T, extents<Extents...>>;
}

int main() {
  // 何かメモリ領域
  double* data = ...;

  // 動的サイズのmdspan構築、この提案の後ではOK
  mdspan a2(data, 64, 64);
}

P2314R2 Character sets and encodings

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

以前の記事を参照

このリビジョンでの変更は、r-char-sequence(生文字列リテラルの文字列)にユニバーサル文字名が含まれていない事を明確にした事と、文字列リテラルオブジェクトが翻訳フェーズ5で初期化済みと取れる文章の削除、提案する文言の修正などです。

P2325R3 Views should not be required to be default constructible

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

以前の記事を参照

このリビジョンでの変更は、標準に提案する文言を編集した事です。

この提案は2021年6月の本会議で投票にかけられ、C++20に向けて採択されました。歴史修正です。

P2328R1 join_view should join all views of ranges

std::ranges::join_viewの制約を緩和して、prvalueviewではないrangeを平坦化できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、LWGのレビューのフィードバックを受けて、constexprが欠けていたところに追加したことなどを修正したことです。

この提案は2021年6月の本会議で投票にかけられ、C++20に向けて採択されました。歴史修正その2です。

P2334R1 Add support for preprocessing directives elifdef and elifndef

#elifでマクロの定義の有無で条件分岐する糖衣構文となるプリプロセッシングディレクティブである#elifdef/#elifndefの提案。

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

この提案は、CWGに転送するためにEWGでの投票待ちをしています。C++23入りを目指しているようです。

P2351R0 Mark all library static cast wrappers as [[nodiscard]]

static_castのラッパであるような標準ライブラリの関数に[[nodiscard]]を付ける提案。

std::moveを始めとする標準ライブラリの型キャストラッパな関数は、コンパイラに対して引数が消費される関数として動作しているため、その戻り値を無視しても警告されません。一方、同等のことをstatic_castを始めとる言語機能のキャストによって行うと、警告が発せられます。

void f(auto T val) {
  // -Wallだと警告が発せられる
  val;
  static_cast<T&&>(val);
  // -Wallでも警告されない
  std::move(val);
}

これらの型キャスラッパな関数はその結果を使用しないと意味がないため、結果が捨てられている場合はほぼバグです。これを検出するために、それらの関数に[[nodiscard]]を付加する事でコンパイル時に警告として発せられるようにしようとする提案です。

提案されている候補は以下のものです。

  • to_integer
  • forward
  • move
  • move_if_noexcept
  • as_const
  • to_underlying
  • identity
  • bit_cast

MSVCではすでにこれらの関数に[[nodiscard]]が付加されており、筆者の方の所属企業(avast)の社内のコードベースにおいていくつかのバグを発見するのに役立っており、他の社員の人からもこのアプローチは好評のようです。

P2367R0 Remove misuses of list-initialization from Clause 24

<ranges>の規定において、誤ったリスト初期化({})の使用を修正する提案。

リスト初期化では、その初期化式の評価順序が決まっており、縮小変換が禁止されています。

<ranges>Range AdopterやCPOなどは、expression-equivalentという言葉によってある式が別の式と同じ効果を持つように規定されています。その効果では、リスト初期化が使用されていたり、カンマによる式が使用されていたりしますが、それら評価順が規定されている式を使用しているために、実装不可能な規定となっている箇所があります。

例えばdrop_viewを見てみると

The name views​::​drop denotes a range adaptor object. Let E and F be expressions, let T be remove_cvref_t<decltype((E))>, and let D be range_difference_t<decltype((E))> (中略) the expression views​::​drop(E, F) is expression-equivalent to:
- If T is a specialization of ranges​::​empty_view, then ((void) F, decay-copy(E)).
- (中略)
- Otherwise, ranges​::​drop_view{E, F}.

この効果の1つ目は((void) F, decay-copy(E))で、2つ目はranges​::​drop_view{E, F}ですが、前者はF -> Eの評価順が規定されている一方で、後者はE -> Fの順の評価順が規定されています。これは矛盾しており、実際にはviews​::​drop(E, F)のように呼び出されてからこれらの効果が選択されるため、効果の中で式の評価順をコントロールすることは実質的に不可能です。

また、リスト初期化においては定数式でその値が受け止められるときにのみ縮小変換を許可するという性質から、CPOは呼び出された定数値を伝播しチェックする必要があります。

template<typename T>
void f(T&& E) {
  // Tのdifference_typeはint32_tとすると

  // これはOKとする
  views::drop(E, int64_t()); 

  int64_t l = 0;
  // こっちはエラーとする
  views::drop(E, l);
}

それによって、このようなハンドリングを要求している事になりますが、どうやらこれは意図されたものではないようです。

この提案では、これらの問題を解消するために{}を用いているところを()に置き換えています。ただしその範囲は限定的であり、明らかにこれらの問題を来している箇所だけに留められています。

さらに、この提案を書いてる最中に発見された、views::singleに関する次の2つの問題についても同時に解決しています。

  • 暗黙の推論補助はCV修飾されたrvalueremove_cvしたり、配列型や関数型をdecayする事ができない。これはRange-v3の仕様と矛盾している
  • views::single(a_single_view)はラップではなくコピーされる

この提案はIssue解決であることもあり、LWGでのレビューが素早く終了していました。そして、2021年6月の全体会議でC++20に向けて採択されました。

P2368R0 2020 Winter Library Evolution Polls

2021年春にLEWGにおける最終投票にかけられる(た)提案のリスト。

これらはLEWGでの議論が完了したためLWGへ送付する確認を取るための投票にかけられます。ここで異論がなければ、LWGにおいて最後のチェックが行われ、ドラフトに反映されます。

このうちP2325R2、P2328R0、P2210R2の3つはC++20へ逆適用しようとしています(その後この3つは2021年6月の全体会議でC++20に向けて承認されました)。

P2372R0 Fixing locale handling in chrono formatters

P2372R1 Fixing locale handling in chrono formatters

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

C++20にて<chrono>にカレンダーとタイムゾーンの拡張が入り、<format>の追加とともにそれらに対してのフォーマッタが提供されました。これはそれぞれ別々の提案によって行われていたため、ローケルに関して設計上の問題が見落とされたままになってしまたようです。

// ロシア語のロケールをグローバルロケールに設定
std::locale::global(std::locale("ru_RU"));

std::string s1 = std::format("{}", 4.2);         // s1 == "4.2" (ローカライズされない)
std::string s2 = std::format("{:L}", 4.2);       // s2 == "4,2" (ローカライズされる)

using sec = std::chrono::duration<double>;
std::string s3 = std::format("{:%S}", sec(4.2)); // s3 == "04,200" (ローカライズされる)

このように、std::formatの設計と一貫しなくなってしまっています。この場合にロケール非依存にするためにはフォーマットを手動で行う必要があります。

また、%S %Mなどのchronoのフォーマット指定子はOを付けて%OS %OMとする事でロケール依存の表現を出力するように説明されているため、あたかもロケール非依存であるかのように思わせてくれますが、実際にはこの二つは同じ効果でどちらもロケール依存の表現を出力します。

この提案は、これらの問題を解決するために、chronoのフォーマッタをデフォルトでロケール非依存にし、他の全ての標準フォーマッタがそうであるようにL指定子でロケール依存フォーマットを明示的にオプトインするように変更するものです。

Before After
auto s1 = std::format("{:%S}", sec(4.2));
// s1 == "04,200"

auto s2 = std::format("{:L%S}", sec(4.2));
// throws format_error
auto s1 = std::format("{:%S}", sec(4.2));
// s1 == "04.200"

auto s2 = std::format("{:L%S}", sec(4.2));
// s2 == "04,200"

この提案はSG16, LEWGの議論と投票においてC++23に導入された場合にC++20へのDRとして適用される事に合意が取れています。まだLEWGでの議論の最中ですが、筆者の方がやる気なのでそうなりそうです。

P2374R0 views::cartesian_product

P2374R1 views::cartesian_product

任意個数のシーケンスの直積を取って、その元のシーケンスを生成するcartesian_product_viewの提案。

これが何をするものなのかは、サンプルコードを見ると一目瞭然でしょう。

Before After
std::vector<int> a,b,c;

for (auto&& ea : a) {
  for (auto&& eb : b) {
    for (auto&& ec : c) {
      use(ea,eb,ec);
    }
  }
}
std::vector<int> a,b,c;

for (auto&& [ea,eb,ec] : std::views::cartesian_product(a,b,c)) {
  use(ea,eb,ec);
}

このような異なるシーケンスを総当たりする多重ループを一つのループに纏めることができます。

また、次のようにすると単なる多重ループをひとまとめにできます(使いやすいかはともかく・・・)

constexpr int N = ...;
constexpr int M = ...;
constexpr int L = ...;

for (auto [i, j, k] : std::views::cartesian_product(std::views::iota(0, N), std::views::iota(0, M), std::views::iota(0, L))) {
  // ...
}

これは入力となるシーケンス1つを1つの集合とみなし、それらの間での直積集合を作ってその元(順序対)をイテレートすることに対応しており、直積のことをデカルト積(cartesian product)と呼ぶためこの名前になっています。

cartesian_product_viewの入力となるrangeforward_rangeでなくてはならず、referencepair/tupleのいずれかになり、その要素型はベースのrangeイテレータreferenceとなります。すなわち、ベースのrangeイテレータが参照を返す場合はその要素はコピーされません。

また、cartesian_product_viewはパイプライン演算子による入力をサポートしていません。なぜなら、a | views::cartesian_product(b, c)views::cartesian_product(b, c)の使用を区別できないためです。

P2375R0 Generalisation of nth_element to a range of nths

std::nth_elementを一般化したnth_elementsの提案。

std::nth_elementは入力の範囲について[first, ..., nth, ..., last)の関係にある3つのイテレータを受け取って、この範囲を[first, nth)[nth](nth, last)の3つの範囲を連結したものになるように並べ替えます。

この時、[first, nth)の最大要素をmax(nth, last)の最小要素をminとするとmax < *nth <= min(デフォルトの比較の場合)とはなりますが、範囲全体は必ずしもソートされません。また、[first, nth)(nth, last)内の順序は未規定です。

そして、nthの位置にある要素は、[first, last)をソートした場合にnthの位置に来る要素に一致します。

nth_elementsはそれを一般化し、n番目を指すイテレータnthを取る代わりに[first, ..., nth_first, ..., nth_last, ..., last)の関係にある4つのイテレータを受け取って、この範囲を[first, nth_first)[nth_first, nth_last)(nth_last, last)の3つの範囲を連結したものになるように並べ替えます。この時、(first, nth_first)の最大要素をmax[nth_last, last)の最小要素をminとするとmax < *nth_first < *nth_last <= min(デフォルトの比較の場合)となります。

そして、[nth_first, nth_last)の範囲はソートされており、、[first, last)全体をソートした場合に[nth_first, nth_last)の範囲に来る要素に一致します。それ以外のことはnth_elementと同様になります。

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

int main() {
  std::vector<int> v1 = {5, 10, 4, 7, 1, 9, 8, 6, 2};

  // 4番目に小さい値より小さい値を前に集める
  std::nth_element(v1.begin(), v1.begin() + 3, v1.end());

  for (auto x : v1) {
    std::cout << x << ", ";
  };
  // 2, 1, 4, 5, 7, 6, 8, 9, 10, 
  
  std::cout << '\n';
  
  std::vector<int> v2 = {5, 10, 4, 7, 1, 9, 8, 6, 2};

  // 3~6番目に小さい値より小さい値を前に集める
  std::ranges::nth_elements(v2, std::ranges::subrange{v2.begin() + 2, v2.begin() + 5});

  for (auto x : v2) {
    std::cout << x << ", ";
  };
  // 2, 1, 4, 5, 6, 7, 8, 9, 10, 
}

[first, last)の長さをN[nth_first, nth_last)の範囲にあるユニークな要素の数をmとすると、この操作はO(N log m)の計算量となるようです。

要するに入力範囲を部分的にソートするものですが、std::partial_sortと異なるのは開始位置が制限されていないことで、範囲の任意の部分範囲についてソートすることができます。

このアルゴリズムはNumPyにおいてnumpy.partitionとして実装されているなど、使用と分析は広く成熟しているため、C++でも利用可能とするために提案されています。

P2376R0 Comments on Simple Statistical Functions (p1708r4): Contracts, Exceptions and Special cases

P1708R4で提案されている統計関数について、例外を投げないようにする提案。

P1708R4では、必要な条件が満たされない場合にstats_errorという例外を投げるようになっています。この提案は、既存の関数(std::log, std::sqrtなど)の振る舞いや他の言語のライブラリなどを参考にして、例外を投げないようにしようとするものです。

P2377R0 [[nodiscard]] in the Standard Library: Clause 23 Iterators library

<iterator>にあるものの一部に[[nodiscard]]を付加する提案。

対象は多岐にわたるため転記しませんが、特筆するところとしては、標準ライブラリにある全てのCPOに対して戻り値型がvoidではない場合にその関数呼び出し演算子[[nodiscard]]が付加することを提案しています。

P2380R0 reference_wrapper Associations

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

この2つはまとめてassociatorと呼ばれ、名前付き要件ProtoAllocatorExecutorを満たすようなアロケータとエグゼキュータを、非同期操作に指定された完了ハンドラから取得します。

Networking TSの非同期モデルでは、associatorによって非同期処理で使用するアロケータとエグゼキュータを取得します。

// 何かの非同期処理
template<typename Handler>
void async_f(handler h) {
  // ハンドラに関連づけられたアロケータの取得
  auto alloc = std::net::associated_allocator<Handler>::get(h);

  // ハンドラに関連づけられたエグゼキュータの取得
  auto ex = std::net::associated_executor<Handler>::get(h);
}

reference_wrapper<T>はクラスオブジェクトの参照を転送するために使用でき、TCallable(関数呼び出し可能)性を受け継ぎ、関数呼び出しを内部参照にバイパスします。別の言い方をすると、Callableオブジェクトに参照のセマンティクスを与える事ができます。

<algorithm>アルゴリズム関数などもそうですが、標準ライブラリのものはCallableオブジェクトを取る時に値として受け取ります。Networking TSでも同様で、例えばコピーが重いハンドラを渡そうとするときにreference_wrapper<T>を使用したくなるのですが、reference_wrapper<T>にはassociatorのサポートが無いためそのままだとTに関連づけられたアロケータ/エグゼキュータを使ってもらう事ができません。

このような場合にreference_wrapper<T>が使えなければ良いのですが、associatorは特殊化が無い場合にデフォルトのアロケータ/エグゼキュータを使うので一見すると問題なく使えているように見えてしまいます。その際、Tに対するassociatorがユーザによって特殊化されている場合、気づかない間に間違ったコードを書いてしまう事になりかねません。

// オリジナルのハンドラ
struct my_handler {
  // ...
};

// associatorの特殊化 (1)
std::net::associated_allocator<my_handler> {
  // ...
};
std::net::associated_executor<my_handler> {
  // ...
};

int main() {
  my_handler h{};

  // カスタマイズされたassociator (1)を使用する
  async_f(h); // OK

  // デフォルトのassociatorを使用する
  async_f(std::ref(h)); // OK
}

reference_wrapperCallableオブジェクトを参照として転送する際の標準的なソリューションであり、このような使用は自然な用法であるため、associatorのサポートをデフォルトで用意しようという提案です。

P2381R0 Pattern Matching with Exception Handling

パターンマッチングにおいて、例外のキャッチのためのパターンを追加する提案。

現在のパターンマッチングP1371R3では、いくつかのエラーハンドリング方法が用意されていますが、例外に対するパターンは提供されていません。

// expectef<T, E>のようにエラーと正常値をまとめて返す
variant<rete, error_code> e() noexcept;
variant<reto, error_object> o() noexcept;

inspect(e()) {
    <rete> //...
    <error_code> //...
}
inspect(o()) {
    <reto> //...
    <error_object> //...
}

// 例外を投げうる
reta a();// throws b, c, d

// 例外をキャッチするにはinspect式をtry-catchで囲う
try {
    inspect(a()) {
        <reta> //...
    }
} catch(b) {
} catch(c) {
} catch(d) {
}

パターンマッチングは例外ベースのエラー処理をサポートしておらず、戻り値ベースの例外処理を優遇しています。標準ライブラリのほとんどのものは例外ベースのエラー処理機構を採用しており、このことは標準の仕様と一貫していません。

この提案は、パターンマッチングにおいて例外パターンを許可する事で、例外ベースのエラー処理を簡潔に書く事ができるようにするものです。この提案の後では、先ほどのコードは次のように書く事ができるようになります。

// 例外を投げうる
reta a();// throws b, c, d

inspect(a()) {
    <reta> //...
    <b> //...
    <c> //...
    <d> //...
}

エラー処理はプログラミング全般にとって基本的であるためこのような小さな改善が大きな利益をもたらす可能性があります。また、C++におけるエラー処理方法と消費方法の多様性は多くの問題を引き起こしていますが、多くの提案や議論では消費の方法よりもパフォーマンスやエラーの生成について焦点を当てています。この提案は消費に焦点を当て、多様なエラー処理機構に対するその消費処理をなるべく統一的に扱えるようにするものです。

P2382R0 Presentation Slides for P2123R0

P2123R0の解説するためのスライド。

おそらくLEWGのメンバに向けて書かれたものです。

P2123R0は、ABI安定性の向上のために型システムを拡張しよう!という提案で、interface(tag){}のようなブロックでABIバージョンごとに処理を分け、それを使用する時にinterface(tag)を付加することでしようするバージョンを決定できるようにしようとするものです。

おわり

[C++]C++20モジュールの変遷 - Module TSからC++20DISまで

C++20のモジュールは確かにある一つの提案がベースになっているのですが、その後C++20策定完了までの間に複数の提案やIssue報告によってそこそこ大きく変化しています。その結果、C++20のモジュールはその全体像を把握するためにどれか一つの提案を読めばわかるものではなく、関連する提案を追うのもC++20のDIS(N4861)を読み解くのも辛いものがあり、ただでさえ難解な仕様を余計に分かりづらくしています。

この記事は、C++20の最初のモジュールの一歩手前から時系列に沿って、モジュールがどのように変化していったのかを眺めるものです。

Working Draft, Extensions to C++ for Modules (Module TS)

Module TSと呼ばれているもので、最終的なC++20の仕様のベースとなっているものです。

意味論の細かい差異はあれど、現在書くことのできる基本的なモジュールの構文や仕様(export/import/module宣言など)はここで決定されました。

Another take on Modules (ATOM Proposal)

この提案は、モジュールシステムを別の観点から見つめ直し、モジュールTSにあったいくつかの問題を修正しようとするものです。

TSからの変更点としては

  • export/moduleの非キーワード化
  • module宣言は翻訳単位の先頭になければならない
  • モジュールパーティションの導入
  • private/publicimport
  • マクロのexport
  • ヘッダユニット(Legacy header unitsと呼ばれていた)
    • ヘッダのimport
    • #includeimportへの置換
  • インスタンス化経路(path of instantiation

これはあくまでモジュールTSをベースとしており、対案と言うよりはモジュールTSを補間し修正しようとする提案です。他の提案からは、ATOM Proposalと呼ばれます。

Merging Modules (最初期のC++20モジュール)

この提案はC++20に最初に導入されたモジュール仕様です。モジュールTS(N4720)にATOM提案(P0947R1)をマージする形でモジュールは導入されました。

ここで、新たに次のものが追加されました

  • グローバルモジュールフラグメントの導入
    • 正確には、グローバルモジュールフラグメントは明示的に導入するものとなった(TSでは#includeによるヘッダインクルードが実質的にグローバルモジュールフラグメントを導入していた)
  • プライベートモジュールフラグメントの導入
  • semantic boundaries ruleの導入
    • 「(定義を持つものは)以前の定義が到達可能なところで再定義されてはならない」と言うルール
  • ヘッダユニットからのマクロの定義位置
    • import宣言の直後と規定

また、次のものは導入されませんでした

  • モジュールTS
    • Proclaimed ownership declaration
  • ATOM提案

参考資料

Relaxing redefinition restrictions for re-exportation robustness

当初のモジュールでは、従来の#includeは可能ならすべてヘッダユニットのimportに置き換えられていました。そして、ヘッダユニットの宣言はグローバルモジュールに属する宣言として扱われます。

またODR要件が緩和されており、以前の定義が到達可能でなければ定義は複数あってもいい、とされていました。ある場所から定義が到達可能というのは、その場所で直接見えているimport宣言をたどっていった先で定義が見つかることで、この到達可能な定義の集合に同じ宣言に対する定義が複数含まれているとエラーとなります。

C++17以前と同様に、グローバルモジュール(非モジュール内)においては、その宣言が同一であれば異なる翻訳単位で定義が重複しても良い、というルールがあります。ただし、それらの定義がモジュールのインポートによって到達可能となってしまう場合は定義の重複は許されません。

/// M.cpp

module;
#include "a.h" // struct A {};
export module M;

// b.hはインポート可能なヘッダ
export import "b.h"; // struct B {};

// 宣言が破棄されないようにする
export A f();
/// src.cpp

import M;
// 構造体AとBはともにこの場所から到達可能

#include "a.h" // error, Aの再定義
#include "b.h" // OK, b.hのインクルードはimportに変換され、Bの定義は再定義とならない

この時、b.hが次のようになっていると多重定義エラーが発生する可能性があります。

/// b-impl.h (インポート可能なヘッダではない

#ifndef B_IMPL_H
#define B_IMPL_H
struct B {};
#endif
/// b.h (インポート可能なヘッダ

#include "b-impl.h"
/// src.cpp

import M;
#include "b-impl.h" // error, Bの再定義

インポート可能なヘッダというのは実装定義であり、このようなエラーはコンパイラによって発生したりしなかったりするかもしれません。また、従来の#includeであれば、このようなケースはODRの例外規定によって特別扱いされていたはずです。

このようなグローバルモジュールに属するエンティティの再エクスポート時の不可思議な振る舞いを避けるために、この提案によって次のように仕様が調整されました。

  • グローバルモジュールに属するエンティティの定義は、定義が到達可能かどうかに関係なく、各翻訳単位で最大1つの定義の存在を許可する。
  • 名前付きモジュールに属するエンティティに対するODRの例外規定の削除。
    • ODRの例外規定とは、定義が同一であれば複数の翻訳単位に現れてもいい、というルール。
    • テンプレートをヘッダに書いてコンパイルする際に実装を容易にするための特殊なルールだったが、モジュールのimportは宣言をコピペしないのでモジュールでは不要。
  • インポート可能なヘッダの#includeは、importに置き換えても よい という表現に変更
    • そもそも、インポート可能なヘッダを常にimportしていたのは、先程のb.hの最初の例のようなケースで再定義エラーが起こらないようにするため。この提案の変更によって前提となる問題が解決された。
// この提案適用後では
import M;
#include "b-impl.h" // OK, Bの定義は到達可能だが、この翻訳単位では最初の定義

この提案によってグローバルモジュールにおけるODR周りの事はC++17までとほとんど同様となり、名前付きモジュール内でだけODR要件が厳しくなります。

  • グローバルモジュール : 宣言に対する定義は各翻訳単位で唯一つであり、全ての定義は同一でなければならない
  • 名前付きモジュール : 宣言に対する定義はプログラム内で唯一つでなければならない

どちらの場合でも、同じ翻訳単位内での多重定義はコンパイルエラーとなりますが、翻訳単位が分かれている場合にこのルールに違反していると必ずしもコンパイルエラーとはならず、未定義動作となります。

参考資料

Mitigating minor modules maladies

この提案はモジュールによって問題となる、3つの特殊なケースのバグを修正するものです。

1. using/typedef

※この問題はコンパイラの実装に大きくかかわる物で、今一よくわかりませんでしたので、結論のみを書いておきます・・・

この提案では、リンケージを与えるためのtypedefの対象となる構造体は次のものを含むことができないようにします。

  • 非静的データメンバ・メンバ列挙型・メンバ型(入れ子クラス)を除くすべてのメンバ
  • 基底クラス
  • データメンバに対するデフォルト初期化子
  • ラムダ式

この提案によって、リンケージを与えるためのtypedefC言語互換のためだけの機能であることが明確となり、その対象となる構造体はC互換の構造体に限定されるようになります。また、typedef/usingによって名前のリンケージを変更できないことが明確となります。

これはモジュールの内外を問わず適用されるため、破壊的変更となります。

2. エクスポートブロック内でのstatic_assert

モジュールにおけるexport宣言では、名前を導入しないタイプの宣言をexportすることができません。

export static_assert(true); // error、エクスポートできない

export {
  struct Foo { /*...*/ };
  static_assert(std::is_trivially_copyable_v<Foo>); // error、エクスポートできない

  struct Bar { /*...*/ };

  template<typename T>
  struct X { T t; };

  template<typename T>
  X(T) -> X<T>;  // error、エクスポートできない

  // ...

#define STR(x) constexpr char x[] = #x;
  // セミコロン(;)が余計に一つ付くが、エクスポートできないのでエラー
  STR(foo);
  STR(bar);
#undef X
}

この提案では、このようなエクスポートブロックの内部でのみ、宣言が少なくとも1つの名前を導入しなければならない、というルールを削除します。

export static_assert(true); // error、エクスポートできない

export {
  struct Foo { /*...*/ };
  static_assert(std::is_trivially_copyable_v<Foo>); // OK

  struct Bar { /*...*/ };

  template<typename T>
  struct X { T t; };

  template<typename T>
  X(T) -> X<T>;  // OK

  // ...

#define STR(x) constexpr char x[] = #x;
  // 両方OK
  STR(foo);
  STR(bar);
#undef X
}

ただし、ブロックではない通常のexport宣言においては名前を導入しない宣言をエクスポートできないのは変わりません。

3. デフォルト引数の不一致

inlineではない関数では、デフォルト引数を翻訳単位ごとに異なるものとすることができます。また、テンプレートのデフォルトテンプレート引数も翻訳単位ごとに異なるものとすることができます。

/// a.h
int f(int a = 123);
/// b.h
int f(int a = 45);
/// main.cpp
import A;     // a.hを間接的にインクルードしているが、エクスポートはしていない
import "b.h";

// a.hのf()は到達可能
// b.hのf()は可視であり到達可能
int main() {
  int n = f();  // 結果は・・・?
}

同じ宣言に対して異なるデフォルト引数が与えられた複数の宣言が同じ翻訳単位内で出現する場合はコンパイルエラーとなりますが、モジュールにおいては一方のみが名前探索で可視であるが、両方の宣言に到達可能となる場合があります。当初の仕様ではこの場合にどう振舞うかは未規定でした。

この提案では、異なる翻訳単位の同じ名前空間スコープの2つの宣言が、同じ関数引数に異なるデフォルト引数を、あるいは同じテンプレート引数に異なるデフォルトテンプレート引数を指定することをそれぞれ禁止します。ただし、異なるデフォルト引数を持つ複数の宣言が同時に到達可能とならない限り、コンパイルエラーとならない可能性があります。

/// main.cpp
import A;     // a.hを間接的にインクルードしているが、エクスポートはしていない
import "b.h";

// a.hのf()は到達可能
// b.hのf()は可視であり到達可能
int main() {
  int n = f();  // NG、コンパイルエラー
}

この変更は、その宣言がモジュールにあるかどうかにかかわらず適用されます。つまり、これは破壊的変更となります。

Recognizing Header Unit Imports Requires Full Preprocessing

この提案は、依存関係スキャンを簡易化・高速化するために、ヘッダユニットのインポートを#includeとほとんど同等に扱えるようにするものです。

当初のモジュールでは、import宣言はほとんどC++のコードとして解釈され、プリプロセス時にはヘッダユニットのインポートに対してマクロのエクスポートを行う以外のことをしていませんでした。そのため、ヘッダユニットのインポートを識別するには翻訳フェーズ4(プリプロセスの実行)を完了する必要がありました。

すなわち、import宣言はほとんどどこにでも現れる可能性があり、マクロ展開を完了しなければimport宣言を抽出することができません。

これは従来#includeに対して行われていた依存関係スキャンに対して、実装が困難になるだけではなく、速度の面でも明らかに劣ることになります。例えば、#includeに対する依存関係スキャンでは、プリプロセッシングディレクティブ以外の行は何もせず無視することができ、#includeは1行で書く必要があるため行をまたぐような複雑なマクロ展開をしなくても良くなります。

この提案では、(export) importによって開始される行をプリプロセッシングディレクティブとして扱うようにします。それによって、(export) importをマクロ展開によって導入する事ができなくなり、(export) importは空白を除いて行の先頭に来ていなければならず、import宣言は1行で書かなければならなくなります。

プリプロセスの最初の段階ではモジュールのインポートもヘッダユニットのインポートもまとめて扱われ、その後ヘッダユニットのインポートに対してエクスポートされたマクロのインポートを行います。最後に、importトークンを実装定義のimport-keywordに置き換えて、importディレクティブのプリプロセスは終了します。

翻訳フェーズ5以降、つまりC++コードのコンパイル時には、このように導入されたimport-keywordによるものだけがimport宣言として扱われるようになります。

なお、(export) importトークンおよびimportディレクティブを終了する;と改行だけがマクロで導入できないだけで、import対象のヘッダ・モジュール名はマクロによって導入することができます。

この提案によって可能な記述は制限される事になります。

Before After
// 行中にあっても良かった
int x; import <map>; int y;
// importディレクティブは1行で独立
int x;
import <map>;
int y;
Before After
import <map>; import <set>;
// それぞれ1行づつ書く
import <map>;
import <set>;
Before After
// 複数行に渡っていても良かった
export
import
<map>;
// importディレクティブは1行で完結する
export import <map>;
Before After
// プリプロセッサによる切り替えが可能だった
#ifdef MAYBE_EXPORT
export
#endif
import <map>;
// importディレクティブの一部だけを#ifで変更できない
#ifdef MAYBE_EXPORT
export import <map>;
#else
import <map>;
#endif
Before After
#define MAYBE_EXPORT export
MAYBE_EXPORT import <map>;
// (export) importはマクロによって導入できない
#define MAYBE_EXPORT
#ifdef MAYBE_EXPORT
export import <map>;
#else
import <map>;
#endif

この提案の内容はのちにP1857R3によって大幅に(より制限する方向に)拡張されることになります。

参考資料

Standard library header units for C++20

この提案は、少なくとも標準ライブラリのヘッダはヘッダユニットとしてインポート可能であることを規定するものです。

C++20にモジュールが導入されるのは確定的で、そうなると標準ライブラリをモジュールとして提供する(できる)必要が生じます。この提案の時点ではその作業が間に合うかは不透明であり(実際間に合わなかった)、間に合わなかった場合は、それぞれのベンダーからそれぞれの(互換性のない)方法でモジュール化された標準ライブラリが提供され、C++エコシステムに分断をもたらす事になりかねません。

この提案では、既存の標準ライブラリをモジュールとして提供するための最低限のメカニズムを提供しつつ、将来的な標準ライブラリの完全なモジュール化を妨げる事が無いようにするものです。

そのために、C++ 標準ヘッダは全てヘッダユニットとしてインポート可能であると規定し、標準ライブラリへのアクセス手段としての標準ヘッダのインポートを規定します。そして、モジュール単位(名前付きモジュール)の中での標準ライブラリヘッダの#includeはグローバルモジュールフラグメントの中でのみ行える事が規定されました(診断は不要とあるので、これに従わなくてもコンパイルエラーとはならない可能性があります)。

なお、C互換の標準ヘッダ(<cmath>, <cassert>などの<cxxx>系のヘッダ)はインポート可能ではありません。これらのヘッダは事前のマクロ定義に大きく影響を受けますが、ヘッダユニットも含めたモジュールは外で定義されたマクロが内部に影響を及ぼさないため、インポータブルでは無いためです。

また同時に、stdから始まる全てのモジュール名を将来の標準ライブラリモジュールのために予約します。

NBコメントへの対応1

このIssueは、規格文書中で標準ライブラリのエンティティ名にアクセスする手段を記述している所にヘッダユニットのimportを加えるものです。P1502R1の内容を補強するもので、P1502R1ではおそらく見落とされていたものです。

このIssueは、モジュールのインターフェースという言葉を使用していたために、エクスポートしていない関数名がADLを介して表示されるかのように読めてしまっていた部分の表現を修正するものです。

意味するところはこの前後で変わらず、モジュールの内部にあるエクスポートされた宣言はテンプレートのインスタンス化経路上で可視となりますが、エクスポートされていない宣言はいかなる場合にも可視になりません。

このIssueは、予約するモジュール名について名前空間の予約と記述を一貫させるものです。

意味するところは変わらず、stdに数字が続く名前空間名、stdから始まるモジュール名は全て予約されます。

このIssueは、グローバルモジュールフラグメントに関する箇所の規格参照用のラベルが[module.global]だったり[cpp.glob.frag]だったりしていたのを、[xxx.global.frag]に一貫させるものです。

このIssueは、モジュールに関するサンプルコードで翻訳単位の境界が曖昧だった所を明確にするものです。

Core Language Changes for NB Comments at the November, 2019 (Belfast) meeting

この提案は2019年11月のベルファストの会議において採択されたコア言語のIssue解決(NBコメントについて)をまとめたものです。モジュールに関連するものは4件あります。

これは、それまで規格としての記述のみで利用法が不明瞭だったプライベートモジュールフラグメントについて、サンプルコードを追加するものです。

これは、ヘッダユニットのインポートが再起して巡回する事が無いことを明確に記述するものです。

それまで、モジュールのインポートはインターフェース依存関係という言葉を用いて巡回インポートが禁止されていましたが、ヘッダユニットについては特に規定がありませんでした。

ここでは、インターフェース依存関係の対象にヘッダユニットを含めることで、モジュールと同様に巡回インポートを禁止します。あらゆる巡回インポートはコンパイラによって検出され、コンパイルエラーとなります。

これは、コマンドラインオプション(-Dなど)によって定義されたマクロ名がヘッダユニットからエクスポートされないことを規定するものです。

これによって、そのようなマクロ名が重複したり、それがコンパイラによって異なったりする事が防止されます。ただ、これはどうやら文面として強制するものでは無いようです・・・

これは、グローバルnew/deleteを使うための<new><=>の戻り値型を使用するための<compare>など、言語機能の利用のために標準ライブラリヘッダのインクルードの必要が規定されているものについて、ヘッダユニットのimportも認めるようにするものです。

Resolution to US086

この提案によって解決されるIssueは、あるモジュール単位Iを同じモジュール内の他のモジュール単位Mがインポートする時に、Iのグローバルモジュールフラグメントにあるインポート宣言を暗黙的にインポートしないようにするものです。

同じモジュール内にあるモジュール単位をインポートするとき、インポート対象のモジュール単位内でインポートされているすべての翻訳単位をインポートします。2つのモジュール単位が別々のモジュールに属する場合のインポートは再エクスポート(export imprt)されている翻訳単位のみをインポートしますが、同じモジュール内ではインポート宣言がより多くの翻訳単位をインポートすることになります。

グローバルモジュールフラグメントは#includeをモジュール内で安全に行うための宣言領域であり、そこにあるインポート宣言は#includeimportへの置換によって導入されたものでしょう。それらはグローバルモジュールに属するものでありIの一部ではなく、IからエクスポートされMから到達可能となるのは不適切です。

export宣言はグローバルモジュールフラグメントに直接的にも間接的(#includeやマクロ展開など)にも書くことはできないので、グローバルモジュールフラグメントでimportされている翻訳単位をインポートしてしまう可能性があるのは同じモジュール内でのモジュール単位のインポート時だけです。

当初の仕様ではその考慮は抜けており(モジュールTSでは考慮されていましたが、ATOMとのマージ時にグローバルモジュールフラグメントが導入されたことで見落とされていた様子)、グローバルモジュールフラグメントのインポート宣言がモジュールのインターフェースの一部となってしまっていたため、この提案では明示的にそうならないことを規定しています。

/// uses_vector.h
import <vector>; // #includeからの置換である可能性がある
/// partition.cpp
module;
#include "uses_vector.h" // import <vector>; と展開される
module A:partition;

// この中でstd::vector<int>を使っているとする。
/// interface.cpp
module A;
import :partition;

// 必ずコンパイルエラーになるようになる
// ここでは<vector>はインポートもインクルードもされていない
std::vector<int> x; 

以前の仕様では、最後のstd::vectorの使用がwell-definedとなってしまっていました。

Dynamic Initialization Order of Non-Local Variables in Modules

当初の仕様では、モジュールとそれをインポートする翻訳単位の間で静的記憶域期間を持つオブジェクト(すなわちグローバル変数)の動的初期化順序が規定されていなかったために、std::coutの利用すら未定義動作を引き起こす可能性が潜んでいました。

import <iostream>;  // <iostream>ヘッダユニットのインポート

struct G {
  G() {
    std::cout << "Constructing\n";
  }
};

G g{};  // Undefined Behaior!?

このような場合でも安全に利用できるようにするために、モジュールを含めた翻訳単位間での静的オブジェクトの動的初期化に一定の順序付けを規定するようにします。

ある翻訳単位がヘッダユニットも含めてモジュールをインポートする時、そのモジュールに対してインターフェース依存関係が発生します。インポートが絡む場合の動的初期化順序はこのインターフェース依存関係を1つの順序として初期化順序を規定します。ただし、この初期化順序は半順序となります(すなわち、順序が規定されない場合があります)。

同じ翻訳単位内での動的初期化順序はその宣言順で変わりありません。これは、別の翻訳単位をインポートしたときに、インポート先にある静的変数とインポート元の静的変数との間の動的初期化順序を最低限規定するものです。

参考資料

Core Language Changes for NB Comments at the February, 2020 (Prague) meeting

これは、言語リンケージ指定を伴うブロック内でのimport宣言を許可するものです。

例えばextern "C"なブロック内でCのヘッダを#includeしている場合にも、そのファイルがC++としてコンパイルされていればそのヘッダをimportに置換することができるはずです。しかし以前の仕様ではimportのリンケージ指定もリンケージブロック内でのimportも許可されていなかった(import宣言はグローバル名前空間スコープにのみ現れることができた)ため、その場合は常に#includeするしかありませんでした。

このIssueの解決では、直接的に書くことができないのは従来通りですが、#include変換の結果としてヘッダユニットのimportが現れるのが許可されるようになります。ただし、C++言語リンケージ指定以外に現れるimport宣言は実装定義の意味論で条件付きのサポートとなります。

extern "C" import "importable_header.h" // NG、直接書けない

extern "C" {
  #include "importable_header.h"  // OK、ヘッダユニットのインポートに変換可能
                                  // ただし、実装依存のサポート

  import "importable_header.h"    // NG、直接書けない
}

extern "C++" {
  #include "importable_header.h"  // OK、ヘッダユニットのインポートに変換可能
  
  import "importable_header.h"    // NG、直接書けない
}

このことは、構文定義を変更してインポート宣言をおよそ宣言が書ける場所にどこでも書けるようにしたうえで、文書でインポート宣言を書ける場所をグローバル名前空間スコープに限定しておき、リンケージ指定ブロック内で(#include置換の結果として)インポート宣言が間接的に現れることを許可する形で表現されており、少しややこしいです。

Translation-unit-local entities

これは名前付きモジュールのインターフェースにある内部リンケージ名がそのモジュールの外部へ露出する事を禁止するものです。

これは特に、モジュール外部でインライン展開されうる関数にて問題になっていました。

/// mymodule.cpp
module;
#include <iostream>
export module mymodule;

// 内部リンケージ
static void internal_f(int n) {
  std::cout << n << std::endl;
}

namespace {
  // 内部リンケージ
  int internal_g() {
    return 10;
  }
}

// エクスポートされている、外部リンケージ
export inline int external_f(int n) {
  // この関数がインライン展開されると・・・
  internal_f(n);
  return n + internal_g();
}

名前付きモジュールにおけるinline関数がexportされる場合、その定義はそのモジュールのインターフェースに無ければなりません。そのため、exportされたinline関数はinline指定の本来の効果(関数のインライン展開の指示)の適用対象となります。

インライン展開される関数の本体から内部リンケージ名を参照していると、本来翻訳単位を超えて参照できないはずの内部リンケージ名がインライン展開によって翻訳単位の外側から参照されてしまう事になります。内部リンケージ名の翻訳単位外への暴露は望ましい動作では無いため、この提案によって禁止されました。

名前付きモジュールのインターフェースに存在する外部への露出が禁止されるもののことを、翻訳単位ローカルのエンティティ(TU-local Entities)と呼びます。TU-localエンティティの正確な定義は複雑ですが、ほぼ内部リンケージ名を持つ関数・変数・型のことを指します。

それらTU-localエンティティがinline関数などによって翻訳単位の外に曝露する可能性のある時、コンパイルエラーとなります。注意なのは、TU-localエンティティが曝露された時ではなく、その可能性がある段階でコンパイルエラーとなる事です。

export module M;

// 内部リンケージの関数
static constexpr int f() { return 0; }

static int f_internal() { return f(); } // 内部リンケージ、OK
       int f_module()   { return f(); } // モジュールリンケージ、OK
export int f_exported() { return f(); } // 外部リンケージ、OK

// 外部orモジュールリンケージを持つinline関数は内部リンケージ名を参照できない
static inline int f_internal_inline() { return f(); } // OK
       inline int f_module_inline()   { return f(); } // NG
export inline int f_exported_inline() { return f(); } // NG

もう一つ、inline関数では無いけれどほぼ同じ振る舞いをするものにテンプレートがあります。テンプレートの厄介なところは、インスタンス化されるまで何を参照しているかが確定しない事にあります。そのため、テンプレートでは、インスタンス化された時に内部リンケージ名を参照する可能性がある場合にコンパイルエラーとなります。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

// 内部リンケージ
static void f(S1);  // (1)

export template<typename T>
void f(T t);  // (2)

// インスタンス化前はエラーにならない
export template<typename T>
void external_f(T t) {
  f(t);
}
/// main,cpp
import mymodule;

struct S2{};

void f(S2);  // (3)

int main() {
  S1 s1{};
  S2 s2{};

  external_f(10);  // OK、(2)を呼ぶ
  external_f(s2);  // OK、(3)を呼ぶ
  external_f(s1);  // NG、(1)を呼ぶ
}

内部リンケージ名を参照する可能性がある場合というのは、直接的に現れていなかったとしても、関数オーバーロードの候補集合に内部リンケージな関数が含まれている場合です。その場合使用する関数の決定を待たずにコンパイルエラーとなります。

テンプレートに関しては例外があり、内部リンケージ名を外部から参照しエラーとなる宣言であっても、モジュールのインターフェース内で予め特殊化されインスタンス化済みである時はエラーとなりません(inline指定が無ければ)。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
void external_f(T t) {
  f(t);
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1); // (2)
/// main,cpp
import mymodule;

int main() {
  S1 s1{};

  external_f(s1);  // OK、(2)でインスタンス化済
}

この場合、モジュールは予めコンパイルされているはずなので、インスタンス化済のテンプレートのインスタンス化を省略し、通常の関数と同様にシグネチャのみで参照することができます。テンプレートはinline指定がなければインライン展開されるとは限らず、その必要がありません。

このようにこの提案の後では、名前付きモジュールにおける関数に対するinline指定はインライン展開の対象であることをコンパイラに伝えるマーカーとしての本来の役割のみを担うようになります。

参考資料

ABI isolation for member functions

これは、名前付きモジュール内で定義されたクラスについて、その定義内で定義されているメンバ関数の暗黙inlineをしなくするものです。

先ほどのP1815R2の変更によって、モジュールのインターフェース内のinline関数内での内部リンケージ名の使用がコンパイルエラーとなるようになります。これによって大きな影響を受けるのは、クラスの定義内で定義されているメンバ関数です。

クラス定義内で定義されているメンバ関数は暗黙inlineであり、P1815R2の影響を強く受けることになります。

export module M;

// 内部リンケージの関数
static constexpr int f() { return 0; }

// エクスポートされ外部リンケージを持つクラス定義
export struct c_exported {
  int mf_exported();
  int mf_exported_inline() { return f(); } // NG、暗黙inline
};

int c_exported::mf_exported() { return f(); } // OK、暗黙inlineではない

これを避けようとすると、メンバ関数は全てクラス外で定義することになってしまい、冗長な記述が増え、非メンバ関数との一貫性がなくなります。この辺りの仕様は複雑なので、このことはユーザーにとって意味がわからないエラーとなるかもしれません。

クラス定義内で定義されたメンバ関数が暗黙inlineなのは、ヘッダに定義を書いて複数の翻訳単位でインクルードした時に多重定義エラーを起こさないためなので、モジュールの利用においてはほとんど必要ありません。

そのため、この提案では名前付きモジュール内に限って、クラス定義内で定義されたメンバ関数に対する暗黙inlineを行わないようにします。これによって、モジュールにおけるクラスの定義は今まで通りに行う事ができ、複雑なことを気にする必要は無くなります。

export module M;

...

export struct c_exported {
  int mf_exported();
  int mf_exported_inline() { return f(); } // OK、暗黙inlineではない
};

ただし、このことはモジュールの外側(グローバルモジュール)においては従来通りです。モジュールではないところで定義されたクラスのクラス定義内で定義されたメンバ関数は相変わらず暗黙inlineです。

Modules Dependency Discovery

これは、moduleimportを書くことのできる場所や形式を制限するものです。

P1703R1も同様の目的の変更でしたが、moduleはなんら制限されておらず、importを使用する既存のコードへの影響が小さくありませんでした。この提案はP1703R1のアプローチをさらに進めて、moduleを用いる構文についても書き方や書ける場所を制限し、かつimportmoduleを使用している既存のコードへの影響を減らそうとするものです。

この提案では、次の条件を満たすもので始まる行は、inportディレクティブとmoduleディレクティブとして扱われるようになります。

  • import : 以下のいずれかが同じ行で後に続くもの
    • <
    • 識別子(identifier
    • 文字列リテラル
    • :::とは区別される)
  • module : 以下のいずれかが同じ行で後に続くもの
    • 識別子
    • :::とは区別される)
    • ;
  • export : 上記2つの形式のどちらかの前に現れるもの

これらは新しいプリプロセッシングディレクティブとして扱われますが、プリプロセッシングディレクティブの扱いは従来通りであるため、ここでのimport, module, exportはマクロによって置換されたり導入されたりせず、ディレクティブは1行で書く必要があります。

// これらは各行がプリプロセッシングディレクティブとみなされる
#                     
module ;              
export module leftpad;
import <string>;      
export import "squee";
import rightpad;      
import :part;

// これらの行はプリプロセッシングディレクティブではない
module            
;                     
export                
import                
foo;                  
export                
import foo;           
import ::             
import ->             

これによってまず、インポート宣言、モジュール宣言、グローバルモジュールフラグメント、プライベートモジュールフラグメントの構文は、マクロによって導入されず、1行で書かなければなりません。ただし、インポート対象の名前やモジュール名はマクロによって導入することができます。

なお、通常のexport宣言はこれらの処理の対象ではありません。exportから始まるプリプロセッシングディレクティブはあくまで、すぐ後にimport/moduleが現れるものです。

これらのディレクティブに含まれるimport, module, exportプリプロセッサによってimport-keyword, module-keyword, export-keywordに置き換えられ、この*-keywordによるものがC++コードとしてのインポート宣言やモジュール宣言として扱われるようになります。

例えば次のようなコードは

module; // グローバルモジュールフラグメント
#include <iosream>
export module sample_module;  // モジュール宣言

// インポート宣言
import <vector>;
export import <type_traits>;

// エクスポート宣言
export int f();

// プライベートモジュールフラグメント
module : private;

int f() {
  return 20;
}

プリプロセス後(翻訳フェーズ4の後)に、おおよそ次のようになります。

__module_keyword;
#include <iosream>
__export_keyword __module_keyword sample_module;

__import_keyword <vector>;
__export_keyword __import_keyword <type_traits>;

// export宣言はプリプロセッシングディレクティブでは無い
export int f();

__module_keyword : private;

int f() {
  return 20;
}

__import_keywordなどは実装定義なので実際にどう書き換えられるかは分からず、それを直接書くための構文は用意されていません。

そしてもう一つの大きな変更は、プリプロセスの一番最初の段階でソースファイルがモジュールファイルなのか通常のファイルなのかを判定し、モジュール宣言、グローバルモジュールフラグメントとプライベートモジュールフラグメントはモジュールファイルだけに現れることが出来るように規定されている事です。

この判定は、ファイルの一番最初に現れる非空白文字がmoduleあるいはexport moduleで始まっているかどうかをチェックすることで行われ、それがある場合にのみモジュールディレクティブに対応するディレクティブ(の処理方法)が定義されます。

通常のファイルとして処理された場合でもインポートディレクティブを処理することはできますが、モジュールディレクティブは対応するディレクティブが定義されないため、コンパイルエラーとなります。

この事は、#ifdefなどによってあるファイルがモジュールであるかヘッダファイルであるかを切り替える、ような事ができないことを意味しています。

これらの事はCプリプロセッサのEBNFによる構文定義の中で表現されており、少し複雑です。

Before After
// OK、モジュール宣言
export
module x
;
// -Dm="export module x;"
m   // OK
module;
#define m x
export module m;  // OK
module;
#if FOO
export module foo;  // OK
#else
export module bar;  // OK
#endif
module;
#define EMPTY
EMPTY export module m;  // OK
#if MODULES
module;
export module m;  // NG
#endif
#if MODULES
export module m;  // OK
#endif
module y = {};         // NG

::import x = {};       // OK
::module y = {};       // OK

import::inner xi = {}; // NG、インポートディレクティブ
module::inner yi = {}; // OK

namespace N {
  module a;            // OK
  import b;            // NG、インポートディレクティブ
}

#define MAYBE_IMPORT(x) x
MAYBE_IMPORT(
  import <a>;          // UB
)
#define EAT(x)
EAT(
  import <a>;          // UB
)

void f(Import *import) {
  import->doImport();  // NG、インポートディレクティブ
}
// NG
export
module x  // モジュールディレクティブ
; 
// -Dm="export module x;"
m   // NG
module;
#define m x
export module m;  // OK
module;
#if FOO
export module foo;  // NG
#else
export module bar;  // NG
#endif
module;
#define EMPTY
EMPTY export module m;  // NG
#if MODULES
module;
export module m;  // NG
#endif
#if MODULES
export module m;  // NG
#endif
module y = {};         // NG

::import x = {};       // OK
::module y = {};       // OK

import::inner xi = {}; // OK
module::inner yi = {}; // OK

namespace N {
  module a;            // NG、モジュールディレクティブ
  import b;            // NG、インポートディレクティブ
}

#define MAYBE_IMPORT(x) x
MAYBE_IMPORT(
  import <a>;          // UB
)
#define EAT(x)
EAT(
  import <a>;          // UB
)

void f(Import *import) {
  import->doImport();  // NG、インポートディレクティブ
}

さらに、モジュールファイル内においては#includeによってimportディレクティブが現れる事が禁止されます。これは#includeによって展開されたファイル内にimportディレクティブがあってはならないという事ですが、インポート可能ヘッダの#includeをヘッダユニットのインポートに置換する事が行われないことも意味します。

/// header.hpp

import <iostream>;
/// mymodule.cpp
module:
#include "header.h"  // NG
export module mymodule;

#include "header.h"  // NG
import "header.h";   // OK

module : private;
#include "header.h"  // NG
/// main.cpp

#include "header.h"  // OK
import "header.h";   // OK

int main(){}

これは検出されコンパイルエラーとなります。

参考資料

Issueの解決3

これは、エクスポートされたインポート宣言がモジュールインターフェース以外の場所で現れることを禁止するものです。

/// M.cpp

export module M;

// OK、モジュールAをインポートしつつエクスポート
export import A;
/// M_impl.cpp

module M; // Mの実装単位(not インターフェース単位)

// NG、export importはここにかけない
export import B;
/// main.cpp

// NG、export importはここにかけない
export import M;

int main() {}

以前は特にケアされていなかったのでコンパイルエラーになっていませんでしたが、この変更によって明確にエラーにされます。

これは、異なるモジュールから到達可能となっている同じ無名のスコープ無し列挙型の定義をマージするためのルールを定めるものです。

無名のスコープ無し列挙型はヘッダファイルで一般的であり、同じものが複数のモジュールから到達可能となるとき、それが同一であることを判定しマージできなければODR違反となります。

これはヘッダユニットのインポートで特に問題となり得るため、そのような複数の定義が同じものであることを認識する方法を指定することでODR違反とならないようにします。

/// importable.h
namespace X { 
  enum { A }; // 無名のスコープ無し列挙型 (1)

  enum {};  // (2)
}
/// M.cpp

module;
#include "importable.h"
export module M;

// 宣言が破棄されないようにする
constexpr int N = X::A;
/// main.cpp

// (1)の定義が異なる経路で到達可能となる
// どちらも全く同じ定義を参照しているためマージされODR違反は起きない
// (2)は翻訳単位毎に別の型として扱われるためODR違反は起きない
import "importable.h"
import M;

int main() {}

これは従来からあるテンプレートのためのODRの例外規則を拡張する形で表現されています。つまり、定義が異なる翻訳単位から到達可能になっているときでも、その定義が意味的に完全に同一である場合にのみODRの例外を適用し定義が一つにマージされます。

列挙型としての名前がないので、これらの識別では最初の列挙子がその列挙型の名前として使用されます。そのため、列挙子を持たない無名の列挙型は常に異なる型として扱われます。

おわり

おおよそ時系列に沿っているはずです。細かいIssue解決は見逃しているかもしれません。あとGithubリポジトリに直接コミットする形のeditorialな修正は追い切れていません・・・

あと、P1787R6 Declarations and where to find themの変更は直接的にはモジュールに対するものではなく、C++23での採択であるので省いています(それでも影響はそこそこありますが)。

もし見落としや間違いなどを発見されたら教えてくださいませ・・・

この記事のMarkdownソース

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

文書の一覧

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

P0323R10 std::expected

エラーハンドリングを戻り値で行うための型、std::expected<T, E>の提案。

std::expected<T, E>のオブジェクトは型TEのどちらかの値を保持しており、Tの値を期待される値(正常値)、Eの値をエラーとして扱うことでエラーハンドリングを行います。これは、RustではResult<T, E>として重宝されています。

std::expectedを使うと、std::optional<T>std::expected<T, std::nullopt_t>のように書くことができ、std::optionalをエラー処理に用いる時に失われてしまっていたエラー詳細を同時に保持しておくことができるようになります。

std::expectedのインターフェースはstd::optionalをベースに、エラー値を保持している場合の操作を加えた形のものになります。

#include <expected>

auto maybe_succeed() -> std::expected<int, std::string_view> {
  bool is_err = false;

  // 何か失敗しうる処理
  // ...

  if (is_err) {
    return 46;  // 正常値を返す
  } else {
    return {std::unexpect, "error!"};  // エラー値を返す
  }
}

int main() {

  auto exp = maybe_succeed();

  // bool値に変換して結果をチェック
  if (exp or exp.has_value()) {
    // 正常値の取り出し
    int n = *exp;
    int m = exp.value();

    // 未定義動作
    exp.error();

  } else {
    // エラー値の取り出し
    std::string_view s = exp.error();
    
    // 未定義動作
    *exp;
    // 例外を投げる
    exp.value(); 
  }
}

優れたエラーハンドリングメカニズムには、次のような事が求められます。

  1. エラーの可視性 : なんらかの処理が失敗するケースがコード上で隠されず、明確に表示されている
  2. エラーの情報 : エラーにはその発生場所や原因などが含まれる
  3. クリーンコード : エラー処理はできる限り目立たないように行われる。エラー処理は読みやすくなければならない
  4. 被侵入的 : エラーが通常の処理フローのためのチャネルを独占しない。それらはなるべく分離されているべき

この観点から、std::expectedと例外機構や通常のエラーコード戻り値によるエラー処理を比較すると

性質 std::expected 例外 エラーコード戻り値
可視性
情報
クリーンコード ◯※ ×
非侵入的 ×

※ monadicインターフェースがある場合(現在の提案には欠けている)

このように、std::expectedはエラー処理という観点から既存の方法よりも優れています。また、例外機構と比べるとパフォーマンスでも勝ります。

この部分の6割は以下の型のご指摘によって構成されています。

P0447R13 Introduction of std::colony to the standard library

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

以前の記事を参照

このリビジョンでの変更は、実装を過度に指定しないように文言を修正したことと、一部の非メンバ関数friend関数に変更したことなどです。

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

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

以前の記事を参照

このリビジョンでの変更は、一般的な設計に関する情報と例の追記、用語の変更・追加・整理、一部のデフォルトコンストラクタを非explicitにし、一部のコンストラクタにnoexeceptを追加したことなどです。

この提案はLWGのレビューを完了しており、このリビジョンをもってConcurency TS v2導入のための全体投票にかけられる事が決まっています。何事もなければ、次の全体会議にてConcurency TSに入ることになります。

P1122R3 Proposed Wording for Concurrent Data Structures: Read-Copy-Update (RCU)

標準ライブラリにRead-Copy-Update(RCU)を導入する提案。

RCUは並行処理におけるデータ共有のための仕組みで、ロックフリーデータ構造の実装に用いることができます。

RCUでは、共有対象のデータはアトミックなポインタによって共有されており、共有データを更新する際は別の領域にデータを構築してから、ポインタを差し替えることで行います(ここまではハザードポインタと同じ)。共有データに触る際のクリティカルセクションでは、スケジューリング(プリエンプション)が起こらないようにして、クリティカルセクションはあるスレッドを占有して実行されるようにしてから、共有データへアクセスします。クリティカルセクションに入る際はOSの機能を使用してそのスレッドがスケジューリングされないようにしますが、何かロックを取得したりするわけではないのでオーバーヘッドはほぼゼロです。

読み取りの際は、共有データを読みだしている間そのスレッドはスケジューリングされず、データを見終わった後で通常通りスケジューリング対象に戻ります。

更新の際は、更新後のデータを別の領域に用意してからクリティカルセクションに入り、共有データを指すポインタを新しいデータを指すように差し替えます(この差し替えはアトミック)。その後、他のスレッドが最低一回以上スケジューリングされるまで待機してから、差し替え前の古いデータを削除しクリティカルセクションを抜けます。

クリティカルセクション中そのスレッドはスケジューリングされないので、処理はOSによって中断される事なく実行されています。そのため、更新スレッドから見て他のスレッドに対してスケジューリング一回以上行われたということは、共有データを読み取っている(更新前の古いデータを見ている)可能性のあるスレッドが読み取り処理を終えている事を意味します。

スレッドがスケジューリングされないようになるということは、クリティカルセクションの実行はCPUの論理コアを占有することになります。従って、更新スレッドが実行されているコアを除いたシステムのCPUの残りの論理コアにおいてスレッドの切り替えが一回以上行われていれば、更新スレッドが保持している古いデータを見ているスレッド(=クリティカルセクション)が無いこと分かるため、安全に削除できるということです。

データの差し替えはポインタによってアトミックに行われるため、更新が同時に行われてもデータ競合を起こしませんし、デッドロックすることもありません。ただし、RCUの全ての保証はユーザーが正しくクリティカルセクションを運用する事を前提としています。

RCUを使用して、複数のリーダー(reader)が並行して存在する時に、共有データの更新を実行する例。

// 共有したいもの
std::atomic<std::string *> name;

// 並行して頻繁に呼ばれる
void print_name() {
  // RAIIによって読み取りクリティカルセクションを構成
  std::rcu_reader rr;
  std::string *s = name.load(std::memory_order_acquire);
  /* ...use *s... */
}

// あまり呼ばれない
void update_name(std::string_view &nn) {
  // 1. 値の更新(新しい値の用意と共有ポインタのアトミックな差し替え)
  std::string *new_name = new std::string(nn);
  std::string *s = name.exchange(new_name, std::memory_order_acq_rel);
  // 2. 遅延削除(更新クリティカルセクション)
  std::rcu_retire(s);
}

リーダー(print_name())は1つのスレッドで実行され、更新処理(update_name())はそれとは別のスレッドで実行されます。print_name()rcu_readerのオブジェクトは、リーダーの処理が完了するまで参照しているオブジェクトが、rcu_retire()によって削除されないように保護しています。

RCUは読み取りが頻繁に起こるが更新はあまりされないような場合に適した方法です。

この提案は別の提案(P0461R2)の標準へ提案する文書をまとめたもので、Concurrency TSに向けて提案され、議論されています。

著者の方(Paul McKenneyさん)の実装経験をベースにして長い時間をかけて議論されてきており、Concurrency TSに向けての議論がLWGまで進行しているのでConcurrency TSには入りそうですが、標準ライブラリの一部として利用可能となるにはもう少しかかりそうです。

P1132R7 out_ptr - a scalable output pointer abstraction

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

C APIでは、関数の中でのメモリアロケーションの結果などを呼び出し元に出力するために、ポインタのポインタ(T** / void**)を引数に取るものがよく見られます。ポインタではなくポインタのアドレスを渡す必要があり、std::unique_ptrをはじめとするスマートポインタはそれを取得する手段がないため、相性が良くありませんでした。

提案文書より、ffmpegにおけるサンプル。

#include <memory>
#include <avformat.h>

// カスタムデリータ
struct AVFormatContextDeleter {
  void operator() (AVFormatContext* c) const noexcept {
    avformat_close_input(&c);
    avformat_free_context(c);
  }
};

using av_format_context_ptr = std::unique_ptr<AVFormatContext, AVFormatContextDeleter>;

// libavformatのシグネチャ
//int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

int main (int, char* argv[]) {
  av_format_context_ptr context(avformat_alloc_context());

  // ...
  // used, need to reopen

  // AVFormatContext**にunique_ptrをセットしたいが・・・
  AVFormatContext* raw_context = context.release();
  if (avformat_open_input(&raw_context, argv[0], nullptr, nullptr) != 0) {
    // 処理が失敗した場合にデリータが走って欲しくないので、一旦release()している

    std::stringstream ss;
    ss << "ffmpeg_image_loader could not open file '"
      << path << "'";
    throw FFmpegInputException(ss.str().c_str());
  }

  // 成功したら出力されたポインタをセット
  context.reset(raw_context);

  // ... off to the races !

  return 0;
}

std::out_ptr/std::inout_ptrはこのような場合のrelease()reset()の呼び出しを自動化し、スマートポインタに内包されているポインタのアドレスを抽出するものです。

int main (int, char* argv[]) {
  av_format_context_ptr context(avformat_alloc_context());

  // ...
  // used, need to reopen

  // release()してからポインタのアドレスを渡し、終了後に元のunique_ptrにreset()する
  if (avformat_open_input(std::inout_ptr(context), argv[0], nullptr, nullptr) != 0) {
    std::stringstream ss;
    ss << "ffmpeg_image_loader could not open file '"
      << argv[0] << "'";
    throw FFmpegInputException(ss.str().c_str());
  }

  // ... off to the races!

  return 0;
}

このようなユーティリティは、大小様々な企業において再発明されていますが、その用途や目的はC APIの出力ポインタ(T**)引数にスマートポインタを直接渡せるようにする事で一貫しています。この提案は、そのような目的が同じでありながらそれぞれで再実装されているユーティリティの、将来性があり高性能で使いやすい共通の実装を標準で提供する事を目指したものです。

std::out_ptr/std::inout_ptrは関数であり、引数で受け取ったスマートポインタをラップした型(std::out_ptr_t/std::inout_ptr_t)のオブジェクトを返します。それらの型は元のポインタ型やvoid**への暗黙変換演算子を備えており、コンストラクタとデストラクタでrelease()reset()を行います。

std::out_ptrstd::inout_ptrの違いは、対象となるスマートポインタが予めリソースの所有権を持っているか否かによって使い分けるためにあります。

error_num c_api_create_handle(int seed_value, int** p_handle);
error_num c_api_re_create_handle(int seed_value, int** p_handle);
void c_api_delete_handle(int* handle);

struct resource_deleter {
  void operator()( int* handle ) {
    c_api_delete_handle(handle);
  }
};

// out_ptrの使用例
void normal_case() {
  std::unique_ptr<int, resource_deleter> resource(nullptr);

  // スマートポインタからそのポインタのアドレスへの変換をラップする
  error_num err = c_api_create_handle(24, std::out_ptr(resource));

  if (err == C_API_ERROR_CONDITION) {
    // handle errors
  }
}

// inout_ptrの使用例
void reallocate_case(std::unique_ptr<int, resource_deleter> resource) {

  // 予めリソースの所有権を保持している場合
  // release()してからポインタのアドレスを渡し、終了後に元のunique_ptrにreset()する
  error_num err = c_api_re_create_handle(24, std::inout_ptr(resource));

  if (err == C_API_ERROR_CONDITION) {
    // handle errors
  }
  // resource.get() the out-value from the C API function
}

std::out_ptrは空のスマートポインタにC API経由でリソースをセットするときに使用し、std::inout_ptrは予めリソースの所有権を保持しているスマートポインタをC APIに渡すときに使用します。

その他のサンプル

#include <memory>
#include <cstdio>

// C API関数
int fopen_s(std::FILE** f, const char* name, const char* mode);

// カスタムデリータ
struct fclose_deleter {
  void operator()(std::FILE* f) const noexcept {
    std::fclose(f);
  }
};

int main(int, char*[]) {
  constexpr const char* file_name = "ow.o";
  std::unique_ptr<std::FILE, fclose_deleter> file_ptr;

  // unique_ptrをFILE**へ渡すためのキャスト構文
  int err = fopen_s(std::out_ptr<std::FILE*>(file_ptr), file_name, "r+b");
  if (err != 0) {
    return 1;
  }
  // *file_ptr is valid
  return 0;
}
#include <memory>

struct StarFish* starfish_alloc();

// C API関数
int starfish_populate(struct StarFish** ps, const char *description);

// カスタムデリータ
struct StarFishDeleter {
  void operator() (struct StarFish* c) const noexcept;
};

using StarFishPtr = std::unique_ptr<StarFish, StarFishDeleter>;

int main(int, char*[]) {
  StarFishPtr peach(starfish_context());
  // ...
  // used, need to re-make
  int err = starfish_populate(std::inout_ptr(peach), "caring clown-fish liker");
  return err;
}

この提案は元々C++20入りを目指していましたが間に合いませんでした。C++20作業終了時点で提案は既にLWGに送付済みで、コロナウィルス流行などによって遅れていましたが、既にLWGでの最後のレビューが完了しています。次の全体会議で投票にかけられ、何事もなければC++23に導入されます。

この部分の6割は以下の方々のご指摘によって構成されています。

P1169R1 static operator()

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

任意の関数オブジェクトを取ることで処理をカスタマイズできるようにすることは、標準ライブラリの多くの所で行われています。関数呼び出し演算子をテンプレートにすることで、オーバーロードを用意するよりもより簡易かつ便利に関数オブジェクトを利用できます。

また、C++20からはカスタマイゼーションポイントオブジェクト(CPO)と呼ばれる関数オブジェクトが多数追加されています。これらのCPOはメンバを持たないことが規定されています。

ラムダ式にせよ、ユーザー定義のものにせよ、CPOにせよ、関数呼び出し演算子オーバーロードによって関数オブジェクトは成り立っていますが、関数呼び出し演算子は非静的メンバ関数としてしか定義できません。従って、その呼び出しに当たっては暗黙のthis引数が渡されています。

CPOやキャプチャしていないラムダのようにメンバアクセスの必要が皆無だったとしても、関数呼び出しがインライン展開されなければ関数呼び出し演算子オーバーロードによる呼び出しは常にthisポインタを引き渡すオーバーヘッドを埋め込んでいます。

struct X {
  bool operator()(int) const;
  static bool f(int);
};

// 簡易的なCPO定義
inline constexpr X x;

int count_x(std::vector<int> const& xs) {
  return std::count_if(xs.begin(), xs.end(),
#ifdef STATIC
  X::f  // 静的メンバ関数を渡す
#else
  x     // 関数オブジェクト(CPO)を渡す
#endif
  );
}    

決して使用されない事が分かっていても、コンパイラは必ずしもこのオーバーヘッドを取り除くことができません。これはゼロオーバーヘッド原則に違反していますが、関数呼び出し演算子を静的メンバ関数として定義することはできません。

この提案は、この様なオーバーヘッドを取り除くために、関数呼び出し演算子を静的メンバ関数として定義できるようにしようとするものです。ステートレスな関数オブジェクトの有用性は、C++11のラムダ式導入以降広く認識されており、この様な制限を課しておく事に利益はありません。

そのほかのオーバーロード可能な演算子にも同様の事が言えますが、関数呼び出し演算子以外のものを静的メンバ関数として定義できるようにするユースケースが見られないため、ここでは関数呼び出し演算子だけを対象としています。

この提案の後では、キャプチャをしていないラムダ式の関数呼び出し演算子staticに定義することができるようになりますが、それをしてしまうとラムダ式の関数呼び出し演算子のメンバポインタを取り出すようなコードが壊れ、またABI破損に繋がります。

そのため、キャプチャしていないラムダ式の関数呼び出し演算子staticになるようにしてしまうことは出来ないため、オプトインする構文を提案しています。

// 関数呼び出し演算子が静的メンバ関数として定義される
auto four = []() static { return 4; };

// 従来通り、非静的メンバ関数として定義される
auto five = []() { return 5; };

P1401R5 Narrowing contextual conversions to bool

constexpr ifstatic_assertの引数でのみ、整数型からbool型への暗黙の縮小変換を定数式で許可する提案。

以前の記事を参照

このリビジョンでの変更は、CWGからの指摘を受けて提案する文言を調整した事です。

この提案はEWGのレビューを通過し、CWGに転送されています。

P1664R3 reconstructible_range - a concept for putting ranges back together

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

Range Adopterの適用では、入力のrangeviewの型に包まれる形で変換され、元の型とは別の型になってしまい、元には戻らなくなります。

template <typename T>
using span = quickcpplib::span<T>;

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

// 現在、コンパイルエラー
span<int> v = s | views::drop(1) | views::take(10)
                | views::drop(1) | views::take(10);

// これならOK
auto v2 = s | views::drop(1) | views::take(10)
            | views::drop(1) | views::take(10);
// decltype(v2) == ranges::take_view<ranges::drop_view<ranges::take_view<ranges::drop_view<span<int, dynamic_extent>>>>>

この例では、decltype(v)からspan<int>への変換方法がないためコンパイルエラーを起こしています。

views::take/views::dropは指定された数だけイテレータを保存するor落とすことで、指定された数だけ要素を取り出すor無視する、という処理を行っており、viewの適用後も元のイテレータをそのまま利用しています。そのため、元のイテレータを用いれば元の型を再構成できるはずです。しかし、現在はその方法や判別方法がありません。

この提案は、そのような場合に再構成できるrangeを表すReconstructible Rangesという概念を導入し、そのためのADL-foundな関数とコンセプトを整備するものです。

先程の例の場合のようにイテレータとセンチネルのペアから元の型を再構成できる場合、それを行うためのカスタマイゼーションポイントとしてreconstruct関数を利用します。

それをstd::ranges::reconstructCPOによって呼び出すようにし、reconstructCPOによってpair_reconstructible_rangereconstructible_rangeの二つのコンセプトを定義します。

namespace std::ranges {
  inline namespace unspecified {

    inline constexpr nspecified reconstruct = unspecified;

  }

  template <class R,
            class It = ranges::iterator_t<remove_reference_t<R>>,
            class Sen = ranges::sentinel_t<remove_reference_t<R>>>
  concept pair_reconstructible_range =
    ranges::range<R> &&
    ranges::borrowed_range<remove_reference_t<R>> &&
    requires (It first, Sen last) {
      reconstruct(
        in_place_type<remove_cvref_t<R>>,
        std::move(first),
        std::move(last)
      );
    };

template <class R, class Range = remove_reference_t<R>>
concept reconstructible_range =
  ranges::range<R> &&
  ranges::borrowed_range<remove_reference_t<R>> &&
  requires (Range first_last) {
    reconstruct(
      in_place_type<remove_cvref_t<R>>,
      std::move(first_last)
    );
  };
}

std::ranges::reconstructCPOはタグ型(in_place_type<R>)とRイテレータペア、もしくはタグ型とrangeを受け取り、そのイテレータ型について呼び出し可能なreconstruct関数を呼び出し、処理を委譲します。

reconstruct関数では、それぞれのイテレータrange)に最適な方法によってイテレータペアから元のrangeの再構成を行います。

そして、標準ライブラリのviews::take/views::dropの呼び出しは、reconstructible_rangeコンセプトのモデルとなる型に対して、元のイテレータを使用して直接元の型を再構成して結果を返すようにします。

これによって冒頭のコードは次のようになります

template <typename T>
using span = quickcpplib::span<T>;

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

// OK
auto v = s | views::drop(1) | views::take(10)
           | views::drop(1) | views::take(10);
// decltype(v2) == span<int>

他の例

std::u8string name = "𐌀𐌖𐌋𐌄𐌑𐌉·𐌌𐌄𐌕𐌄𐌋𐌉𐌑 𐑡𐑹𐑡 ·𐑚𐑻𐑯𐑸𐑛 ·𐑖𐑷";
char16_t conversion_buffer[432];

std::u8string_view name_view(name);
std::span<char16_t> output(conversion_buffer, 432);

auto encoding_result = ztd::text::transcode(input, output);

// 全てOK
auto unprocessed_code_units = encoding_result.input;
auto unconsumed_output = encoding_result.output;
// decltype(unprocessed_code_units) == std::u8string_view
// decltype(unconsumed_output) == std::span<char16_t>

P1673R3 A free function linear algebra interface based on the BLAS

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

この提案は、BLASAPIをベースとした密行列のための各種操作を行うフリー関数を追加する提案もので、ベクトル型や行列型を追加するものではなく、これらの関数は特定のデータ構造に依存していません。

関数はテンプレートで定義され、提案中のmdspanを用いることで特定のデータ型に依存しないようにされています。また、演算子オーバーロードを用いたexpressionテンプレートなインターフェースでもありません。

線形代数ライブラリ(ベクトル/行列型)を追加する提案は別に進行しておりユーザーが触れるインターフェースとなるのはそちらで、これは線形代数ライブラリの基礎となるものです。

提案文書より、コレスキー分解のサンプル

#include <linalg>
#include <cmath>


template<class inout_matrix_t, class Triangle>
int cholesky_factor(inout_matrix_t A, Triangle t)
{
  using element_type = typename inout_matrix_t::element_type;

  constexpr element_type ZERO {};
  constexpr element_type ONE (1.0);
  const ptrdiff_t n = A.extent(0);

  if (n == 0) {
    return 0;
  }
  else if (n == 1) {
    if (A(0,0) <= ZERO || std::isnan(A(0,0))) {
      return 1;
    }
    A(0,0) = std::sqrt(A(0,0));
  }
  else {
    // Partition A into [A11, A12,  A21, A22],
    // where A21 is the transpose of A12.
    const std::ptrdiff_t n1 = n / 2;
    const std::ptrdiff_t n2 = n - n1;
    auto A11 = std::subspan(A, std::pair{0, n1}, std::pair{0, n1});
    auto A22 = std::subspan(A, std::pair{n1, n}, std::pair{n1, n});
    
    // Factor A11
    const int info1 = cholesky_factor(A11, t);
    if (info1 != 0) {
      return info1;
    }

    using std::linalg::symmetric_matrix_rank_k_update;
    using std::linalg::transposed;
    
    if constexpr (std::is_same_v<Triangle, upper_triangle_t>) {
      // Update and scale A12
      auto A12 = subspan(A, std::pair{0, n1}, std::pair{n1, n});

      using std::linalg::triangular_matrix_matrix_left_solve;

      triangular_matrix_matrix_left_solve(transposed(A11),upper_triangle, explicit_diagonal, A12);

      // A22 = A22 - A12^T * A12
      symmetric_matrix_rank_k_update(-ONE, transposed(A12), A22, t);
    }
    else {
      //
      // Compute the Cholesky factorization A = L * L^T
      //
      // Update and scale A21
      auto A21 = std::subspan(A, std::pair{n1, n}, std::pair{0, n1});

      using std::linalg::triangular_matrix_matrix_right_solve;

      triangular_matrix_matrix_right_solve(transposed(A11), lower_triangle, explicit_diagonal, A21);

      // A22 = A22 - A21 * A21^T
      symmetric_matrix_rank_k_update(-ONE, A21, A22, t);
    }

    // Factor A22
    const int info2 = cholesky_factor(A22, t);
    if (info2 != 0) {
      return info2 + n1;
    }
  }
}

なお、Wordingのページだけで120P近くを占めており、とてつもなく巨大です・・・

P1706R3 Programming Language Vulnerabilities for Safety Critical C++

MISRA C++およびWG23の文書についてのWG21 SG12でのレビュー作業の進捗状況に関する文書。

WG23は様々なプログラミング言語における脆弱性を調査するISOのワーキンググループです。2017年以降、WG23とWG21はC++脆弱性を文書化するための作業を共同で行っています。その後、途中でMISRA C++の関係者もレビュープロセスに加わりました。

この作業の目的は単に脆弱性がどのようなコードから発生するのかを文書化することにあります。

P1708R4 Simple Statistical Functions

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

以前の記事を参照

このリビジョンでの変更は、NumPyを参考に一部の関数仕様を調整したことと、stats_errorを定数からクラスへ変更した事です。

この提案はSG16でのレビューを通過し、LEWGに転送されています。どうやらNumeric TSへ導入することを目指して議論されています。

P1847R4 Make declaration order layout mandated

クラスのデータメンバのメモリレイアウトを、宣言順に一致するように規定する提案。

現在の規定では、アクセス制御(private, public, protected)が異なる場合、実装はデータメンバを並べ替えてメモリに配置することができます。ただ、実際にそれを行う処理系は存在せず、実際のプログラムでは並べ替えを考慮されていないことがほとんどです。

この提案は、そのような慣行に従うように規定を修正し、クラスのデータメンバのメモリレイアウトが常にコード上の宣言順と一致するようにするものです。それによって、規則が単純になり、将来クラスレイアウトをコントロールするための機能を追加する際の土台とすることができます。

クラスレイアウトに関するこの制限は当初のC++から存在していたものではなく、C++11から偶発的に混入したもののようです。

当初のC++には、POD構造体はC言語との互換性がある必要がありましたがそれ以外のところに制限は特にありませんでした。そこで、クラスレイアウトをコントロールしたいユーザーから、ブロックやラベルによってクラスのデータメンバの配置をコントローする構文が提案されていました。これはC++11以前のことです。

C++11でその提案は採択されず、アクセス制御のみによってデータメンバの配置が変更されうるようにされました(N2342)。これは標準レイアウトクラスに関する作業で、クラスのレイアウトが不定になるような制限を導入する意図はなかったようです。

N2342による変更はC++が標準化されてから10年以上経過した後になされたもので、潜在的にはABIを破損する可能性がありましたが、これを活用する実装は現れなかったため実際に問題になることはありませんでした。

N2342による変更は最適ではなく、この点からもこのことは修正すべきです。ABI破損の可能性は悩ましいですが、目的を持って制御できなければ利点はありません。

P1938R3 if consteval

constevalstd::is_constant_evaluated()にある分かりづらい問題点を解決するためのconsteval ifステートメントの提案

以前の記事を参照

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

この提案は、CWGとLWGでのレビューを終えており、次の会議で全体投票にかけられることが決まっています。何事もなければC++23に入りそうです。

P1949R7 C++ Identifier Syntax using Unicode Standard Annex 31

識別子(identifier)の構文において、不可視のゼロ幅文字や制御文字の使用を禁止する提案。

以前の記事を参照

このリビジョンの変更点は、よくわかりません。

この提案はCWGのレビューを終え、次の全体投票にかけられることが決まっています。何事もなければC++23に入りそうです。

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

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

ハードリセット時にハードをフラッシュするためのベースイメージ、アイコンファイル、プログラムと強く連携するスクリプトなど、アプリケーションにバイナリデータを同梱したい場合があります。しかし、C/C++においてそれを行う簡易な方法はありません。

xxdコマンドを利用して16進リテラルとして埋め込んだり、環境のリンカを直接叩いてデータを埋め込み、それを指すexternポインタを利用するなど、様々な方法が考案されてきましたが、それを移植可能にしたり、ビルドシステムで管理しようとしたりすることは非常に困難です。

この提案は#embedというプリプロセッシングディレクティブを導入することで、簡易かつ移植可能な方法でバイナリデータをプログラムに埋め込めるようにしようとするものです。

新しいプリプロセッシングディレクティブという言語機能としてサポートしようとしているのは、数値リテラルの配列としてバイナリデータを埋め込む方法が非効率極まりないためです。

#embed#includeに従うように設計されており、ほぼ同様に使用できます。

// デフォルトはunsigned char
const unsigned char icon_display_data[] = {
    #embed "art.png"
};

// 整数定数式で初期化可能な任意の型を指定できる
const char reset_blob[] = {
    #embed "data.bin"
};

ただしこれは、あたかもバイナリデータを整数値としてコンマ区切りリストで展開しているかのように動作するというだけで、実際にそのような展開が起きているわけではありません。それをするとコンパイル時間を著しく増大させるためです。

すなわち、#embedによるバイナリデータ読み取りと展開はコピペではなく、コンパイラによって最適な方法で処理されています。

また、無限あるいは巨大なファイルの一部だけを読むために、長さを指定できるようになっています。

// 最大で32バイトの乱数を読み取る
const int please_dont_oom_kill_me[] = {
    #embed 32 "/dev/urandom"
};

ただし、この指定は上限であり厳密な要求値ではありません。実際に読み取った長さはより小さくなる可能性があります(その場合でも、配列長でコンパイル時に判定可能)。

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

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

以前の記事を参照

このリビジョンでの変更は、LWGのレビューを受けて文言を修正したことです。

この提案はLWGのレビューを終えており、次の全体会議で投票にかけられることが決まっています。これもC++23に入りそうです。

P2128R4 Multidimensional subscript operator

P2128R5 Multidimensional subscript operator

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

前回の記事を参照

R4での変更は、動機付けや代替案についての議論をより追記した事と、機能テストマクロを追加した事などです。

このリビジョンでの変更は、EWGの指示に基づいて、動機付けやユースケースについて追記した事などです。

この提案はEWGでのレビューが終了し、CWGに転送するための投票にかけられる予定です。

P2168R2 generator: A Synchronous Coroutine Generator Compatible With Ranges

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

前回の記事を参照

このリビジョンでの変更は、文言の修正、アロケータサポートの解説の改善、実装例の更新などです。

P2186R2 Removing Garbage Collection Support

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

以前の記事を参照

このリビジョンでの変更は、削除するライブラリ名の名前を専用のセクションに追記した事です。

この提案はCWGとLWGのレビューを終えており、次の全体会議で投票にかけられる事が決まっています。何事もなければ、C++23に適用されます。

P2201R1 Mixed string literal concatenation

異なるエンコードプレフィックスを持つ文字列リテラルの連結を禁止する提案。

このリビジョンでの変更は、この変更を規格書のAnnex Cセクションに追記されるように文言を追加した事です。

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

また、これと同等の内容がすでにC言語には適用されているようです(N2594)。

P2223R2 Trimming whitespaces before line splicing

バックスラッシュ+改行による行継続構文において、バックスラッシュと改行との間にホワイトスペースの存在を認める提案。

以前の記事を参照

このリビジョンでの変更は、CWGのレビューを受けて提案する文言を修正した事です。

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

P2251R1 Require span & basic_string_view to be Trivially Copyable

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

以前の記事を参照

このリビジョンでの変更は、動機付けを追記した事、LEWGでの投票結果を記載した事、3つの主要実装がすでにそうなっている事を確認できるCompiler Explorerへのリンクを追記した事などです。

この提案は非常に小さいため、LEWGのレビューを簡易にパスして、LWGに送るためのLEWGでの投票待ちをしています。

P2255R1 A type trait to detect reference binding to temporary

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

以前の記事を参照

このリビジョンでの変更は、この提案によって影響を受けるtuple/pairのコンストラクタをオーバーロード解決から除外するのではなくdeleteとして定義するようにした事です。

この提案はEWGでのレビューをパスしてLEWGのレビュー待ちをしています。

P2265R1 Renaming any_invocable

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

以前の記事を参照

このリビジョンでの変更はよくわかりません。この提案はすでにP0288に適用されています。

P2273R1 Making std::unique_ptr constexpr

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

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを__cpp_lib_constexpr_memoryをバージョンアップするように変更したことと、nullptrに対してもconstexprswap、比較とdefault_deleterが機能するようにしたことです。

P2283R1 constexpr for specialized memory algorithms

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

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加したこと、文言の影響範囲を明確にしたこと、default_construct_atの必要性の説明を追記したことです。

P2295R1 Correct UTF-8 handling during phase 1 of translation

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

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

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

以前の記事を参照

R1での変更は、ホワイトスペースについてのセクションと関連する文言を取り除いたことです。どうやら、空白文字を具体的に指定することを避けたようです(SG16では合意自体は取れていたようですが)。

R2での変更は、BOMについてのガイドラインを追記したこと、Clangが将来的に幅広いエンコーディングを採用する予定であることを明確にしたこと、翻訳フェーズ5時点でのコードポイントの保存がP22314R1によって処理されることを明確にしたことなどです。

R3での変更は、SG16のガイダンスに従って、文言を修正したことです。

P2321R1 zip

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

以前の記事を参照

このリビジョンでの変更は機能テストマクロを追加したこと、前方向またはそれよりも弱いzip_viewイテレータoperator==についての説明を追記したこと、adjacent_viewinput_rangeをサポートしない事についての説明、簡単なサンプルコードの追加および文言の修正です。

std::vector v1 = {1, 2};
std::vector v2 = {'a', 'b', 'c'};
std::vector v3 = {3, 4, 5};

fmt::print("{}\n", std::views::zip(v1, v2));                               // {(1, 'a'), (2, 'b')}
fmt::print("{}\n", std::views::zip_transform(std::multiplies(), v1, v3));  // {3, 8}
fmt::print("{}\n", v2 | std::views::pairwise);                             // {('a', 'b'), ('b', 'c')}
fmt::print("{}\n", v3 | std::views::pairwise_transform(std::plus()));      // {7, 9}

P2322R2 ranges::fold

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

以前の記事を参照

このリビジョンでの変更は、weakly-assignable-fromコンセプトを使用していたところをassignable_fromコンセプトに置き換えた事、foldの戻り値型は初期値の型ではなくなった事です。

foldの戻り値型の問題は次のようなコードで結果がどうなるのかという事です。

namespace std::ranges {
  template <range R, movable T, typename F,
            typename U = /* ... */>
  auto fold(R&& r, T init, F f) -> U {
    // ...
  }
}

std::vector<double> v = {0.25, 0.75};
auto r = ranges::fold(v, 1, std::plus());
// r == 2.0 ? r == 1 ??

この場合に、ranges::foldの戻り値型をU = std::decay_t<std::invoke_result_t<F&, T, ranges::range_reference_t<R>>の様に決めることで戻り値型が初期値から決まらないようにしています。それに伴って必要な制約を追加して、この提案では上記のrdouble2.0になります。

P2325R2 Views should not be required to be default constructible

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

以前の記事を参照

このリビジョンでの変更は、標準に提案する文言を追加した事です。

この提案はまだLEWGでのレビュー中ですが、採択された際にC++20にさかのぼって適用される可能性があります。

P2327R0 De-deprecating volatile compound assignment

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

C++はOSの無い組み込みのプログラミングにおいても利用されています。そこではメモリにマップされたレジスタ(メモリマップドI/O)を操作することによってハードウェアを操作し、多くの場合特定の1bitにしか触りません。そこでは次のようなコードが頻出します。

// ベンダ提供の抽象化レイヤ
struct ADC {
  volatile uint8_t CTRL;
  volatile uint8_t VALUE;
  ...
};

#define ADC_CTRL_ENABLE ( 1 << 3 )


// in u se r code
ADC1−>CTRL |= ADC_CTRL_ENABLE;  // ADCのスタート

ADC1−>CTRL &= ~ADC_CTRL_ENABLE; // ADCをストップ

このようなコードは、ベンダから提供されたマクロや関数の中で使用される場合もあるほか、コードジェネレーターが生成するコードに含まれていることもあります。

しかしこのようなvolatile値に対する複合代入演算子の使用は、アクセス回数が誤解されやすいためC++20からは非推奨とされました。

今日、Cライブラリの多くの所でこのような操作は利用されており、非推奨化はそれらのライブラリがC++から利用できなくなることを意味します。ベンダ提供のヘッダファイルは多くの場合安定性を優先するために更新されることは稀で、むしろ新しいバージョンのコンパイラを使用しないようにする可能性があります。

また、volatile値に対する複合代入演算子の仕様を推奨されているように書き直したとき、気づき辛いバグを埋め込むことになる可能性があります

UART1−>UCSR0B |= (1<<UCSZ01 ) ; // 現在使用されているコード
UART1−>UCSR0B = UART1−>UCSR0B | (1<<UCSZ01 ) ; // C++20に伴って書き直したコード
UART2−>UCSR0B = UART1−>UCSR0B | (1<<UCSZ01 ) ; // それを別のデバイス用にコピペしなおしたコード
                ^^^^^

このように、別のデバイスの特定のレジスタを読まなければならないのに、コピペミスによって元のデバイスレジスタを参照し続けてしまいます。このエラーは発見が難しく、volatileが誤って使用された場合のエラーとコードの冗長化によるこのエラーとを交換しているだけになっています。

これらの理由により、volatile値に対する複合代入演算子の非推奨化は間違った判断であり、元に戻そうという提案です。

P2337R0 Less constexpr for <cmath>

<cmath>および<complex>の数学関数のconstexpr対応を、ランタイムの実装が正しくなるまで遅らせる提案。

C++における数学関数はIEEE754を参照しているため、そこで定義される正しい丸めによって結果を返す必要がありますが、現在のランタイムの実装は特に32bit浮動小数点数で誤っています。すなわち、ある数学関数に対する同じ入力に対して得られる出力は必ずしも実装間で一致しません。


(色のついた部分が誤った丸めが行われている部分)

これは浮動小数点数計算の移植性を損ねていると同時に、コンパイル時と実行時でその結果が一致しないことが同じ実装においても生じうることを意味しています。

正しいにしても間違っているにしても、ある特定の実装における丸めの結果に依存している様なコードは、コンパイル時計算と実行時計算の間の結果の不一致によって静かなバグを埋め込むことにつながりかねません。

実行時に同様の問題が既に存在していることが分かっているのに、同じ問題を追加するのは避けるべきであり、ランタイムの実装が正しくなるまではconstexpr対応をしない方がいい、という提案です。

また、C言語に対して進行中のISO/IEC TS 18661-4:2015で提案されている、crプリフィックスをもつ関数(正しい丸めによる結果を返すことが保証されている関数)をC++にも追加して、それに対してのみconstexprを付加することを代替案として挙げています。

この部分の6割は以下の型のご指摘によって構成されています

P2342R0 For a Few Punctuators More

ドル記号($)、アットマーク(@)、バッククオート(`)の3つが将来の機能のためのトークンとして使用可能であることを説明する文書。

AsciiとEBCDICにある記号のうち、$と@と`の3つだけがC++で使用されていません。これらによる構文は将来の提案のために使用できる可能性があります。この文書はその可能性を検討したものです。

バッククオート以外の記号はC++を拡張した言語や外部ツールなどでよく使用されており、そこでの構文とバッティングする可能性が高そうです。どうやら、@<...>, ${...}, $(...)のような構文は使用可能のようです。

P2345R0 Relaxing Requirements of Moved-From Objects

ムーブ後オブジェクトの要件を緩和する提案。

movableコンセプトあるいはassignable_fromコンセプトでは、ムーブ後オブジェクトの状態を「有効だが未規定(valid but unspecified)」と定めています。一方で、ライブラリ要件ではムーブ後オブジェクトの状態は単に「未規定(unspecified)」とされています。

ここでの「有効」は定義されていませんが、クラスの不変条件を満たしていて、クラスの全ての操作が規定された振る舞いをしたうえで、未規定の状態、のような意味だと思われます。

しかし実際のところ通常のムーブ操作では「有効」な状態にしておくことは難しく、これを強いることは余計なオーバーヘッドの原因や、ユーザー定義操作の可能性を妨げています。一方で完全に未規定にしてしまうのも望ましくないため、「有効」の意味を詳しく規定することでムーブ後オブジェクトの状態をある程度規定しようとする提案です。

既存の標準ライブラリの実装では、ムーブ後オブジェクトに対して次のような操作だけが必要なようです。

  • mf.~()
  • mf = a
  • mf = move(a)
  • mf = move(mf)

この提案はムーブ後オブジェクトは少なくともこれらの操作は行えることを要求しようとするものです。

この提案では特に、セルフスワップstd::swap(a, a))をサポートすることに焦点を当てています。セルフスワップstd::random_shuffe()などの古い標準ライブラリの実装に現れることがあり、結局はセルフムーブ代入(a = std::move(a))操作に帰着します。

ただし、セルフムーブ代入が有効な操作となるのはムーブ後オブジェクトにおいてのみです。a = std::move(b)の様な代入では、事後条件としてabの操作前の値を保持し、bは未規定の値を保持します。しかし、a = std::move(a)を考慮するとその指定は矛盾しており、それでもその両方の保証が満たされるのはaが既に未規定の値となっている、すなわちムーブ後オブジェクトである場合のみです。ここに有効性を要求してしまうと、セルフムーブ代入操作は有効な操作とはなりません。

この提案では、ムーブ後オブジェクトに対してのみセルフムーブ代入操作を有効であると規定する案と、より一般のオブジェクトに対してセルフムーブ代入操作を有効であると規定する案の二つのどちらかを選択することを提案しています。

P2348R0 Whitespaces Wording Revamp

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

この提案では以下の点が変更されます。

  • whitespaceを文法用語として導入する
    • whitespaceという言葉は別に定義するwhitespaceの集合を参照する
  • 垂直タブは//コメントを終了することを規定
  • new-lineline-breakで置き換える
    • new-lineはLFによる改行文字を示すようになる
  • 翻訳フェーズ1-7まで、コメントを含めたすべてのホワイトスペースが保存されるようになる
    • ただし、そのことは観測可能ではない
  • 文字の集合は、現在のものから拡張しない
    • ホワイトスペースとみなされるものを追加しない
    • ただし、CRLFが一つの改行(line-break)とみなされるようになる
  • line-breakは生文字列リテラル中ではLF(new-line)にマップされる

変更は多岐にわたりますが、破壊的な変更は意図されていません。

P2350R0 constexpr class

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

C++20にてstd::vector/std::stringの全てのメンバがconstexpr対応され、そのほかのコンテナやスマートポインタも対応させる提案が提出されています。

クラスメンバ関数constexpr対応させるにはすべてのメンバ関数constexprを付けて回らねばならず、冗長な記述となっています。

この提案では、クラス宣言にconstexprを付加することでクラスのメンバがすべて暗黙constexpr関数となるようにする構文を提案しています。

C++20 この提案
class SomeType {
public:
  constexpr bool empty() const { /* */ }
  constexpr auto size() const { /* */ }
  constexpr void clear() { /* */ }
};
class SomeType constexpr {
public:
  bool empty() const { /* */ }
  auto size() const { /* */ }
  void clear() { /* */ }
};

これは丁度finalを指定できるところにおけるようにするものです。

また、これはちょうどそのクラスのみに作用するように提案されています。つまり、派生先や基底クラスには影響を与えません。

他にもconstconstevalも考えられますが、この提案ではconstexprに絞られています。

P2352R0 SG16: Unicode meeting summaries 2020-12-09 through 2021-03-24

SG16(Unicode study group)の2020/12/09-2021/03/24までの間のミーティングにおける議事録。

P2353R0 Metaprograms and fragments are needed in comma-separated contexts

P2237で提案されたmetaprograminjectionをテンプレートパラメータなど、カンマ区切りリストの文脈でも使用可能に拡張する提案。

P2237およびP0712では、constevalを用いたシンプルなコードジェネレーション機能であるmetaprogramを導入しました。

consteval {
  for (int i = 0; i < 10; ++i) {
    generate_some_code(i);
  }
}

generate_some_code()はここにソースコードを注入するconsteval関数です。metaprogramは無名変数の初期化子として呼び出される無名のconsteval関数と捉えることができます。

P2237ではmetaprogramを発展させたコードfragmentinjectionを提案しています。

template<struct T>
class Interface {
public:
  consteval {
    // リフレクションによってTのメンバ変数をイテレートする
    template for (auto data : meta::data_members_of(^T))
      // "class fragment"のinjection
      // Tの各メンバに対するgetterの純粋仮想関数宣言をinjectionする
      << <class {
            virtual typename [:meta::type_of(data):]
            |#"get_" + meta::name_of(data)#|() const = 0; 
          }>;
  }
};

consteval void inject_field() {
  // class fragment injection
  << <class { int i = 4; }>;
}

consteval void inject_vardecl() {
  // statement injection
  << <{ int i = 4; }>; 
}

class A {
  // int i = 4; というメンバ変数宣言をinjection
  consteval { 
    //inject_vardecl(); //ERROR
    inject_field(); 
  } 
};

void f() {
  // int i = 4; という変数宣言をinjection
  consteval { 
    //inject_field(); //ERROR
    inject_vardecl(); 
  }
}

これらの構文は名前空間スコープやクラススコープ、ブロックスコープ内でのみ使用可能なものとして提案されており、それ以外の所、とくにテンプレートパラメータリストをはじめとするカンマ区切りリスト内では使用できませんでした。

この提案は、consteval{}を書くことのできるコンテキストを拡張したうえで、新しいいくつかのfragmentを追加することを提案しています。そして、fragmentの種類を増やしたことで、P2237の構文の拡張が困難になったことから、^<K>{}の形式の新しいfragmentの構文も提案しています。

// ステートメントと式のfragment
^<frag::stmt>{ [:m:] = 42; }  // was <{ [:m:] = 42; }>
^<frag::expr>{ 3, [:m:] + 4 } // was <( 3, [:m:] + 4 )>

// この提案で追加される新しいfragment
^<frag::parm>{ int i, int j } 
^<frag::tparm>{ typename T, int N }
^<fram::targ>{ [:Trefl:], [:Nrefl:] }
^<frag::init>{ 3, .[:m:] = 4 }
^<frag::cinit>{ [:m:](3), [:n:](4) } 
^<frag::base>{ public Foo, private virtual [:Barrefl:] }

P2355R0 Postfix fold expressions

可変長テンプレートの畳み込み式において、() []の2つの演算子を使用可能にする提案。

[]は添え字演算子に複数の引数を渡せるようにする提案(P2128R3)の検討で発案され、言語サポートによってそれを達成する代わりに使用できる可能性があります。

// P2128R3による複数引数をとる[]演算子
decltype(auto) index(auto &arr, auto ...args) {
  return arr[args...];
  // この様に展開
  return arr[arg1, arg2, ..., argN];
}

// この提案によるプロクシベース[]演算子
decltype(auto) index(auto &arr ,auto ...args) {
  return (arr[...][args]);
  // この様に展開
  return arr[arg1][arg2]...[argN];
}

この[]による構文と展開を自然に()に拡張することができます。それによって、畳み込み式で利用するためだけに演算子オーバーロードを使用するようなワークアラウンドをいくらか簡単にすることができます。

namespace detail {
  template<class F>
  struct call {
    F &&f;
    template<class T>
    decltype(auto) operator|(T &&t) const {
      return std::forward<F>(f)(std::forward<T>(t));
    }
  };
}

template<class T, class X>
decltype(auto) nest_tuple(T &&t,X &&x) {
  return std::apply(
    [&x]<class ...TT>(TT &&...tt) -> decltype(auto) {
      return (detail::call<TT>{std::forward<TT>(tt)} | ... | std::forward<X>(x));
      // forwrdとかは無視して、展開は次のようになる
      return tt1 | (tt2 | (... | (ttN | x)));
    },
    std::forward<T>(t));
}

// ↑を↓こう書けるようになる

template<class T,class X>
decltype(auto) nest_tuple(T &&t,X &&x) {
  return std::apply(
    [&x]<class ...TT>(TT &&...tt) -> decltype(auto) {
      return (std::forward<TT>(tt)(...(std::forward<X>(x)));
      // forwrdとかは無視して、展開は次のようになる
      return tt1(tt2(...(ttN(x))));
    },
    std::forward<T>(t));
}

これらの利点から、畳み込み式で[] ()を使えるようにしようとする提案です。

パラメータパックをpack[] or ()による呼び出しが可能な型のオブジェクトをc、任意の初期項をaとすると、この提案による拡張は次の様な構文になります

(pack[...]);    // 単項右畳み込み
(...[pack]);    // 単項左畳み込み
(pack[...[a]]); // 二項右畳み込み
(c[...][pack]); // 二項左畳み込み

(pack(...));    // 単項右畳み込み
(...(pack));    // 単項左畳み込み
(pack(...(a))); // 二項右畳み込み
(c(...)(pack)); // 二項左畳み込み

packの中身をarg1, arg2, ..., argNとして、それぞれ次のように展開されます

arg1[arg2[arg3[...[argN]]]];  // 単項右畳み込み
arg1[arg2][arg3]...[argN];    // 単項左畳み込み
arg1[arg2[...[argN[a]]]];     // 二項右畳み込み
c[arg1][arg2]...[argN];       // 二項左畳み込み

arg1(arg2(arg3(...(argN))));  // 単項右畳み込み
arg1(arg2)(arg3)...(argN);    // 単項左畳み込み
arg1(arg2(...(argN(a))));     // 二項右畳み込み
c(arg1)(arg2)...(argN);       // 二項左畳み込み

複雑ではありますが、()[]は対応する構文によって同じ記述が可能で、その構文によって従来の4つの畳み込みのいずれかに帰着され、適用される演算子op[] ()に変更しパックの要素を包み込むように展開されます。

さらに、これらの構文による二項畳み込みの...の後の()内には、pack, lists...の形のリストを書くことができます。

(pack(...(a, b, c)));     // 二項右畳み込み
(c(...)(pack, a, b, c));  // 二項左畳み込み

これは次のように展開されます

arg1(arg2(...(argN(a, b, c)))); // 二項右畳み込み
c(arg1, a, b, c)(arg2, a, b, c)...(argN, a, b, c);  // 二項左畳み込み

[]の時はこのリストを{}で包む必要がある以外は、()と同様になります。

(pack[...[{a, b, c}]]);     // 二項右畳み込み
(c[...][{pack, a, b, c}]);  // 二項左畳み込み

P2356R0 Implementing Factory builder on top of P2320

Factoryパターンを自動的に実装する方法についてのプレゼンテーション資料。

これはSG7のメンバに向けて、現在提案中のリフレクションを用いて、外部のメタデータから任意のクラスのオブジェクトを構築するコードを生成するメタプログラミング手法について解説されています。

P2358R0 Defining Contracts

SG21で議論されているContracts関連の用語を定義する文書。

これはContractsについて何か設計を提案するものではなく、SG21の議論で頻出する概念について、対応する言葉と意味を定義しておくものです。

主に想定されるバグの種類、契約違反を検出したときの振る舞いの各候補についてなどに名前を当て意味を説明しています。

P2360R0 Extend init-statement to allow alias-declaration

if forなどのinit-statementに、using宣言を書けるようにする提案。

C++17でif switchC++20で範囲forの構文が拡張され、init-statementという初期化領域を置けるようになりました。

そこには通常の変数の初期化宣言の他にtypedef宣言も書くことができますが、なぜかusingは書けません。

この提案はusing/typedefの一貫性を向上させるために、init-statement内でusingによるエイリアス宣言を書けるようにする提案です。

// C++20
for (typedef int T; T e : v) { ... }

// この提案
for (using T = int; T e : v) { ... }

モダンC++ではtypedefよりもusingの使用が推奨されていますが、init-statementはそれができない唯一の場所のようです。

また、対応としては逆にinit-statementにおけるtypedefを禁止するという方法もありますが、typedefのスコープを制限するために利用されているらしく、usingでも同じことができるようにすることを提案しています。

P2361R0 Unevaluated string literals

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

_Pragma, asm, extern, static_assert, [[deprecated]], [[nodiscard]]など、文字列リテラルコンパイル時にのみ使用される文脈に現れることができます。このような文字列はナローエンコーディングエンコーディングプリフィックスで指定されたエンコーディングに変換されるべきではありません。

そのため、これらの文字列にはユニコード文字を含めることができる点を除いて、エンコーディングに関して制約されるべきではありません。

しかし、現在これらの文字列は区別されることなく実行時にも使用される文字列と同じ扱いを受けており、エンコーディングの制約も同様です。

この提案は、コンパイル時メッセージなどでユニコードを活用できるようにするために、コンパイル時にのみ使用される文字列について特別扱いするようにする提案です。

この提案は、コンパイル時にのみ使用される文字列について次のように扱われるようにします

この変更は破壊的なものですが、オープンソースのコードベース調査ではほとんど影響がないようです。

P2362R0 Make obfuscating wide character literals ill-formed

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

文字リテラルには複数の文字を指定することができ、それはワイド文字リテラルにおいても同様です。ワイド文字リテラルではそれに加えて、1文字が1つのコード単位に収まらない文字リテラルを書くことができます。

wchar_t a = L'🤦<200d>♀️';  // \U0001f926
wchar_t b = L'ab';  // multi character literal
wchar_t c = L'é́';   // \u0065\u0301

上記のawchar_tのサイズが4バイトである環境(Linuxなど)では書いたままになりますが、2バイトの環境(Windowsなど)だと表現しきれないためUTF-16エンコーディングで読み取られた後に、上位か下位の2バイトが最終的な値として取得されます(Windowsは上位2バイトが残る)。

bはマルチキャラクリテラルと呼ばれるもので、どの文字が残るか、あるいはどういう値になるかは実装定義とされます。MSVCでは最初の文字が、GCC/Clangでは最後の文字が残るようです。

cは2つのユニコード文字から構成されており、これもマルチキャラクリテラルの一種です。これは1文字で同じ表現ができる文字がユニコードに存在していますが(\u00e9)、é́の2文字を組み合わせて表現することもでき、後者の場合は表示上は1文字ですが、1コード単位ではなく2コード単位の文字列となります。

このように、これらの文字列の扱いは実装間で一貫しておらず移植性もなく、視認しづらいことからバグの原因となりえるため、禁止しようという提案です。

ただし、wchar_tのサイズが4バイトである環境の上記aのケースは適正であるため、引き続き使用可能とされます。

これは破壊的変更となりますが、コンパイラのテストケースを除いて、オープンソースのコードベースでは使用されているコードは発見できなかったようです。

P2363R0 Extending associative containers with the remaining heterogeneous overloads

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

C++20では、非順序連想コンテナに対して透過的な検索を行うことのできるオーバーロードが追加されました。「透過的」というのは連想コンテナのキーの型と直接比較可能な型については、一時オブジェクトを作成することなくキーの比較を行う事が出来ることを指します。これによって、全ての連想コンテナで透過的な検索がサポートされました。

現在、C++23に向けて削除の操作に関して同様にしようとする提案(P2077R2)がLWGにおいて議論中です。

この提案は、その対象さらに広げて、以下の操作を透過的にするものです。

  • std::set/std::unorderd_setinsert
  • std::map/std::unorderd_mapinsert_or_assign/try_emplace/operator[]/at
  • 非順序連想コンテナのbucket

この提案の後では、これらの操作の際にKeyと異なる型の値について一時オブジェクトの作成が回避されるようになり、パフォーマンス向上が期待できます。

[C++]TU-local Entityをexposureするのこと(禁止)

モジュールと内部リンケージ名

内部リンケージをもつエンティティ(関数、クラスや変数)は、外部リンケージをもつ関数など翻訳単位のインターフェースの実装詳細をある粒度で分解して表現するために使用されています。それによって、コードの保守性や再利用性の向上が見込めます。

従来のヘッダファイルとソースファイルからなる一つの翻訳単位の構成においては、内部リンケージ名はソースファイル内に隠蔽されているか、ヘッダファイルに書かれていてもヘッダをインクルードした翻訳単位それぞれで内部リンケージを持つ別のものとして扱われるため、内部リンケージを持つエンティティが翻訳単位外部から参照されることはありませんでした。

/// header.h

#include <iostream>

int external_f(int);

// 翻訳単位毎に定義される
// それぞれの定義が翻訳単位を超えて参照されることはない
static void internal_f(int n) {
  std::cout << n << std::endl;
} 
/// source.cpp

#include "header.h"

namespace {
  // この翻訳単位の外から呼ぶことはできない
  int internal_g() {
    return 10;
  }
}

int external_f(int n) {
  internal_f(n);
  return n + internal_g();
}

しかしC++20以降のモジュールにおいては、そのインターフェース単位で定義されている内部リンケージ名がそのモジュール(インターフェース単位)をインポートした先の翻訳単位からも参照できてしまいます。

ヘッダファイルとは異なり、モジュールのインターフェース単位は一つの翻訳単位であり、そのインポートはインターフェース単位にあるすべての宣言を(インポートした側から)到達可能にし、エクスポートされている宣言を可視(名前探索で見えるように)にします。この到達可能性は、内部リンケージを持つ名前であってもインポートした側の翻訳単位の宣言に影響を及ぼす可能性があります。

また、モジュールで定義されたテンプレートがインスタンス化されたとき、インスタンス化が発生した地点から定義に至る一連のインスタンス化経路上で可視になっている宣言を参照することができます。このインスタンス化経路は翻訳単位を超えて定義され、そこでは内部リンケージを持つ名前を参照することができます。

/// mymodule.cpp
module;
#include <iostream>
export module mymodule;

// 内部リンケージ名を翻訳単位外から参照できないのは変わらないが
// インポートした側から宣言は到達可能となる
static void internal_f(int n) {
  std::cout << n << std::endl;
}

namespace {
  int internal_g() {
    return 10;
  }
}

// エクスポートされている、外部リンケージ
export inline int external_f(int n) {
  // 外部リンケージを持つ定義内での内部リンケージ名の使用
  // 例えばこの関数がインライン展開されたとき、何が起こる・・・?
  internal_f(n);
  return n + internal_g();
}

直接的に別の翻訳単位にある内部リンケージを持つ名前を参照できるわけではありませんが、間接的に内部リンケージを持つ名前が翻訳単位から露出してしまいます。例えばエクスポートされた関数が使われた先でインライン展開されるとき、内部リンケージの名前が参照されていたらどうするべきでしょうか・・・?

そのため、最終的なC++20モジュール仕様では、内部リンケージを含めた翻訳単位ローカルのエンティティ(TU-local Entities)が翻訳単位の外から間接的にも直接的にも参照されることが禁止されました。内部リンケージ名は実装の整理や分割のために用いられるものであり、そのような実装詳細はモジュールの外部に公開されるべきではない、という判断です。

export module M;

// 内部リンケージの関数
static constexpr int f() { return 0; }

static int f_internal() { return f(); } // 内部リンケージ、OK
       int f_module()   { return f(); } // モジュールリンケージ、OK
export int f_exported() { return f(); } // 外部リンケージ、OK

// 外部orモジュールリンケージを持つinline関数はTU-localエンティティを参照できない
static inline int f_internal_inline() { return f(); } // OK
       inline int f_module_inline()   { return f(); } // ERROR
export inline int f_exported_inline() { return f(); } // ERROR

// constexpr/consteval関数は暗黙inline
static constexpr int f_internal_constexpr() { return f(); } // OK
       constexpr int f_module_constexpr()   { return f(); } // ERROR
export constexpr int f_exported_constexpr() { return f(); } // ERROR

static consteval int f_internal_consteval() { return f(); } // OK
       consteval int f_module_consteval()   { return f(); } // ERROR
export consteval int f_exported_consteval() { return f(); } // ERROR

// 戻り値型に露出しているのも禁止
static decltype(f()) f_internal_decltype() { return 0; } // OK
       decltype(f()) f_module_decltype()   { return 0; } // ERROR
export decltype(f()) f_exported_decltype() { return 0; } // ERROR


namespace {
  struct c_internal {
    int mf();
    int mf_internal_inline() { return f(); } // OK
  };
  int c_internal::mf() { return f(); } // OK
} // namespace

// モジュールリンケージのクラス定義
struct c_module {
  int mf_module();
  int mf_module_inline() { return f(); }  // OK、暗黙inlineではない
};
int c_module::mf_module() { return f(); } // OK

// 外部リンケージのクラス定義
export struct c_exported {
  int mf_exported();
  int mf_exported_inline() { return f(); } // OK、暗黙inlineではない
};
int c_exported::mf_exported() { return f(); } // OK


static int v_internal = f(); // OK
       int v_module   = f(); // OK
export int v_exported = f(); // OK

static inline int v_internal_inline = f(); // OK
       inline int v_module_inline   = f(); // ERROR
export inline int v_exported_inline = f(); // ERROR

struct c_sdm_module {
  static int sdm_module;
  static constexpr int sdm_module_constexpr = f(); // ERROR
};
int c_sdm_module::sdm_module = f(); // OK

より深遠なサンプルコードはP1498R1をご覧ください。ただし、メンバ関数とテンプレートの例は最終的な仕様とは異なります。

このようなことを規格書では、TU-local Entitiesとその曝露(exposure)として表現しています。

TU-local Entities?

TU-localとは、翻訳単位内にローカルな、みたいな意味です。

TU-localとなるエンティティは基本的には内部リンケージなものを指しています。より正確には次のものです

  1. 内部リンケージ名をもつ関数、型、変数、テンプレート
  2. TU-localエンティティの定義内で、ラムダ式によって導入または宣言された、リンケージ名を持たない関数、型、変数、テンプレート
  3. クラスの宣言・定義、関数本体、初期化子、の外側で定義されている名前のない型
  4. TU-localエンティティを宣言するためだけに使用される、名前のない型
  5. TU-localテンプレートの特殊化
  6. TU-localテンプレートを実引数として与えられたテンプレートの特殊化
  7. その宣言が曝露されているテンプレートの特殊化
    • 特殊化は、暗黙的あるいは明示的なインスタンスによって生成される

基本的には1と3が1次TU-localエンティティであり、他はそれによって副次的にTU-localとなっています。つまり、ほとんどの場合に気にすべき対象は内部リンケージ名を持つものです。

7だけは意味が分かりませんが、テンプレートの事を考えるとおぼろげながら浮かんでくるものがあります。後程振り返ることにします。

また、値やオブジェクトは次の場合にTU-localとなります

  1. TU-local関数またはTU-local変数に関連付けられているオブジェクトであるか、そのポインタ型の場合
  2. クラスか配列のオブジェクトであり、そのサブオブジェクト(メンバ、基底クラス、要素)のいずれかがTU-localである
  3. クラスか配列のオブジェクトであり、その参照型の非静的データメンバが参照するオブジェクトまたは関数のいずれかがTU-localである

ややこしいですが、TU-localなものの中にある変数や値はTU-localで、TU-localなものを参照しているものも、それを含むものもTU-localという事です。

説明のため、以降TU-localと言ったらTU-localエンティティとTU-localな値(オブジェクト)両方を指すことにします。ですが、分かり辛かったらTU-local=内部リンケージ名と思っても差し支えありません。

TU-local Entityの曝露(exposure

ある宣言は次の場合にTU-localなものを曝露(exposure)しています

  1. TU-localな値によって初期化されたconstexpr変数を定義する場合
  2. 次の場合を除いて、TU-localエンティティを参照する場合
    1. . 非inline関数、または関数テンプレートの本体
      • TU-localな型が、プレースホルダによる戻り値型で宣言された関数の推定された戻り値型となる場合を除く
    2. . 変数または変数テンプレートの初期化子
    3. . クラス定義内フレンド宣言
    4. . 非volatileconstオブジェクトへの参照、またはodr-useされておらず定数式で初期化された内部リンケージかリンケージ名の無い参照

TU-localなものが曝露されているとはすなわち、TU-localなものが翻訳単位外部から参照できうる場所に現れている事です。

たとえば1のケース、constexpr変数は定数伝播の結果、その初期化子が参照先にインライン展開される可能性があります。それがTU-localな値を使って初期化されている場合、そのTU-localな値が翻訳単位外に曝される可能性があります。

注意としては、TU-localなものを一切含まない宣言は、外部リンケージを持つものであっても何かを曝露しているわけではありません。曝露されているというのはTU-localなものに対してのことで、TU-localなものを含んでいてかつそれが翻訳単位外に曝される可能性がある場合です。

また、TU-localエンティティを曝露(exposure)するのは常にTU-localではないものです。そして、inline関数がTU-localエンティティを参照する場合、常に曝露する事になります。

export module tu_locale.sample;

// TU-localなもの
namespace {
  constexpr int tul_n = 10;

  void tul_f();

  struct tul_C {};
}


// 曝露していない例

export void ext_f() {
  tul_f();  // 暴露していない(条件2-1)
}

export int ext_n = tul_n; // 暴露していない(条件2-2)

export struct S {
  friend tul_C; // 暴露していない(条件2-3)

  friend void mem_f(tul_C); // 暴露していない(条件2-3)
};

export const int& ext_ref = tul_n; // 暴露していない(条件2-4)


// 曝露している例

constexpr int mod_n = tul_n;  // 曝露している

export inline void ext_f() {
  tul_f();  // 曝露している
}

export decltype(tul_n) ext_g();  // 曝露している

この例で示されていることは、exportの有無で変わらないはずです。つまり、外部リンケージとモジュールリンケージの違いでは曝露するかしないかは変化しません。

TU-local Entityを曝露してたら・・・

モジュールインターフェース単位(プライベートモジュールフラグメントの外側)、あるいはモジュールパーティションにおいて、TU-localではない宣言(あるいはそのための推論補助)がTU-localエンティティを曝露している場合、コンパイルエラーとなります。

TU-localな宣言が単にモジュールのインターフェース単位に書いてあるだけではコンパイルエラーとはなりません。それらの宣言が別の宣言を介して翻訳単位の外から参照される 可能性がある 場合にコンパイルエラーとなります。実際に参照されたときではなく、参照することができる状態になっていたらエラーです。

モジュール単位も一つの翻訳単位をなすため、あるモジュール単位のTU-localなものは同じモジュール内の他の翻訳単位に対しても曝露されてはなりません。

ただし、非モジュールなところ(グローバルモジュール)、あるいはモジュール実装単位(notパーティション)においては、この事は単に非推奨とされコンパイルエラーとはなりません。

そしてもう一つ、ある宣言が、ヘッダユニットではない別の翻訳単位のTU-localエンティティを参照する場合もコンパイルエラーとなります。

こちらの条件はモジュールであるかどうかにかかわらずすべての所に適用されます。ヘッダユニットが例外として除かれているのは、#includeから置換されたときでも後方互換を損ねないようにするためだと思われます。つまりほとんど、モジュールをインポートした時にインポート先にあるTU-localエンティティを参照することを禁ずるものです。

/// mymoudle.cpp
export module mymodule;

static int f() { /* ... */ }
/// main.cpp
import mymodule;
// f()が到達可能となる

int f();  // f()が可視になる

int main() {
  int n = f();  // NG!
}

まとめると、次のどちらかの場合にTU-localエンティティを参照することはコンパイルエラーとなります

  1. ヘッダユニットを除いたインポート可能なモジュール単位において、TU-localではない宣言(あるいはそのための推論補助)がTU-localエンティティを曝露している
  2. ある宣言が、ヘッダユニットではない別の翻訳単位のTU-localエンティティを参照している

テンプレート

TU-localエンティティを曝露してはならないのはテンプレートも同様です。しかし、テンプレートがTU-localエンティティを曝露するのかどうかはインスタンス化されるまでわかりません。そのため、テンプレートがTU-localなものを曝露しているかの判定はテンプレートがインスタンス化される時まで延期されます。

そして、インスタンス化される時、以下のどちらかに該当する場合にコンパイルエラーとなります。

  1. 現れる名前が内部リンケージ名である
  2. 関数名のオーバーロード候補集合に内部リンケージ名が含まれている
/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1);  // (1)

export template<typename T>
void f(T t);  // (2)

// 宣言はOK
export template<typename T>
void external_f(T t) {
  f(t);
}
/// main,cpp
import mymodule;

struct S2{};

void f(S2);  // (3)

int main() {
  S1 s1{};
  S2 s2{};

  external_f(10);  // OK、(2)を呼ぶ
  external_f(s2);  // OK、(3)を呼ぶ
  external_f(s1);  // NG、(1)を呼ぶ
}

勘のいい人は引っかかるかもしれません。さっきと言ってたこと違わない?と

  • 次の場合を除いて、TU-localエンティティを参照する場合
    • inline関数、または 関数テンプレートの本体

これはインスタンス化が発生する前は非依存名であっても、とりあえず内部リンケージを参照する式を書いてもいいよ、という事を言っているにすぎません。インポートした先でインスタンス化が発生したとき、そこでの名前解決の結果、あるいはオーバーロード候補集合にインポート元の内部リンケージなものが含まれているとエラーになります。これはどうやら、TU-localなものの曝露とは別ルートの規定の様です。

明示的インスタンス

テンプレートがその翻訳単位で明示的インスタンス化されていれば、本体で内部リンケージ名を参照していてもコンパイルエラーにはなりません。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
void external_f(T t) {
  f(t);
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1);
/// main,cpp
import mymodule;

int main() {
  S1 s1{};

  external_f(s1);  // OK
}

この様な場合、普通の関数をその宣言によって参照しているのと同じとみなすことができます。どうやら、インポート元に明示的インスタンス化の定義がある場合、インポートした側ではそれに対応する特殊化の暗黙的インスタンス化は発生しない様です(明確に発生しないとされているわけではないですが)。

これを踏まえると、先ほどのTU-localなものの中の条件に羅列されていた謎が一つ解決されます

  • その宣言が曝露されているテンプレートの特殊化
    • 特殊化は、暗黙的あるいは明示的なインスタンスによって生成される

つまりは、TU-localなものを曝露しているテンプレートがその翻訳単位で明示的インスタンス化されているとき、それに対応する特殊化もまたTU-localとなり、それを曝露することも禁止です。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
void external_f(T) {
  f();
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1);

export void g() {
  S1 s1{};
  f(S1);  // NG、TU-localな特殊化external_f<S1>()の曝露
}

なお、この明示的インスタンス化による例外は、inlineとマークされている関数テンプレートでは無効です。inlineの示すとおりにインライン展開された場合、結局その定義がインポートした側に展開されることになるためです。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
inline void external_f(T t) {
  f(t);
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1);
/// main,cpp
import mymodule;

int main() {
  S1 s1{};

  external_f(s1);  // NG!
}

メンバ関数の暗黙inline

ここまで見てきたようにおおよそinline関数(テンプレート)においては、TU-localなものの曝露がごく簡単に起きます。それで困ってしまうのが、クラスのメンバ関数が暗黙的にinlineになることです。

export module mymodule;

static void internal_f();

// モジュールリンケージのクラス定義
struct c_module {

  // inlineではない
  int mf_module();

  // 暗黙inline
  int mf_module_inline() { 
    return internal_f();  // NG、内部リンケージ名を曝露している
  }
};

int c_module::mf_module() { 
  return internal_f();  // OK、曝露していない
}

モジュール内でいつものようにクラスを書いたとき、ヘッダファイルからモジュールへ移行するとき、この様なエラーに遭遇する可能性は非常に高いでしょう。これを回避しようとすると、内部リンケージ名を使用しているメンバ関数は全てクラス外で定義しなければなりません。明らかに冗長な記述が増え、とても面倒くさいです・・・

C++20の最終仕様では、モジュール内でのみメンバ関数の暗黙inline化は行われなくなります。これによって、クラスの定義は今まで通りに行うことができるようになります。

export module mymodule;

static void internal_f();

// モジュールリンケージのクラス定義
struct c_module {

  // inlineではない
  int mf_module();

  // inlineではない
  int mf_module_inline() { 
    return internal_f();  // OK、曝露していない
  }

  // inliine
  inline int inline_f() {
    // ...
  }
};

int c_module::mf_module() { 
  return internal_f();  // OK、曝露していない
}

暗黙のinlineが行われない事によってインライン展開がされなくなり、パフォーマンスが低下する可能性は無くもありません。inlineが必要な場合は明示的に指定するようにしましょう。

なお、ここでのモジュールにはグローバルモジュールを含んでいません。モジュールの外ではこれまで通りにメンバ関数は暗黙inlineです。

さんぷるこーど

規格書より、複雑な例。

/// A_interface.cpp(プライマリインターフェース単位)
export module A;

static void f() {}

inline void it() { f(); }           // error: fを曝露している
static inline void its() { f(); }   // OK

template<int>
void g() { its(); }   // とりあえずはOK、これはモジュールリンケージ
template void g<0>();

decltype(f) *fp;                    // error: fはTU-local(fの型ではない)
auto &fr = f;                       // OK
constexpr auto &fr2 = fr;           // error: fを曝露している(fのアドレスはTU-localな値)
constexpr static auto fp2 = fr;     // OK

struct S { void (&ref)(); } s{f};               // OK, 値(fのアドレス)はTU-local
constexpr extern struct W { S &s; } wrap{s};    // OK, 値(sのアドレス)はTU-localではない

static auto x = []{f();};           // OK
auto x2 = x;                        // error: decltype(x)を曝露している(クロージャ型はTU-local)
int y = ([]{f();}(),0);             // error: fを曝露している(クロージャ型はTU-localではない)
int y2 = (x,0);                     // OK

namespace N {
  struct A {};
  void adl(A);
  static void adl(int);
}
void adl(double);

inline void h(auto x) { adl(x); }   // OK, ただしその特殊化はN::adl(int)を曝露しうる
/// A_impl.cpp(実装単位)
module A;
// Aのインターフェースを暗黙的にインポートしている

void other() {
  g<0>();                   // OK, 特殊化g<0>()は明示的にインスタンス化されている
  g<1>();                   // error: 特殊化の実体は、TU-localなits()を使用している
  h(N::A{});                // error: オーバーロード候補集合はTU-localなN::adl(int)を含んでいる
  h(0);                     // OK, adl(double)を呼ぶ
  adl(N::A{});              // OK; N::adl(N::A)を呼び、N::adl(int)は見つからない
  fr();                     // OK, f()を呼ぶ
  constexpr auto ptr = fr;  // error: frは定数式で使用可能ではない
}

テンプレートの例。

/// moduleM.cpp
export module M;

namespace R {
  export struct X {};
  export void f(X);
}
namespace S {
  export void f(R::X, R::X);  // (1)
}
/// moduleN.cpp
export module N;
import M;

export R::X make();

namespace R {
  static int g(X);  // (2)
}

// 宣言まではOK
export template<typename T, typename U>
void apply(T t, U u) {
  f(t, u);  // (1)を参照、OK
  g(t);     // (2)を参照、内部リンケージ名の曝露、NG
}
/// main.cpp
module Q;
import N;

namespace S {
  struct Z { 
    template<typename T>
    operator T();
  };
}

int main() {
  auto x = make();  // OK、decltype(x)はR::Xでmodule Mにあり、可視ではないが名前を参照していない

  apply(x, S::Z()); // NG、S::fはインスタンス化コンテキストで可視
                    // しかし、R::gは内部リンケージであり、翻訳単位の外からは呼べない
}

モジュールにおけるinlineの意味

これらの変更によってモジュールにおけるinlineはある意味で本来の意味と役割を取り戻します。すなわち、inlineとマークされた関数のみをインライン展開するという意味になり、その他の効果はほぼ意味をなさなくなります。

意味をなさなくなるというのはinlineの持つ定義の唯一性などの効果がなくなるわけではなく、モジュールにおいてはその意味がなくなるということです。例えば、モジュールでエクスポートされているinline関数・変数はインポートされた側から可視かつ到達可能となり参照できるようになりますが、そこでは#includeの時のように定義が翻訳単位ごとにコピペされる事はないので、inlineの定義を一つに畳み込む効果は必要ありません。

モジュール内部ではODRが厳しくなっており、モジュール内で定義されたinline関数の定義はただ一つでなくてはならず、参照する場合はその定義に到達可能となっている必要があります。このことにリンケージは関係なく、1つのモジュール内においてもinlineはインライン展開のためのコンパイラへの口添え以外の意味を持っていません。

ただし、ここでモジュールと言っているものにグローバルモジュールは含んでいません。すなわち、モジュールの外側では従来と変わりありません。

また、モジュールにおいてinlineと宣言されていない関数をインライン展開してはいけないという規定はありません。どうやらそのような実装を可能にするために意図的に空白を設けているようです。

モジュールリンケージ

これらの変更によって、内部リンケージ名はモジュールのABIの一部とはなることは無くなり、完全に翻訳単位内に隠蔽されるようになります。

一方、モジュールリンケージ名はそうではなく、エクスポートされたinline関数の内部など、使用される場所によってはモジュールのABIの一部となる事があります。

export module M;

// 内部リンケージ
static void internal_f() { /* ... */ }

// モジュールリンケージ
void module_f() { /* ... */ }

export inline void ng() {
  internal_f();  // NG
}

export inline void ok() {
  module_f();    // OK
}

なおどちらもAPIの一部となることはありません。

わからん、3行で

モジュールでは内部リンケージ名を
inline関数や関数テンプレートから
参照するのはやめましょう

参考文献

この記事のMarkdownソース

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

文書の一覧

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

採択された文書

P2313R0 Core Language Working Group "tentatively ready" issues for the February, 2021 meeting

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

解決されたIssueは一件だけです。

  1. 2470. Multiple array objects providing storage for one object
    • unsigned char/std::byteの配列上に別のオブジェクトを構築する時、新しく作成されるオブジェクトにどの配列がストレージを提供したのかが曖昧にならないようにした。

その他文書

N4884 WG21 2021-02 Virtual Meeting Minutes of Meeting

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

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

N4885 Working Draft, Standard for Programming Language C++

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

N4886 Editors’ Report - Programming Languages - C++

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

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

P0448R4 A strstream replacement using span as buffer

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

以前の記事を参照

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

この提案は既にこのリビジョンのLWGでのレビューと投票を終えており、全体会議での投票待ちをしています。そこで反対が無ければ、C++23に導入されます。

P0958R3 Networking TS changes to support proposed Executors TS

Networking TSのExecutorの依存部分をP0443のExecutor提案の内容で置き換える提案。

以前の記事を参照

このリビジョンでの変更は、executorコンセプトについてsatisfiesを使用していた所をmodelへ変更したことや、timersocketなどにbasicプリフィックスを加えたことなど、文面の調整です。

P1018R9 C++ Language Evolution status 🦠 pandemic edition 🦠 2021/01–2021/03 - 2021/01–2021/03

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

これらの提案はコンセンサスが得られ、CWGに転送されています。また、その議論や投票の際の賛成・反対のコメントが記載されています。

また、次の提案はライブラリ機能についてのものですが、その内容を言語サポートとすべきかが投票にかけられました。

Executorのプロパティ指定の方法は独立したライブラリ機能として提案されていますが、これを言語サポートする方向でコンセンサスが得られたようです。

P1315R7 secure_clear

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

以前の記事を参照

このリビジョンでの変更は、C言語に向けた文言の表現の選択肢の改善や、選択されなかったものの削除、C/C++委員会での投票結果の記載などです。

P1425R4 Iterators pair constructors for stack and queue

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

以前の記事を参照

このリビジョンでの変更は、2つに分かれていた機能テストマクロを__cpp_lib_adaptor_iterator_pair_constructor一つに統一した事と、提案する文言の調整です。

このリビジョンは既にLWGのレビューと投票が済んでおり、次の会議の全体投票を待っています。

P1518R1 Stop overconstraining allocators in container deduction guides

P1518R2 Stop overconstraining allocators in container deduction guides

コンテナとコンテナアダプタのクラステンプレート引数推論時の振る舞いを修正し、pmrコンテナの初期化を行いやすくする提案。

std::pmr::monotonic_buffer_resource mr;
std::pmr::polymorphic_allocator<int> a = &mr;
std::pmr::vector<int> pv(a);

// CTADを使用しない構築、全てok
auto s1 = std::stack<int, std::pmr::vector<int>>(pv);
auto s2 = std::stack<int, std::pmr::vector<int>>(pv, a);
auto s3 = std::stack<int, std::pmr::vector<int>>(pv, &mr);

// CTADを使用する構築
auto ds1 = std::stack(pv);
auto ds2 = std::stack(pv, a);
auto ds3 = std::stack(pv, &mr); // NG!

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

※CTAD = Class Template Argument Deduction (クラステンプレートの実引数推定)

stackをはじめとするコンテナアダプタのアロケータ引数は、クラステンプレートのテンプレートパラメータの推論に寄与しません。従って、対応するCTADを使用しない構築の時と同様にコンテナ型からの推論を行うのが望ましいはずです。

namespace std {

  template<typename Container, typename Allocator>
  class stack;

  // stackの2引数推論補助
  template<class Container, class Allocator>
  stack(Container, Allocator)
    -> stack<typename Container::value_type, Container>;
}

このことは何か意図があってのものではなく、単に見落とされただけだと思われるので修正しようとする提案です。

なぜこのようなことが起こるのかというと、コンテナアダプタの推論補助について次のような規定が存在しているためです。

A deduction guide for a container adaptor shall not participate in overload resolution if any of the following are true: - ... - It has an Allocator template parameter and a type that does not qualify as an allocator is deduced for that parameter. - ...

[N4861 container.adaptors.general]/4.4

アロケータとしての資格のない型がアロケータ型として渡されている場合、そのアロケータ型がテンプレートパラメータの推論に寄与しないとしても、アロケータ型を受け取る推論補助を無効化してしまいます。

アロケータとしての資格がある型というのは実装定義ですが、最小の要件の一つとして、アロケータ型Aについてメンバ型A::value_typeが利用可能であることがあります([container.requirements.general]/17)。

auto ds3 = std::stack(pv, &mr); // NG!

先程の例のここでは、第二引数の&mrの型はstd::pmr::monotonic_buffer_resource*というポインタ型であって、当然メンバ型を持っておらず、アロケータとしての資格がある型ではないため対応する推論補助は考慮されなくなります。ただ一方で、std::pmr::vector<int>std::pmr::monotonic_buffer_resource*をアロケータとして利用して構築することができます。

さらに、似た問題がstd::vectorそのものにも存在しています。

std::pmr::monotonic_buffer_resource mr;
std::pmr::polymorphic_allocator<int> a = &mr;
std::pmr::vector<int> pv(a);

// CTADによらない構築、全てok
auto v1 = std::vector<int, std::pmr::polymorphic_allocator<int>>(pv);
auto v2 = std::vector<int, std::pmr::polymorphic_allocator<int>>(pv, a);
auto v3 = std::vector<int, std::pmr::polymorphic_allocator<int>>(pv, &mr);

// CTADを使用する構築
auto dv1 = std::vector(pv);
auto dv2 = std::vector(pv, a);
auto dv3 = std::vector(pv, &mr);  // NG!

ここでの問題は先ほどとは少し違っていて、暗黙に生成される推論補助を利用する経路で問題が起きています。

推論補助が無い場合、対応するコンストラクタから推論補助を生成してテンプレートパラメータを推論しようとします。ここで対応しているコンストラクタはアロケータを受け取るコピーコンストラクタです。

namespace std {

  template<typename T, typename Allocator>
  class vector {

    // アロケータを受け取るコピーコンストラクタ
    vector(const vector<T, Allocator>&, const Allocator&);
  };
}

そして、第1引数からはT = int, Allocator = std::polymorphic_allocator<int>が導出され、第2引数からはAllocator = std::pmr::monotonic_buffer_resource*が導出されます。同一のパラメータに対して衝突する候補が発生しているので、推論は失敗しコンパイルエラーとなります。

この提案ではこれらの解決のために、すべてのコンテナのアロケータを受け取るコンストラクタ引数をstd::type_identity_tで包むことでアロケータ引数をCTAD推論の対象から外し、またコンテナアダプタのアロケータ型がある場合の推論補助の要件を「コンテナ型が無く、アロケータ型がある場合」のように少し緩和します。

namespace std {

  template<typename T, typename Allocator>
  class vector {

    // 現在
    vector(const vector<T, Allocator>&, const Allocator&);

    // この提案
    vector(const vector<T, Allocator>&, const type_identity_t<Allocator>&);
  };
}

void stack() {
  std::pmr::monotonic_buffer_resource mr;
  std::pmr::polymorphic_allocator<int> a = &mr;
  std::pmr::vector<int> pv(a);

  // この提案の後では、共にOK
  auto ds3 = std::stack(pv, &mr);
  auto dv3 = std::vector(pv, &mr);
}

std::vectorの場合は、std::type_identity_tによって第2引数の&mrからAllocatorパラメータを推論しなくなるのでAllocatorが一つに定まるようになり、std::stackの場合は文言の変更によって&mrから推論されるアロケータ型の適格性がチェックされなくなるので既存の推論補助によって正しく推論が行われるようになります。

この提案の内容は標準ライブラリの主要な3実装がそれぞれ、誤って 先行して実装しているようです。特に連想コンテナはMSVCとClang(と一部GCCも)すでにこうなっているようです。

P1875R2 Transactional Memory Lite Support in C++

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

以前の記事を参照

変更履歴が無いため変更点はよくわかりませんが、おそらくP2066の議論の進行に伴って必要となった変更などを反映したのだと思われます。

P2025R2 Guaranteed copy elision for return variables

NRVO(Named Return Value Optimization)によるコピー省略を必須にする提案。

以前の記事を参照

この提案は主に明示的に注釈することで(N)RVOをオプトインする構文を追加するために、EWGに差し戻されました。

このリビジョンでは、その検討のセクションおよびABIレベルのコピー省略の問題についての説明のセクションを追加しています。

そこでは、[[nrvo]]属性や変数宣言時のreturn注釈、return explicit文、関数宣言(定義)のreturn指定など、様々な構文が検討されています。

P2041R1 template = delete

関数テンプレートがdeleteオーバーロードを提供可能なように、クラス/変数テンプレートでもdelete指定オーバーロード(特殊化)を提供できるようにする提案。

複数の事を意味する同じ名前があり、それらのうちのいくつかは定義されるべきではないとき、現在はその定義を禁止するシンプルで簡易な方法がありません。変数・クラステンプレートについてdelete指定できるようにすることで、一部の特殊化を禁止したり、逆に一部の特殊化だけを許可したりすることができるようになります。

// プライマリ変数テンプレートはdelete、特殊化は許可
template<typename>
int x = delete;
template<>
int x<int> = 5;

// 変数テンプレートの特定の特殊化を禁止
template<typename T>
auto y = T();
template<>
auto y<int> = delete;

// プライマリクラステンプレートはdelete、特殊化は許可
template<typename>
struct s = delete;
template<>
struct s<int> {
};

// クラステンプレートの特殊化を禁止
template<typename>
struct t {
};

template<>
struct t<int> = delete;

非テンプレートのクラスなどでは、deleteするクラスを定義するのではなく最初から定義しないようにすれば同じ効果が得られます。そのため、ここではテンプレートではないものまでdelete指定できるようにすることは提案されていません。

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

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

以前の記事を参照

このリビジョンの変更点は、R5で標準ライブラリのもののほとんどがatomicブロックでの使用を許可されましたが、そのうち同期の問題が発生しうるものを除外した事です。例えば、shared_ptrsynchronus_memory_resourceタイムゾーンのデータベースなどが該当します。

P2093R5 Formatted output

p2093r4.html)

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

前回の記事を参照

このリビジョンでの変更は、ostreamを取るオーバーロードをヘッダ分けしたことと、ostreamを取らないものの配置するヘッダの候補をリスト化した事です。現在は<io>に配置することを提案しています。

この提案はSG16での議論とレビューを終えて、LEWGに送られました。非常に素早く作業が進行しているため、C++23に入る可能性は高そうです。

P2210R2 Superior String Splitting

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

前回の記事を参照

このリビジョンでの変更は、現在のviews_splitlazy_split)にある問題の解決を含んだうえで文言を調整し、実装例を追記した事です。

この提案はLEWGでの議論が完了しLWGに転送する最終投票を待っていますが、C++20にDRとして適用するためにLWGで先行してレビューが完了しています。LEWGでの投票が問題なく終われば、C++23(C++20)導入に向けて全体会議での投票に進みます。

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

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

以前の記事を参照

このリビジョンでの変更は、機能テストマクロに関する議論を追記したことと、サンプルと文言の微修正です。

この提案の修正は実際にはconstexpr関数で実行可能なものを増やしているわけではありませんが、__cpp_constexprの値を微増させています。検出が必要になるとは思えないけれどconstexprの許容範囲を広げるときに値を更新する方向性を支持している、ということが説明されています。

P2266R1 Simpler implicit move

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

以前の記事を参照

このリビジョンでの変更は、提案する文言のリファクタリングと、ラムダの内部でローカル変数がスローされるときにも、スローされる変数を暗黙ムーブするかのような文言になっている部分を修正する文言を追加した事です。

P2299R0 mdspan and CTAD

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

P0009R10で提案されているstd::mdspanは、std::basic_mdspanを基礎として、そのエイリアスとしてstd::mdspanを定義しています。

template <class ElementType, ptrdiff_t... Extents>
using mdspan = basic_mdspan<ElementType, extents<Extents...>>;

これは例えば次のように利用できます

int main() {
  // 何かメモリ領域
  double* data = ...;

  // 静的サイズ
  mdspan<double, 64, 64> a1(data);
  // 動的サイズ
  mdspan<double, dynamic_extent, dynamic_extent> a2(data, 64, 64);
  // 静的+動的サイズ
  mdspan<double, 64, dynamic_extent> a3(data, 64);
}

C++20からはエイリアステンプレートに対するCTADが利用可能となっているので、上記の2つめの例は次のように書けるはずです。

int main() {
  // 何かメモリ領域
  double* data = ...;

  // 動的サイズ
  mdspan a2(data, 64, 64);
}

冗長なdynamic_extent指定を排除し非常に読みやすくなります。しかし、これはどうやら意図通りに動かないようです。

ここでは、std::mdspan<double>を推論した後、std::basic_mdspan<double, extents<>>を推論しインスタンス化します。これはstd::basic_mdspanの動的エクステントのコンストラクタでstatic_assertに引っかかりコンパイルエラーを起こします。

単純には、std::mdspanに推論補助を追加すればこの問題の解決は図れるはずですが、現在の仕様ではエイリアステンプレートに推論補助を追加できません。かといって、std::mdspanを単独のクラスにしてしまうとstd::basic_mdspanとの相互運用性がなくなるなど様々な問題が発生します。

この提案はこの問題の周知を図り、よりよい解決策を募るものです。

P2314R1 Character sets and encodings

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

以前の記事を参照

このリビジョンでの変更は、文言の修正とP2297R0との目的の差異を説明するセクションの追加、ロケール依存の実行文字集合execution (wide) character set)の文言の場所をライブラリに移動した事です。

この提案は、既存の振る舞いを維持しながら、言葉の定義を明確にすることで字句解析におけるコア言語のルールの再構築を目指すものです。従って、EWGの負担は軽くなる筈、という事が説明されています。

P2322R1 ranges::fold

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

以前の記事を参照

このリビジョンでの変更は、以前にregular_invocableコンセプトを用いていたところをinvocableコンセプトに変更した事(regularであることは必要なかったため)と、以前にfold_firstなどとしていた初項を範囲から補う関数ファミリを初項の提供有無でのオーバーロードに変更したことです。

以前に書いたサンプルは次のようになります。

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

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

std::vector<std::string> vec2 = {"aaa", "bbb", "ccc"};
std::string concat1 = std::ranges::fold(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(vec2, std::ranges::plus{});
// concat2 == concat3 == "cccbbbaaa"

P2325R1 Views should not be required to be default constructible

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

以前の記事を参照

このリビジョンでの変更は、パイプラインでの左辺値/右辺値の固定エクステントstd::spanの扱いに関する例を追加した事です。

std::spanviewであるはずなのですが、固定エクステントの場合はデフォルト構築できないようになっています。これによって、右辺値と左辺値でそれぞれ異なる、非効率な結果が得られます

std::span<int, 5> s = /* ... */;

// spanはviewではないため、transform_viewはコピーする代わりに`ref_view<span<int, 5>>`を保持する
// sの寿命が先に尽きる場合、これはダングリング参照となる
auto lvalue = s | views::transform(f);

// spanはborrowed_rangeであるため、これはコンパイル可能
// ただ、sをコピーするのではなく、subrange<span<int, 5>::iterator>を保持する
auto rvalue = std::move(s) | views::transform(f);

左辺値の場合はダングリングの危険があり、追加の間接参照が必要となります。右辺値の場合はダングリングの危険はないものの、2つのイテレータを保持する必要からspanを直接保持する場合の倍のストレージを使用します。この値カテゴリの違いによる異なった振る舞いはどちらにせよ非効率で、固定エクステントspanviewだった場合のデメリットよりもひどいものです。

固定エクステントのspanは正当な理由でデフォルトコンストラクタを持たず、viewにするためにデフォルトコンストラクタを構築するとユーザーが気を付けてチェックしなければならない追加の状態が導入されます。spanの全ての操作に文書化の必要がある事前条件が課されることになり、これはまたすべてのviewにも当てはまっています。

この観点からもやはり、viewコンセプトのデフォルト構築要求は不要であるといえます。

P2328R0 join_view should join all views of ranges

std::ranges::join_viewの制約を緩和して、prvalueviewではないrangeを平坦化できるようにする提案。

join_viewrangerangeとなっているシーケンスを1つのrangeに平坦化するもので、他のところではflattenとも呼ばれています。

ここに1つ機能を加えて、要素(内側のrange)を変換しつつ平坦化するflat_mapというものがあります。そのまま書くと、C++20時点では次のようになるはずです。

template<std::ranges::range R, std::invocable<std::ranges::range_value_t<R>> F>
  requires std::ranges::range<std::ranges::range_value_t<R>> and
           std::ranges::range<std::invoke_result_t<F, R>>
auto flat_map(R&& r, F&& f) {
  return r | std::views::transform(f) | std::views::join;
}

このようなrangeアダプタのチェーンはほとんどの場合にコンパイルエラーとなります。

現在のjoin_viewが平坦化できるのは次の2つのどちらかです。

  • glvaluerangerange
  • prvalueviewrange

r | std::views::transform(f)の結果はfの結果のrangeによるprvalueviewとなり、fの結果のrangeprvalueviewとならない場合にコンパイルエラーとなります。少し考えてみると、これはかなり一般的なユーズケースであることがわかると思います(fの結果として範囲を返すとき、std::vectorを使いたくなりませんか?)。

現在のjoin_viewprvalueな(viewではない)rangerangeサポートが欠けていることによってこの問題は発生しています。

Range-v3ライブラリでは、views::cache1(提案されている名前はviews::cache_latest)というviewを間に挟むことでこの問題を解決し、views::flat_mapを導入しています。

cache1は元のrangeイテレータの間接参照結果をキャッシュとして持っておくことによって、prvaluerangerangeglvaluerangerangeに変換するものです。これによってjoin_viewはあらゆるケースで平坦化できるようになります。

ただ、cache1cache_latest)にはいくつかの問題があります

  • イテレータoperator*() conststd::indirectly_readbleコンセプトで要求される)は内部でキャッシュを操作するため、スレッドセーフではない。
    • 標準ライブラリのconstメンバ関数はスレッドセーフであることを表明している。
  • cache1の理解や発見は自明ではない。
    • なぜそれが必要なのかのヒントはなく、join_viewの使用を通してその存在の必要性を納得するしかない。

現在のjoin_viewは、prvalueviewrangeを処理する際に、内側のprvalueviewを内部でキャッシュしておくことによって処理しています。この提案では、cache_latestを導入する代わりに、このキャッシュ機構をprvalueな(viewではない)rangerangeに対しても行うことで上記の問題の解決を図ります。

ただし、この場合のキャッシュは伝播しません。すなわち、そのようなキャッシュを保持しているjoin_viewをコピー/ムーブすると、コピー/ムーブ先ではキャッシュは空となります。これによって、join_viewのコピー/ムーブが元のrangeの生成するものに依存しないことが保証されています。また、このようなキャッシュをしている場合のjoin_viewinput_rangeであり、begin()の呼び出しは1度しか行えません(最初のbegin()の呼び出し後にrangeとして使用不可能となる)。

P2330R0 WG21 2021-02 Virtual Meeting Record of Discussion

2月のWG21本会議における発言記録。

コア言語に関して

  • 昨年11月の本会議で採択されたP2238R0にあるコア言語IssueがC++20に対するDefect Report(DR)として扱うことを決定。
  • P1787R6の内容をDRとして扱うことを決定(バージョンは指定されていない)。
  • P2313R0の内容をDRとして扱うことを決定(バージョンは指定されていない)。

ライブラリ機能に関してはP0533R7 constexpr for <cmath> and <cstdlib>(リンクはR6)がリジェクトされた過程が記載されています。

それによれば、精度低下によるエラーが定数評価を妨げる可能性があるという問題提起があり、その解決策について十分に議論が尽くされておらず、このままだと実装が困難となるか実装間で相違が発生する可能性があるという点が懸念され、投票の結果反対および中立が多く出たため、リジェクトされたようです。

P2332R0 Establishing std::hive as replacement name for the proposed std::colony container

提案中のstd::colonyの名前をstd::hiveに変更する提案。

LEWGの議論の過程で、std::colonyという名前に関して何人かのメンバが次のような疑問を抱いているようです

  • colonyという単語は多くの意味を含んでいる
  • colonyという名前は馴染みがなく、一般的では無い

色々な名前の候補が上がった結果、hive(ミツバチの巣)が選ばれたようです。これにはLEWGの多くのメンバとstd::colonyの作者(著者)の方も同意を示しているようです。

colony(集団・居住地・村など)のお気持ちは

  • コンテナそのものがコロニー
  • 記憶ブロック(配列)が家
  • 要素の記憶域が部屋
  • 要素は人
  • 人の行き来(要素の挿入・削除)に対して家や部屋に変化はない(記憶域と配列は安定)

hiveのお気持ちは

  • コンテナは巣箱
  • 1記憶ブロック(配列)が1つの巣板
  • 要素の記憶域は6角形のセル
  • 要素はミツバチ

hiveではミツバチがセルに住んでいる訳ではないことから要素の安定性に関しての表現が足りていないとのことですが、LEWGでは名前の意味の単純化のために許容されたようです。

また、std::colonyが力を発揮する用途がイテレーション時に要素の挿入・削除が頻繁に起こるようなケースであり、蜂の巣の出入りの忙しさがこの側面を表現しているとして好まれたようです。

P2333R0 2021 Winter Library Evolution Poll Outcomes

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

以下の9つの提案が投票にかけられ、どれもLWGに転送されることが可決されています。

P2334R0 Add support for preprocessing directives elifdef and elifndef

#elifでマクロの定義の有無で条件分岐する糖衣構文となるプリプロセッシングディレクティブである#elifdef/#elifndefの提案。

#ifdef/#ifndef#if defined(macro_name)/#if !defined(macro_name)の糖衣構文として随分前から利用可能ですが、#elif defined(macro_name)/#elif !defined(macro_name)に対応する糖衣構文はありません。

このような構文の一貫性のなさは、一部のユーザーにとっては予測可能ではありません。

#elifdef/#elifndefを追加し一貫性を改善することで、ユーザビリティの向上を図る提案です。

#ifdef M1
...
#elif defined(M2)
...
#endif

// ↑が↓こう書ける

#ifdef M1
...
#elifdef(M2)
...
#endif

この提案はすでにC23に導入されており、C/C++間の互換性確保のためにC++に対しても提案されているものです。

P2338R0 Freestanding Library: Character primitives and the C library

<charconv>std::char_traitsをはじめとするいくつかのヘッダをフリースタンディングライブラリ指定する提案。

C/C++の標準ライブラリにはシステムプログラムにおいて有用な機能が多数存在していますがそれらのライブラリ機能は必ずしもフリースタンディング指定されていないため、コンパイラ拡張などの移植性の乏しい形で利用するか、自分で実装するしかない場合が多いようです。

そのようなライブラリ機能のうち、OSのサポートや動的メモリ確保を必要とせずに実装できるものをフリースタンディングライブラリとして提供することによって、より高レベルにシステムプログラムを記述し、かつそれを移植可能にすることができます。

この提案は、フリースタンディングライブラリをOSや動的なメモリ確保に依存しない標準ライブラリの最大のサブセットとして提供することを目的とした一連の取り組みの一環です。

この提案でフリースタンディング指定を提案されているのは次のものです

  • <string>
    • std::char_traits
  • <charconv>
  • <cinttypes>
  • <cstdlib>, cmath
  • <cstring>
  • <cwchar>
  • <cerrno>, <system_error>
    • errnoを除く各種エラー定義マクロとstd::errc

この提案は同時にC標準に対しても行われています。

P2339R0 Contract violation handlers

契約プログラミングについて、契約違反を起こしたときに終了するのではなく継続する場合のユースケースについて説明した文書。

C++20でリジェクトされたコントラクト仕様にも、現在議論されているものにも、契約違反が起きた時のデフォルトの対応はstd::terminate()std::abort()を実行してその場で終了することです。

しかし、契約違反が起きた時でも実行を継続したい場合があり、C++20の仕様には違反継続モードがあり、現在の議論では例外を投げる、return;するなどが議論されているようですが、どちらも反対意見があるようです。

この文書はC++23の契約プログラミング導入にむけて、契約違反時に実行を継続するユースケースやその利点についてSG21内で共通の理解を得るためのものです。

P2340R0 Clarifying the status of the ‘C headers’

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

ISO規格の文脈での非推奨(deprecated)という言葉は、使用されない・推奨されない・将来削除されうる、などの意味を持ちます。C++が規格化されて以降、Cのヘッダは非推奨という扱いで規定されており、C++のエコシステムとして提供はされているが使用しづらいものとなっていました。

C++におけるCヘッダの主な役割は、C言語との相互運用性およびC言語のABIとリンケージ規則を使用する他のシステムとの相互運用のためにあり、非推奨なのは相互運用を目的としないC++コードでの利用時だけなはずです。この提案は、CヘッダをCおよびその互換システムとの相互運用の目的のために利用することができるように、Cヘッダの非推奨を解除してCのヘッダ群がC++のエコシステムとして提供されることを明確にしようとする提案です。

なお、ここでのCヘッダとは<cxxx>という形式のC++におけるC互換ヘッダではなく、Cの標準ライブラリヘッダとして定義されている<xxx.h>の形式のものを指しています。

この提案の後でも、Cコードとして有効である必要がないC++コードでのCヘッダの使用は推奨されません。あくまでCのABIおよびリンケージを利用するシステムとの相互運用性確保のための変更です。

P2400R0 Library Evolution Report

2020年10月27日~2021年2月16日の間の、LEWGの活動記録。

LEWGにおいて議論されている機能の現状について、及びレビューや投票を行った提案のリストなどがあります。

Executor

P0443R14のレビューが完了し、そこで得られたフィードバックに基づいてP0443R15を準備中のようです。投稿され次第、再びレビューが行われる予定です。

コルーチンのライブラリサポート

P2168R1std::generator)の改訂版が近々提出される予定で、そのレビューはすぐに行われる予定です。

しかし、他の提案は一度のレビューの後改訂されておらず、std::generator以外のコルーチン関連のライブラリサポートの議論は止まっているようです(ただし、Executorに依存しているために止まっているものがある可能性があります)。

Networking TS

Networking Study Groupで議論が続いており、まだLEWGに提出されていません。これはExecutorに依存していますが、大きな機能であるために並行して作業が可能なはずです。

2021年春頃までにLEWGでのレビューに進めない場合、C++23に間に合わなくなる可能性があります。

標準ライブラリのモジュール化

2020年春以降、関連した活動が見られないようです。このまま議論もされなければ、C++23には間に合いません。

ranges

P2214R0をレビューし、この提案の方向性でのRangeライブラリの拡張をLEWGは支持しています。

P2210R0std::ranges::split_viewの改善)はLEWGにおける最終レビューを通過し、LWGに転送するための電子投票にかけられる予定です。前回の電子投票の期限に間に合わなかったためまだ投票は行われていませんが、2021年春頃には投票が開始される予定です。

ranges関連の提案をよく書いている著者の方が協力的かつ活発なためranges関連のレビューはスムーズに進行しており、提案は迅速に処理されているようです。

format

P2216R2std::foramtの改善)はLEWGにおける最終レビューを通過しており、2021年1月に電子投票にかけられました。この提案にはC++20に対する破壊的な変更が含まれていますが、既存実装が無いために影響は無いと判断されました。

P2093R2std::print)はまだLEWGで議論の真っ最中ですが、著者の方の応答性が高いために作業は順調に進行しているようです。早ければ2021年春頃にLWGに転送される可能性があるようです。

Text and Unicode

P1885R3がLEWGに提出されており、メーリングリストレビューおよびテレカンレビューを完了したようです。改善の必要があったため、改訂待ちをしています。

フリースタンディング

P1462R5<utility>, <ranges>, <iterator>のフリースタンディングサポート)のLEWGでの最終レビューが完了し、2021年1月にLWGに転送するための最後の電子投票にかけられました。