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

文書の一覧

全部で29本あります。

N4917 Working Draft, Standard for Programming Language C++

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

N4918 Editors’ Report - Programming Languages – C++

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

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

N4919 Programming Languages - C++

C++23のCommittee Draft

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

次期標準ライブラリ機能候補の実装経験を得るためのTSである、Library Fundamental TS v3のドラフト文書。

N4921 Editor’s Report: C++ Extensions for Library Fundamentals, Version 3

↑の変更点を記した文書。

この版での変更は、typoの修正などです。

N4922 INCITS C++/WG21 agenda: 7-12 November 2022, Kona, HI US

11月に行われる予定の、WG21全体会議のアジェンダ

P0543R2 Saturation arithmetic

整数の飽和演算を行うライブラリ機能の提案。

以前の記事を参照

このリビジョンでの変更は、提案する関数を<numeric>に配置するとともにフリースタンディング機能として指定したことです。

P0792R11 function_ref: a non-owning reference to a Callable

Callableを所有しないstd::functionであるstd::function_refの提案。

以前の記事を参照

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

この提案はLWGの時間がなかったため、C++26をターゲットとしています。

P0957R9 Proxy: A Polymorphic Programming Library

静的な多態的プログラミングのためのユーティリティ、"Proxy"の提案。

以前の記事を参照

このリビジョンでの変更は、proxyに適したポインタ型の制約の追加、proxiableコンセプトの修正、proxy::invoke()const修飾を付加した、などです。

P0987R1 polymorphic_allocator instead of type-erasure

std::functionstd::pmr::polymorphic_allocatorによるアロケータサポートを追加する提案。

std::functionのアロケータサポートはそれが実装されなかったことからC++14で削除されており、現在のstd::functionはアロケータのカスタマイズができなくなっています。一方、Library Fundamental TS v3にあるstd::experimental::functionにはアロケータを受け取るコンストラクタとアロケータを型消去して保持するためのユーティリティが存在しています。

この提案は、そのLFTSv3にあるstd::experimental::functionのアロケータサポートを専用の型消去アロケータではなくC++20で導入されたstd::pmr::polymorphic_allocator<>で置き換えるものです。

現在のLFTSv3にあるstd::experimental::functionのコンストラクタは次のようになっています。

template<class F, class A> 
function(allocator_arg_t, const A&, F);

ここのF, Aの2つのパラメータはクラステンプレートには現れていません。ここでは2つの型の型消去が必要となり実装が複雑になります。また、現在のLFTSv3にある型消去アロケータはそれを受け取る他のオブジェクト(ここではstd::experimental::function)の領域内にうまく配置(スペースの節約)することができるようにより複雑になっています。

この提案はこれを次のように置き換えます

template<class F> 
function(allocator_arg_t, const pmr::polymorphic_allocator<>&, F);

また同時に、現在使用しているアロケータを取得するためのインターフェースも追加します。

polymorphic_allocator<> get_allocator() const noexcept;

std::pmr::polymorphic_allocator<>を用いることでアロケータの型消去と複雑な型消去アロケータが不用になり実装が簡単になります。また、この提案ではこのstd::pmr::polymorphic_allocator<>の保持方法を特に指定しないため、単にメンバとして保持するのではなく型消去のための領域に保持するなどの効率化が図れます。

P1030R5 std::filesystem::path_view

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

以前の記事を参照

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

  • ベースとなるワーキングドラフトの更新
  • 機能テストマクロ__cpp_lib_filesystemの値を更新
  • c_strクラスの名前をrendered_pathに変更
  • rendered_pathクラスに(not_)zero_terminated_rendered_path関数を追加
  • ナル終端の指定をテンプレートパラメータに移動した、rendered_path::c_str()を追加
  • rendered_pathクラスのアロケータカスタムのサポートをSTLアロケータに限定
  • path_view::render()を追加
  • 各クラスにhash_value()を追加

などです。

P1985R3 Universal template parameters

任意の型、テンプレートテンプレート...、非型テンプレートパラメータなど、テンプレートの文脈で使用可能なものを統一的に受けることのできるテンプレートパラメータ(Universal template parameter)の提案。

以前の記事を参照

R2での変更は、template autoパラメータに関する共変/反変性に関する懸念と議論の追加、などです。

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

  • 何が検討され、何が提案されているのかが明確になるように提案を書き直し
  • 提案のサンプルを拡充
  • ユニバーサルエイリアスを追加
  • 変数テンプレートテンプレートパラメータを追加
  • コンセプトテンプレートパラメータを追加
  • ユニバーサルテンプレートパラメータ(UTP)を依存名であると指定

ユニバーサルエイリアスとはこの提案によって導入される、テンプレートで使用できるもののエイリアスのことです。template auto identifier = template-argument ;のような構文で定義します。

template<template auto U> struct box {
  template auto result = U; // ユニバーサルエイリアス
};

template<template<template auto> template auto Map,
         template<template auto...> template auto Reduce,
         template auto... Args>
template auto map_reduce_best = Reduce<Map<Args>...>; // ユニバーサルエイリアス

これは型であったり値であったりしますが、コンパイル時の静的なものです。

コンセプト/変数のテンプレートテンプレートパラメータは、既存の型のテンプレートテンプレートパラメータを拡張した構文によって導入されます。

template <auto N> // 非型テンプレートパラメータ
template <template </*...*/> typename>  // 型テンプレートテンプレートパラメータ
template <template </*...*/> auto>      // 変数テンプレートテンプレートパラメータ
template <template </*...*/> concept>   // コンセプトテンプレートテンプレートパラメータ

コンセプトテンプレートテンプレートパラメータによって、コンセプトの制約対象をある型ではなく型のグループのように指定することができるようになります

// あるコンセプトCを満たす要素からなるrangeである
template <typename R, template<typename> concept C>
concept range_of =
  ranges::input_range<R> &&
  C<remove_cvref_t<ranges::range_reference_t<R>>>;


// 整数の範囲のみ受ける
void f(range_of<std::integral> auto&& r);

// ムーブ可能な要素による範囲のみ受ける
void f(range_of<std::movable> auto&& r);

UTPとコンセプトテンプレートパラメータによって、このようなコンセプトは型と型のグループの両方を制約することができるようになります

// プライマリユニバーサルテンプレート
template <typename R, template auto T> 
constexpr bool is_range_of = delete;

// コンセプトによる特殊化
template <typename R, template <typename> concept C> 
constexpr bool is_range_of<R, C> = C<R>;

// 型による特殊化
template <typename R, typename T> 
constexpr bool is_range_of<R,T> = std::is_same_v<R, T>;

// ある型TもしくはコンセプトTを満たす型の要素からなるrangeである
template <typename R, template auto T>
concept range_of = is_range_of<std::remove_cvref_t<std::ranges::range_reference_t<R>>, T>;


// 整数の範囲のみ受ける
void f(range_of<std::integral> auto&& r);

// ムーブ可能な要素による範囲のみ受ける
void f(range_of<std::movable> auto&& r);

// char型の範囲を受け取る(この時の順序は・・・?)
void f(range_of<char> auto&& r);

この3つの機能はもともと別の提案にあったものですが、UTP及び既存テンプレートと一貫性を保った機能として導入するために、この提案に統合されました。

この提案では、UTP構文を統一的にtemplate autoとしていますが、これはプレースホルダであり名前及びキーワードについては議論中です。EWGでの決定次第では、新しいキーワードが導入される可能性もあります。

P2348R3 Whitespaces Wording Revamp

ユニコードの仕様に従う形で、改行と空白を明確に定義する提案。

以前の記事を参照

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

P2495R1 Interfacing stringstreams with string_view

std::stringstreamstd::string_viewを受けとれるようにする提案。

以前の記事を参照

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

  • LWG Issue 2496に合わせて提案内容を調整
    • 互換が無い、もしくはABI破壊を招く代替手段についての記述を削除
    • const Char T*openmode、アロケータを受け取るコンストラクタを削除
  • std::istringstreamのコンストラクタに欠けていた制約を追加
  • よく聞かれる質問とその回答についてのセクションを追加

などです。

これによって、以前のサンプルで許可されるようになっていた文字列リテラルopenmode、アロケータを渡す例は禁止されるようになります。

const std::ios_base::openmode mode;
const std::allocator<char> alloc;
const std::string str;
// mystringはstring_viewに暗黙変換可能だとする
const mystring mstr;

std::stringstream s0{""};                  // ok
std::stringstream s1{"", alloc};           // ng -> ok -> ng
std::stringstream s2{"", mode, alloc};     // ng -> ok -> ng

P2586R0 Standard Secure Networking

Networking TSに代わる、セキュアネなットワークライブラリの提案。

この提案は次のような特徴があります

  • TLSがデフォルト
  • 動的メモリ確保を使用しない
  • 例外を使用しない
  • スレッドローカルストレージを使用しない
  • 暗号化やTLSの詳細について指定しない
    • TLSネットワーク機能をコプロセッサにオフロードするような組み込み環境でも使用可能なように設計されている
    • TLSバックエンドを再コンパイルせずに入れ替えられる

また、この提案は現在のところ、非同期やコルーチン対応を含んでいないため(asioで問題となった)P2300とはあまり関連がありません。

HTTPSでwebページを取得するサンプル

// 接続先のホスト
static constexpr string_view test_host = "github.com";
// HTTPリクエストペイロード
static constexpr string_view get_request = "GET / HTTP/1.0\r\nHost: github.com\r\n\r\n";

// ライブラリが推奨する、現在のプラットフォームのデフォルトTLSソケットソースを取得
// tls_socket_source_registryには異なるプロパティによって様々なTLSソケットソースが登録される
// tls_socket_source_ptrはスマートポインタ型ではあるが、必ずしもヒープ領域のオブジェクトを指していない
// 参照カウントシングルトンであったり、静的なシングルトンであったりして、動的確保を要求しない
// この提案の他の部分でも同様
tls_socket_source_ptr tls_socket_source = tls_socket_source_registry::default_source().instantiate().value();

// TLSソケットソースから、多重化可能な接続ソケットを作成する
// 多重化可能とは、1つのソケットによって異なるスレッドから複数のI/Oを実行可能であることを意味する
// 例えば、Linuxではnon-blocking、WindowsではOVERLAPPED
tls_byte_socket_ptr sock = tls_socket_source->multiplexable_connecting_socket(ip::family::any).value();
{
  // 接続先ホストの443番ポートへ接続する、タイムアウトは5秒
  // デフォルトでは、ローカルシステムの証明書ストアを使用して接続先ホストの証明書を検証する
  // この関数は利便性のためのAPIであり、接続の各ステップは個別のAPIによって順番に実行していくこともできる
  result<void> r = sock->connect(test_host, 443, std::chrono::seconds(5));
  if (r.has_error()) {
    if(r.error() == errc::timed_out
    || r.error() == errc::host_unreachable
    || r.error() == errc::network_unreachable)
    {
      std::cout << "\nNOTE: Failed to connect to " << test_host
                << " within five seconds. Error was: " << r.error().message()
                << std::endl;
      return;
    }
    r.value(); // throw the failure as an exception
  }
}

// ここで出力される文字列は実装定義、カンマ区切りであることを提案する
std::cout << "\nThe socket which connected to " << test_host
          << " negotiated the cipher " << sock->algorithms_description() << std::endl;

// ホストへのHTTP/1.0リクエスト内容を記述するための定数バッファの作成
// tls_socket_handleではこれはstd::span<const byte>
tls_socket_handle::const_buffer_type get_request_buffer(reinterpret_cast<const llfio::byte*>(get_request.data()), 
                                                        get_request.size());

// HTTP/1.0リクエストをホストへ送信する
size_t written = sock->write({get_request_buffer}).value();
// 結果のテスト(事後条件)
TEST_REQUIRE(written == get_request.size());

// リクエスト結果(githubフロントページ)を取得
// HTTP/1.0の動作として、すべてのデータが送信されると接続が閉じられ、read()は0を返す
std::vector<byte> buffer(4096);
size_t offset = 0;

for (size_t nread = 0; (nread = sock->read({{buffer.data() + offset, buffer.size() - offset}}, 
                                           std::chrono::seconds(3)).value()) > 0;)
{
  offset += nread;
  if (buffer.size() - offset < 1024) {
    buffer.resize(buffer.size() + 4096);
  }
}
buffer.resize(offset);

// 取得した結果の出力(最初の1024バイト分)
std::cout << "\nRead from " << test_host << " " << offset
          << " bytes. The first 1024 bytes are:\n\n"
          << std::string_view(reinterpret_cast<const char*>(buffer.data()), buffer.size()).substr(0, 1024) << "\n" << std::endl;

// TLS接続をシャットダウンし、ソケットを閉じる
sock->shutdown_and_close().value();

提案には他にも、多重化されたTLSサーバのサンプルと、サードパーティーのソケット実装をTLSでラップするサンプルが記載されています。

この提案は、昨年の秋ごろにLEWGにおいてC++標準ネットワークライブラリとしてasioの追求を(一旦)停止することを決定した後に、LEWGの数人のメンバから、LEWGの歴史的な懸念事項に応える標準ネットワークの提案を考案するように依頼されて書かれたもののようです。

この提案の内容は著者自身によって参照実装がなされており、そこではLLFIOというライブラリの一部としてGCC/clang/MSVC等C++14以上のコンパイラによってx86/x64/ARM環境で動作することが確かめられているようです。ただし、LLFIOというライブラリそのものは数年の実装・実用経験を持っているものの、この参照実装はこの提案のためにかかれたもので実装経験としては弱いとも注記されています。

P2587R3 to_string or not to_string

std::to_string浮動小数点数出力を修正する提案。

以前の記事を参照

このリビジョンでの変更は、LEWG投票の結果を追記した事です。

P2588R1 Relax std::barrier phase completion step guarantees

std::barrierのバリアフェーズ完了時処理が、同じバリアで同期する任意のスレッドから起動できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、SG1での投票結果を追加したことと実装へのリンクを追加したことです。

SG1での議論と投票の結果では、この提案の方向性で進めることとそれをDRにすることに合意が取れているようです。

P2603R1 member function pointer to function pointer

メンバ関数ポインタから、基底クラスの関数を明示的に呼び出せるようにする提案。

以前の記事を参照

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

  • member_function_pointer_to_free_function_pointerからto_free_function_pointerへ名前を変更
  • to_free_function_pointerconsteval関数とした
  • 関数ポインタのサポート
    • 関数ポインタが渡された場合に何もしない

などです。ただし、この提案はまだ標準への文言を含んでいないため、これらの事は文章による説明に対して適用されています。

P2620R2 Improve the wording for Universal Character Names in identifiers

ユニコード文字名によって指定するユニバーサルキャラクタ名(名前付文字エスケープ)を識別子に使用した時の制限を解除する提案。

以前の記事を参照

このリビジョンでこの提案は、当初の提案を放棄しています。

この提案はユニバーサルキャラクタ名(UCN)を識別子に使用した時の振る舞いを明確化しようとするものでした。しかし、キーワード文字列中にUCNが含まれてしまう場合にその扱いをどうするかの良いモデルが無く、マクロ名やプリプロセッシングディレクティブ、文脈依存キーワードを考慮するとそのようなモデルの構築はより困難になります。それを許可することも拒否することも実装の負担と過度の複雑さにつながるため望ましくなく、当初の目的(識別子としてUCNを使用した際の挙動の明確化)を諦めることになったようです。

ただし、この提案の文言が行っていたUCNの記述の整理の部分についてはそのまま取り入れたいということで意見が一致し、この提案は現在それだけを含んでいます。この内容については、何らかの機能追加や変更をするものではなく、単純に規格書の記述の整理のみです。

P2623R2 implicit constant initialization

一時オブジェクトへの参照によるダングリング発生を削減する提案。

以前の記事を参照

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

  • 「Other Anonymous Things」セクションの追加
    • ラムダ式とコルーチンにおける即時ダングリングについて(この提案によって解決可能)
  • 「Summary」セクションの文書等の調整
  • 「Frequently Asked Questions」セクションの追加
  • 全体的な文書の調整
  • 根本的な欠陥についての説明を追加
    • 一時オブジェクトの生存期間が含む完全式の終わりまで、という規則がそもそも役に立っているのか?について

などです。

C++において言語機能が匿名オブジェクトを生成する場合があります。例えば、ラムダ式とコルーチンです。

// ラムダ式をその場で呼び出し
[&c1 = "hello"s](const std::string& s) // c1はダングリングしない
{
  return c1 + " "s + s;
}("world"s);

// ラムダ式を変数へ保存
auto lambda = [&c1 = "hello"s](const std::string& s) // c1はダングリング
{
  return c1 + " "s + s;
}

// ...
lambda("world"s); // UB
// 引数をconst参照で受けている
// コルーチン本体は構文的な一度の関数呼び出しの後で何度も実行されうる
generator<char> each_char(const std::string& s) {
  for (char ch : s) {
    co_yield ch;
  }
}

int main() {
  for (char ch : each_char("hello world")) {  // std::stringの一時オブジェクトが作成され、ダングリングする
    std::print(ch);
  }
}

この2つの問題はいずれも、この提案の一時オブジェクトの寿命を囲むスコープに紐づける(延長する)ことによって解決されます。

P2631R0 Publish TS Library Fundamentals v3 Now!

Library Fundamental TS v3を正式に発効するよう促す提案。

現在のLibrary Fundamental TSはv2が正式に発効された最後のもので、C++14をベースとして2016年に発行されました。発効後の6年間、LWGはこのv2をベースとして機能の追加や調整などを行っており、それはLFTSv3としてドラフト文書になっています。しかし、それはまだ正式なTSとして発行されていません。

この提案は、LFTSv3のベースをC++20に改訂するとともにLFTS関連の作業をいったん終了させ、C++23 DISよりも前にLFTSv3を正式なTSとして発効することを目指すものです。

LFTSv2からはanyoptionalstring_viewなど多くのものが既にC++標準に導入されている一方で、新しく追加されたものはscope_exitunique_resourceなど少数で、かなり規模としては小さくなっています。

P2636R0 References to ranges should always be viewable

ムーブオンリーなrangeの左辺値参照をviewable_rangeとなるようにする提案。

例えば次のようなコードは、当初のC++20とその実装において全ての種類のforward_rangeに対して有効でした

void foobar(ranges::forward_range auto && r) {
  auto v = r | views::take(3);  // この|でエラーが起こりうる
}

しかし、その後のP2415による変更の後、ムーブオンリーなrangeの左辺値参照の入力に対してコンパイルエラーを起こすようになります。例えばfoobar(std::views::all(std::vector<int>{1, 2, 3, 4}));などとすると観測できます。

<ranges>のパイプライン演算子|)は左辺にくる入力に対してviewable_rangeであることを要求しますが、ムーブオンリーなrangeの左辺値参照はviewable_rangeとならないため(ならなくなったため)にエラーとなります。

より詳しくみてみると

まず左辺値参照からのviewの構築の場合

{
  string_view s{"foobar"};
  auto v = s | views::take(3);  // sはdecay_copyされる
}
{
  string s{"foobar"};
  auto v = s | views::take(3);  // sはref_viewで参照される
}
{
  Generator g{};  // コピー不可なrange
  auto v = g | views::take(3);  // sはref_viewで参照される
}
{
  GeneratorView g{};  // viewとなるGenerator
  auto v = g | views::take(3);  // error
}

ただし、4つ目のコードはP2415以前の仕様では正常にコンパイルされます。

次に左辺値参照から構築されたview|でRangeアダプタと接続する場合

{
  string_view s{"foobar"};
  auto v1 = s | views::take(3);           // sはdecay_copyされる
  auto v2 = v1 | views::transform(/**/);  // v1はdecay_copyされる
}
{
  string s{"foobar"};
  auto v1 = s | views::take(3);           // sはref_viewで参照される
  auto v2 = v1 | views::transform(/**/);  // v1はdecay_copyされる
}
{
  Generator g{};  // コピー不可なrange
  auto v1 = g | views::take(3);           // sはref_viewで参照される
  auto v2 = v1 | views::transform(/**/);  // v1はdecay_copyされる
}
{
  GeneratorView g{};  // viewとなるGenerator
  auto v1 = g | views::take(3);           // error
  auto v2 = v1 | views::transform(/**/);
}

エラーの理由は1つ前と同様です。

次に、右辺値rangeからのviewの構築の場合

{
  auto v = string_view{"foobar"} | views::take(3);  // string_viewはdecay_copyされる
}
{
  auto v = string{"foobar"} | views::take(3); // stringはowning_viewに保存される
}
{
  auto v = Generator{} | views::take(3);  // Generatorはowning_viewに保存される
}
{
  auto v = GeneratorView{} | views::take(3);  // GeneratorViewはdecay_copy(ムーブ)される
}

そして、右辺値rangeから構築されたview|でRangeアダプタと接続する場合

{
  auto v1 = string_view{"foobar"} | views::take(3); // string_viewはdecay_copyされる
  auto v2 = v1 | views::transform(/**/);            // v1はdecay_copyされる
}
{
  auto v1 = string{"foobar"} | views::take(3); // stringはowning_viewに保存される
  auto v2 = v1 | views::transform(/**/);       // error
}
{
  auto v1 = Generator{} | views::take(3);  // Generatorはowning_viewに保存される
  auto v2 = v1 | views::transform(/**/);   // error
}
{
  auto v1 = GeneratorView{} | views::take(3);  // GeneratorViewはdecay_copy(ムーブ)される
  auto v2 = v1 | views::transform(/**/);       // error
}

ここの2~4番目のv2はムーブオンリーなviewです。

これらの4パターンのコード例中でエラーとなっているのは、ムーブオンリーな左辺値viewをRangeアダプタに入力しようとしているためです。この提案は、これらのエラーを修正しようとするものです。

この提案による修正は、左辺値ムーブオンリーrangeviewable_rangeとなるようにviewable_rangeの定義を修正するとともに、views::allが左辺値ムーブオンリーrangeに対してはref_viewを生成するようにします。

namespace std::ranges {
  template<class T>
  concept viewable_­range =
    range<T> &&
    ((view<remove_cvref_t<T>> && constructible_­from<remove_cvref_t<T>, T>) ||
    //(!view<remove_cvref_t<T>> && (is_lvalue_reference_v<T> || (movable<remove_reference_t<T>> && !is-initializer-list<T>))));
    is_lvalue_reference_v<T> || (movable<remove_reference_t<T>> && !is-initializer-list<T>));
}

P2637R0 Member visit and apply

std::visitstd::applyなどをメンバ関数として追加する提案。

std::visitは1つのCallableと1つ以上のstd::variantオブジェクトをとって、std::variantオブジェクトが実際に保持している型に応じたディスパッチを行う可変長関数テンプレートです。しかし、std::visitの多くのユースケースは1つのstd::variantによる単項のディスパッチです。その場合、メンバ関数として実装してあったほうが使いやすいように思えます。

std::visitが非メンバ関数として定義されている理由の一つは、constと値カテゴリの適切な転送を行うための最適な方法だったことがあります。これをメンバ関数で行おうとすると、const有無と値カテゴリによって4つのオーバーロードが必要となってしまい、実装が複雑化します。

ところが、C++23ではまさにこの問題を解決する機能であるDeducing thisP0847R7)が導入されたため、そのようなハンドリングを1つのメンバ関数のみで行うことができるようになっています。

この提案は、Deducing thisを利用してstd::visit等の非メンバ関数テンプレートをメンバ関数として追加しようとするものです。

std::visitと同じ理由から非メンバ関数として定義されているものにstd::applyがあり、std::visitのインターフェースに合わせるためだけに非メンバ関数として定義されているものにstd::visit_format_argがあります。この提案ではこれらのものも対象にしています。この提案で追加するメンバ関数と追加対象は次のものです

  • .visit()
    • std::variant
    • std::basic_format_arg
  • .apply()
    • std::pair
    • std::tuple
    • std::array
    • std::ranges::subrange

例えばstd::variant::visit()は次のように簡単に実装可能です

namespace std {
  template <class... Types>
  class variant {
  public:
    ...

    // Deducing thisによるメンバvisit()の実装例
    template <class Self, class Visitor>
      requires convertible_to<add_pointer_t<Self>, variant const*>
    constexpr auto visit(this Self&& self, Visitor&& vis) -> decltype(auto) {
      return std::visit(std::forward<Visitor>(vis), std::forward<Self>(self));
    }
  };
}

P2638R0 Intel's response to P1915R0 for std::simd parallelism in TS 2

Parallelism TS v2にある、std::simdに対するintelのフィードバック文書。

std::simd<T, ABI>はクラステンプレートであり、各種CPUの持つSIMD命令に対する抽象化レイヤとして機能するデータ並列処理に特化したクラスです。

std::simdに対しては[]+ - * /などの各種演算子オーバーロードされており、std::simdオブジェクトに対する演算をコンパイル時にプラットフォームのSIMD命令に変換することを意図しています。std::valarrayと異なるのはおそらく、第二テンプレート引数でABI(使用するCPU命令種別等)に対する指定を行えることで、固定長配列やCPUに応じた可変長(コンパイル時定数)配列の指定や、ABI境界での安全性とパフォーマンスのバランスを制御することができるようになっています。

using std::experimental::native_simd;
using Vec3D = std::array<native_simd<float>, 3>;

// 3要素ベクトルの内積
native_simd<float> scalar_product(Vec3D a, Vec3D b) {
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}

このコードはプラットフォームのCPUに応じたSIMD命令によって並列に計算され、要素数を変更しようとした時でも用意に行えます。

例えばこのコードをSSEを用いて書くと次のようになります

using Vec3D = std::array<__m128, 3>;

// 3要素ベクトルの内積
__m128 scalar_product(Vec3D a, Vec3D b) {
  return _mm_add_ps(_mm_add_ps(_mm_mul_ps(a[0], b[0]), _mm_mul_ps(a[1], b[1])),
                    _mm_mul_ps(a[2], b[2]));
}

これはCPU命令と一対一対応した関数を用いており、要素数の変更が難しく、異なるISAへの移植性がありません。

std::simdはフィードバックと実装経験を得るためにParallelism TS v2に配置されており、GCC特化の実装経験があります。

P1915R0はそこでの経験と議論の過程から得られたいくつかの設計上の疑問点や選択肢について記載するとともにそのフィードバックを募るものでした。この文書は、それに対するintelによる回答です。

P2639R0 Static Allocations

定数初期化されるstatic変数において、コンパイル時動的メモリ確保を行えるようにする提案。

C++20から、定数式で動的メモリ確保が行えるようになりましたが、そのメモリ領域を実行時へ持ち越すにはいくつかの問題があったためそれはとりあえず禁止とされ、コンパイル時に動的に確保されたメモリ領域はコンパイル時に解放されなければなりません。また、それが許可されたとしてもそのメモリ領域は不変でなければならず(実行時には静的ストレージに配置される)、constexpr std::vectorオブジェクトに対して実行時に要素の挿入・削除や変更は行えないものになる予定です。それをしようとすると実行時オブジェクトへコピーする必要があるでしょう。

一方、実行時に静的ストレージに配置される変数で、初期化が定数式で行われる場合があるものがあります。それは、初期化式が定数式で実行可能なstatic/thread_local変数で、例えば定数で初期化されているグローバルの整数型変数などが該当します。この場合に行われる初期化のことを(静的変数に対する)定数初期化と呼びます。

この提案は、定数初期化が実行される場合にのみコンパイル時に確保されたメモリ領域を実行時に持ち越すことを可能とするとともに、それを実行時に変更することも許可しようとするものです。

// コンパイル時に実行可能とする
constexpr auto compute_my_vector() -> std::vector<int>;

constinit std::vector<int> my_vector = compute_my_vector(); // 現在、エラー
                                                            // この提案後、定数初期化される

int main() {
  my_vector.insert(10); // この提案後、ok
  my_vector.pop_back(); // この提案後、ok
}

my_vector.insert(10)では、追加のメモリが必要になった場合は通常の実行時の動的確保が行われ、静的に確保された領域は解放されます。この場合の解放とは実際には何もせず、その領域が実際に解放されるのはプログラム終了時になるでしょう。

このように、静的ストレージで確保されているメモリをdeleteする際に問題となるのは、ユーザーによってoperator deleteオーバーロードされうることです。静的ストレージでdeleteされた領域へのアドレスがユーザー定義operator deleteに渡されると未定義動作となります。

この提案では、それを検出するためにstd::is_static_allocation()という関数を提案しています。

namespace std {
  bool is_static_allocation(void* address);
}

これはaddressの領域が静的に確保されたものであるのかを判別するものです。ユーザー定義operator deleteではこれを用いて本当にメモリの解放を行うかを判断することを意図しています。とはいえこれは既存のユーザー定義operator deleteコードに変更を強いるものです。

既存のコードへの影響を抑えるために、確保領域前後にメタデータを仕込むためのツールであるstd::static_allocation_wrapperと、静的に確保された領域についての情報を取得するためのstd::static_allocation_infoを提案しています。

例えば次のようなユーザー定義operator deleteがある時

void operator delete(void* ptr) throw() {
  AllocationManager* manager = static_cast<AllocationManager**>(ptr)[-1];
  manager->deallocate(ptr);
}

std::is_static_allocation()は次のように使用します

void operator delete(void* ptr) throw() {
  if (std::is_static_allocation(ptr))
    return;
  AllocationManager* manager = static_cast<AllocationManager**>(ptr)[-1];
  manager->deallocate(ptr);
}

std::static_allocation_wrapper等は次のように使用します

// static_allocation_wrapperはユーザーが定義する
template<std::size_t size, std::size_t alignment, bool array>
struct std::static_allocation_wrapper {
  // there needs to be padding here for alignment > sizeof(void*)
  static_assert(alignment <= sizeof(void*));

  AllocationManager* manager = nullptr;
  
  // this needs to be initialized, otherwise default construction
  // will not result in a constant expression
  std::byte storage alignas(alignment) [size] = {};

  const void* construct_at() const { return storage; }
};

// プログラム開始時に呼ばれるものとする
void setup_static_allocation_manager(AllocationManager* manager) {
  // std::static_allocationsは確保された静的領域全ての情報をもつ、static_allocation_infoのrange
  for (const auto& alloc_info : std::static_allocations) {
    // ユーザー定義のstatic_allocation_wrapperで領域がラップされているかを取得
    if (!alloc_info.user_wrapped()) {
      // generate error here
      continue;
    }

    // ユーザーのアロケータ(AllocationManager)をセットする
    static_cast<AllocationManager**>(alloc_info.wrapper_begin())[0] = manager;
  }
}

// 変更は必要ない
void operator delete(void* ptr) throw() {
  AllocationManager* manager = static_cast<AllocationManager**>(ptr)[-1];
  manager->deallocate(ptr);
}

P2640R0 Modules: Inner-scope Namespace Entities: Exported or Not?

モジュール内で名前空間スコープに直接宣言を持たずに導入されるものについて、そのリンケージをどうするかを規定しようとする提案。

例えばクラス内で宣言されたfriend関数のように、名前空間スコープのエンティティでありながら名前空間スコープに直接宣言が現れないものがあります。モジュール内で、そのようなエンティティが名前空間スコープで宣言されている場合、外部リンケージもしくはモジュールリンケージを持つとされています。一方で、friend関数はモジュール本文内にはありますが名前空間スコープに宣言がなく、直接的にはexportできないため、規定に厳密に照らすとモジュールリンケージを持つことになります。

// Friend.cpp
export module Friend;

export class X {
  friend int Frob (X *); // このリンケージは?
};

// User1.cpp
import Friend;
int V = Frob ((X *)nullptr); // 呼び出せるはず?

friend関数の役割や意味を考えると、この場合に適切なのは外部リンケージを与えることであるように思えます。

この規格の矛盾についてIssueが開かれて(DR2588)おり、クラス内friend関数はそれが定義である場合にのみ属する(囲む)クラスのリンケージに従う、のような解決に向かおうとしています。

しかしその場合でも、クラスは定義だけをエクスポートしないことができるので、問題は完全に解決していません。

// モジュールのインターフェース
export module Friend;

// 前方宣言
export class X;

// モジュールの実装単位
module Friend;

// 定義、エクスポートされていない
class X {
  friend int Frob (X *); // このリンケージは?
};

さらには、同様の問題を持つものがこのクラス定義内friend関数以外にも見つかったため、この問題の影響範囲は大きくなってしまいました。すなわち、宣言が再宣言もしくは後の宣言で定義される状況で、その宣言が新しい名前空間スコープのエンティティを導入する場合、そのエンティティのリンケージはどのように決定されるべきなのか?という問題に一般化されます。

この提案は、そのような事例について紹介するとともに、その解決を図るものです。

エンティティのリンケージはコード生成(シンボル名生成、名前マングリング)において重要となるため、少なくともその時点までにエンティティのリンケージは判明している必要があり、モジュールリンケージと外部リンケージではシンボル名(マングリング名)が異なる可能性があるためリンケージ種別の決定は重要な問題となります。

上記クラス定義内friend関数の他には例えば、クラスのメンバ型

export module Struct;

export struct Y {
  struct Z1 *p; // このリンケージは?
};

void Toto (Z1 *) {}; // Totoのシンボル名にはY::Z1が含まれるため、リンケージが判明している必要がある

関数引数

export module FnParm;

export void FnParm (struct Z2 *); // #1 Z2のリンケージは?

void Toto (Z2 *) {}; // Cでは、ここのZ2は#1のものとは異なる型

関数のデフォルト引数

export module DfltFnArg;

export void Fn (void * = (struct Z3 *)0); // Z3のリンケージは?
void Corge (Z3 *) {}

このようなことは、以前にexportされた宣言の再宣言において起こるかもしれません。

export module DfltMemArg;

export struct S2 {
  void Fn (void *);
};
void S::Fn (void * = (struct Z4 *)0) {} // Z4のリンケージは?
void Beans (Z4 *) {}

namespace B {
  export void Fn (void *);
}
void B::Fn (void * = (struct Z5)0); // Z5のリンケージは?
void Beans2 (Z5 *) {};

非型テンプレートパラメータ(NTTP)

export module NTTP;

export template<struct NTTP *> class T1;  // NTTPのリンケージは?
void TUse1 (NTTP *) {}

スコープなしenum

通常スコープなしenumは定義を分割(再宣言)できませんが、基底型を指定することで宣言と定義を分けることができます。

export module E;

export enum E2 : int;
enum E2 : int { B };  // ok、Bのリンケージは?

この他にも、非型テンプレートパラメータのデフォルト引数、変数の初期化式、using宣言、decltype、言語リンケージによる同様の例と、それによる名前探索への影響についてが文書には記載されています。

この提案はこれらの解決を明確に提示してはいませんが、モジュール本文内においてはこれらの宣言を全て禁止(ill-formedと)するか、それができない場合は(上記のようなエンティティが属する)宣言に字句的にexportが存在するかどうかによってそのリンケージを決定する(つまり、再宣言においてこのようなエンティティが現れても、その宣言がexportを伴っていなければモジュールリンケージとする)、のどちらかを提案しているようです。また、この解決はC++20に対するDRとすることを提案しています。

P2641R0 Checking if a union alternative is active

定数式において、unionのどのメンバがアクティブメンバかを調べるためのstd::is_active_member()の提案。

sizeof(Optional<bool>) == 1となるように最適化された型を作成したいとします。boolは2パターンの値しか取りませんが、1バイトのサイズを持つので、残りの7ビット分を使用することでこのような型は作成可能です。例えば次の2つの実装が考えられます。

1つはunionを用いたもの

struct OptBool {
  union { bool b; char c; };

  OptBool() : c(2) { }
  OptBool(bool b) : b(b) { }

  auto has_value() const -> bool {
    return c != 2;
  }

  auto operator*() -> bool& {
    return b;
  }
};

もう一つは再解釈を用いたもの

struct OptBool {
  char c;

  OptBool() : c(2) { }
  OptBool(bool b) { new (&c) bool(b); }

  auto has_value() const -> bool {
    return c != 2;
  }

  auto operator*() -> bool& {
    return (bool&)c;  // *reinterpret_cast<bool*>(&c)
  }
};

これはどちらも未定義動作を含みません。operator*の読み取りでは事前条件が満たされている限り実際にはboolのオブジェクトの値を読み取っており、.has_value()においてはchar型の左辺値(式)からはあらゆる型のオブジェクトのバイト表現を読みだすことが許可されているため、このような再解釈は常に合法となります。

ただ、これをconstexpr対応させようとするといくつかの問題があります。

1つ目の実装の.has_value()では、unionの非アクティブメンバ読み取りが定数式では常に許可されないためエラーとなります。

2つ目の実装では、まずコンストラクタのplacment newが問題となり、これは定数式で実行できません。このためにC++20でstd::construct_atが追加されたのですが、この引数はboolのオブジェクトを構築したい場合bool*を渡さなければなりません。
次に、operator*においては再解釈キャストが定数式で実行できません。

2つ目の実装の再解釈を定数式で許可するには標準の規定もそうですが実装のコストが高くなります。現在のC++コンパイラは定数式においてある領域にあるオブジェクトの型を追跡していますが、現在はそれは常に単一の型になっているところを複数の型を許可するようにしなければならないためです。

逆に、1つ目のunionによる実装の問題点を解決することは簡単にできます。なぜなら、現在のC++コンパイラは定数式におけるunionオブジェクトのアクティブメンバを常に追跡しているためです。その際重要なのは、今回のようなケースにおいては非アクティブメンバを読みだす必要はなく、どちらのメンバがアクティブメンバかどうかを知るだけで良いということです。前述のように、定数式を実行中のコンパイラはそれを知っているため、コンパイラに問い合わせるだけでこれを知ることができるはずです。

struct OptBool {
  union { bool b; char c; };

  constexpr OptBool() : c(2) { }
  constexpr OptBool(bool b) : b(b) { }

  constexpr auto has_value() const -> bool {
    if consteval {
      return std::is_active_member(&b); // 定数式ではbがアクティブメンバであるかを問い合わせる
    } else {
      return c != 2;  // 実行時は今まで通り
    }
  }

  constexpr auto operator*() -> bool& {
    return b;
  }
};

このstd::is_active_member()のような関数を追加するだけでこれは達成でき(コア言語の変更を必要としない)、これは実装の負担もかなり小さいはずです。この提案は、このstd::is_active_member()を標準に追加しようとするものです。

std::is_active_member()は引数にunionメンバへのポインタを取り、そのメンバがアクティブメンバである場合にtrueを返す関数です。

namespace std {
  template<class T>
  consteval bool is_active_member(T*);
}

コンパイラunionのアクティブメンバを追跡しているのはコンパイル時のみであり、当然実行時にそれを行っている主体はいない(あるいはそれを強制できない)ため、この関数はコンパイル時にしか実行できないようにconsteval関数となっています。

P2642R0 Padded mdspan layouts

std::mdspanpadding strideをサポートするためのレイアウト指定クラスを追加する提案。

strideとは、配列における単位要素のサイズ(ある要素の先頭から次の要素の先頭までの長さ、通常バイト単位)のことを言います。strideは次元ごとに指定できて、例えば2次元行列だと単位要素は要素列あるいは行となり、1次元のstrideは1要素ごとのサイズですが、2次元のstrideはある行から次の行(あるいは列)までの長さになります(そして、3次元行列の3次元の単位要素は2次元行列になります)。strideは、1要素中の有効なデータのサイズと要素のサイズが異なる場合、すなわち1要素に無効な部分(パディング)が含まれている場合に配列要素を適切に引き当てるために使用されます。それは例えば、SIMD命令を使用する際にそのレジスタ幅に合わせるために無効な要素を追加したり、24bit画像において1ピクセルを32bitのデータとして扱うために8bit分を無視して扱うなど、主にメモリレイアウトの調整による処理の高速化のためにパディングが挿入されます。

多次元配列のメモリレイアウトにそのようなパディングが含まれる場合、strideを考慮したインデックス->アドレスの変換が必要となります。std::mdspanではそのためにstd::layout_strideというクラスによってメモリレイアウトをカスタムすることができ、std::layout_strideは次元ごとにstrideの値を保持しておいてインデックス->アドレスの変換に使用します。

しかし、そのようなパディングが挿入されるメモリレイアウトにおいては、必ずしも全ての次元でstrideの考慮が必要にならない場合があります。例えば、SIMD命令のレジスタ幅に合わせて配置したデータがパディングを含む場合、通常パディングは1度のSIMDで実行するデータ列の最後にだけ挿入されます。また、BLAS/LAPACKの行列を扱う関数においてはある行列の1部を部分行列として参照する際のアクセスのためにLeading dimensionと呼ばれる値を指定します。これは部分行列に対する親行列の行/列の要素数で、これはまさにstrideであり、この場合のパディングは行/列ごとにだけ挿入されています。

この提案のいうpadding strideとは、このように多次元配列の一部の次元だけがパディングを持つようなレイアウトにおけるstrideのことを言っています。

strideの考慮が必要なレイアウトはstd::layout_strideを用いてstd::mdspanで適切にハンドルでき、std::layout_strideは次元ごとにstrideを指定できるためpadding strideにも対応可能です。しかし、padding strideの場合は何処か1つの次元だけがstrideを持つ、あるいは何処か1つ以上の次元はstrideの考慮が必要ないことがわかっており、std::layout_strideでは非効率である可能性があります。

この提案は、std::mdspan及びstd::submdspanでこのpadding strideを効率的にサポートするために、新しいレイアウト指定クラスlayout_left_paddedlayout_right_paddedを追加するものです。名前にあるleft/rightとはleftが列優先、rightが行優先のレイアウトであることを表しています(これはstd::layout_left, std::layout_rightと同様の意味)。

layout_left_paddedlayout_right_paddedstd::layout_strideに対して次の2つの利点があり、これによる最適化が可能となります

  1. コンパイル時に少なくとも何処か1次元のデータはstrideが1(メモリ上で連続)であることを保証する
  2. コンパイル時にpadding strideの値がわかっていれば、それをメンバとして保持する必要がない
    • 実行時の値であっても1つのstrideの値だけを保持すればいい(std::layout_strideは全ての次元のstrideを保持する)

前述のように、この2つのクラスはBLSA/LAPACKのようなライブラリ(P1673)の入出力や、SIMDレジスタに渡すためにオーバーアラインされた配列をstd::mdspanで取り扱うのに使用可能です。

int main() {
  // 1次元配列に配置された、4x4 2次元行列
  int storage[] = { 1,  2,  3,  4,
                    5,  6,  7,  8,
                    9, 10, 11, 12,
                   13, 14, 15, 16};

  // 4x4 2次元行列を参照するmdspan
  std::mdspan<int, std::extents<std::size_t, 4, 4>, std::layout_right> mat44(storage);

  for (auto i : std::views::iota(0, 4)) {
    for (auto j : std::views::iota(0, 4)) {
      std::cout << mat44[i, j] << ", ";
    }
    std::cout << "\n";
  }

  // 2x2 2次元部分行列を参照するmdspanを作る
  // | 7,  8|
  // |11, 12|
  {
    // layout_strideを使用
    using mapping = layout_stride::mapping<extents<std::size_t, 2, 2>>;
    std::mdspan<int, std::extents<std::size_t, 2, 2>, std::layout_stride> submat22(&mat44[1, 2], mapping{{}, std::array{4, 1}});
    //                                                                                                       ^^^^^^^^^^^^^^^^
    //                                                                                                       次元ごとのstrideの指定
  }
  {
    // layout_right_paddedを使用
    std::mdspan<int, std::extents<std::size_t, 2, 2>, std::layout_right_padded<4>> submat22(&mat44[1, 2]);
    //                                                                         ^
    //                                                                         2次元目のstrideを指定
  }
}

この例は使用感しか示していませんが、layout_left_padded, layout_right_paddedを使用することでこのような場合のレイアウト指定を簡略化できるとともに、strideの指定が完全に定数となっていることがわかります。

この部分の10割は、以下の方のご協力によって成り立っています

P2643R0 Improving C++ concurrency features

C++20で追加された動機プリミティブ周りの機能を強化・改善する提案。

C++20では多くの並行処理のためのライブラリ機能(特に、同期プリミティブ)が追加されました

  • std::atomic.wait(), .notify_one(), .notify_all()
  • std::atomic<>std::atomic_flag
  • <semaphore>
  • <latch>, <barrier>

これらのものは多くの実装経験と長期の使用経験がありましたが、それでもいくつかのフィードバックがあり、改善の余地があるようです。

この提案は、これらの並行処理のためのライブラリ機能に対する実装者/ユーザーからのフィードバックを取りまとめて、その改善を促す提案です。

この提案の概要は次のようなものです

  1. std::atomic::wait()の時間を指定して待機するオーバーロードの追加する
    • std::atomicを用いて実装される動機プリミティブの実装を容易にするため
    • 他の同期プリミティブは時限待機関数を提供することが多く、その内部実装にstd::atomicが使用されることが多い
  2. 値を返すstd::atomic::wait()
    • std::atomic::wait()による待機が解除した後、多くの場合その値を読みに行く
    • しかし、std::atomic::wait()は起床のために値を読み込んでいるはずで、これを返すようにしたほうが効率的
  3. 次のどちらかによって、std::atomic::wait()spurious pollingを回避する
    1. . std::atomic::wait()には値の代わりに述語を渡す
      • アトミック値の等しさ以外の条件で待機している場合、std::atomic::wait()には囲むループがある
      • std::atomic::wait()が個別に何度も呼ばれると、実装は呼び出しの度に既に待機に時間を費やしていることを忘れてしまう(知ることができない)
      • これは待機戦略の決定に影響し、待機が非効率となりうる
        • デフォルトでは、短期の待機戦略はしばらくの間std::atomicオブジェクトに対してポーリング(値の問い合わせ)を行うこと(処理のブロッキングを避けるため)
    2. . 内部実装を制御するためのヒントとなる引数を渡す
      • std::atomic::wait()の待機について事前の過程がある場合にそれを実装に伝達する
      • 例えば、待機開始してからしばらくの間に起床イベントが発生しないことがわかっている場合、長期の待機戦略をとることができるなど
  4. std::barrierstd::latch.wait()に時間を指定して待機するオーバーロードを追加する
    • 他の同期プリミティブは時限待機関数を提供しているため、ここでも提供しない理由はない

なお、順番は優先度を表しています。

また、これらのうちのいくつかは、既存の標準ライブラリ実装において内部的に実装済みであるようです。

この提案では、1と2に対する変更のための文言を既に用意していて、1のために.try_wait(), .try_wait_for(), .try_wait_until()関数を追加し、2のためにstd::atomic<T>::wait()の戻り値型をvoidからTに変更しています(ただし、これはABI破壊を伴います)。.try_wait(), .try_wait_for(), .try_wait_until()の関数はstd::atomic<T>に対してstd::optional<T>を返すことで時間切れで戻ったのか起床されたのかを判別できるようにしています。

おわり

この記事のMarkdownソース