🍄

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

2020/12/06に公開

文書の一覧

全部で42本あります。

採択された文書

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

P0943R6 Support C atomics in C++

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

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

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

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

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

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

P1787R6 Declarations and where to find them

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

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

その他文書

[N4869 WG21 Pre-Autumn 2020 telecon minutes](WG21 Pre-Autumn 2020 telecon minutes)

N4871 WG21 Pre-Autumn 2020 telecon minutes

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

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

N4869とN4871の違いは不明。

N4870 WG21 2020-02 Prague Minutes of Meeting

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

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

Library Fundamentals TSの最新の仕様書。

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

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

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

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

N4875 WG21 admin telecon meeting: Winter 2021

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

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

N4876 WG21 virtual meeting: Winter 2021

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

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

N4877 WG21 2020-11 Virtual Meeting Minutes of Meeting

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

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

P0447R11 Introduction of std::colony to the standard library

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

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

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

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

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

colonyのイメージ図

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

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

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

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

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

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

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

#include <colony>

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

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

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

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

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

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

以前の記事を参照

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

P0401R4 Providing size feedback in the Allocator interface

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

説明は次の項で。

P0901R7 Size feedback in operator new

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

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

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

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

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

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

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

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

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

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

  inline constexpr return_size_t return_size{};

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

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

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

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

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

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

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

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

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

P1012R1 Ternary Right Fold Expression

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

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

#include <functional>
#include <stdexcept>

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

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

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

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

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

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

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

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

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

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

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

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

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

P1102R1 Down with ()!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P1478R5 Byte-wise atomic memcpy

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

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

P1885R4 Naming Text Encodings to Demystify Them

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

以前の記事を参照

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

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

P1950R1 An indirect value-type for C++

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

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

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

/// ヘッダファイル

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

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

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

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

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

const性伝播の遮断

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

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

コピーの問題

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

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

std::indirect_value<T>

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

/// ヘッダファイル

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{
	init-statement(opt)

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

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

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

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

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

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

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

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

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

// prvalueを返す
Person createPerson();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3. 文章で規定する

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

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

P2160R1 Locks lock lockables (wording for LWG 2363)

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

以前の記事を参照

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

P2164R3 views::enumerate

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

以前の記事を参照

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

P2181R1 Correcting the Design of Bulk Execution

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

以前の記事を参照

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

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

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

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

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

P2182R1 Contract Support: Defining the Minimum Viable Feature Set

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

以前の記事を参照

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

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

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

P2211R0 Exhaustiveness Checking for Pattern Matching

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

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

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

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

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

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

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

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

P2212R2 Relax Requirements for time_point::clock

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

以前の記事を参照

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

P2233R1 2020 Fall Library Evolution Polls

P2233R2 2020 Fall Library Evolution Polls

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

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

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

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

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

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

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

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

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

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

P2246R0 Character encoding of diagnostic text

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

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

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

P2247R0 2020 Library Evolution Report

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

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

P2248R0 Enabling list-initialization for algorithms

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

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

struct point { int x; int y; };

std::vector<point> v;

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

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

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

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

struct point { int x; int y; };

std::vector<point> v;

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

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

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

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

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

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

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

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

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

P2250R0 Scheduler vs Executor

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

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

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

P2251R0 Require span & basic_string_view to be Trivially Copyable

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

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

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

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

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

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

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

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

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

P2254R0 Executors Beyond Invocables

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

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

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

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

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

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

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

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

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

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

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

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

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

P2255R0 A type trait to detect reference binding to temporary

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

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

using namespace std::string_literals;

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

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

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

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

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

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

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

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

  template<class T, class U> struct reference_converts_from_temporary;

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

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

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

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

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

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

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

P2257R0 Blocking is an insufficient description for senders and receivers

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

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

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

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

P2259R0 Repairing input range adaptors and counted_iterator

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

iterator_categoryが取得できない問題

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

#include <vector>
#include <ranges>

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

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

  auto it = r.begin();
}

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

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

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

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

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

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

  • C++20で追加されたイテレータ(range adopterのイテレータ含む)のiterator_categoryメンバ型は自身がforward_iteratorであるときにのみ定義されるようにする。
  • 一部のイテレータアダプタの操作を入力イテレータがforward_iteratorならば自身もforward_iteratorとなるように修正する。

counted_iteratorの問題

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2260R0 WG21 2020-11 Virtual Meeting Record of Discussion

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

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

Discussion