[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に転送するための最後の電子投票にかけられました。

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

文書の一覧

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

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

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

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

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

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

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

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

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

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

N4883 WG21 February 2021 admin telecon minutes

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

P0401R6 Providing size feedback in the Allocator interface

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

以前の記事を参照

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

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

P0448R3 A strstream replacement using span as buffer

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

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

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

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

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

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

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

char input[] = "10 20 30";

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

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

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

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

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

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

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

auto const sp = os.span();

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

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

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

以前の記事を参照

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

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

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

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

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

P1072R7 basic_string::resize_and_overwrite

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

以前の記事を参照

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

P1322R3 Networking TS enhancement to enable custom I/O executors

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

以前の記事を参照

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

P1425R3 Iterators pair constructors for stack and queue

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

以前の記事を参照

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

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

P1659R2 starts_with and ends_with

P1659R3 starts_with and ends_with

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

以前の記事を参照

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

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

P1682R3 std::to_underlying

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

以前の記事を参照

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

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

P1885R5 Naming Text Encodings to Demystify Them

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

以前の記事を参照

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

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

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

P1951R1 Default Arguments for pair's Forwarding Constructor

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2047R1 An allocator-aware optional type

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

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

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

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

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

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

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

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

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

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

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

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

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

以前の記事を参照

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

P2093R4 Formatted output

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

前回の記事を参照

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

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

P2128R3 Multidimensional subscript operator

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

前回の記事を参照

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

P2162R2 Inheriting from std::variant (resolving LWG3052)

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

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

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

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

P2164R4 views::enumerate

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

以前の記事を参照

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

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

struct result {
  count index;
  T value;
};

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

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

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

P2195R2 Electronic Straw Polls

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

以前の記事を参照

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

P2216R3 std::format improvements

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

以前の記事を参照

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

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

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

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

以前の記事を参照

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

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

P2234R1 Consider a UB and IF-NDR Audit

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

以前の記事を参照

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

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

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

以前の記事を参照

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

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

P2249R0 Mixed comparisons for smart pointers

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2280R1 Using unknown references in constant expressions

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

以前の記事を参照

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

P2281R1 Clarifying range adaptor objects

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

以前の記事を参照

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

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

P2286R1 Formatting Ranges

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

以前の記事を参照

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

P2287R1 Designated-initializers for base classes

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

以前の記事を参照

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

struct A {
  int a;
};

struct B : A {
  int b;
};

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

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

P2289R0 2021 Winter Library Evolution Polls

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

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

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

P2290R0 Delimited escape sequences

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2295R0 Correct UTF-8 handling during phase 1 of translation

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

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

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

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

P2297R0 Wording improvements for encodings and character sets

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

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

P2301R0 Add a pmr alias for std::stacktrace

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2314R0 Character sets and encodings

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

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

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

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

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

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

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

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

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

P2316R0 Consistent character literal encoding

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

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

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

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

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

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

sqliteより

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

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

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

P2317R0 C++ - An Invisible foundation of everything

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

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

目次

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

C++ヲタク必見です!

P2320R0 The Syntax of Static Reflection

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

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

リフレクション

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

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

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

スプライシング

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

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

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

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

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

パックのスプライシング

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

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

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

P2321R0 zip

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

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

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

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

std:tupleへの出力

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

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

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

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

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

std:tupleの読み取り

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

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

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

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

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

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

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

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

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

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

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

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

zip, zip_tranformvalue_type

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

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

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

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

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

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

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

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

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

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

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

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

P2322R0 ranges::fold

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2325R0 Views should not be required to be default constructible

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

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

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

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

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

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

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

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

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

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

多分2週間後くらい

この記事のMarkdownソース

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

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

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

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

const_cast<const iter_reference_t<Out>&&>(*o) = std::forward<T>(t);
const_cast<const iter_reference_t<Out>&&>(*std::forward<Out>(o)) = std::forward<T>(t);

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

cpprefjpには

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

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

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

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

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

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

  std::string bar;
};

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

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

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

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

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

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

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

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

prvalueの区別

この問題は突き詰めると

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

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

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

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

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

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

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

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

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

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

この結果得られたのが、indirectly_writableにある謎の制約式です。

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

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

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

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

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

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

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

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

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

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

std::vector<bool>::reference

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

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

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

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

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

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

参考文献

謝辞

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

この記事のMarkdownソース