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

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

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

正しいキャプチャ順序

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

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

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

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

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

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

詳細

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

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

capture-default:
  &
  =

capture-list:
  capture
  capture-list , capture

capture:
  simple-capture
  init-capture

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

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

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

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

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

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

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

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

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

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

参考文献

この記事のMarkdownソース

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

文書の一覧

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

SD-1 2021 PL22.16/WG21 document list

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

P0447R12 Introduction of std::colony to the standard library

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

以前の記事を参照

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

P0847R6 Deducing this

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

以前の記事を参照

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

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

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

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

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

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

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

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

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

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

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

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

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

std::atomic<Name*> name;

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

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

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

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

P1425R2 Iterators pair constructors for stack and queue

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

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

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

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

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

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

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

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

P1682R2 std::to_underlying

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

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

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

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

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

#include <cstdint>

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

// from before:

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

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

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

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

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

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

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

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

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

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

P1708R3 Simple Statistical Functions

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

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

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

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

基本統計量のサンプル

#include <stats>
#include <vector>

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

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

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

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

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

Accumulator Objectのサンプル

#include <stats>

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

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

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

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

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

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

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

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

struct buffer {
  buffer() {};

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

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

private:
  char data[42];
};

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

  return strview;
}

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

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

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

namespace std {

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

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

    //...

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

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

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

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

P2036R1 Changing scope for lambda trailing-return-type

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

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

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

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

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

int j = 0;

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

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

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

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

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


int i;

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

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

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

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

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

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

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

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

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

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

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

int i;

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

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

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

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

P2072R1 Differentiable programming for C++

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

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

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

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

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

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

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

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

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

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

P2093R3 Formatted output

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

前回の記事を参照

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

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

などです。

P2168R1 generator: A Synchronous Coroutine Generator Compatible With Ranges

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

前回の記事を参照

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

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

などです。

P2210R1 Superior String Splitting

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

前回の記事を参照

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

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

P2216R2 std::format improvements

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

以前の記事を参照

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

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

P2232R0 Zero-Overhead Deterministic Exceptions: Catching Values

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2245R0 SG19: Machine Learning Meeting Minutes

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

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

P2246R1 Character encoding of diagnostic text

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

以前の記事を参照

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

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

P2259R1 Repairing input range adaptors and counted_iterator

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

以前の記事を参照

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

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

P2266R0 Simpler implicit move

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2277R0 Packs outside of Templates

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

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

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

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

int g(int);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2278R0 cbegin should always return a constant iterator

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

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

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

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

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

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

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

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

P2279R0 We need a language mechanism for customization points

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

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

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

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

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

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

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

各行の意味はそれぞれ

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

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

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

P2280R0 Using unknown references in constant expressions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2281R0 Clarifying range adaptor objects

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2283R0 constexpr for specialized memory algorithms

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

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

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

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

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

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

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

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

P2285R0 Are default function arguments in the immediate context?

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

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

template <typename T, typename Allocator>
struct container {

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

};

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

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

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

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

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

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

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

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

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

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

P2286R0 Formatting Ranges

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

P2287R0 Designated-initializers for base classes

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

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

struct A {
  int a;
};

struct B : A {
  int b;
};

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

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

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

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

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

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

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

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

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

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

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

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

この記事のMarkdownソース

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

A : いりません

根拠

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

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

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

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

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

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

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

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

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

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

/// mymodule.cpp
export module mymodule;

import <ranges>;

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

public:

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

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

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

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


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


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

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

import <iterator>;
import mymodule;

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

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

参考文献

この記事のMarkdownソース

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

文書の一覧

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

N4878 Working Draft, Standard for Programming Language C++

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

N4879 Editors' Report - Programming Languages - C++

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

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

P0401R5 Providing size feedback in the Allocator interface

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

以前の記事を参照

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

P0561R5 An RAII Interface for Deferred Reclamation

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

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

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

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

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

#include <snapshot> // 新ヘッダ

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

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

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

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

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

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

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

以前の記事を参照

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

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

P0901R8 Size feedback in operator new

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

以前の記事を参照

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

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

P1030R4 std::filesystem::path_view

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

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

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

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

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

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

P1072R6 basic_string::resize_and_overwrite

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

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

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

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

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

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

namespace std {

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

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

  };
}

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

P1102R2 Down with ()!

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

以前の記事を参照

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

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

P1315R6 secure_clear (update to N2599)

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

以前の記事を参照

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

P1478R6 Byte-wise atomic memcpy

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

以前の記事を参照

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

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

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

前回の記事を参照

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

P1689R3 Format for describing dependencies of source files

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

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

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

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

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

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

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

export module my.module;

import other.module;
import <header>;

#include "config.h"

次のようになります。

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

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

P2077R2 Heterogeneous erasure overloads for associative containers

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

以前の記事を参照

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

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

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

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

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

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

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

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

void foo() {
  map_type m;
  HeterogeneousKey key;

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

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

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

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

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

void foo() {
  map_type m;

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

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

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

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

P2136R2 invoke_r

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

以前の記事を参照

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

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

P2175R0 Composable cancellation for sender-based async operations

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

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

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

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

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

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

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

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

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

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

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

P2186R1 Removing Garbage Collection Support

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

以前の記事を参照

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

P2195R1 Electronic Straw Polls

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

以前の記事を参照

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

P2213R1 Executors Naming

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

以前の記事を参照

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

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

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

P2216R1 std::format improvements

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

以前の記事を参照

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

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

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

P2233R3 2020 Fall Library Evolution Polls

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

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

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

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

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

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

P2247R1 2020 Library Evolution Report

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

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

P2248R1 Enabling list-initialization for algorithms

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

以前の記事を参照

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

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

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

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

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

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

P2262R0 2020 Fall Library Evolution Poll Outcomes

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

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

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

P2263R0 A call for a WG21 managed chat service

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

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

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

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

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

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

#include <cassert>
#include <type_traits>

using Int=int;

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

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

#include <assert.h>

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

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

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

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

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

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

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

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

P2265R0 Renaming any_invocable

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

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

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

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

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

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

P2268R0 Freestanding Roadmap

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

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

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

P2272R0 Safety & Security Review Board

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

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

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

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

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

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

P2273R0 Making std::unique_ptr constexpr

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

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

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

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

#include <memory>

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

  return *p;
}

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

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

P2274R0 C and C++ Compatibility Study Group

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

WG21から見たWG14

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

WG14から見たWG21

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

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

この記事のMarkdownソース

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

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

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

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

iterator

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

C++17

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

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

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

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

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

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

C++20

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

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

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

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

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

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

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

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

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

差異

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

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

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

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

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

input iteratorr

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

C++17

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

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

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

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

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

C++20

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

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

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

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

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

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

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

差異

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

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

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

ouptut iterator

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

C++17

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

  • Cpp17Iterator要件を満たす

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

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

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

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

C++20

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

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

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

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

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

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

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

差異

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

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

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

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

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

forward iterator

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

C++17

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

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

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

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

C++20

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

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

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

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

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

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

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

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

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

差異

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

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

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

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

bidirectional iterator

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

C++17

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

  • Cpp17ForwardIterator要件を満たす

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

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

C++20

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

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

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

差異

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

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

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

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

random access iterator

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

C++17

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

  • Cpp17BidirectionalIterator要件を満たす

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

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

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

C++20

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

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

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

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

差異

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

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

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

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

contiguous iterator

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

C++17

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

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

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

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

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

C++20

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

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

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

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

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

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

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

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

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

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

差異

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

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

まとめ

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

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

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

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

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

この記事のMarkdownソース

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

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

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

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

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

目次

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

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

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

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

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

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

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

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

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

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

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

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

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

ITER_CONCEPT(I)

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

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

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

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

itereator_conceptiterator_category

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

iterator_traitsの明示的特殊化

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

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

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

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

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

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

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

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

iterator_traitsの2つの役割

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

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

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

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

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

参考文献

この記事のMarkdownソース

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

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

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

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

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

difference_type

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

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

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

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

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

std::incrementable_traits

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

イテレータ型をIとして

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

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

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

value_type

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

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

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

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

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

std::indirectly_readable_traits

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

イテレータ型をIとして

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

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

reference

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

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

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

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

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

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

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

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

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

std::iter_rvalue_reference_t

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

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

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

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

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

std::iter_common_reference_t

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

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

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

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

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

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

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

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

pointer

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

iterator_category

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

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

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

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

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

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

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

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


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

iterator_concept

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

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

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

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

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

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

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

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

C::iterator

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  cpp17_iter& operator++();

  reference operator*();

  difference_type operator-(const cpp17_iter&) const;

  // 以下略
};

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

  cpp20_iter& operator++();

  T& operator*();

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

  // 以下略
};

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

参考文献

この記事のMarkdownソース