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

文書の一覧

全部で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

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>イテレータIC++17イテレータの要件を満たさない場合にいかなるメンバも定義しません。言い換えると、iterator_taraitsはあくまでC++17以前のイテレータを期待する場合に使用するものであって、C++20以降は積極的に使用するものではありません。
C++20イテレータC++17イテレータ互換にしようとする場合は、そのiterator_traitsが有効になるようにする必要があります。

C+;20のinput_iteratorC++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が定義されている必要があります。

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

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_traitsC++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_categoryC++17互換イテレータとして使用される場合のカテゴリの表明なので、常にinput_iteratorとなります。C++20移行のイテレータは基本的にiterator_traitsにアダプトしておらず、iota_viewイテレータに対するiterator_traitsもプライマリテンプレートが使用されることになります。プライマリテンプレートのiterator_traits<I>は任意のイテレータIC++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よりもだれがどんな発言をしたのかが詳細に記録されています。

多分2週間後くらい

この記事のMarkdownソース

[C++]inline名前空間の使途

inline名前空間C++11から追加された機能で、その中にあるものは透過的に(名前空間がないかのように)アクセスすることができます。一見使いどころがなく見られがちですが、うまく使えばとても便利に活用することができます。

1. using namespaceの範囲を限定する

これは標準ライブラリではユーザー定義リテラルの定義と利用でよく利用されます。

例えば、std::string_viewを簡易利用するためのsvリテラルは次のように宣言されています。

namespace std {

  // string_view本体
  template <class CharT, class Traits = char_traits<CharT>>
  class basic_string_view;

  inline namespace literals {
    inline namespace string_view_literals {

      // svリテラル
      constexpr std::string_view operator""sv(const char* str,   std::size_t len) noexcept;
    }
  }
}

これによって、using namespace std;という広すぎる範囲をusingすることなく、次のいずれかによってこのsvリテラルを使用することができます。

#include <string_view>

int main() {
  {
    // 1、標準ライブラリの定義する全リテラルだけが使用可能になる
    using namespace std::literals;

    auto str = "literals"sv;
  }
  {
    // 2、string_viewのsvリテラルだけが使用可能になる
    using namespace std::string_view_literals;

    auto str = "string_view_literals"sv;
  }
  {
    // 2、std名前空間の神羅万象が利用可能になる
    using namespace std;

    auto str = "std"sv;
  }
}

std::literals名前空間は標準ライブラリの定義するすべてのユーザー定義リテラルs, h, m, s, iなどなど)が定義されているinline名前空間であり、それをusing namespaceするとそれらのリテラルの全てがそのスコープで見えるようになります。
std::string_view_literalssvリテラルだけが定義されているinline名前空間であり、using namespaceしてもsvリテラル以外のものは見えません。
そして、これらのユーザー定義リテラルstd名前空間直下からも参照できます。

このようにライブラリの提供するものをinline名前空間である程度グループ化しておくことで、使う側は適宜必要な範囲だけをusing namespaceすることができるようになります。

2. APIのバージョニング

これは多分最もポピュラーなinline名前空間の使い方でしょうか。

例えば、既に利用している関数があり、その関数のAPI(インターフェース)は変えないけれども内部実装を変更したい時を考えます。

namespace mylib {
  int f(int a) {
    return a;
  }
}

この処理を、2倍してから返すようにしたいとします。

namespace mylib {
  int f(int a) {
    return 2 * a; // 2倍して返すようにしたい
  }
}

しかし、この関数は既に至るところで使われており、変更するならしっかりテストしてからにしたいし、使われているとこを書き換えて回るのは嫌です。そんな時、変更前のバージョンをinline名前空間で、変更後のバージョンを名前空間で囲っておきます。

namespace mylib::inline v1 {

  // 現在の処理
  int f(int a) {
    return a;
  }
}

namespace mylib::v2 {

  // 変更後の処理
  int f(int a) {
    return 2 * a; // 2倍して返すようにしたい
  }
}

そして、新しい関数mylib::v2::f()の実装とテストが完了してすべてのf()を新バージョンへ切り替えることができるようになったら、それぞれの名前空間inline指定を逆にします。

// 古いバージョンを非inlineに
namespace mylib::v1 {

  // 現在の処理
  int f(int a) {
    return a;
  }
}

// 新しいバージョンをinlineに
namespace mylib::inline v2 {

  // 変更後の処理
  int f(int a) {
    return 2 * a; // 2倍して返すようにしたい
  }
}

inline名前空間は透過的です。そのため、これだけで、f()を使用しているところを一切書き換えることなくその処理内容をアップデートできます。もし古いバージョンを使いたい場合はmylib::v1::f()のように名前空間を明示的に指定してやればよく、古いバージョンを使用していることも分かりやすくなります。

3. ABIのバージョニング

inline名前空間APIでは省略可能ですが、ABI(マングル名)では通常の名前空間と同じ扱いをされ、常にマングル名に表示されていますし、参照するときも省略できません。inline名前空間の効果は純粋にC++の意味論上だけのものです。

この性質によって、APIは変わらないけれどABIを破壊するような変更がなされたときにその影響を軽減することができます。

/// header.h

namespace mylib {
  class S {
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

namespace mylib {

  int S::get_m() const {
    return this->m;
  }
}
/// main.cpp

#include <iostream>

#include "header.h"

int main() {
  mylib::S s{};
  
  std::cout << s.get_m();
}

例えばこんなクラスとそのメンバ関数がありヘッダと実装が分かれているとき、これを利用するmain.cppが同じコンパイラを使ってsource.cppコンパイルしている間は何も問題はありません。

しかし、source.cppを静的ライブラリや動的ライブラリの形であらかじめコンパイルしてから利用しているときに、このmylib::SにABIを破壊する変更がなされてしまったとします。

/// header.h

namespace mylib {
  class S {
    float f = 1.0;  // メンバを追加した
    int m = 10;
  public:

    int get_m() const;
  };
}

このような変更はAPIに何も影響を及ぼしませんが、クラスのレイアウトが変更されているのでABIから見ると重大な変更です。ABIを破壊しています。

このとき、source.cppコンパイルした静的or動的ライブラリを再コンパイルせずに使い続けて(リンクして)いたとしてもコンパイラは何も言わないでしょう。この場合の変更によってはマングル名は変化しておらず、コンパイルもリンクもつつがなく完了します。

/// main.cpp

#include <iostream>

// これは最新のものを参照しているとする
#include "header.h"
// source.cppは10年前にコンパイルしたものをリンクして使い続けているとする

int main() {
  mylib::S s{};
  
  std::cout << s.get_m(); // 未定義動作!
}

古いS::get_m()関数の定義はsource.cppコンパイルした外部ライブラリにあり、新しいmylib::Sのレイアウトを知りません。したがって、レイアウト変更後のクラスのどこかの領域をint型のメンバS::mとして読みだした値を返してくれるでしょう(たぶん実行時エラーも起きないのではないかと思われます)。これは紛う事なき未定義動作です・・・

これは稀によくあるビルド済みバイナリをリンクして利用する時の問題で、C++でヘッダオンリーライブラリが好まれる傾向にある事の一つの理由でもあります。

こんな時、inline名前空間を利用することでこの問題の軽減を図れます。

リンクエラーにする

解決策の一つ目は、変更後のコードをそれを表すinline名前空間で囲ってしまう事です。

/// header.h

// inline名前空間を追加する
namespace mylib::inline v2 {
  class S {
    float f = 1.0;  // メンバを追加した
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

// inline名前空間を追加する
namespace mylib::inline v2 {

  int S::get_m() const {
    return this->m;
  }
}

inline名前空間APIC++コード上)からは透過的ですがABI(マングル名)には表示されます。従って、この変更後のmylib::Sおよびそのメンバ関数を利用するコードに変更は必要ありませんが、そのマングル名は変更前のものと異なっています。
結果、再コンパイルしないで用いている変更前ソースによるビルド済みバイナリからはそのようなシンボルが見つからずリンクエラーによってコンパイル時に気づくことができます。

/// main.cpp

#include <iostream>

// これは最新のものを参照しているとする
#include "header.h"
// source.cppは10年前にコンパイルしたものをリンクして使い続けているとする

int main() {
  mylib::S s{};
  
  std::cout << s.get_m(); // リンクエラー、シンボルが見つからない
}

ABI互換性を確保する

もう一つの方法は初めからinline名前空間を利用していた場合にのみ利用可能となります。例えば先程のサンプルは初めから次のようにinline名前空間に囲まれていたとします。

/// header.h

// inline名前空間に包まれている
namespace mylib::inline v1 {

  class S {
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

// inline名前空間に包まれている
namespace mylib::inline v1 {

  int S::get_m() const {
    return this->m;
  }
}

このコードに対して先程の変更がなされたとしましょう。その際、古いバージョンのinline名前空間を非inlineにし、新しいバージョンのinline名前空間名を変更しておきます。

/// header.h

// 古いバージョン、inline名前空間ではなくする
namespace mylib::v1 {

  class S {
    int m = 10;
  public:

    int get_m() const;
  };
}

// 最新版
namespace mylib::inline v2 {

  class S {
    float f = 1.0;  // メンバを追加した
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

// 古いバージョン、inline名前空間ではなくする
namespace mylib::v1 {

  int S::get_m() const {
    return this->m;
  }
}

// 最新版
namespace mylib::inline v2 {

  int S::get_m() const {
    return this->m;
  }
}

こうしておいた状態で、さっきまでのように使用しようとすれば正しくリンクエラーになります。

今回のようにした場合は、逆に変更前のバージョンのものを参照し続けているようなプログラムに最新のsource.cppをビルドした(変更が反映された)バイナリをリンクした時でも、リンクエラーも未定義動作も起こさずに使用することができます。

/// main.cpp
// 10年前のコードを参照ヘッダも含めて変更せずに使い続けているとする

#include <iostream>

// 変更前のものを参照している
#include "header.h"
// source.cppはさっきコンパイルした最新のものをリンクしているとする

int main() {
  // mylib::v1::Sを参照している
  mylib::S s{};

  // mylib::v1::S::get_m()を参照している
  std::cout << s.get_m(); // 10
}

inline名前空間はマングル名レベルでは名前空間と区別なく扱われます。すなわち、ABIからは名前空間名がinlineであるかどうかは分かりません。したがって、このように変更を追記し古いバージョンを維持しておけば、古いバージョンを利用しているプログラムに対しても古いバージョンを提供し続けることができます。これによって、ABI互換性を維持し、ABIを保護することができます。

GCCやclangの最近の標準ライブラリの実装では、ABI保護のためにinline名前空間が多用されています。

4. 名前の衝突を回避する

これは特にCPO(Customization Point Object)の定義で利用されています。

CPOはその呼び出しに当たって自身と同名の非メンバ関数をADLで探索するようになっていることがよくあります。これはあるクラスに対してHidden friendsと呼ばれるfriend関数を探し出すものです。

一方で、標準ライブラリにあるCPOは一部のものを除いてstd名前空間のすぐ下に定義されることになります。

すると、標準ライブラリにあるクラスに対するHidden friends関数とCPOとで名前が衝突してしまいます。

namespace mystd {
  
  namespace cpo_impl {
    
    // swap CPOの実装クラス
    struct swap_cpo {
      
      template<typename T, typename U>
      void operator()(T&, U&) const;
    };
  }

  // swap CPO #1
  inline constexpr cpo_impl::swap_cpo swap{};
  
  struct S {
    
    // Hidden friendsなswap関数 #2
    friend void swap(S& lhs, S& rhs);
  };
}

この例では#1と#2の異なる宣言が同じ名前空間にあるためにコンパイルエラーになっています。

このように、CPOと同じ名前空間にあるものがそのCPOにアダプトしようとすると名前衝突してしまうわけです。

この場合にCPOの定義をinline名前空間で囲ってやることでこの問題を解決できます。しかも、呼び出す際に名前空間名が増えることもありません。

namespace mystd {
  
  namespace cpo_impl {
    
    // swap CPOの実装クラス
    struct swap_cpo {
      
      template<typename T, typename U>
      void operator()(T&, U&) const;
    };
  }

  // CPO定義をinline名前空間で囲う
  inline namespace cpo {
    // swap CPO #1
    inline constexpr cpo_impl::swap_cpo swap{};
  }
  
  struct S {
    
    // Hidden friendsなswap関数 #2
    friend void swap(S& lhs, S& rhs);
  };
}

こうしてもmystd::swapという名前でswapCPOを参照できますし、#1と#2のswapは別の名前空間にいるために名前は衝突していません。
このため、標準ライブラリにあるCPOはほとんどのものがinline名前空間に包まれています。

ここでは説明のために変な名前空間と適当なクラスを用意しましたが、mystdstdmystd::Sstd::vectorとかに読み替えるとつかみやすいかもしれません。

参考文献

この記事のMarkdownソース

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

文書の一覧 www.open-std.org

提案文書で採択されたものはありません。全部で35本あります。

N4863 Agenda for Fall Virtual WG21/PL22.16 Meeting

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

C++23への機能追加のための投票も行われると思われるので、ようやくC++23入りするものが出てきそうです。

N4864 WG21 virtual meeting: Autumn 2020

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

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

N4865 Response to Editorial Comments: ISO/IEC DIS 14882, Programming Language C++

C++20のDIS(Draft international standard)に対して寄せられた各国の委員会からのコメントのまとめ。

N4866 WG21 admin telecon meeting: Pre-Autumn 2020

2020年11月9日に行われるWG21本会議のスケジュール表。先程のN4863よりも少し詳しく書かれています。

N4867 Editors' Report - Programming Languages - C++

↓の更新されたWorking Draftの差分をまとめたもの。

今回は新しい機能の追加はありません。

N4868 Working Draft, Standard for Programming Language C++

C++23のWorking Draft第二弾。↑のEditors' Reportにあるように、新規追加された機能はなく、文言の調整などのみの変更です。

P0847R5 Deducing this

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

現在のC++では、メンバ関数のCV修飾と参照修飾によって暗黙のthisパラメータのconst/volatile性と値カテゴリを指定したオーバロードを行うことができます。それはクラスのオブジェクトの実際の状態に応じて処理内容を切り替えるのに必要ではありますが、ほぼ同じ処理をいくつも(大抵は2×2)書くことになります。おおよそ次の3つ方法のどれかによって実装されます。

  1. 4つのメンバ関数それぞれに処理を記述する
  2. どれか1つに委譲するようにする
  3. 4つ全てで別の実装関数に委譲する

例えばstd::optionalvalue()関数はまさにconst有無と参照修飾で4つのオーバーロードを提供しています。おおよそ1つ目の方法で実装されており、次のようになります。

template <typename T>
class optional {
  // ...
  constexpr T& value() & {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  constexpr T const& value() const& {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  constexpr T&& value() && {
    if (has_value()) {
      return move(this->m_value);
    }
    throw bad_optional_access();
  }

  constexpr T const&& value() const&& {
    if (has_value()) {
      return move(this->m_value);
    }
    throw bad_optional_access();
  }
  // ...
};

この様にほぼ同じ実装を微妙に異なって複数書かなければいけない事はバグを誘発しやすく、また保守性も低下します。

一方でこれがもしメンバ関数ではなかったとしたら、次のように簡潔な実装を選択できます。

template <typename T>
class optional {
  // ...
  template <typename Opt>
  friend decltype(auto) value(Opt&& o) {
      if (o.has_value()) {
          return forward<Opt>(o).m_value;
      }
      throw bad_optional_access();
  }
  // ...
};

この1つの関数テンプレートでさきほどの4つのメンバ関数と全く同じ動作をさせることができます。ただ、これはメンバ関数ではないのでopt.value()のように呼び出すことは出来ません。

この2種の関数の差は、thisに相当する引数を明示的に書けるかどうかという事から来ています。明示的に書くことができれば、フォワーディングリファレンスと完全転送によって4つの実装を1つに圧縮できます。

この提案は、このような問題を解決するためにメンバ関数でも非メンバ関数のようにthisに相当する引数を明示的に取れるようにしつつ、呼び出し側は従来通りに呼び出せるようにするものです。

非静的メンバ関数の第一引数にthisによって注釈をつけておく事でそれ以外のものと区別します。その場合はCV/参照修飾を行えなくなります。

struct X {
  // void foo(int i) const & 相当の宣言
  void foo(this X const& self, int i);

  // フォワーディングリファレンスによる宣言
  template <typename Self>
  void bar(this Self&& self);
};

struct D : X { };

void ex(X& x, D const& d) {
  x.foo(42);      // selfはxを束縛し、iに42が渡される
  x.bar();        // SelfはX&に推論され、X::bar<X&>が呼ばれる
  move(x).bar();  // SelfはXに推論され、X::bar<X>が呼ばれる

  d.foo(17);      // selfはdを束縛する
  d.bar();        // SelfはD const&に推論され、X::bar<D const&>が呼ばれる
}

この引数のことをexplicit object parameterと呼びます。

.によってメンバ関数呼び出しされた時、explicit object parameterには呼び出したオブジェクトが渡されます。それ以降は通常の関数引数と同じ扱いとなり、テンプレートの恩恵を受けることができます。

これによって、先ほどのstd::optional<T>::value()の実装は次のように改善されます。

template <typename T>
class optional {
  // ...
  template <typename Self>
  constexpr auto&& value(this Self&& self) {
    if (!self.has_value()) {
      throw bad_optional_access();
    }

    return forward<Self>(self).m_value;
  }
  // ...
};

また、これはラムダ式においても使用する事ができます。

std::vector captured = {1, 2, 3, 4};
[captured](this auto&& self) -> decltype(auto) {
  // forward_like<T>(U u)はTのCV修飾と値カテゴリをUにコピーした上でuを転送するもの
  return forward_like<decltype(self)>(captured);
}

[captured]<class Self>(this Self&& self) -> decltype(auto) {
  return forward_like<Self>(captured);
}

これが可能になる事によって例えばCR抜きのCRTPができるようになります。

CRTP この提案
template <typename Derived>
struct add_postfix_increment {
  Derived operator++(int) {
    auto& self = static_cast<Derived&>(*this);

    Derived tmp(self);
    ++self;
    return tmp;
  }
};

struct some_type
  : add_postfix_increment<some_type> {
    some_type& operator++() { ... }
};
struct add_postfix_increment {
  template <typename Self>
  auto operator++(this Self&& self, int) {
      auto tmp = self;
      ++self;
      return tmp;
  }
};

struct some_type : add_postfix_increment {
    some_type& operator++() { ... }
};

他にも、再帰ラムダ、値によるメンバ関数、SFINAE-friendlyで完全なCall wrapperなど、新しいイディオムへの道が開けるようです。

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

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

decay-copyというのは関数テンプレートに引数を渡すときに行われる変換のことです。関数テンプレートのテンプレートパラメータによる引数に値を渡すとき、左辺値は右辺値に、配列はポインタに、CV修飾を除去しつつ変換されます。

template<typename T>
void f(T t);

std::vector<int> vec{};
const std::vector<int> cvec{};

// 全て T = std::vector<int>
f(vec);                 // コピーされる
f(cvec);                // コピーされる
f(std::vector<int>{});  // ムーブされる

int arr[] = {1, 2, 3};

// T = int*
f(arr);

std::decaydecay-copyの型の変換をシミュレートするものです。このような振る舞いはauto copy = value;のように書くことで再現できますが、この提案はその意図を明確にするためにもワンライナーで書くことができるようにするものです。

auto(value)という構文でvalueをコピーしたprvalueを生成するもので、例えば次のように利用できます。

現在 この提案
// Containerはコンセプトとする
void pop_front_alike(Container auto& x) {
  auto a = x.front();
  std::erase(x.begin(), x.end(), a);
}
void pop_front_alike(Container auto& x) {
  std::erase(x.begin(), x.end(), auto(x.front()));
}

std::eraseは指定されたイテレータ範囲から、第3引数で渡された値と同じものを削除する関数です。イテレータ範囲に含まれている要素を削除するときは、その操作の最中でダングリング参照とならないようにあらかじめコピーする必要があります。その際に、auto()によるdecay-copy構文を使用できます。

現在 この提案
struct S {
  S(const S&) {
    /**/
  }
  S& operator=(S&& other) {
    /**/
  }

  // コピー構築とムーブ代入を利用した簡略化
  S& operator=(const S& other) {
    if (this != &other) {
      auto copy = other;
      *this = std::move(copy);
    }

    return *this;
  }
}
struct S {
  S(const S&) {
    /**/
  }
  S& operator=(S&& other) {
    /**/
  }


  S& operator=(const S& other) {
    if (this != &other) {
      *this = auto(other);
    }

    return *this;
  }
}

auto a = x.front();によるコピーは変数宣言構文であり、ここでの主目的であるコピーは変数宣言の持つプロパティの一つでしかありません。一方、auto(x.front())は明確にコピーという操作を表しています。

関数キャストT(x)Tautoに置き換えることによって、auto(x)の構文は関数キャストの亜種であると見ることができます。クラステンプレートの引数推論を考慮すれば、この構文には次のような直交・一貫性があります。

変数定義 関数キャスト new
auto v(x) auto(x) new auto(x)
auto v{x} auto{x} new auto{x}
ClassTemplate v(x) ClassTemplate(x) new ClassTemplate(x)
ClassTemplate v{x} ClassTemplate{x} new ClassTemplate{x}

ライブラリサポートではなく言語サポートすることによって、このように変数宣言や関数スタイルキャストなどの構文との一貫性を向上することができます。

P0870R4 A proposal for a type trait to detect narrowing conversions

Tが別の型Uへ縮小変換(narrowing conversion)によって変換可能かを調べるメタ関数is_convertible_without_narrowing<T, U>を追加する提案。

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、名前がis_narrowing_convertible<T, U>からis_convertible_without_narrowing<T, U>に変更されたことと、使用例のサンプルコードが追加されたことです。

P1048R1 A proposal for a type trait to detect scoped enumerations

scoped enumenum class)を識別するためのメタ関数であるstd::is_scoped_enum<T>の提案。

SFINAEによって、型がenumなのかenum classなのかで処理を分けたいときにあると有用なので追加しようというものです。
筆者の方は、古いC++ライブラリをアップデートする際にそこに含まれるenumからenum classへの移行を追跡するためのテストにおいて活用したそうです。

#include <type_traits>

enum E1{};

enum class E2{};

int main() {
  bool b1 = std::is_enum_v<E1>; // false
  bool b2 = std::is_enum_v<E2>; // true
}

これは例えば次のように実装できます。

template<class T, bool = is_enum_v<T>>
struct is_scoped_enum_helper : false_type {};

template<class T>
struct is_scoped_enum_helper<T, true> : public bool_constant<!is_convertible_v<T, underlying_type_t<T>>> {};

template<class T>
struct is_scoped_enum : public is_scoped_enum_helper<T> {};

この提案は次の本会議での投票にかけられる予定で、C++23入りがほぼ確実そうです。

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

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

現在 この提案
std::list<int> list = {...};

std::vector<int> vec(list.begin(), list.end());
std::list<int> list = {...};

auto vec = list
  | std::ranges::to<std::vector<int>>(list);

このようなコンテナの変換はとても基本的な操作ですがこの提案のメインはこれではなく、Viewの実体化を簡易化することにあります。

標準コンテナは上記のようにイテレータペアを受け取るrangeコンストラクタを持っていますがそのイテレータペアは同じ型となる事を前提としています。ところが、<ranges>Viewは多くがそのようなcommon_rangebegin()/end()イテレータ型が同じrange)ではありませんので、少し遠回りをしなければなりません。そこにranges::toを用いると簡潔に書くことができるようになります。

現在 この提案
std::iota_view v{0, 1024};
std::vector<int> vec;
std::copy(v, std::back_inserter(vec));

// あるいは
auto iota = std::views::iota(0, 1024)
  | std::views::common;

std::vector<int> vec(iota.begin(), iota.end());
auto vec = std::views::iota(0, 1024)
  | std::ranges::to<std::vector<int>>(list);

これによって、range adoptorのチェーンから任意のコンテナへの変換が簡単に行えるようになります。

std::ranges::toによる変換は指定するコンテナによって最も効率的な方法で実装されます。例えば、reserve可能な標準コンテナに関しては、変換元のrangeがその距離を効率的に求められればreserveしてから代入されます(ただし、この振る舞いはとりあえず標準コンテナのみとなるようです)。

また、std::ranges::toにはクラステンプレートの実引数推定によって値型を推定してもらうことのできるオーバーロードが提供されています。

std::list<int> list = {...};

// std::vector<int>を推論してくれる
auto vec = list
  | std::ranges::to<std::vector>(list);

P1401R4 Narrowing contextual conversions to bool

constexpr ifstatic_assertの引数でのみ、整数型からbool型への暗黙の縮小変換を定数式で許可する提案。

onihusube.hatenablog.com

このリビジョンでの変更は、EWGでの指摘を受けてサンプルをいくつか追加した事と、提案する文言を調整した事です。

P1525R1 One-Way execute is a Poor Basis Operation

Executor提案(P0443R14)におけるstd::execution::execute及びstd::execution::executorコンセプトは、Executorライブラリにおける基本的なものとしては不適格であるという報告書。

std::execution::executeは任意のexecutorと引き数なしで呼び出し可能な処理を受け取って、そのexecutorの実行コンテキストで処理を即座に実行します。その戻り値はvoidであり、処理の結果やキャンセル、エラーを受け取ったり、処理をチェーンする方法も提供しません。つまりは処理を投げたらその処理について何かする方法が一切ありません。

int main() {
  std::execution::executor auto ex = ...; // 任意のexecutor
  std::invocable auto f = []() { /*何か処理*/ };

  // 処理fをexの実行コンテキストで即座に実行し、何も返さない
  std::execution::execute(ex, f);
}

このために、execute()による実行は処理の発行時、発行と実行の間、実行中のそれぞれで発生するあらゆるエラーをハンドリングする方法を提供せず、それは実装定義となりexecutorによって異なる事になります。
そのため、ジェネリックなコードでは非同期に発生するエラーに対応するポータブルな方法が無く、柔軟なエラー処理を必要とする高レベルな非同期アルゴリズムexecute()上で構築する事を妨げています。

さらに、execute()は処理の実行そのものが何らかの理由でキャンセルされた事を伝達するためのチャネルも持たず、execute()による非同期タスクの実行ではその状態のための動的なアロケーションが必要ですが、そのアロケーションを制御する方法もありません。

一方で、schedule()およびsender/receiverによる設計ではそれらの問題は全て解決されています。

int main() {
  std::execution::executor auto ex = ...; // 任意のexecutor
  std::invocable auto f = []() -> int { /*何か処理*/ };

  // 実行のスケジューリング、senderを返す
  std::execution::sender auto s1 = std::execution::schedule(ex);
  // 処理の登録
  std::execution::sender auto s2 = std::execution::then(s1, f);
  // 処理をチェーン
  std::execution::sender auto s3 = std::execution::transform(s2, [](int n) { return std::to_string(n); });

  // receiverは単なるコールバック
  // 処理の結果、エラー、完了(キャンセル)を受ける3つのチャネルを持つ
  std::execution::receiver auto r = ...;  // 任意のreceiver

  // senderにreceiverを接続する
  std::execution::operation_state auto state = std::execution::connect(s3, r);

  // 処理の実行
  // senderとreceiverの実装によって、実行中のアロケーションを制御できる
  std::execution::start(state);
}

この文書では、execute()及びexecutorコンセプトよりもschedule()及びschedulerコンセプトの方が、Executorライブラリの基本的な操作とコンセプトとして相応しいと述べています。

P1759R3 Native handles and file streams

標準ファイルストリームに、OSやプラットフォームネイティブのファイルを示すものを取得する方法およびその型エイリアスを追加する提案。

この提案の対象の標準ファイルストリームとは以下のものです。

  • basic_filebuf
  • basic_ifstream
  • basic_ofstream
  • basic_fstream

例えば開いているファイルの最終更新日を取得したい場合に、ファイルストリームからそれを取得する手段はありません。標準ライブラリでそれを行うには、例えばstd::filesystem::last_write_timeを利用しますが、これは引数としてstd::filesystem::pathをとります。そのため、どうしてもファイルオープンと最終更新日取得のタイミングは開いてしまう事になり、同じpathが同じファイルを指していなかったり、そもそもファイルがない可能性があります。
また、標準ライブラリにはないファイル操作を行いたい場合は、ファイルストリームを必要になるタイミングで再構築するかプラットフォーム依存のコードを書くかの選択になります。

// 最終更新日を取得する
std::chrono::sys_seconds last_modified(int fd) {
  ::stat s{};
  int err = ::fstat(fd, &s);
  return std::chrono::seconds(s.st_mtime.tv_sec);
}

int main() {
  // ファイルストリームの再オープン
  {
    // 最終更新日をまず取得
    int fd = ::open("~/foo.txt", O_RDONLY); // CreateFile on Windows
    auto lm = last_modified(fd);
    ::close(fd); // CloseFile on Windows

    // このパスは本当に同じファイルを指している?
    std::ofstream of("~/foo.txt");
    of << std::chrono::format("%c", lm) << '\n';
  }

  // プラットフォーム固有APIを常に使用
  {
    int fd = ::open("~/foo.txt", O_RDWR);
    auto lm = last_modified(fd);
  
    auto str = std::chrono::format("%c\n", lm);
    ::write(fd, str.data(), str.size());
  
    // 閉じるのを忘れずに!
    ::close(fd);
  }
}

この提案は、このような場合のためにOSネイティブのファイルハンドル(POSIXならファイルディスクリプタWindowsならファイルハンドル)を取得できるようにし、標準ファイルストリームを使用しつつ、必要な時にプラットフォーム固有のファイル操作を行えるようにするものです。

int main() {
  std::ofstream of("~/foo.txt");
  // ネイティブファイルハンドルの取得
  auto lm = last_modified(of.native_handle());
  of << std::chrono::format("%c", lm) << '\n';
}

この例の他にも、ファイルロックやステータスフラグの取得、Vectored I/Onon-blocking I/Oなどのユースケースがあります。

これはstd::threadstd::mutexなどがすでに持っているnative_handle()と同じものです。同じように、ネイティブファイルハンドルの型を示すエイリアスnative_handle_typeがファイルストリームのクラスに入れ子型として追加されます(POSIXならintWindowsならHANDLEvoid*))。

P1938R2 if consteval

constevalstd::is_constant_evaluated()にある分かりづらい問題点を解決するためのconsteval ifステートメントの提案

constevalstd::is_constant_evaluated()を組み合わせた時、あるいはstd::is_constant_evaluated()そのものの用法について、次の2つの問題があります。

constexpr関数でのconsteval関数の条件付き呼び出し

consteval関数は即時関数と呼ばれ、その呼び出しは必ずコンパイル時に完了しなければならず、コンパイル時に実行できないような呼び出しはコンパイルエラーとなります。

consteval int f(int i) { return i; }

constexpr int g(int i) {
  if (std::is_constant_evaluated()) {
      return f(i) + 1; // ng
  } else {
      return 42;
  }
}

consteval int h(int i) {
  return f(i) + 1;  // ok
}

g()h()を実行時にも呼び出し可能なように拡張したものです。一見、このコードは何の問題もなく意図通りに動作しそうに思えます。しかし、h()は問題ありませんがg()コンパイルエラーが発生します。

f()consteval関数でありその引数は定数式でなければなりません。g()で呼ばれるf()の引数iは単にconstexpr関数の引数であり定数式ではありません。従って、このf()呼び出しはstd::is_constant_evaluated()の結果に関わらず常に失敗します。
一方、h()で呼ばれるf()h()consteval関数であるのでこの制約を受けません。

しかし、g()内のf()の呼び出しが例えばf(42)の様になっているとその呼び出しは成功し、コンパイルエラーは起きません。

この問題は即時関数が呼ばれるコンテキストの問題ですが、constexpr ifの特性を知っている人はif (std::is_constant_evaluated())のようにすればg()が実行時評価されたときにはf(i)の呼び出しはコンパイルされないので行ける!と思うかもしれません・・・

if constexpr (std::is_constant_evaluated())

std::is_constant_evaluated()コンパイル時に呼び出されたときにtrueを返し、実行時に呼ばれるとfalseを返す関数、と単純に説明されることが多いです。するとおそらく誰もが考えるでしょう、実行時にまでifを残したくないのでif constexprを使おう!と。

#include <type_traits>

constexpr int f() {
  if constexpr (std::is_constant_evaluated()) {
    return 20;
  } else {
    return 0;
  }
}

int main() {
  // コンパイル
  constexpr int n = f();
  // 実行時
  int m = f();
  
  std::cout << n << '\n' << m << std::endl;
  // 20
  // 20
}

std::is_constant_evaluated()の正確な効果は、コンパイル時実行されることが確実な特定のコンテキストで呼び出されたときにのみtrueを返し、それ以外の場合はfalseを返す、というものです。
特に、if constexprの条件式で呼び出されたときは常にtrueを返します。

std::is_constant_evaluated()if文と組み合わせて使うのが正しい用法です。

この関数の呼び出しはおそらく常にコンパイル時に行われます。その際、特定のコンテキストにある呼び出しのみがtrueを返しそれ以外はfalseとなります。実際の所、普通のifと共に使ったとしてもその条件分岐が実行時まで残ることは無いでしょう。

if consteval

とはいえ、この2つの振る舞いは非直感的であり、特に2つ目の方は罠になり得ます。この提案は新しくif constevalという条件分岐構文を追加することでこの解消を図る物です。

constexpr int f() {
  if consteval {
    return 20;  // コンパイル時の処理
  } else {
    return 0;   // 実行時の処理
  }
}

if constevalは次の事を除くと、殆どif (std::is_constant_evaluated())シンタックスシュガーです。

  • <type_traits>のインクルードが必要ない
  • 構文が異なるため、誤用や誤解のしようがない
    • コンパイル時に評価されているかをチェックする適切な方法についての混乱を完全に解消できる
  • if constevalを使用してconsteval関数を呼び出すことができる
consteval int f(int i) { return i; }

constexpr int g(int i) {
    if consteval {
        return f(i) + 1; // ok!
    } else {
        return 42;
    }
}

consteval int h(int i) {
    return f(i) + 1;  // ok
}

if constevalコンパイル時評価ブロック内では、consteval関数の呼び出しが特別扱いされて、定数式ではない引数を受けていても呼び出すことができるようになります。

このように、if constevalの導入によってC++20で導入されてしまった2つの非自明な点を解消できます。

この提案はEWGでの議論をほぼ終えていて、CWGへ転送するための投票待ちをしています。CWGでの議論次第ではありますがC++23に入る可能性は高そうです。

P2029R4 Proposed resolution for core issues 411, 1656, and 2333; escapes in character and string literals)

文字(列)リテラル中での数値エスケープ文字('\xc0')やユニバーサル文字名("\u000A")の扱いに関するC++字句規則の規定を明確にする提案。

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンの変更点は、提案している文言を調整したことです。

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

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

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンの変更点は、atomicブロックでのthrow式が未定義動作であると変更されたことです(以前は実装定義)。

P2093R2 Formatted output

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

前回の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、std::printlnFILE*std::ostream&を取るオーバーロードを追加した事とstd::ostream&を取るオーバーロードについて<ostream>へ移動したことで<format><ostream>に依存しなくなった事、vprint_*関数の有用性を明確にしたこと及び提案する文言の調整です。

std::printが可変長テンプレートで任意個数の引数を受け取り出力を行うのに対して、std::vprint_unicode()/std::vprint_nonunicode()は型消去された引数参照の配列であるformat_argsオブジェクトを引数に取る非テンプレートの関数です。
std::print等他のものは内部でこれらに委譲して実装することで、余分なテンプレートのインスタンス化を減らしてバイナリサイズを削減することができます。

P2148R0 Library Evolution Design Guidelines

C++に新しいライブラリ機能を提案する際の設計のガイドラインの提案。

型やコンセプトなどの命名、関数オーバーロードの追加方法、クラスにおける特定のメンバ関数や変換、例外についてが簡単にまとめられています。自分でライブラリを書く際にも参考にできそうな内容です。

P2171R1 Rebasing the Networking TS on C++20 (revision 1)

P2171R2 Rebasing the Networking TS on C++20 (revision 2)

現在のNetworking TS(N4771)のベースとなっている規格はC++14なので、C++20ベースに更新する提案。

以前の記事(参照するほどの事は書いてない) onihusube.hatenablog.com

P2187R5 std::swap_if, std::predictable

より効率的な条件付きswapを行うためのstd::swap_ifと、その使用を制御するstd::predictableの提案。

前回の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

onihusube.hatenablog.com

前回からの変更は、標準への影響を説明するセクションと報告された既知の問題点についてのセクションが追加されたことと、機能テストマクロが追加されたことです。

P2192R3 std::valstat - Returns Handling

関数の戻り値としてエラー報告を行うための包括的な仕組みであるvalstatの提案。

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、サンプルコードを明確にしたことです。

P2198R1 Freestanding Feature-Test Macros and Implementation-Defined Extensions

フリースタンディング処理系でも使用可能なライブラリ機能について、機能テストマクロを追加する提案。

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、どのワーキングドラフトをベースとするか明示されたこと、P1642が採択されることに依存している部分があることを明示したこと、P2013R3に関する機能テストマクロを追加した事です。

P2214R0 A Plan for C++23 Ranges

C++23に向けてのrangeライブラリの拡張プランについてまとめた文書。

コロナウィルスの流行によって対面のミーティングが行えなくなったため、委員会のメンバーにrangeライブラリ周りでC++23に向けて何をすべきかを共有するために書かれた文書のようです。

この文書では、機能に三段階の優先度を設けた上で、機能をView adjunctsViewsAlgorithmActionsに分けてそれぞれについて解説しています。

かなり膨大な数の新規機能がリストアップされていますが、最優先のものだけを列挙してみます。

  • ranges::to
  • std::formatによるviewのフォーマット
  • range adopter
    • views::cache_latest
    • views::cartesian_product
    • views::chunk
    • views::group_by
    • views::iter-zip-transform<V> (exposition-only)
    • views::iter-adjacent-transform<V> (exposition-only)
    • views::index-view<S, D> (exposition-only)
    • views::join_with
    • views::slide
    • views::stride
    • views::transform_maybe
    • views::enumerate
    • views::flat_map (renamed to… something)
    • views::zip
    • views::zip_transform
    • views::adjacent
    • views::adjacent_transform
  • range algorithm
    • ranges::iota
    • ranges::fold

使い道や必要性はわかりますが、これだけでもかなり巨大です。この優先度最高のものはC++23を目指して議論されるようです。

P2223R1 Trimming whitespaces before line splicing

バックスラッシュ+改行による行継続構文において、バックスラッシュと改行との間にホワイトスペースの存在を認める提案。

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、この変更が生文字列リテラルに影響を与えないことを明記した事と、CWG Issue 1698の修正をここではしない事にしたことです。

P2226R0 A function template to move from an object and reset it to its default constructed state

std::exchangeによるmoveしてリセットするイディオムを行う新しいCPO、taketake_assignの提案。

std::exchangeによるmoveしてリセットするイディオムとは次のようなものです。

// old_objの値をムーブしてnewobjを構築し、old_objをデフォルト状態にリセット
T new_obj = std::exchange(old_obj, {});

// old_objの値をnewobjにムーブ代入して、old_objをデフォルト状態にリセット
new_obj = std::exchange(old_obj, {});

このイディオムの利点の1つは複数の操作をひとまとめにする事でエラーが起きにくくすることにあります。std::unique_ptrの様にポインタを所有するようなクラスのムーブコンストラクタとreset()で次のようにコードを改善できます。

struct MyPtr {
  Data *d;

  // BAD, ポインタのコピーとnullptr代入が分割されているため、nullptr代入が忘れられうる
  MyPtr(MyPtr&& other) : d(other.d) { other.d = nullptr; }

  // BETTER, use std::exchange
  MyPtr(MyPtr&& other) : d(std::exchange(other.d, nullptr)) {}

  // GOOD, std::exchangeによる一般化されたイディオム(知っていれば意図が明確)
  MyPtr(MyPtr&& other) : d(std::exchange(other.d, {})) {}


  void reset(Data *newData = nullptr)
  {
    // BAD, 読みづらい
    swap(d, newData);
    if (newData) {
      dispose(newData);
    }

    // BETTER, 読みやすい
    Data *old = d;
    d = newData;
    if (old) {
      dispose(old);
    }

    // GOOD, 合理的
    if (Data *old = std::exchange(d, newData)) {
      dispose(old);
    }
  }
};

もう一つの利点は、move後の抜け殻となっているオブジェクトの状態を確定できる事です。

f(std::move(obj));          // objの状態は良く分からない・・・

f(std::exchange(obj, {}));  // objはデフォルト構築状態にリセットされる

例えば標準ライブラリのものであれば、moveした後の状態は「有効だが未規定な状態」と規定されています。とはいえ結局どういう状態なのか分からず、より一般のライブラリ型などではドキュメント化されていることの方が稀です。
このイディオムを用いることによって、moveとその後のオブジェクトの状態の確定を1行で簡潔に書くことができます。

このイディオムは名前がついていたわけではありませんが、既存の大規模なC++コードベース(Boost, Qt, firfox, Chromium等)で広く使われており、かつ有用性も明らかです。これらのパターンに名前を付けてイディオムとして広めることはC++コミュニティ全体にとって有益であり、その名前が明確かつ簡潔であれば、std::exchangeによる物よりもイディオムの意図が明快になります(std::exchange(old, {})というのは一見すると分かり辛いです)。そのような理由からtake/take_assignCPOとして提案に至ったようです。

現在 この提案
class C {
  Data *data;
public:
  // idiomatic, C++14
  C(C&& other) noexcept
    : data(std::exchange(other.data, {}))
  {}
};
void Engine::maybeRunOnce() {
  if (std::exchange(m_shouldRun, false)) {
    run();
  }
}
template <
  typename K, typename V,
  template <class...> class C = std::vector
>
class flat_map {
  C<K> m_keys;
  C<V> m_values;

public:

  flat_map(flat_map&& other) noexcept(/**/)
    : m_keys(
        std::exchange(other.m_keys, {})),
      m_values(
        std::exchange(other.m_values, {}))
  {}

  flat_map &operator=(flat_map&& other)
    noexcept(/**/)
  {
    m_keys
      = std::exchange(other.m_keys, {});
    m_values
      = std::exchange(other.m_values, {});
    return *this;
  }
};
class C {
  Data *data;
public:
  // idiomatic, C++2?
  C(C&& other) noexcept
    : data(std::take(other.data))
  {}
};
void Engine::maybeRunOnce() {
  if (std::take(m_shouldRun, false)) {
    run();
  }
}
template <
  typename K, typename V,
  template <class...> class C = std::vector
>
class flat_map {
  C<K> m_keys;
  C<V> m_values;

public:

  flat_map(flat_map&& other) noexcept(/**/)
    : m_keys(std::take(other.m_keys)),
      m_values(std::take(other.m_values))
  {}

  flat_map &operator=(flat_map&& other)
    noexcept(/**/)
  {
    std::take_assign(m_keys, other.m_keys);
    std::take_assign(m_values, other.m_values);
    return *this;
  }
};

P2227R0 Update normative reference to POSIX

現在のC++標準規格が参照しているPOSIX規格への参照を更新する提案。

現在のC++規格は「ISO/IEC 9945:2003 (POSIX.1-2001 または、The Single UNIX Specification, version 3)」を主に標準ライブラリの定義中で現れるPOSIX関数のために参照しています。
ただ、これは古い規格であり、現在のC++標準ではそこに載っていない関数を参照していることがあるようです。

そのため、POSIX規格の参照を最新の「ISO/IEC/IEEE 9945:2009 (POSIX.1-2008 aka SUSv4)」に更新しようとするものです。

P2228R0 Slide Deck for P1949 EWG Presentation 20200924

P1948R6 C++ Identifier Syntax using Unicode Standard Annex 31のプレゼンの際に使われたスライド。

EWGで行われたP1948の内容を解説するプレゼンの際に使用された資料のようです。

P2231R0 Add further constexpr support for optional/variant

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

C++20では共用体のアクティブメンバの切り替えplacement new(std::construct_atが定数式で可能となりました。std::optionalstd::variantはこれらを実装に利用しているため、いくつかの関数をさらにconstexpr対応させることができるようになっています。
この提案はそれに従ってconstexprを付加するだけで対応可能なものにconstexprを追加するものです。

どちらに対しても、コピー/ムーブコンストラクタや代入演算子emplace(), swap()constexpr対応が提案されています。

P2233R0 2020 Fall Library Evolution Polls

LEWGが2020年秋に投票を行うことが予定されている提案についてのリスト。

Executor提案の調整や、いくつかの提案をLWGに転送することを決める投票がメインです。C++23に何かを導入するものではありません。

P2234R0 Consider a UB and IF-NDR Audit

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

UBとIF-NDRC++の多くの所に潜んでおり、特に文書化されておらず、出会ってしまうとプログラムのデバッグをより困難にしてしまいます。このことは、C++に深く精通していないプログラマC++プログラムについて推論することを妨げています。

この提案の目的は、多くのUBとIF-NDRの全てについて専門家の小さなグループによって監査し、より良い振る舞いを規定できるものを特定し、その変更の方法や影響範囲を見積もることを継続的に行っていくことです。

この提案ではUBを改善可能なものとして、nullptrや使用できないポインタに対するサイズ0のmemcpyの動作や副作用のない無限ループを挙げています。

P2235R0 Disentangling schedulers and executors

現在のExecutor提案(P0443R14)について、schedulerexecutorの設計を簡素化し、schedulerexecutorの絡み合いをほどく提案。

P0443のexecutorコンセプトによって定義されるexecutorは引数も戻り値もないCallableオブジェクトを受け取って即座に実行する能力しかありません。schedulerコンセプトによって定義されるschedulerはそれに加えて実行の遅延と、sender/receiverと組み合わせた結果の受け取りやエラーハンドリング、そして処理のチェーンをサポートします。
schedulersender/receiverと共に、C++ Executorライブラリ上での多彩なジェネリックアルゴリズムの実装をサポートします。

schedulerexecutorの持つ能力を包含していますが、executorはそうではありません。schedulerからexecutorへの変換は縮小変換の様なもので、ソースコード上の見えないところで変換が起きた場合静かなバグの源となり得ます。にもかかわらず、現在のP0443は相互の暗黙変換をサポートしています。

一方、C++ Executorライブラリが非同期並行処理のための基盤となるものであることを考えると、どこかから渡されてきたschedulerexecutorとして扱うことも避けるべきです。これは不可逆変換ではありませんが、広い契約を持つ関数が中でより狭い契約を持つ関数に丸投げしているようなもので、広い契約を期待する呼び出し元の期待は満たされません。
schedulerexecutorとして扱ってexecuteCPOに投入してしまうと、まず処理のスケジューリングの機会がありません。そして、スケジューリングエラー(この場合、executeCPOが処理を受け取り実行環境に投入してから実際に実行されるまでの間のエラー)をハンドルする機会もありません。ユーザーがschedulerをカスタマイズしてスケジューリングエラーをハンドルする仕組みを備えていたとしても、schedulerexecutorとして扱ってしまえばそれが活かされる機会はありません。

このように、この2つのものは混ざり合いません。一方を他方として扱うとすれば、それは目に見える形で明確に細心の注意を払って行われるべきです。

この提案では次の変更によってこの絡み合いを解消し、問題の解決を図ります。

  • scheduleCPOはschedulerのみを受け付ける
  • executeCPOはexecutorのみを受け付ける
  • connectCPOなどのsenderreceiverに対する操作はsenderreceiverのみを受け付ける
  • executorからexecutorへの一方向の明示的な変換を追加する
    • 双方向の暗黙変換を削除する
  • schedulesenderの単純なfire-and-forget実行(executeの行うような実行)を可能にする個別のアルゴリズムは、execute以外の名前を使用するようにする

これは既に次のLEWGの投票にかけられることが決まっていて、そこでコンセンサスを得られればすぐにP0443に適用されることになります。

P2236R0 C++ Standard Library Issues to be moved in Virtual Plenary, Nov. 2020

標準ライブラリのIsuueのうち2020年11月のオンライン投票にかけられるもののリスト。

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

P2237R0 Metaprogramming

C++23以降に予定されている、あるいは現在提案中のメタプログラミングサポートに関連する機能に関するサーベイ論文。

リフレクション、メタクラスexpansion statementstemplate引数、コンパイル時I/Oなどコンパイル時にあれこれするための機能についてどう使うかや何に役立つかなど多岐にわたって述べられています。
なお、ここに上がっているものはまだ提案中のものばかりです。

11月半ばごろ?

この記事のMarkdownソース

[C++]std::exchangeによるmoveしてリセットするイディオムの御紹介

2020年10月公開分の提案文書を眺めていたら良さげなものを見つけたので宣伝です。そのため、この記事の内容は次の提案文書を元にしています。

もくじ

moveしてリセット!

std::exchangeを用いてmoveしてリセットするとは、次のようなものです。

// old_objの値をムーブしてnewobjを構築し、old_objをデフォルト状態にリセット
T new_obj = std::exchange(old_obj, {});

// old_objの値をnewobjにムーブ代入して、old_objをデフォルト状態にリセット
new_obj = std::exchange(old_obj, {});

std::exchangeC++14で追加された関数で、第二引数の値を第一引数に転送し、第二引数の元の値を戻り値として返すものです。

template <class T, class U=T>
constexpr T exchange(T& obj, U&& new_val);

ストリーム的に見ると、第二引数から戻り値まで右から左へその値が玉突き的に流れていくように見えます。

第二引数の型も個別のテンプレートパラメータになっており、デフォルトでは第一引数の型が推論されます。そのため、第二引数に{}を指定したときはT{}と指定したのと同等になるわけです。
そして、そのようにデフォルト構築(正確には値初期化(Value initialization))された値が第一引数にmoveされ、第一引数の元の値が戻り値として返されます。その際、可能であればすべてmoveされます。

このイディオムには次の2つの利点があります。

1. 操作の複合化

このイディオムの利点の1つは複数の操作をひとまとめにする事でミスやエラーを起きにくくすることです。
例えば、std::unique_ptrの様にポインタを所有するようなクラスのムーブコンストラクタとreset()で次のようにコードを改善できます。

struct MyPtr {
  Data *d;

  // BAD, ポインタのコピーとnullptr代入が分割されているため、nullptr代入が忘れられうる
  MyPtr(MyPtr&& other) : d(other.d) { other.d = nullptr; }

  // BETTER, std::exchangeを利用してポインタを移動しリセット
  MyPtr(MyPtr&& other) : d(std::exchange(other.d, nullptr)) {}

  // GOOD, std::exchangeによる一般化されたイディオム(知っていれば意図が明確)
  MyPtr(MyPtr&& other) : d(std::exchange(other.d, {})) {}


  void reset(Data *newData = nullptr)
  {
    // BAD, 読みづらい
    std::swap(d, newData);
    if (newData) {
      dispose(newData);
    }

    // BETTER, 意図は分かりやすい
    Data *old = d;
    d = newData;
    if (old) {
      dispose(old);
    }

    // GOOD, 1行かつストリーム的
    if (Data *old = std::exchange(d, std::exchange(newData, {}))) {
      dispose(old);
    }
  }
};

このように、値が右から左へストリーム的に流れていくように見ることができ、そして移動とリセットの操作が合成されているために、意図が明確でミスも起こしにくいコードを書くことができます。

2. move後状態の確定

もう一つの利点は、move後の抜け殻となっているオブジェクトの状態を確定できる事です。

f(std::move(obj));          // objの状態は良く分からない・・・

f(std::exchange(obj, {}));  // objはデフォルト構築状態にリセットされる

例えば標準ライブラリのものであれば、moveした後の状態は「有効だが未規定な状態」と規定されています。とはいえ結局どういう状態なのか分からず、より一般のライブラリ型などではドキュメント化されていることの方が稀です。
このイディオムを用いることによって、moveとその後のオブジェクトの状態の確定を1行で簡潔に書くことができます。

とはいえ完全にmoveと同等ではなくいくつか違いがあります。

move(old_obj) exchange(old_onj, {})
例外を投げる? 通常ムーブコンストラクタはnoexcept デフォルトコンストラクタ次第
処理後のold_objの状態は? 有効だが未規定 デフォルト構築状態
呼び出しのコストは? ムーブコンストラクタ次第 ムーブ/デフォルトコンストラクタ次第
obj = xxxx(obj);は何をする? 実装依存 例外を投げないと仮定すると、何もしない
old_objをその後使用しない場合に最適な書き方? Yes No

いくつかのサンプル

class C {
  Data *data;
public:
  // ムーブコンストラクタの改善
  C(C&& other) noexcept
    : data(std::exchange(other.data, {}))
  {}
};
template <typename K, typename V, template <class...> class C =  std::vector>
class flat_map {
  C<K> m_keys;
  C<V> m_values;

public:

  // ムーブ後の有効だが未規定な状態を達するにはデフォルトmoveに任せられない
  // Cのムーブ操作によってはflat_mapの不変条件が破られ有効ではなくなってしまう可能性がある
  // 言い換えると、有効だが未規定な状態は合成されない
  // そのため、明示的なリセットが必要
  flat_map(flat_map&& other) noexcept(/**/)
    : m_keys(std::exchange(other.m_keys, {})),
      m_values(std::exchange(other.m_values, {}))
  {}

  flat_map &operator=(flat_map&& other) noexcept(/**/) {
    m_keys = std::exchange(other.m_keys, {});
    m_values = std::exchange(other.m_values, {});
    return *this;
  }
};
void Engine::processAll() {
  // m_dataを消費しつつループする
  for (auto& value : std::exchange(m_data, {})) {
      // この処理はm_dataを変更する可能性がある
      // イテレータ破壊を回避する
      processOne(value);
  }
}
void ConsumerThread::process() {
  // pendingDataをmutexの保護の元で取得し
  // 現在のスレッドがそれを安全に使用できるようにする
  Data pendingData = [&]() {
      std::scoped_lock lock(m_mutex);
      return std::exchange(m_data, {});
  }();

  for (auto& value : pendingData)
      process(value);
}
// 一度だけ実行される関数
void Engine::maybeRunOnce() {
  if (std::exchange(m_shouldRun, false)) {
    run();
  }
}
// Dataのオブジェクトを貯めておくクラス
struct S {
    // C++ Core Guideline F.15に基づいたオーバーロードの提供
    void set_data(const Data& d);
    void set_data(Data&& d);
} s;

Data d = ~~~;

// dをため込むが、明示的にデフォルト状態にする
s.set_data(std::exchange(d, {}));

assert(d == Data());

参考文献

この記事のMarkdownソース

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

文書の一覧 www.open-std.org

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

P0288R7 : any_invocable

ムーブのみが可能で、関数呼び出しのconst性やnoexcept性を指定可能なstd::functionであるstd::any_invocableの提案。

先月の記事を参照

onihusube.hatenablog.com

前回からの変更は、規格書に追加するための文言を調整しただけの様です。

P0443R14 : A Unified Executors Proposal for C++

処理をいつどこで実行するかを制御するための汎用的な基盤となるExecutorライブラリの提案。

以前の記事を参照。

onihusube.hatenablog.com

R13からの変更は、いくつかEditorialなバグを修正した事です。

P0881R7 : A Proposal to add stacktrace library

スタックトレースを取得するためのライブラリを追加する提案。

先月の記事を参照。

onihusube.hatenablog.com

LWGでのレビューが終了し、この提案は本会議での投票に向かうようです。このリビジョンはLWGでのレビューを受けて最終的な文言調整を反映したものです。多分C++23入り確定でしょう。

P0958R2 : Networking TS changes to support proposed Executors TS

Networking TSのExecutorの依存部分をP0443のExecutor提案の内容で置き換える提案。

現在のNetworking TSは以前のBoost.asioをベースにしており、asioの持つExecutorはP0443のものとは異なっているので、Networking TSのExecutor部分もまたP0443のものとは異なった設計となっています。

Networking TSはExecutorに強く依存しており、C++23に導入される予定のExecutorはP0443のものがほぼ内定しているので、Networking TSもそれに依存するように変更するものです。

Boost.asioライブラリは既にP0443ベースのExecutor周りを実装し終えて移行しており、その実装をベースとする形で書き換えています。その経験によれば、一部のものを除いて殆どのアプリケーションでは追加の変更を必要としなかったとのことです。

P1322R2 : Networking TS enhancement to enable custom I/O executors

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

Networking TSのio_contextはBoost.asioではio_serviceと呼ばれていたもので、P0443のExecutorにいくつかの機能が合わさったものです。

io_contextは実行コンテキストを抽象化し表現するという役割がありますが、実行コンテキストを変更する事は単体ではできません。P0443ではexecutorコンセプトによってそれらExecutorと実行コンテキストを表現します。

Networking TSのI/Oオブジェクト(socket, acceptor, resolver, timer)はio_contextを構築時に受け取り、それを利用して実行します。そこに任意のExecutorを渡せるようにすることでユーザーが用意したものやプラットフォームネイティブの実行コンテキストでそれらの処理を実行できるようにしようとする提案です。

また、各I/OオブジェクトのクラスのテンプレートパラメータにExecutorを指定できるようにし、それらの動作をカスタマイズ可能としています。

この提案はおそらくNetworking TS仕様のExecutorをベースとしており、P0443ベースで書かれていないようです。

P1371R3 : Pattern Matching

オブジェクトの実際の状態に応じた分岐処理のシンタックスシュガーであるパターンマッチング構文の提案。

現在のC++にはifswitchという二つの条件分岐用の構文があります。ifは複雑なboolの式を扱うことができますが、switchは整数値しか扱えず、ifは分岐条件を一つづつ書いて行くのに対してswitchは条件を羅列した形で(宣言的に)書くことができるなど、は2つの構文の間にはギャップがあります。
パターンマッチングはそれらの良いとこどりをしたような構文によって、特に代数的データ型の検査と分岐を書きやすく、読みやすくするものです。

この提案ではinspect式によって構造化束縛宣言を拡張する形でswitch的な書き方によってパターンマッチングを導入しています。これを提案中ではstructured inspection(構造化検証?)と呼んでいます。

std::string

`if`文 `inspect`式
if (s == "foo") {
  std::cout << "got foo";
} else if (s == "bar") {
  std::cout << "got bar";
} else {
  std::cout << "don't care";
}
inspect (s) {
  "foo" => { std::cout << "got foo"; }
  "bar" => { std::cout << "got bar"; }
  __ => { std::cout << "don't care"; }
};

std::tuple

`if`文 `inspect`式
auto&& [x, y] = p;
if (x == 0 && y == 0) {
  std::cout << "on origin";
} else if (x == 0) {
  std::cout << "on y-axis";
} else if (y == 0) {
  std::cout << "on x-axis";
} else {
  std::cout << x << ',' << y;
}
inspect (p) {
  [0, 0] => { std::cout << "on origin"; }
  [0, y] => { std::cout << "on y-axis"; }
  [x, 0] => { std::cout << "on x-axis"; }
  [x, y] => { std::cout << x << ',' << y; }
};

std::variant

`if`文 `inspect`式
struct visitor {
  void operator()(int i) const {
    os << "got int: " << i;
  }
  void operator()(float f) const {
    os << "got float: " << f;
  }
  std::ostream& os;
};
std::visit(visitor{strm}, v);
inspect (v) {
  <int> i => {
    strm << "got int: " << i;
  }
  <float> f => {
    strm << "got float: " << f;
  }
};

polymorphicな型

継承ベースの動的ポリモルフィズム `inspect`式
struct Shape { 
  virtual ~Shape() = default;
  virtual int Shape::get_area() const = 0;
};

struct Circle : Shape {
  int radius;

  int Circle::get_area() const override {
    return 3.14 * radius * radius;
  }
};

struct Rectangle : Shape {
  int width, height;

  int Rectangle::get_area() const override {
    return width * height;
  }
};
struct Shape { 
  virtual ~Shape() = default;
};

struct Circle : Shape {
  int radius;
};

struct Rectangle : Shape {
  int width, height;
};

int get_area(const Shape& shape) {
  return inspect (shape) {
    <Circle> [r] => 3.14 * r * r;
    <Rectangle> [w, h] => w * h;
  };
}

この例だけを見てもかなり広い使い方が可能であることが分かると思います。しかし、この例以上にinspect式による構文は柔軟な書き方ができるようになっています(ここで説明するには広すぎるので省略します。そこのあなたm9!解説記事を書いてみませんか??)。

P1701R1 : Inline Namespaces: Fragility Bites

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

当初(C++11)のインライン名前空間名前空間名の探索には次のようなバグがありました。

namespace A {
  inline namespace b {
    namespace C {
      template<typename T>
      void f();
    }
  }
}

namespace A {
  namespace C {
    template<>
    void f<int>() { }  // error!
  }
}

当初の名前空間名の探索は宣言領域 (declarative region) という概念をベースに行われていました。宣言領域とは簡単にいえば、ある宣言を囲む宣言の事です。
宣言領域の下では、2つ目の名前空間A::Cの宣言は1つ目のA::b::Cとは宣言領域が異なるため、それぞれの名前空間Cは同一のものとはみなされません。

しかし、これはテンプレートの特殊化を行う際に問題となるため、DR 2061によって規格書の定義としては修正されました。その修正方法は、名前空間名を宣言するとき、そのコンテキストからネストしたinline名前空間も考慮したうえで到達可能な名前空間名を探索し、同じ名前が見つかった場合は同一の名前空間として扱い、見つからない場合にのみ新しい名前空間名を導入する。という感じです。

これによって先程のバグは解決されましたが、筆者の方がそれをGCCに実装する時に問題が浮かび上がったようです。

inline namespace A {
  namespace detail { // #1
    void foo() {} // #3
  }
}

namespace detail { // #2
  inline namespace C {
    void bar() {} // #4
  }
}

DR2061以前は2つ目の名前空間detailは新しい名前空間を導入し、#3, #4はそれぞれA::detail::foodetail::C::barという修飾名を持ちます。

しかしDR2061による修正によれば、2つ目の名前空間detailの宣言は一つ目の名前空間A::detailと同一視されます。その結果、#3, #4はそれぞれA::detail::fooA::detail::C::barという修飾名を持つことになります。

このことはヘッダファイルやモジュールのインポートを介すことで、意図しない名前空間名の汚染を引き起こすことになります。
これによって、ヘッダと実装でファイルを分けている場合、実装ファイルで名前空間名の指定が意図通りにならず、最悪別の名前に対する実装を行ってしまうかもしれません。また、inline名前空間usingすることでAPIの一部だけを有効化するような手法をとるライブラリでは、意図しないものがスコープ中にばらまかれることにもなりかねません。

namespace component {
  inline namespace utility {
      namespace detail {
        // component::utility::detail
      }
  }
}

namespace component {
  namespace detail {
    // DR2061以前は component::detail
    // DR2061の後は component::utility::detail
  }
}

一方、DR2061が無いとC++20以降は特に困ったことが起きます。

namespace std {
  namespace ranges {
    template<>
    constexpr bool disable_sized_range<MyType> = true;
  }
}

std::range::disable_sized_rangeは変数テンプレートであり、ユーザー定義の型について特殊化することでstd::range::sized_rangeコンセプトを無効化するものです。C++20のrangeライブラリ周りではこのようなオプトアウトのメカニズムがほかにもいくつかあります。

現在、多くの実装はABI保護のためにstd名前空間内の実装には何かしらの形でインライン名前空間を挿入しています。すると、このコードで行っているような特殊化はDR2061による変更が無ければ特殊化としてみなされなくなります。

この提案ではこれらの問題の解決として次の2つのことをサジェストしています。

  1. DR2061を元に戻す
  2. 標準ライブラリ内でユーザーが特殊化することを定義されているものについて、修飾名で特殊化すること、という規定を追加する。

つまり、先程の特殊化は次のように書くことを規定するということです。

template<>
constexpr bool std::ranges::disable_sized_range<MyType> = true;

こうすれば、名前空間の宣言を伴わないため上記のような問題に悩まされることはありません。

P1885R3 : Naming Text Encodings to Demystify Them

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

C++の標準は文字エンコーディングを参照する時にロケールを介して参照しています。これは歴史的なものですが、ユニコードの登場によってもはや機能しなくなっています。そして、C++はシステムがどのエンコーディングを使用し、また期待しているかを調べる方法を提供していないためそれを推測しなければならず、文字列の取り扱いを満足に行う事ができません。

この提案は、現在取得する方法のないシステムやコンパイラの使用するエンコーディングを取得・識別できるようにし、また、エンコーディングを表現する標準的なプリミティブを提供することを目指すものです。
なお、ここでは文字コード変換のための標準機能の追加を目指してはいません。

#include <text_encoding>  // 新ヘッダ
#include <iostream>

int main() {
  // char文字(列)リテラルのエンコーディングを取得
  std::cout << std::text_encoding::literal().name() << std::endl;  
  // wchar_t文字(列)リテラルのエンコーディングを取得
  std::cout << std::text_encoding::wide_literal().name() << std::endl;
  // システムのマルチバイト文字エンコーディングを取得 
  std::cout << std::text_encoding::system().name() << std::endl;  
  // システムのワイド文字エンコーディングを取得 
  std::cout << std::text_encoding::wide_system().name() << std::endl;  
}

この提案による全てのものはstd::text_encodingクラスの中にあります。上記の4つの関数はその環境で対応する文字エンコーディングを表すstd::text_encodingのオブジェクトを返します。

std::text_encodingは非staticメンバ関数name(), mib(), aliases()によってその名前、IANAのMIBenum、文字エンコーディング名の別名(複数)を取得することができます。

また、システムの文字エンコーディングが特定のエンコーディングであるかを判定する機能も提供されています。

int main() {
  assert(std::text_encoding::system_is<std::text_encoding::id::UTF8>());
  assert(std::text_encoding::system_wide_is<std::text_encoding::id::UCS4>());
}

ただ、残念ながらこれらはconstexprではありません。

std::text_encoding::idは各種文字エンコーディングを表すenum classで、IANAの定義するCharacter Setにある名前が列挙値として登録されています。この列挙値か、対応する文字コード名文字列をによってもstd::text_encodingクラスを構築することができます。

また、std::text_encodingのオブジェクト同士を==で比較することもできます。

P1949R6 : C++ Identifier Syntax using Unicode Standard Annex 31

識別子(identifier)の構文において、不可視のゼロ幅文字や制御文字の使用を禁止する提案。

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンの変更点は、この提案によって変更されるものと変更されないものを明記したのと、いくつかのサンプルを追加した事です。

P2013R3 : Freestanding Language: Optional ::operator new

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

以前の記事を参照 onihusube.hatenablog.com

このリビジョンの変更点は、8月のEWGの電話会議での投票結果を記載した事と、いくつかの文言の修正、機能テストマクロについてはP2198の将来のリビジョンで検討することになったことです。

P2029R3 : Proposed resolution for core issues 411, 1656, and 2333; escapes in character and string literals

文字(列)リテラル中での数値エスケープ文字('\xc0')やユニバーサル文字名("\u000A")の扱いに関するC++字句規則の規定を明確にする提案。

以前の記事を参照 onihusube.hatenablog.com

このリビジョンの変更点は、電話会議の結果を受けて提案している文言を調整しただけの様です。

P2066R3 : Suggested draft TS for C++ Extensions for Transaction Memory Light

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

以前の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、EWGのフィードバックを受けて、完全式(full-expression)の定義にatomicブロック(atomicステートメント)の開始と終了という定義を追加した事と、トランザクションの並行処理に関するメモの追加や、atomicステートメントの導入キーワードのatomic doへの変更と、atomicステートメントで使用可能なライブラリ関数のリストが追加されたことなどです。

int f() {
  static int i = 0;

  // atomicブロック、atomicトランザクションを定義
  // このブロックは全てのスレッドの間で1つづつ実行される
  atomic do {
    ++i;
    return i;
  }
}

int main() {
  std::array<std::thread, 100> threads{};

  // 関数f()の呼び出し毎に一意の値が取得される
  // 同じ値を読んだり、更新中の値を読むことはない
  for (auto& th : threads) {
    th = std::thread([](){
      int n = f();
    });
  }

  for (auto& th : threads) {
    th.join();
  }
}

このatomicブロックで使用可能なものには以下が挙げられています。

  • std::memset, std::memcpy, std::memmove
  • std::move, std::forward
  • 基本型(組み込み型)に対するstd::swap, std::exchange
  • std::array<T>の全てのメンバ関数
    • Tに対する必要な操作がatomicブロックで使用可能である場合のみ
  • std::list<T>, std::vector<T>
    • Tに対する必要な操作がatomicブロックで使用可能である場合のみ
  • TMPとtype traits
  • <ratio>
  • std::numeric_limitsの全てのメンバ
  • placement newと対応するdelete

 

P2077R1 : Heterogeneous erasure overloads for associative containers

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

C++20では、非順序連想コンテナに対して透過的な検索を行うことのできるオーバーロードが追加されました。「透過的」というのは連想コンテナのキーの型と直接比較可能な型については、一時オブジェクトを作成することなくキーの比較を行う事が出来ることを指します。これによって、全ての連想コンテナで透過的な検索がサポートされました。

一方、削除の操作に関しては順序/非順序どちらの連想コンテナも透過的な削除をサポートしていませんでした。この提案は、全ての連想コンテナのerase()extract()に対しても同じ目的のオーバロードを追加しようとするものです。

std::unordered_map<std::string, int> map = {{"16", 16}, {"1024", 1024}, {"65536", 65536}};

const std::string key{"1"}

// C++20より、どちらも一時オブジェクトを作成しない
map.contains(key);
map.contains("1024");

map.erase(key);     // 一時オブジェクトを作成しない
map.erase("1024");  // stringの一時オブジェクトが作成される

この提案によるオーバロードを有効化する手段は透過的な検索と同様の方法によります。
順序連想コンテナではその比較を行う関数オブジェクトの型がis_transparentというメンバ型を持っていて、渡された型がイテレータ型に変換されない場合に有効になり、非順序連想コンテナではそれに加えてハッシュ関数オブジェクトの型がis_transparentというメンバ型を持っていれば有効化されます。

P2138R3 : Rules of Design <=> Specification engagement

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

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、対象とする読者への説明、提案文書作成時に文言の作成に支援を求める方法、Tentatively Readyステータスをスキップできる条件を明確にしたことと、Tentatively Ready for Plenaryというステータスは次のミーティングの本会議の採決にかける事を意味していることを明確にしたことです。

P2145R1 : Evolving C++ Remotely

コロナウィルスの流行に伴ってC++標準化委員会の会議がキャンセルされている中で、リモートに移行しつつどのように標準化作業を進めていくのかをまとめた文章。

onihusube.hatenablog.com

このリビジョンでの変更は、以前のリビジョン時からの状況変化を反映した様です。

P2164R2 : views::enumerate

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

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、既存のviewと同様にvalue_typeを非参照にしたこと、説明と文言間の矛盾を正したこと、サンプルコードに関連ヘッダ(<ranges>)を追記した事です。

P2166R1 : A Proposal to Prohibit std::basic_string and std::basic_string_view construction from nullptr

std::stringstd::string_viewnullptrから構築できないようにする提案。

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、googleのProtbufで見つかったコード例の追加と類似した問題へのリンクが追加されたことです。

string GetCapitalizedType(const FieldDescriptor* field) {

  switch (field->type()) {
      // handle all possible enum values, but without adding default label
  }

  // Some compilers report reaching end of function even though all cases of
  // the enum are handed in the switch.
  GOOGLE_LOG(FATAL) << "Can't get here.";
  return NULL;
}

この関数はswitchでハンドルされなかった場合にNULLから構築されたstringを返しています。ただ、実際にはこのコードは到達可能ではないらしく、この提案を妨げるものではないようです。

P2169R2 : A Nice Placeholder With No Name

宣言以降使用されず追加情報を提供するための名前をつける必要もない変数を表すために_を言語サポート付きで使用できる様にする提案。

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更はP2011での_の利用(プレイスホルダーとしての利用)との構文の衝突について追記されたことです。

int _ = 0;      // この提案の用法、プレイスホルダーではない
f() |> g(_, _); // P2011の用法、プレイスホルダー

この提案での_の特別扱いは変数宣言にのみ作用するため、上記P2011の用法による場合は常にプレイスホルダーとして扱えば良いと述べられています。

P2192R2 : std::valstat -Transparent Returns Handling

関数の戻り値としてエラー報告を行うための包括的な仕組みであるvalstatの提案。

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、動機やメタステートの説明、付録のサンプルコードなどを改善・書き足しした事と、タイトルが"std::valstat - transparent return type"から"std::valstat -Transparent Returns Handling"に変更されたことです。

P2194R0 : The character set of the internal representation should be Unicode

C++標準の想定するコンパイル中のC++ソースコードの表現について、ユニコード以外を認める必要が無いという根拠を示す文書。

これはSG16(Unicode Study Group)での議論のために書かれた報告書です。

C++20では、翻訳フェーズ1以降のソースコードの規格的な表現(エンコーディング)はユニコードです。SG16での議論の中で、そこにユニコードのスーパーセットたるエンコーディングを許可しようというアイデアが提出されたらしく、この文書はそのアイデアを採用する根拠がない事を説くものです。

P2195R0 : Electronic Straw Polls

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

これは標準化のプロセスについてのもので、C++言語そのものに対する提案ではありません。

コロナウィルスの流行によって対面での会議が行えなくなったため、現在のC++標準化委員会の作業は主に電話会議で行われており、投票が行われる場合も電話会議でリアルタイムに行われていました。

より議論と作業を効率化させるために、この投票を電子メールなどによって非同期的、かつ定期的(四半期ごと)に行うことを提案しています。
また、CWGやEWGでは現状の体制でも特に問題が無かったため、この提案の対象はLWG/LEWGに絞られています。

他にも、標準化の作業の投票やコンセンサスがどのように決定されるかなどについて詳細に書かれています。

P2206R0 : Executors Thread Pool review report

Executor提案(P0443R13)のstatic_thread_pool周りのレビューの結果見つかった問題についての報告書。

P2212R1 : Relax Requirements for time_point::clock

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

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、提案する既存規格の文言の変更についてフィードバックを受けて修正した事です。

P2215R1 : "Undefined behavior" and the concurrency memory model

out-of-thin-airと呼ばれる問題の文脈における、未定義動作とは何かについての報告書。

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、memory_order_load_storeの順序の問題が逐次一貫性のあるloadにも適用されることを明確にしたことと、"Consequences"セクションが追加されたことです。

P2216R0 : std::format improvements

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

主に、次の二点を改善しようとするものです。

コンパイル時にフォーマット文字列をチェックするようにする

C++20のstd::formatでは、フォーマットエラーは実行時にstd::format_error例外を投げることで報告されます。

std::string s = std::format("{:d}", "I am not a number");

例えばこのコードは、:dが通常の文字列に対するフォーマットとして不適切なため、実行時に例外を投げます。
この提案では、このようなフォーマットのエラーをコンパイル時にコンパイルエラーとして報告することを提案しています。

ただし、言語と実装の特別なサポートなくしてはこのままのコードでコンパイル時のフォーマットチェックを行うことはできません。そのような特別扱いを嫌う場合、ユーザー定義リテラルや静的な文字列型など何かしらの手段が必要となります。

std::vformat_toのバイナリサイズの削減

std::vformat_tostd::format_toの引数型を型消去したもので、フォーマット文字列とフォーマット対象を受けとり、任意の出力イテレータにフォーマット済み文字列を出力します。主にformatライブラリの内部で使用されるものです。

次のように宣言されています。

template<class Out, class charT>
using format_args_t = basic_format_args<basic_format_context<Out, charT>>;

template<class Out>
Out vformat_to(Out out, string_view fmt,
                 format_args_t<type_identity_t<Out>, char> args);

ここで問題となるのは、第二引数の型format_args_t<type_identity_t<Out>, char>が出力イテレータOutに依存していることによって、異なるOut毎に個別のformat_args_tインスタンス化されてしまい、コードサイズが肥大化する原因となることです。これはフォーマットしない引数型に対しても引き起こされうるので、使用しないものに対して余計なコストを支払うことになりかねません。
また、std::format/vformatは内部バッファを介してイテレータ型を型消去しており、同じ方法をとればこの特殊化は必要ありません。

そこで、シグネチャを次のように変更することを提案しています。

template<class Out>
Out vformat_to(Out out, string_view fmt, format_args args);

format_argsはあらかじめ定義されているイテレータ型を型消去し引数を保持しておくものです。これによって、std::format/vformatと同様の方法でイテレータ型の型消去を行うようになり、不要なテンプレートのインスタンス化を防ぐことができます。

なお、これらの事は明らかにC++20の<format>ライブラリに対する破壊的変更となりますが、<format>は現在のところ誰も実装していないので壊れるコードは無い、と筆者の方は述べています(個人的にはDRにしてほしい気持ちです・・・)。

P2217R0 : SG16: Unicode meeting summaries 2020-06-10 through 2020-08-26

SG16(Unicode Study Group)の会議の議事録の要約。

P2218R0 : More flexible optional::value_or()

std::optionalvalue_or()メンバ関数をベースとした、利便性向上のための新しいメンバ関数を追加する提案。

value_or()メンバ関数std::optionalが有効値を保持していればその値を返し、無効値を保持している場合は指定された値を代わりに(std::optionalの要素型に変換して)返すものです。
value_or()関数は有効値からのフォールバックという性質上、多くの場合その型のデフォルト値が使用されます。その場合リテラルを持つ組み込み型ならばあまり困る事も無いのですが、コンストラクタを持つユーザー定義型では意図通りにならない事があります。

例えばstd::optional<std::string>value_orstd::stringのデフォルト値(空文字列)を返そうとしてみると

std::optional<int> oi{};
std::optional<bool> ob{};
std::optional<std::string> os{};

// 無問題、意図通り  
std::cout << oi.value_or(0) << std::endl;
std::cout << ob.value_or(false) << std::endl;

// 実行時エラー
std::cout << os.value_or(nullptr) << std::endl;
// パフォーマンス的に最適ではない
std::cout << os.value_or("") << std::endl;
// コンパイルエラー
std::cout << os.value_or({}) << std::endl;
// 正解、でも型名を省略したい
std::cout << os.value_or(std::string{}) << std::endl;

特に3番目の様な書き方ができないのがとても不便です。要素型がさらに複雑な型になるとより面倒になります。

この原因はvalue_or()のテンプレートパラメータが要素型とは無関係になっていることにあります。そのため、要素型からの推論を行う事が出来ません。

template<typename T>
class optional {

  // おおよそこんな感じの実装になっている
  template<typename U>
    requires (is_copy_constructible_v<T> and is_convertible_v<U&&, T>)
  constexpr T value_or(U&& u) const & {
    return this->has_value() ? this->value() : static_cast<T>(std::forward<U>(u));
  }
};

そこで、このテンプレートパラメータUのデフォルト引数として要素型Tを与えておくことで、UTから推論することができるようになります。ただし、std::optionalの要素型はconst修飾を行っておくことが可能なので、それは除去しておきます。

template<typename T>
class optional {

  // おおよそこのような実装だが、おそらくこのままだとエラーになる
  template<typename U = remove_cv<T>>
    requires (is_move_constructible_v<T> and is_convertible_v<U&&, T>)
  constexpr T value_or(U&& u) const & {
    return this->has_value() ? this->value() : static_cast<T>(std::forward<U>(u));
  }
};

これによって、既存の振る舞いを壊すことなく全ての要素型においてvalue_or({})の様な呼び出しが出来るようになります。

次に、value_or()を発展させて、無効値を保持している場合に指定された引数で要素を構築して返すvalue_or_construct()関数を追加することを提案しています。これは、あくまでその場で構築して返すだけで、そのstd::optionalの無効状態を変更するものではありません。

std::optional<std::string> os{};

// デフォルト構築したstd::stringを返す
os.value_or_construct();

// 指定した文字列で構築したstd::stringを返す
os.value_or_construct("left value");

最後に、value_or_construct()をさらに発展させて、構築のための引数の評価を遅延させるvalue_or_else()関数を提案しています。

using namespace std::string_literals;
std::optional<std::vector<std::string>> ovs{};

// 呼び出し時点でstd::initializer_list<std::string>が構築される
ovs.value_or_construct({"Hello"s, "World"s});

// 呼び出し時点ではstd::vector<std::string>>は構築されない
// 無効値を保持している場合にのみラムダ式の呼び出しを通して構築される
ovs.value_or_else([]{ return std::vector{"Hello"s, "World"s}; });

value_or_construct()value_or_else()は一見似通ったものに見えますが、それぞれ異なる欠点があり使いどころが異なっているため、両方追加することを提案しています。

P2219R0 : P0443 Executors Issues Needing Resolution

Executor提案(P0443R13)のレビューの結果見つかった解決すべき問題点の一覧。

地味に63個も問題があり、これの解決後にさらなるレビューが必要となるので、もう少し時間がかかりそうです・・・

P2220R0 : redefine properties in P0443

Executor提案(P0443R13)における、Executorに対するプロパティ指定の方法を別に提案中のtag_invokeによって置き換える提案。

CPO(Custmization Point Object)は呼び出された引数に対して定義されたいくつかの方法のうちの一つが可能であれば、それによってその目的を達します。中でもほぼ必ず、引数に対してそのCPO自身と同名の関数をADLで探索することが行われます。この時、意図せずに同じ名前の関数を同じ名前空間に持つようなクラス型に対して誤った呼び出しを行ってしまう可能性があります。それを防ぐためにコンセプトによって制約されていますが、それすらもすり抜けるケースが無いとは言えません。

tag_invokeはCPOのその部分(ADLによる探索)をより安全に置き換えるためのものです。CPOの型そのものをタグとして用い、std::tag_invoke(これ自身もCPO)を介して、ユーザー定義型に対してADLでtag_invokeという名前の関数を呼び出します。CPOにアダプトするユーザーはtag_invokeという名前の関数を例えばHidden friendsとして定義し、対象のCPOの型をタグとして受け取れるようにした上で、そこにCPOによって呼び出された時の処理を記述します。要するに少し複雑なタグディスパッチを行うものです。

この提案は、そんなtag_invokeを用いてP1393require/preferによるプロパティ指定の方法をより簡潔に定義し直そうとするものです。

require/preferは主にExecutorライブラリのexecutorに対してプロパティを設定するもので、CPOとして定義されています。呼び出すとメンバ関数またはADLによってrequire/preferという名前の関数を探し処理を委譲します。require/preferによってプロパティ指定可能なクラスを作成する際は、そのように呼ばれるrequire/prefer関数内でプロパティ指定の処理を行い、その後プロパティ指定済みのオブジェクトを返します。

// 何かしらのexecutor
executor auto ex = ...;

// 実行にはブロッキング操作が必要という要求(require
executor auto blocking_ex = std::require(ex, execution::blocking.always);

// 特定の優先度pで実行することが好ましい(prefer
executor auto blocking_ex_with_priority = std::prefer(blocking_ex, execution::priority(p));

// ブロッキングしながら実行、可能ならば指定の優先度で実行
execution::execute(blocking_ex_with_priority, work);

requireは必ずそのプロパティを設定するという強い要求を行い、設定できない場合はコンパイルエラーとなります。対して、preferは弱い要求を行い、そのプロパティ指定は無視される可能性があります。

require/preferは処理の実行方法などのプロパティ(ブロックするかしないか、優先度など)と、処理の実行のインターフェース(std::execution::executeなどのCPO)とを分離し、プロパティ指定毎にその実行用インターフェースが増加することを回避するための仕組みです。

この提案ではrequire/preferの大部分をtag_invokeを使って実装するように変更しています。先程のコードは次のように変化します。

// 何かしらのexecutor
executor auto ex = ...;

// 実行にはブロッキング操作が必要という要求(require
executor auto blocking_ex = execution::make_with_blocking(ex, execution::always_blocking);

// 特定の優先度pで実行することが好ましい(prefer
executor auto blocking_ex_with_priority = std::prefer(execution::make_with_priority, blocking_ex, p);

// ブロッキングしながら実行、可能ならば指定の優先度で実行
execution::execute(blocking_ex_with_priority, work);

require/preferCPOそのものだけではなく、プロパティを表現する型やプロパティサポートを問い合わせるためのquery、プロパティを受け入れるための実装方法などもtag_invokeを使うように変更されています。結果として、requireは消えているようです。

元のrequire/preferはユーザーから見れば効率的で柔軟なプロパティ指定の方法でしたが、それを受け入れるようにする実装は少し大変でした。しかし、tag_invokeを利用することで実装者の負担がかなり軽減されています。

P2221R0 : define P0443 cpos with tag_invoke

Executor提案(P0443R13)で提案されているCPO(Custmization Point Object)を別に提案中のtag_invokeによって定義する提案。

先程も出て来たtag_invokeは、CPOの行う呼び出しのうちCPOと同名の関数をADLによって探索する部分を置き換えるためのものです。

CPO等によってカスタマイゼーションポイントを導入することは実質的にその関数名をグローバルに予約してしまう事になります。CPOはコンセプトによるチェックを行うとは言え、意図せずチェックを通ってしまう型が無いとも限りません。tag_invokeはCPOの型をタグとしてstd::tag_invokeという1つのCPOだけを通してADLによる関数呼び出しを行います。その結果、グローバルに予約する名前はtag_invoke一つだけになります。

Executorでは、executeなど全部で9つのCPOが用意されており、全て同名の関数をADLで探索します。従って、9つの名前を実質的に予約してしまうことになります。tag_invokeを使ってその部分の定義だけを置き換える事で、これらの名前を予約することを避けようとする提案です。

この提案の後でもCPOを使う側は一切何かする必要はありません。CPOにアダプトしようとする場合にtag_invokeを利用することになります。例えば、std::execution::executorCPOにアダプトする例を比較してみます。

P0443 この提案
struct my_executor {

  template<typename F>
  friend void executor(
    const my_executor& ex,
    F&& f)
  {
    // オリジナルのexecutorの処理、略
  }

  auto operator<=>(const my_executor&)
    = default;
};
struct my_executor {

  template<typename F>
  friend void tag_invoke(
    std::tag_t<std::execution::execute>,
    const my_executor& ex,
    F&& f)
  {
    // オリジナルのexecutorの処理、略
  }

  auto operator<=>(const my_executor&)
    = default;
};
my_executor ex{};

// 上記のどちらが採用されるにせよ、こう呼び出せる
std::execution::execute(ex, [](){ /* 何か処理*/ });

これらのfriend関数はHidden friendsというイディオムであり、同じ名前空間のフリー関数として定義していても構いません。しかしどちらにせよ、そこの関数名にCPO名ではなくtag_invokeという名前を使うようになることで、CPOをいくら増やしてもユーザーコードではそれを気にせずに同じ名前を使用することができるようになります。

P2223R0 : Trimming whitespaces before line splicing

バックスラッシュ+改行による行継続構文において、バックスラッシュと改行との間にホワイトスペースの存在を認める提案。

CとC++では行末にバックスラッシュがある場合に、その行と続く行を結合して論理的に1行として扱います。これはプリプロセスより前に処理されるため、マクロを複数行で定義する時などに活用されます。

しかし、バックスラッシュと改行の間にホワイトスペースしかない場合に問題が潜んでいます。

int main() {
  int i = 1  
  // \  
  + 42
  ;
  return i;
}

3行目のバックスラッシュの後にはスペースが挿入されています。この時、このバックスラッシュを行継続のマーカーとして扱うかどうかが実装によって異なっています。EDG(ICC),GCC,Clangはホワイトスペース列を除去して行継続を行い、結果1を返します(+ 42コメントアウトされる)。MSVCはバックスラッシュの直後に改行が無いことから行継続とは見なさず、結果43を返します。

これはどちらも規格的に正しい振る舞いで、実装定義の範疇です。

ただ、この振る舞いは直感的では無いため、バックスラッシュ後のホワイトスペース列は除去した上で行継続を行うように規定しようとする提案です。つまり、MSVCの振る舞いに修正が必要となります。

このような振る舞いに依存しているコードはバックスラッシュの後の見えない空白を維持し続けているはずですが、それを確実に保証することはできずその有用性も無いため、このような振る舞いをサポートし続ける必要はないだろうという主張です。

他にも次のようなコードで影響があります。

auto str = "\ 
";

MSVC以外ではstrは空文字列で初期化されますが、MSVCは\エスケープシーケンスとして有効ではないためコンパイルエラーとなります。

P2224R0 : A Better bulk_schedule

P2181R0にて提案されているbulk_scheduleのインターフェース設計の改善提案。

現在のbulk_scheduleは次のようなインターフェースになっています。

template<executor E, sender P>
sender auto bulk_schedule(E ex, executor_shape_t<E> shape, P&& prologue);

bulk_scheduleExecutorex上で、入力の処理prologueshapeによって表現される範囲内でバルク実行をし、かつその実行は遅延可能であるものです。その戻り値はそれらバルク実行のそれぞれの処理の開始を表すsenderで、呼び出し側はそこから各バルク実行の処理本体(あるいは後続の処理)を表現するsenderを構築する義務を負います(つまり、そのsenderに処理をチェーンする形でバルク実行の本体を与える)。
この場合、そのようなsenderを作成しバルク実行後、そこにどのように後続の処理をチェーンさせるかは不透明であったため、それを解決するためにbulk_join操作が検討されていました。

この提案では、bulk_scheduleのインターフェースと担う役割を変更しそのような問題の解消を目指します。提案されているインターフェースは次のようになります。

template<scheduler E, invocable F, class... Ts>
sender_of<Ts...> auto bulk_schedule(sender_of<Ts...> auto&& prologue, E ex, executor_shape_t<E> shape, F&& factory);

この戻り値はバルク処理全体を表現するsenderで、検討されていたbulk_joinの戻り値に近いものになります。
最後の引数にあるfactoryは、バルク実行の一つ一つの処理を表現するsenderを構築する責任を担うsender factoryで、次のようなインターフェースを持ちます。

auto factory(sender_of<executor_shape_t<E>, Ts&...>) -> sender_of<void>

sender factoryは個別の処理(prologue)の開始を表すsenderを一つ受け取ります。このsenderは接続されたreceiverに各処理の固有番号(インデックス)と処理の結果(あれば)を渡すものです。そして、このsender factoryはそれら1つ1つの処理の全体を表すsender_of<void>を返します。

このsender factoryへの引数は元の(この提案の対象の)bulk_scheduleが返していたsenderに対応しており、この提案によるbulk_scheduleが返すsenderは元のbulk_scheduleの呼び出し元が構築する義務を負っていた(bulk_joinが返していた)senderに対応しています。

結局、これらの変更によってbulk_scheduleを使った処理のチェーンは次のように変化します。

// 元のbulk_schedule
// N個の処理(prologue)に継続して処理Aをバルク実行し、bulk_joinによって統合後処理B(非バルク実行)を実行する
auto S = bulk_schedule(ex, N, prologue) | ..A.. | bulk_join() | ..B..;

// この提案のbulk_schedule
// 上記と同じことを行うもの
auto S = bulk_schedule(prologue, ex, N, [](auto begin) { return begin | ..A..; }) | ..B..;

ユーザーが負っていた責任の多くをbulk_scheduleが担うようになり、バルク処理のそれぞれに後続の処理をチェーンさせることと、バルク処理の全体に後続の処理をチェーンさせることをより明確に書くことができるようになっています。

多分2週間後くらい

この記事のMarkdownソース

[C++]モジュールにおける所有権モデル

モジュールにおける所有権とは、名前付きモジュールに属しているエンティティの名前に関する所有権の事で、次のようなコードがどう振舞うかを決定するものです。

/// moduleA.ixx
export module moduleA;

export int munge(int a, int b) {
    return a + b;
}
/// moduleB.ixx
export module moduleB;

export int munge(int a, int b) {
    return a - b;
}
/// libA.h

int libA_munge(int a, int b);
/// libA.cpp

#include "libA.h"
import moduleA;

int libA_munge(int a, int b) {
    return munge(a, b);
}
/// main.cpp

#include "libA.h"
import moduleB;

// ここでは、moduleAはインポートされていない

int main() {
  // それぞれ、どちらが呼ばれる?
  int a = munge(1, 2);      // moduleBのものが呼ばれる?
  int b = libA_munge(1, 2); // moduleAのものが呼ばれる?
}

こちらのサンプルコードを改変しています)

C++の意味論としてはこれは当然明白なことで、コメントにある通りに呼ばれるはずです。しかし、このプログラムはコンパイルされリンクされた結果として一つの実行ファイルになります。C++20の名前付きモジュールはそれそのものが1つの翻訳単位をなすので、そこには2つのモジュール(moduleAmoduleB)をコンパイルした2つのオブジェクトファイルが含まれます。
その時、翻訳単位main.cpplibA.cppはそれぞれのコードの意味論から期待されるモジュールからの関数を呼び出すでしょうか?

これはつまりODR違反が起きている状態です。C++17まではこれは未定義動作であり、リンカがオブジェクトファイルをリンクする順番に依存して結果が変わります。そして、モジュールファイルでもこれは未定義動作とされています。

モジュールの所有権とはこの未定義動作をどう実装するか?という問題の解です。そこには、2つの選択肢があります。

なお、同じ場所で2つの異なるモジュールからの同じ宣言が見えている場合は確実にコンパイルエラーとなります、今回のケースはリンカからしかそれが見えていません。

import moduleA;
import moduleB;

int main() {
  int a = munge(1, 2);  // コンパイルエラー!
}

弱い所有権(weak ownership)モデル

弱い所有権(weak ownership)はこの問題を解決しないモデルです。すなわち、C++17以前と同じくこれは未定義動作であり、リンカがリンクする順番に依存します。

/// main.cpp

#include "libA.h"
import moduleB;

// ここでは、moduleAはインポートされていない

int main() {
  // 弱い所有権モデルの下では未定義動作!!
  int a = munge(1, 2);
  int b = libA_munge(1, 2);
}

強い所有権(strong ownership)モデル

強い所有権(strong ownership)はこれを解決するモデルです。すなわち、異なるモジュールからエクスポートされているものは、その宣言が完全に同一であったとしても、リンク時にもモジュール名によって別のものとして扱われます。

/// main.cpp

#include "libA.h"
import moduleB;

// ここでは、moduleAはインポートされていない

int main() {
  // 強い所有権モデルの下では期待通りに呼ばれる
  int a = munge(1, 2);      // -1(moduleBのものが呼ばれる)
  int b = libA_munge(1, 2); // 3(moduleAのものが呼ばれる)
}

モジュールリンケージ

C++20モジュールにおいてはリンケージ区分にもう一つモジュールリンケージが追加されました。モジュールリンケージは名前付きモジュール内で外部リンケージを持ち得る名前のうち、エクスポートされていないものが持つリンケージです。モジュールリンケージをもつ名前は同じモジュール内からしか参照できません。つまりは、内部リンケージを同じモジュール内部の範囲まで拡張したものです。

export module moduleA;

// モジュールリンケージ
int func1(int a, int b) {
  return a + b;
}

// 外部リンケージ
export int func2(int a, int b) {
  return a + b;
}

// 内部リンケージ
static int func3(int a, int b) {
  return a + b;
}

リンケージの観点からは、2つの所有権モデルの差異は外部リンケージを持つ(エクスポートされている)名前をモジュールが所有するかどうか?という問題とみることができます。

弱い所有権モデルでは、内部リンケージとモジュールリンケージを持つ名前だけをモジュールが所有し、エクスポートされている名前をモジュールは所有しません。

強い所有権モデルでは、エクスポートされている名前を含めたモジュールに属するすべての名前がモジュールによって所有されます。

所有権と名前マングリング

この2つの所有権モデルのどちらを選択するか?という問題は、実装から見ると名前マングリングの問題となるでしょう(他の実装もあり得ますが)。

1つのモジュールは複数の翻訳単位から構成されうるので、リンカは少なくともモジュールリンケージを識別する必要があります。そのため、モジュールに所有されている名前のマングル方法にはいくつかの選択肢が生まれます。そして、現在の実装では次の2つが存在しています。

  1. モジュールリンケージを持つ名前のマングル方法を変更して、それだけをリンカが特別扱いする
    • エクスポートされている名前のマングル方法は従来通り(現在の外部リンケージを持つ名前と同じ)
  2. エクスポートされている名前のマングル方法を変更して、リンカがモジュールを認識することでリンケージを処理する
    • モジュールリンケージを持つ名前のマングル方法は従来通り

1つ目の方法では、エクスポートされている名前は既存の外部リンケージを持つ名前と同じマングルとすることができ、それ以外はこれまで通りです。また、モジュールリンケージの処理に関してもモジュールを構成する時だけ考慮したうえで、モジュールと他の翻訳単位をリンクするときは内部リンケージと同様に扱うことができ、リンカにかかる負担はかなり軽くなります。また、リンク時の扱いもこれまでと大きく変わらなさそうです。

2つ目の方法では、まずリンカはオブジェクトファイルがモジュールファイルなのかどうかを認識し、そこにある名前を見てエクスポートされている名前かどうかを識別します。この方法ではモジュール毎に名前を識別することができますが、コンパイラだけでなくリンカにもそれなりの変更が必要です。

そして、1つ目の方法はまさに弱い所有権モデルの実装であり、2つ目の方法は強い所有権モデルの実装です(他にも方法はあるでしょうが、現状はこの2つに1つです)。外部リンケージを持つ名前に対してマングル名で区別をつけないということは、リンク時に別々のモジュール(翻訳単位)からの同じ名前を区別できません。

名前マングリングの観点から見ると、所有権の強弱はモジュールリンケージを持つ名前とエクスポートされている名前のどちらのマングリング方法を変更するのか?という問題とみることもできます。そしておそらく、そのマングル方法とは単にマングル名にモジュール名とモジュールであることを示す文字列が入るだけでしょう。

実装の選択

現在のC++処理系の主要3実装は既にある程度モジュールを実装しています。そこでは、GCC\Clangは弱い所有権モデルを採用しており、MSVCは強い所有権モデルを採用しています。

例えばGCC/Clangで次のようなモジュールコードをコンパイルすると、外部リンケージを持つ名前はこれまで通りのマングルであるのに対して、内部リンケージとモジュールリンケージを持つ名前のマングル名にはモジュール名が含まれるようになっていることが分かります。また、モジュールリンケージと内部リンケージはマングル名レベルでは同じ扱いをされていることも分かります。

export module moduleA;

// モジュールリンケージ
int func1(int a, int b) {
  return a + b;
}

// 外部リンケージ
export int func2(int a, int b) {
  return a + b;
}

// 内部リンケージ
static int func3(int a, int b) {
  return a + b;
}

GCCのモジュール実装に関するWikiによれば、GCCとClangは互いの相互運用性のために連携してモジュールの実装を進めており、その結果として双方共に弱い所有権モデルを採用しているようです。

一方、MSVCは単独ですでにC++モジュールの実装をいったん完了しており、そこでは強い所有権モデルを採用していると明言されています。

この実装間の差異は、おそらくリンカの扱いから来ています。MSVCは1つの社内でリンカとコンパイラを開発しており、ターゲットはWindowsのみで、MSVCコンパイラは自社のリンカ(link.exe)以外を考慮する必要はありません。そしてそれはリンカ側から見ても同様です。そのため、リンカとコンパイラをより密に連携することができ、リンカに対して思い切った変更を加えることができます。

一方GCC/ClangはMSVCに比べるとリンカとコンパイラの開発体制には距離があります。また、それぞれ使われうるリンカは特定の一つではありません。例えば、GNU ld、lld、goldなどいくつかの実装があります。また、ClangとGCCはマングル名に関してお互いに強く互換性を意識しており、片方でコンパイルされたバイナリをもう片方でコンパイルされたバイナリとリンクすることができます(標準ライブラリ周りは別として)。そのため、モジュールの大部分を実装するコンパイラとしてはなるべくリンカに影響を与えないアプローチをとらざるを得なかったものと思われます。
また、既存のABIを大きく変更しないという方針を取ったものでもあるようです。FFIで他言語から呼び出すときにも、モジュール前後で変更が必要ないはずです。

(この辺りのことは完全に私の想像なので正しくないかもしれません)

明確にこの所有権が問題となるコードを書くことはないと思いますが、結果としてこの問題にあたってしまうことはあり得ます。その時に、実装のこの差異はモジュールコードのポータビリティに問題を与えるかもしれません。

なお、GCC/Clangはモジュールを実装中なので、このことは最終的には変化する可能性があります(低いと思わますが)。

参考文献

この記事のMarkdownソース

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

文書の一覧 www.open-std.org

提案文書で採択されたものはありません。全部で21本あります。

N4862 : Business Plan and Convener's Report

C++標準化委員会の全体的な作業の進捗状況や今後の予定などについての報告書。

おそらく、C++を利用している企業などに向けて書かれたものです。

P0288R6 : any_invocable

ムーブのみが可能で、関数呼び出しのconst性やnoexcept性を指定可能なstd::functionであるstd::any_invocableの提案。

std::any_invocableは次の点を除いてほとんどstd::functionです。

  1. ムーブのみ可能
    • std::unique_ptrをキャプチャしたラムダのように、コピー不可能なCallableオブジェクトを受け入れられる
  2. 関数型にconst/参照修飾やnoexceptを指定可能
    • const性を正しく伝播できる
  3. target_type()およびtarget()を持たない
  4. 呼び出しには強い事前条件が設定される
    • これによって、呼び出し時のnullチェックが省略される
#include <any_invocable>  // 専用の新ヘッダ
#include <functional>

struct F {
  bool operator()() {
    return false;
  }
  
  bool operator()() const {
    return true;
  }
};

int main() {
  std::cout << std::boolalpha;
  
  const std::function<bool()> func1{F{}};
  const std::any_invocable<bool()> func2{F{}};
  const std::any_invocable<bool() const> func3{F{}};
  
  std::cout << func1() << '\n';  // false
  std::cout << func2() << '\n';  // false
  std::cout << func3() << '\n';  // true
}

このようにconst性を指定して正しく呼び出しが行えることで、並列処理においてスレッドセーフな呼び出しができるようになります。

他にも、noexceptや参照修飾は次のように指定します。なお、volatile修飾は指定することができません。

#include <any_invocable>

struct F {
  int operator()(int n) const & noexcept {
    return n;
  }
  
  int operator()(int n) && {
    return n + 1;
  }
};

int main() {
  std::any_invocable<int(int) const & noexcept(true)> func1{F{}};
  std::any_invocable<int(int) && noexcept(false)> func2{F{}};

  std::cout << func1(1) << '\n';  // 1
  std::cout << func2(1) << '\n';  // 2
}

これらの修飾や指定は全て省略することもできます。

std::any_invocableでも小さいオブジェクトで動的メモリ確保を避けるように規定されているので、std::functionに比べると若干パフォーマンスが良さそうです。

std::any_invocableはすでにLWGでのレビューに入っていて、C++23に入る可能性が高そうです。

P0881R6 : A Proposal to add stacktrace library

スタックトレースを取得するためのライブラリを追加する提案。

このライブラリの目的はひとえにデバッグをより効率化することにあります。例えば次のような実行時アサーションメッセージを

boost/array.hpp:123: T& boost::array<T, N>::operator[](boost::array<T, N>::size_type): Assertion '(i < N)&&("out of range")' failed. Aborted (core dumped)

次のような出力にできるようにします

Expression 'i < N' is false in function 'T& boost::array<T, N>::operator[](boost::array<T, N>::size_type)': out of range.
Backtrace:
0# boost::assertion_failed_msg(char const, char const, char const, char const, long) at ../example/assert_handler.cpp:39
1# boost::array<int, 5ul>::operator at ../../../boost/array.hpp:124
2# bar(int) at ../example/assert_handler.cpp:17
3# foo(int) at ../example/assert_handler.cpp:25
4# bar(int) at ../example/assert_handler.cpp:17
5# foo(int) at ../example/assert_handler.cpp:25
6# main at ../example/assert_handler.cpp:54
7# 0x00007F991FD69F45 in /lib/x86_64-linux-gnu/libc.so.6
8# 0x0000000000401139

このライブラリはBoost.Stacktraceをベースに設計されています。スタックトレースの一行(すなわちスタックフレーム)はstd::stacktrace_entryというクラスで表現され、std::stacktraceというクラスが一つのスタックトレースを表現します。std::stacktraceはほとんどstd::stacktrace_entrystd::vectorです。

#include <stacktrace> // このヘッダに定義される

void f() {
  // 現在のスタックトレースを取得
  auto st = std::stacktrace::current();

  // スタックトレース全体の出力
  std::cout << st << std::endl;

  // スタックトレースのイテレーション
  for (const auto& entry : st) {
    // 例えばソースファイル名だけを取得
    std::cout << entry.source_file() << std::endl;
  }
}

スタックトレースの取得はプラットフォーム毎にそこでのシステムコールAPI呼び出しにマッピングされます。情報を取り漏らさないためにスタックトレースの取得サイズは可変長としているので動的メモリ確保が伴います。また、取得したスタックトレース情報のデコードはギリギリまで遅延されます。上記でいうと、std::stacktrace_entry::source_file()が呼び出された時、あるいは内部でそれを呼び出すstd::stacktraceの標準出力への出力時に取得した情報が逐次デコードされます。

提案によれば、コンパイラオプションによってABIを変化させずにこれらの関数が何もしないように制御する実装をサポート可能としているようで、これもBoost.Stacktraceから受け継いでいる機能です。

この提案は元々C++20に導入することを目指していたようで(間に合いませんでしたが)、すでにLWGでのレビューが一旦完了しており大きな問題もなさそうなので、C++23に入る可能性が高そうです。

P1787R5 : Declarations and where to find them

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

ほとんど規格用語の新規定義とそれを用いた既存の表現の変更で、主に名前解決に関連したバグが解決されますがユーザーにはほぼ影響はないはずです。

この提案によるコア言語の文言変更は多岐に渡りますが、これによって61(+19)個のCore Issueを解決できるとの事です。

P1875R1 : Transactional Memory Lite Support in C++

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

以前に紹介したP2066R2の元となったもので、提案の動機などが述べられています。P2062はこの提案を規格に反映させるための文言変更点だけをまとめたものです。

onihusube.hatenablog.com

P1949R5 : C++ Identifier Syntax using Unicode Standard Annex 31

識別子(identifier)の構文において、不可視のゼロ幅文字や制御文字の使用を禁止する提案。

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

前回(R3)との変更点は絵文字の問題についての説明が追記されたこととGCC10.1で解決されたUTF-8文字を識別子へ使用できなかったバグについての言及が追記されたことです。

絵文字について

現在C++で絵文字の使用が可能なのは、たまたまC++で許可していたユニコードのコードポイント範囲に絵文字が割り当てられたためで、全てが使用可能であるわけではなく、FFFF未満のコードポイントを持つものなど、一部の絵文字の使用は禁止されています。

例えばそこには女性記号(♀)が含まれています。これは結合文字と共に人を表すタイプの絵文字に作用してその絵文字の性別を変化させます。つまり、現在のC++の仕様では男性の絵文字を使用することは合法ですが、女性の絵文字を使用することはできないのです。

これは意図的なものではなく偶然の産物です。意図的にこのこと及びあらゆる絵文字(例えば、肌の色なども含めて)の使用を許可しようとすると、かなり多くの作業が必要となります。

この提案は識別子で使用可能な文字をUnicodeのUAX31規格を参照して決定するように変更するものであり、UAX31でも絵文字の利用については安定していないのでこの提案では全て禁止とする方向性のようです。

GCCUTF-8文字関連のバグ

GCCは長らくUTF-8文字をソースコード中で使用することについてバグを抱えていたようで、GCC10.1でそれが解決されました。ClangとMSVCではすでに問題なく利用可能だったので、これによってUTF-8文字をソースコード中で使用することはほぼポータブルになります。このことは、この提案をより後押しするものです。

P2013R2 : Freestanding Language: Optional ::operator new

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

以前の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、グローバル::operator newを提供しない場合でもplacment newは利用可能であることを明確にした事と、<new>の宣言やポインタ安全性の文言などに変更はないことを明記した事です。この提案はほぼ規格書に文章を少し追記するだけのものです

EWGでのレビューの結果を次のリビジョンで反映させた上で、CWGに転送される見込みのようです。

P2053R1 : Defensive Checks Versus Input Validation

プログラムの動作の前提条件が満たされないケース2つにカテゴライズし、それらを区別することの重要性を説く報告書。

これは契約プログラミングサポートの議論のために、SG21(Contracts Study Group)のメンバーに向けて書かれたものです。

P2079R1 : Parallel Executor

ハードウェアの提供するコア数(スレッド数)に合わせた固定サイズのスレッドプールを提供するExecutorであるstd::execution::parallel_executorの提案。

C++23を目指して進行中のExecutor提案(P0443R13)では、スレッドプールを提供するExecutorであるstatic_thread_poolが唯一の標準Executorとして提供されています。

しかし、このstatic_thread_poolatach()メンバ関数によってスレッドプールにスレッドを追加することができます。
static_thread_poolがただ一つの標準Executorであることによって、プログラムの様々な場所(例えば外部dll内など)でとありあえずstatic_thread_poolが使われた結果、static_thread_poolはハードウェアが提供するスレッド数を超えて処理を実行してしまう可能性があります。これが起きると実行環境のシステム全体のパフォーマンス低下につながります。

これを防ぐために、ハードウェアの提供するスレッド数固定のスレッドプールを提供するparallel_executorを、標準Executorのもう一つの選択肢として追加しようという提案です。

parallel_executorはデフォルトではstd::thread::hardware_concurrencyに等しい数のスレッドを保持するように構築されます。その後で実行スレッド数の上限を設定することはできますが、ハードウェアの提供するスレッド数を超えて増やすことはできないようです。

P2096R2 : Generalized wording for partial specializations

変数テンプレートの部分特殊化を明確に規定するように文言を変更する提案。

以前の記事を参照 onihusube.hatenablog.com

このリビジョンでは、CWGのレビューを受けて文言を調整したようです。

この提案が導入されると、変数テンプレートはクラステンプレートと同じことができる!という事が規格上で明確になります。C++14からそうだったのですが、あまりそのように認識されていないようで、おそらくそれは規格上で不明瞭だったことから来ているのでしょう・・・。

P2162R1 : Inheriting from std::variant (resolving LWG3052)

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

以前の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、主要な3実装ではこの問題はどうなっているのかが詳細に記述されるようになっています。

それによれば、MSVC STLは完全に対応し、libc++はvalueless_by_exceptionというメンバを上書きしているような型への対応に問題があるもののほぼ対応しており、libstdc++はstd::variantが空になっているかのチェックがstd::variantだけでしか動作しないためGCC9.1から無効化されているようです。

P2187R4 : std::swap_if, std::predictable

より効率的な条件付きswapを行うためのstd::swap_ifと、その使用を制御するstd::predictableの提案。

前回の記事を参照 onihusube.hatenablog.com onihusube.hatenablog.com

前回からの変更は、一部の関数にnoexceptが付加されたことです。

P2192R1 : std::valstat - function return type

関数の戻り値としてエラー報告を行うための包括的な仕組みであるvalstatの提案。 onihusube.hatenablog.com

変更点は文書の文面を変更しただけの様です。

P2197R0 : Formatting for std::complex

std::formatstd::comlexのサポートを追加する提案。

// デフォルト
std::string s1 = std::format("{}", 1.0 + 2i);     // s == "(1+2i)"
// iostream互換
std::string s2 = std::format("{:p}", 1.0 + 2i);   // s == "(1,2)"
// 精度指定
std::string s3 = std::format("{:.2f}", 1.0 + 2i); // s == "1.00+2.00i"

虚数単位にはiが使用され、複素数の数値型Tのフォーマットは再帰的にTのフォーマット指定(std::formatter<T>)に委譲されます。虚数部が0.0の時、虚数部を消して出力するのか0.0を出力するのかはさらなる議論を必要としているようです。

P2205R0 : Executors Review - Polymorphic Executor

Executor提案(P0443R13)のPolymorphic Executor周りのレビューの結果見つかった問題についての報告書。

P0443のPolymorphic Executorには次のものが提案されています。

  • bad_executor
  • any_executor
  • prefer_only

bad_executorは空のExecutorで、execute()は常に例外を投げます。

any_executorは簡単に言えばstd::functionExecutor版で、任意のExecutorを型消去しつつ保持し、実行時に切り替える事が出来るラッパーなExecutorです。

prefer_onlyPolymorphic Executorにテンプレートパラメータでサポートするプロパティを指定する際に、そのプロパティのサポートが必須(require)ではないことを示すのに使用するものです。

この文書はP0443のうちこれらのものをレビューし、見つかった問題点の報告と、修正案を提案するものです。

P2207R0 : Executors review: concepts breakout group report

Executor提案(P0443R13)のコンセプト周りのレビューの結果見つかった問題についての報告書。

P2209R0 : Bulk Schedule

P2181R0にて提案されているbulk_scheduleの設計についての補足となる報告書。

bulk_scheduleは遅延(任意のタイミングで)実行可能なbulk_executeで、bulk_executeはバルク実行を行うexecuteで、executeは任意のExecutorを受けてそのExecutorの指す実行コンテキストで処理を実行するものです。また、バルク実行とはSIMDなどのように複数の処理をまとめて同時実行することです。つまり、bulk_scheduleはバルク処理に対応したExecutor上で、バルク処理を任意のタイミングで実行するためのものです。

using namespace std::execution;

// バルク実行可能なExecutorを取得
bulk_executor auto ex = ...;

// 処理のチェーンを構成する
auto s1 = bulk_schedule(ex, [](){...}, 10); // 並列数10で2番目の引数の処理をex上で実行する
auto s2 = bulk_transform(s1, [](){...});    // バルク実行の結果をバルクに変換する
auto s3 = bulk_join(s2);                    // 各バルク実行の結果を1つに結合する

// この時点では一連の処理は実行されていない

// 一連の処理を実行し、完了を待機(ブロックする)
sync_wait(s3);

executeの遅延実行のためのものはscheduleが対応し、bulk_scheduleはバルク実行という点でscheduleと対称的に設計され、また対称的に使用可能となるようになっています。

// Executorを取得
executor auto ex = ...;

// 処理のチェーンを構成する
auto s1 = schedule(ex, [](){...});  // 2番目の引数の処理をex上で実行する
auto s2 = transform(s1, [](){...}); // 実行結果を変換する
auto s3 = join(s2);                 // 処理を統合する(この場合は意味がない)

// この時点では一連の処理は実行されていない

// 一連の処理を実行し、完了を待機(ブロックする)
sync_wait(s3);

これらの対称性と抽象化を支えているのはsenderと呼ばれる抽象で、senderは未実行の一連の処理を表現するものです(サンプル中のs1, s2, s3)。このsenderを用いる事で、処理のチェーンをどう実行するのかをユーザーやライブラリが定義するのではなく、実行環境となるExecutor自身が定義する事ができ、その実行環境において最適な方法によって安全に処理のDAGを実行する事ができます。

bulk_scheduleによって返されるバルク処理を表現するsenderは、そのバルク処理の完了を通知できる必要があります。すなわち、バルク処理のうち一つが終わった時の通知とは別にバルク処理の全体が終わったことを通知できなければなりません。そうでないと、バルク処理を安全にチェーンする事ができません。バルク処理の実行順は通常不定であるので、このことにもsenderによる抽象が必要です。

そのために、バルク処理を表現するsenderにはset_next操作(これはexecuteなどと同じくカスタマイゼーションポイントオブジェクトです)が追加されます。senderに対するset_nextの呼び出しによって1つのバルク処理全体が完了したことを通知し、それを受けた実行環境はそのsenderにチェーンされている次の処理を開始する事ができます。

そして、そのようなsenderを表現し制約するためのコンセプトとしてmany_receiver_ofコンセプトを追加します。many_receiver_ofコンセプトによって制約される事で、バルク処理ではない普通の処理をバルク処理にチェーンする事(あるいはその逆)は禁止され、コンパイルエラーとなります。

P2210R0 : Superior String Splitting

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

views::splitを使った次のようなコードはコンパイルエラーになります。

std::string s = "1.2.3.4";

auto ints =
    s | std::views::split('.')
      | std::views::transform([](auto v){
          int i = 0;
          std::from_chars(v.data(), v.data() + v.size(), &i);
          return i;
        })

なぜなら、views::splitすると得られるこのvforward_rangeforward iteratorで構成されたrangeオブジェクト)なので、data()size()などというメンバ関数はなく、そのイテレータはポインタですらありません。ついでに言うとそのvcommon_rangebegin()end()イテレータ型が同じrange)でもありません。

文字列に対するviews::splitの結果として直接得られるのは、分割後のそれぞれの文字列のシーケンスとなるrangeオブジェクトです(これを構成するイテレータを外部イテレータと呼びます)。そして、それをデリファレンスして得られるのが分割された1つの文字列を示すrangeオブジェクトで、これが上記のコードのvにあたります(こちらを構成するイテレータは内部イテレータと呼びます)。内部イテレータforward iteratorであり、文字列を直接参照するポインタではありません。

例えば次のように文字列を分割したとき

string s = "abc,12,cdef,3";
auto split = s | std::views::split(",");

生成されるrangeの様子は次のようになります。

https://github.com/onihusube/blog/blob/master/2020/20200918_wg21_paper_202008/view_split.png?raw=true

これを踏まえると、正しくは次のように書かなければなりません。

std::string s = "1.2.3.4";

auto ints =
    s | views::split('.')
      | views::transform([](auto v){
          auto cv = v | views::common;  // イテレータ型と番兵型を合わせる
          return std::stoi(std::string(cv.begin(), cv.end()));
        });
}

主に次の理由により、このような事になっています。

  • views::splitはその処理を可能な限り遅延させる
  • views::splitの後に任意のrangeアルゴリズムをチェーンさせられる
  • views::splitは文字列に限らず任意の範囲について動作する

しかし、文字列の分割という基本的な操作は頻繁に必要になるのに対して、文字列以外の範囲の分割というユースケースは滅多に必要にならず、既存の多くの文字列処理のアルゴリズムstd::from/to_charsstd::regexなど)の多くは少なくとも双方向イテレータbidirectional range)を要求します。

これらのことから、views::splitの現在の仕様は一般化しすぎており、文字列の分割という観点からはとても使いづらいので再設計が必要である、という主張です。

主に次のように変更する事を提案しています。

  • forward rangeまたはより強い範囲をsplitすると、同じ強さの部分範囲が得られる
    • さらに、P1391 Range constructor for std::string_viewが採用されれば、そこから文字列への変換も容易になる
      • std::string_viewC++20ですでにRange constructorが追加されていました・・・
    • input rangeでしかない範囲の分割に関しては現状通り
  • split_viewviews::splitの返すrange)はconst-iterableではなくなる
    • begin()の呼び出しで単にイテレータを返す以上のことをする(しなければならい)ようになる

この変更はC++20でviews::splitを用いた既存のコードを壊すことになりますが、これによって得られる文字列分割での利便性向上という効用は互換性を破壊することによるコストを上回る、と筆者の方は述べています。

この部分は以下の方のご指摘によって構成されています。

P2212R0 : Relax Requirements for time_point::clock

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

std::chrono::time_pointは時間軸上の一点を指定する型で、その時間基準(始点(epoch)と分解能)を指定するために時計型と時間間隔を表す2つのテンプレートパラメータを受け取ります。うち1つ目の時計型ClockにはCpp17Clock要件という要求がなされています。これは、std::chrono::system_clockstd::chrono::steady_clockなどと同じ扱いができることを要求しています。

C++20ではローカル時間を表す型としてstd::chrono::local_tが導入されました。これは、std::chrono::time_zoneとともに用いることで任意のタイムゾーンにおけるローカルの時刻を表現することができるものです。

local_tに対する特殊化std::chrono::time_point<local_t, Duration>も提供されますが、local_tは空のクラスでありCpp17Clock要件を満たしていません。規格ではlocal_tだけを特別扱いする事でtime_pointを特殊化することを許可しています。

このlocal_tのように、時計型ではないが任意の時間を表す型を定義し、その時間表現としてtime_pointを特殊化することは有用なのでできるようにしよう、という提案です。

例えば次のような場合に便利であると述べられています

  • 状態を持つ時計型を扱いたい場合(非staticnow()を提供したい)
  • 異なるtime_pointで1日の時間を表現したい場合
    • 年月日のない24時間のタイムスタンプで表現される時刻を扱う
  • 手を出せないシステムが使用しているtime_pointを再現したい場合
  • 異なるコンピュータ間でタイムスタンプを比較する

この変更がなされたとしても既存のライブラリ機能とそれを使ったコードに影響はなく、local_tが既にあることから実装にも実質的に影響はないだろうとのことです。

P2213R0 : Executors Naming

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

P0443R13のエンティティ名及びP1897R3で提案されているアルゴリズム名のうち一部について、代わりとなりうる名前とその理由が書かれています。

P2215R0 : "Undefined behavior" and the concurrency memory model

out-of-thin-airと呼ばれる問題の文脈における、未定義動作とは何かについての報告書。

out-of-thin-airとは定式化されたメモリモデルから導かれる1つの起こり得る振る舞いのことです。さも虚空から値を読みだしているかのように見えることからこの名前がついているようです。

int main() {
  int r1, r2;
  std::atomic_int x = 0;
  std::atomic_int y = 0;

  std::thread t1{[&]() {
    r1 = x.load(std::memory_order_relaxed);
    y.store(r1, std::memory_order_relaxed);
  }};
  std::thread t2{[&](){
    r2 = y.load(std::memory_order_relaxed);
    x.store(r2, std::memory_order_relaxed);
  }};

  t1.join();
  t2.join();

  // r1 == 1, r2 == 1, x == 1, y == 1
  // となることがありうる(現実的にはほぼ起こらない)

  return 0;
}

このように、コード中どこにも表れていないはずの値が読み出されることが起こりうることが理論的に導かれます。この事がout-of-thin-air(またはThin-air reads)と呼ばれています。

C++ではメモリモデルにこの事を禁止する条項を加える事でout-of-thin-airの発生を禁止しています。しかし、メモリモデルの定式化を変更する事でこれが起きないようにしようという試みが続いているようです(Javaはそうしている)。

この文書はおそらく、そのような定式化のためにout-of-thin-airの文脈における未定義動作という現象をしっかりと定義しようとするものです。

この部分は以下の方のご指摘によって構成されています。

多分2週間後くらい

この記事のMarkdownソース