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

文書の一覧

全部で52本あります(SG22のWG14からのものは除きます)。

P0447R18 Introduction of std::hive to the standard library

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

以前の記事を参照

このリビジョンでの変更は、<=>を追加したこと、標準ライブラリ内外のコンテナ選択ガイドをapendixに追加したこと、提案する文言の改善、などです。

P0792R6 function_ref: a non-owning reference to a Callable

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

C++において高階関数(関数を受け取る/返す関数)を書くときに、既存の関数(Callable)を参照したいことがよくあります。しかし、現在の標準ライブラリにはそのために使用できる効果的なものがありません。

現在使用可能な手段には以下の3つがあります

  • 関数ポインタ
    • 参照するものがステートレスである場合にのみ使用可能。また、メンバ関数ポインタやメンバ変数ポインタを処理しなければならない
  • std::function
    • 格納するCallableを所有するため、意図しないオーバーヘッドが発生しうる
    • 所有権が必要ない場合には不適格
  • テンプレート
    • 特定のシグネチャに制約するのが難しく、ヘッダにコードを書かなければならない

特定のCallablelを単に参照したいだけの場合に最適なものが現在のC++には欠けており、std::function_refはそのギャップを埋めるためのユーティリティです。std::function_refはその名前の通り受け取ったCallableを所有せず参照し、なおかつstd::functionとほぼ同等のインターフェースと型消去を提供します。

std::function_refは特に、関数の引数で任意のCallablelを受け取りたい場合に有用です。例えば次のような関数を書くとき

struct payload { /* ... */ };

// times回までactionを繰り返す
// actionが有効なpayloadを返したときはそこで終了
// それ以外の場合はnulloptを返す
std::optional<payload> retry(std::size_t times, /* ????? */ action);

この/* ????? */の部分には前述のいくつかの書き方があります

// 関数ポインタ
// 利点 : 実装が簡単で追加の制約が必要なく、オーバーヘッドも最小(コンパイラは呼び出しをインライン化できるはず)
// 欠点 : ステートフルなCallableを扱えない
std::optional<payload> retry(std::size_t times,
                             std::optional<payload>(*action)())
{
    /* ... */
}

// std::function
// 利点 : ステートフルも含めて、指定したシグネチャに合う任意のCallableを受け取れる
// 欠点 : 所有権が不明瞭(reference_wrapperを使用している可能性がある)、大きなオーバーヘッドを発生させうる(動的割り当て、分岐、例外、インライン化困難)
std::optional<payload> retry(std::size_t times,
                             std::function<std::optional<payload>()> action)
{
    /* ... */
}

// テンプレート
// 利点 : ステートフルも含めて任意のCallableを受け取れる、オーバーヘッドがない
// 欠点 : 実装しづらく読みづらい(enable_ifやis_invocableによる制約周り)、ヘッダに書かざるを得ない
template <typename F>
auto retry(std::size_t times, F&& action)
    -> std::enable_if_t<std::is_invocable_r_v<std::optional<payload>, F&&>,
                        std::optional<payload>>
{
    /* ... */
}

std::function_refはこれらのいいとこ取りをするもので、次のように使用できます

// function_ref
template <typename F>
auto retry(std::size_t times, std::function_ref<std::optional<payload>()> action)
{
    /* ... */
}

使用感はstd::fucntionと同様で、std::fucntionのメリットをそのまま享受でき、所有権は明確(所有していない)です。オーバーヘッドに関しては関数ポインタとほぼ同等であり、コンパイラによるインライン展開を期待できます。また、std::function_refは空の状態がないため呼び出し時のnullチェックが不要で、target_typeなどのRTTIが必要なインターフェースも持ちません。

他にも、std::function_refstd::move_only_functionと同様にconst, noexceptを指定することができます。

struct A {
  int operator()() const noexcept { return 1; }
  int operator()() { return 2;}
} obj;

function_ref<int() const noexcept> fr = obj;
fr();  // 1

この提案は、C++23を目指してLWGでのレビュー中です。

P1018R14 C++ Language Evolution status 🦠 pandemic edition 🦠 2021/09–2022/01

2021年9月から2022年1月の間のEWGの活動についてのレポート。

2022年1月末までにEWGの電子投票にかけられる提案は以下のものです

これらの提案は全て、C++23に導入するべくEWGからCWGへ転送するための最終確認の投票にかけられています。特に、最初の3つとP2448R0はDRです。

C++23に向けたEWGの活動としてはあとはコア言語Issueの解決のみで、おそらくこれ以上コア言語に対する機能追加がC++23に対して行われることはなさそうです。

P1083R4 Move resource_adaptor from Library TS to the C++ WP

pmr::resource_adaptorをLibrary Foundermental TSからワーキングドラフトへ移動する提案。

pmr::resource_adaptorは任意のアロケータをmemory_resourceのインターフェースにアダプトさせるものです。これによって、std::pmr::polymorphic_allocatorで任意のアロケータをより簡易に利用できるようになります。

template<typename Alloc>
class resource_adaptor_impl : std::memory_resource {
  // ...
};

template<typename Alloc>
using resource_adaptor = resource_adaptor_impl<typename std::allocator_traits<Allocator>::template rebind_alloc<char>>;

このresource_adaptor_implのようなものは説明専用のクラスとして指定されているため、実際にはこの名前ではありません。これは例えば次のように使用します

// オレオレアロケータ
class my_allocator;

int main() {
  std::pmr::resource_adaptor<my_allocator> mr{};

  std::pmr::polymorphic_allocator<> pmralloc{&mr};
}

pmr::resource_adaptorがない場合、my_allocatorをラップしてmemory_resourceにアダプトするようなクラスを手書きする必要がありました。

polymorphic_allocatorはアロケータカスタマイズポイントとして便利であるため、今後テンプレートパラメータでアロケータを取る代わりに利用されることが増えていくと思われます。そこにアロケータを渡すには、それがすでにpolymorphic_allocatorそのものであるかmemory_resourceにアダプトされている必要があります。resource_adaptorは後者のケースのヘルパとなるユーティリティであり、これがあることでpolymorphic_allocatorの利用がより促進されます。また、resource_adaptorは長期間Library Foundermental TSに存在しており、その実装は安定しているためワーキングドラフトへの導入の敷居も低いと思われます。この提案はこれらの理由からresource_adaptorの標準入りを目指すものです。

ただし、この提案ではアライメント指定周りの設計が少し変更されています。memory_resource.do_allocate()(および.do_deallocate())では第二引数にアライメント指定を受け取ります。resource_adaptorがラップしているアロケータに正しくこのアライメント指定を伝達するには、そのアライメント指定に沿ったアライメントを持つ別の型Uで元のアロケータをrebindすることですが、.do_allocate()にはアライメント指定が実行時に渡ってきます。そのため、resource_adaptor::do_allocate()では、可能なアライメント指定毎にそのアライメントを持つ型を求めてそれによって元のアロケータをrebindしてからallocate()を呼び出します。

template<typename Alloc>
class resource_adaptor_impl : std::memory_resource {
  Alloc m_alloc;

  // ...

  void* do_allocate(std::size_t bytes, std::size_t alignment) override {
    // alignmentは2のべき乗の値だけを考慮する
    // alignmentの値毎に適切な型Uでrebindする
    switch(alignment) {
      case 4 :
        {
          using U = int32_t;
          const std::size_t n = (bytes + sizeof(U) - 1) / sizeof(U);
          return allocator_traits<Alloc>::template rebind_traits<int32_t>::allocate(m_alloc, n);
        }
      case 8 :
        {
          using U = int64_t;
          const std::size_t n = (bytes + sizeof(U) - 1) / sizeof(U);
          return allocator_traits<Alloc>::template rebind_traits<int32_t>::allocate(m_alloc, n);
        }
      case 16 :
        ...
      case 32 :
        ...
      case 64 :
      ...
      default : assert(false);
    };

  }
};

おおよそこのような実装が行われるらしいですが、この実装は明らかに余計なテンプレートの実体化を伴うため、コードの肥大化やコンパイル時間の増大といった問題があります。それはどうしようもないですが、せめて考慮すべき数を減らすためにこの提案のresource_adaptorは第二テンプレート引数にこのアライメントの最大値を指定できるように修正されています。この最大値によって上記のようなコード生成の上限を指定します。

また、上記例ではUint32_tなどを使用しましたがこれも適切ではなく、この指定したアライメントを持つUを求めるためのaligned_raw_storage<Align, Size>(オーバーアラインされる場合のUを求める)、aligned_type<Align>(オーバーアラインされない場合にUとして適切なスカラ型を指定する)、aligned_object_storage<T>Tについてaligned_raw_storage<sizeof(T), sizeof(T)>を求め、ストレージとして利用しやすくするヘルパ型)といった型特性クラスも同時に提案されています

P1206R7 Conversions from ranges to containers

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

このリビジョンでの変更は、全体的なtypoの修正と、提案する文言の改善です。

この提案は、2月の全体会議に投票にかけられ、C++23WD入りが決定しています。

P1240R2 Scalable Reflection

値ベースの静的リフレクションの提案。

現在のReflection TS(N4766)はテンプレートメタプログラミングとの親和性を意識して型ベースのリフレクションとして設計されています。そこでは、reflexpr()という構文によって任意のエンティティからメタ情報を型として抽出し、各種traits型によってそれを処理します。

#include <reflect>

// 型名を取得する
template <typename T>
std::string get_type_name() {
  namespace reflect = std::experimental::reflect;
  
  // T(テンプレートパラメータ)のメタ情報を取得
  using T_t = reflexpr(T);

  // テンプレートパラメータから元の型のメタ情報を得る
  using aliased_T_t = reflect::get_aliased_t<T_t>;

  // Tの元の型名をstringで取得
  return reflect::get_name_v<aliased_T_t>;
}

このような型ベースリフレクションはTMPの文脈では扱いやすいものですがconstexprの文脈においては扱いづらく、静的リフレクションで得たメタ情報からコード生成をする処理などが書きづらくなります。そこで、constexprと親和性の高い値ベースのリフレクションが提案され(P0425R0)、SG7(Reflection Study Group)ではその方向性でリフレクションを検討していくことにしたようです。

この提案はその後の議論や提案の作業を反映した、値ベースの静的リフレクションを提案するものです。

先ほどの型名取得コードは、この提案では次のようになります。

#include <meta>

template<typename T>
std::string get_type_name() {

  // Tのメタ情報を取得
  constexpr std::meta::info reflected = ^T;

  // Tの型名をstring_viewで取得
  std::string_view name = std::meta::name_of(reflected);

  return std::string(name);
}

reflexpr^になり、その結果は型ではなく値となります。その後でも、get_name_v<T>のような型特性クラスではなくname_of()のような関数によってメタ情報を処理していくことができます。テンプレートパラメータに対するreflexpr(T)ではテンプレートパラメータそのもののメタ情報がまず得られていましたが、^Tではインスタンス化後、具体的な型で置き換えられたTについてのメタ情報(すなわち与えられた型の直接のメタ情報)が得られます。むしろ、この提案ではテンプレートパラメータそのもののメタ情報を得る手段を提供していません。

別のサンプル

// 列挙値名を文字列化する
template<Enum T>
std::string to_string(T value) {
  // Tの列挙値のシーケンスを取得
  template for (constexpr auto e : std::meta::members_of(^T)) {
    // valueに一致する列挙値名を返す
    if ([:e:] == value) {
      return std::string(std::meta::name_of(e));
    }
  }

  return "<unnamed>";
}

このコードは次の3つのことを行なっています

  1. 列挙型から列挙値のシーケンスを取得する
  2. 列挙値のシーケンスから、valueと一致するものを探す
  3. 一致した列挙値の名前を返す

1は、^Tによって取得したTのメタ情報からmembers_of()によって列挙型Tのメンバ(すなわち列挙値)のメタ情報のシーケンスを取得します。Tの列挙値それぞれに対して^した結果の型をinfoとすると(これは実際にstd::meta::infoという型です)、members_of()の戻り値型はstd::span<info, N>となります。

2はtemplate for構文によって行われており、これはexpansion-statementという構文です。members_of()の戻り値のspanの各要素に対してその本体が繰り返し適用されますが、これは実行時ループではなく、I = 0...N-1毎に次のようなコードの繰り返しとして展開されます。

{
  constexpr std::meta::info e = s[I];
  if ([:e:] == value)
    return std::meta::name_of(e);
}

expansion-statement内では、[:e:]の構文によってTの列挙値のメタ情報eからその値を復元します。この処理はスプライシングと呼ばれ、これによって実行時の値valueとの比較が可能となり、最初にマッチしたeについて、その名前(コード上での列挙値名)をname_of()によって取得(std::string_viewで得られる)して返します。もし一致するものが見つからない場合(|でビット合成した列挙値など)、"<unnamed>"を返します。

このコードは実行時とコンパイル時が入り乱れていてわかりづらいですが、^などによって得られるメタ情報やexpansion-statementによるイテレート、[:...:]によるスプライシングなどは全てコンパイル時に実行されており、これらの機能によって生成・追加される実行時メタ情報は何もありません。^から始まる一連のメタ情報の処理は全て静的に解決されており、これが値ベースの静的リフレクションです。

この提案も含めて値ベースリフレクションはまだSG7での議論中であるので、C++23に入ることはないでしょう。現在のReflection TSも、値ベースリフレクションによって書き換えられるはずなので、それもC++23に導入されることはなさそうです。静的リフレクションがC++に導入されるのは早くてもC++26、TSとしての経験を重視する場合はもっと後になるかもしれません。

P1664R6 reconstructible_range - a concept for putting ranges back together

viewによって別のrangeに変換されてしまった範囲を、元のrange(と同じ型)に戻す操作、std::ranges::reconstructと関連するコンセプトの提案。

以前の記事を参照

このリビジョンでの変更は、モチベーションセクションの改善、カスタマイゼーションポイントの調整、ranges::reconstructに4つのオーバーロードがあることの説明の追記、提案する文言の改善、SG9からのフィードバックの反映、などです。

P1841R2 Wording for Individually Specializable Numeric Traits

std::numeric_limitsに代わる新たな数値特性(numeric traits)取得方法を導入する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善とP2485R0の内容を反映したことです。

この提案はC++23に導入すべく、現在LWGにおいて最終レビューが行われています。

P1854R3 Conversion to literal encoding should not lead to loss of meaning

文字列リテラルエンコーディングを実行時エンコーディングに変換する際、文字表現が失われる場合をコンパイルエラーとする提案。

以前の記事を参照

このリビジョンでの変更は、typo修正と、提案する文言についてP2362R3によるものとの違いを明確化したことです。

P1885R9 Naming Text Encodings to Demystify Them

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

以前の記事を参照

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

  • 提案する文言の修正
  • ほとんどの関数がCHAR_BIT == 8を適格要件(破られればコンパイルエラー)とした
  • ワイド文字列版のオーバーロードを削除(P2419R0への対応)
  • P2498R0への対応(提案されている解決策を採用しない)

などです。

P1899R2 stride_view

範囲を等間隔の要素からなる範囲に変換するRangeアダプタstride_viewの提案。

以前の記事を参照

このリビジョンでの変更は、P2442R1の内容を反映するように提案する文言を修正した事です。

P2000R3 Direction for ISO C++

C++の標準化の方向性を記した文書。

C++23の設計終了を受けて、C++26に向けて内容を更新したものです。

P2079R2 System execution context

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

この提案はR1ではP0443のexecutorとして設計されていましたが、このリビジョンではP2300のschedulerとして再設計されています。

提案されているのは、実行コンテキストのviewとなるsystem_contextとそれを用いたschedulerであるsystem_schedulerおよび、system_contextで実行される処理を表現するsenderであるsystem_sender(名前は実装定義)の3つです。

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

  system_context(const system_context&) = delete;
  system_context(system_context&&) = delete;
  system_context& operator=(const system_context&) = delete;
  system_context& operator=(system_context&&) = delete;

  system_scheduler get_scheduler();

  implementation-defined_delegation_sender execute_chunk() noexcept;
  implementation-defined_delegation_sender execute_all() noexcept;
  
  size_t max_concurrency() noexcept;
};

system_contextは任意の実行コンテキストの上にかぶせて使用し、system_contextのオブジェクトはそこから取得したschedulerおよびその上で起動されたすべての作業よりも長生きする必要があります。また、system_contextの全てのオブジェクトは同じ実行コンテキストを参照しており、異なるsystem_contextのオブジェクトから投入された処理は同じ実行コンテキストで実行されます。

execute_chunk()は実装定義の数(チャンク)の作業だけを開始するためのsenderを返す関数で、execute_all()は現在system_contextにキューイングされているすべての作業の実行を開始するsenderを返すものです。これらのsenderは処理の完了までを表現するものではなく、処理の投入だけを担うもので、実行コンテキストへ処理を投入し終えた段階で完了し、結果(set_value()チャネル)として投入(開始)することに成功した作業の数を返します。

max_concurrency()system_contextの最大スレッド数を返す関数ですが、0を返すことができ、その場合はメインスレッドを利用します。

system_contextはスレッドプールを意識してはいるものの、その実行コンテキストの詳細についてはほぼ何も指定していません。そのため、提案ではリリースビルドとデバッグビルドで実行コンテキストを変更可能とすることを提案しています。

class system_scheduler {
public:
  system_scheduler() = delete;
  ~system_scheduler();

  system_scheduler(const system_scheduler&);
  system_scheduler(system_scheduler&&);
  system_scheduler& operator=(const system_scheduler&);
  system_scheduler& operator=(system_scheduler&&);

  bool operator==(const system_scheduler&) const noexcept;

  friend implementation-defined-system_sender tag_invoke(
    std::execution::schedule_t, const system_scheduler&) noexcept;

  friend std::execution::forward_progress_guarantee tag_invoke(
    std::execution::get_forward_progress_guarantee_t,
    const system_scheduler&) noexcept;

  friend implementation-defined-bulk-sender tag_invoke(
    std::execution::bulk_t,
    const system_scheduler&,
    Sh&& sh,
    F&& f) noexcept;
};

system_schedulersystem_context::get_scheduler()から取得できるschedulerで、system_contextへの参照を保持しています。2つのsystem_schedulerオブジェクトは、同じsystem_contextを共有している場合に同値(==true)となります。system_schedulersystem_contextオブジェクトの破棄後に生存している場合、デストラクタ呼び出し以外のすべての操作が未定義動作となります。

提案文書より、使用例。

using namespace std::execution;

// system_contextオブジェクトの作成
system_context ctx;
// system_schedulerの取得
scheduler auto sch = ctx.get_scheduler();

// 処理のチェーン
sender auto begin = schedule(sch);
sender auto hi = then(begin, []{
    std::cout << "Hello world! Have an int.";
    return 13;
});
sender auto add_42 = then(hi, [](int arg) { return arg + 42; });

auto [i] = this_thread::sync_wait(add_42).value();


// execution::onを使用すると、よりよいstructured concurrencyな表現が得られる

sender auto hi = then(just(), []{
    std::cout << "Hello world! Have an int.";
    return 13;
});
sender auto add_42 = then(hi, [](int arg) { return arg + 42; });

auto [i] = this_thread::sync_wait(on(sch, add_42)).value();

バルク実行のサンプル

auto bar() {
  return
    ex::let_value(
      ex::get_scheduler(),          // 接続されたreceiverからschedulerを取得する
      [](auto current_sched) {
        return bulk(
          current_sched.schedule(),
          16,                       // 例えば16並列
          [](auto idx){
            // idxにはShape(ここでは16)に応じた作業カウントが渡される
            std::osyncstream{std::cout} << "Index: " << idx << "\n";
          })
      });
}

void foo() {
  using namespace std::execution;

  system_context ctx;

  auto [i] = this_thread::sync_wait(
    on(
      ctx.scheduler(),  // bar()をsystem_schedulerで開始する
      bar()))           // onによって用意されたreceiverを通してsystem_schedulerを伝播する
    .value();
}

これは標準ライブラリの提供する基本的で基礎的なschedulerとなり、もっともよく使用されるものとなるはずです。ただし、これはまだ提案としては初期段階で、C++23には間に合いません。

P2093R12 Formatted output

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

以前の記事を参照

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

この提案はC++23を目指してLWGでのレビュー待ちをしています。

P2165R3 Compatibility between tuple, pair and tuple-like objects

std::pairと2要素std::tuple及びtuple-likeな型の間の非互換を減らし比較や代入をできるようにする提案。

前回の記事を参照

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

  • 対象とするtuple-likeな型をstd名前空間にあるものに絞った
  • views::zip関連の変更の追加
  • 連想コンテナに対する変更を削除(別の提案とする)
  • pair/tupleの既存のコンストラクタ/代入演算子/比較演算子を変更しないようにした
  • uses_allocator_construction_argsを取るオーバーロードの追加
  • 機能テストマクロの追加
  • tuple-likeコンセプトを参照型をサポートするように修正

などです。

この提案はC++23を目指してLWGでのレビュー待ちをしています。

P2248R4 Enabling list-initialization for algorithms

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

以前の記事を参照

このリビジョンでの変更はstd::replace_copystd::rangesのものではない)のデフォルト指定をドロップした事、提案する文言の修正と明確化などです。

この提案はC++23を目指してLWGでのレビュー待ちをしています。

P2249R4 Mixed comparisons for smart pointers

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

以前の記事を参照

このリビジョンでの変更は、P2405へのリンクを修正した事のみです。

P2286R5 Formatting Ranges

P2286R6 Formatting Ranges

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

以前の記事を参照

R5での変更は

  • デリミタの変更をドロップした
  • retargeted_format_context, end_sentryを削除
  • range_formatterが必要となる理由について追記
  • 文字エスケープの振る舞いについて、Python/Rustを例にして追記
  • コンテナアダプタに関する説明を追加
  • 提案する文言を追加

R6での変更は

  • 機能テストマクロの追加
  • 提案する文言の改善
    • rangeRについてのformatter<R, charT>range_formatter<range_reference_t<R>>を継承するように変更
    • formattableコンセプトの出力イテレータについての指定を実装定義から未規定へ変更

などです。

この提案はC++23へ向けて、LEWGでの最終投票待ちをしています。

P2300R4 std::execution

P0443R14のExecutor提案を置き換える、任意の実行コンテキストで任意の非同期処理を構成・実行するためのフレームワークおよび非同期処理モデルの提案。

以前の記事を参照

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

  • いくつかのバグ修正
  • Dependently-typed senderのサポート
  • receiverに対してqueryを発行しその結果をset_value()チャネルで返すsenderファクトリ、read(query)の追加
  • typed senderメタデータを宣言的に定義するためのcompletion_signaturesの追加
  • 別のsendercompletion signatureからアダプトしてsendercompletion signatureを指定するためのmake_completion_signaturesの追加
  • senderの定義のデフォルトをtyped senderに変更し、そのチャネル型指定のないsenderサポートをなくした
    • typed senderとは、そのset_value(), set_error()チャネルを介して送信する値の型をS::value_types, S::error_typesとして公開するsender
  • set_done()チャネル(receiverの完了(not 成功)チャネル)の名前をset_stopped()に変更
  • senderアダプタのレイヤを介してscheduler, sender, receiverおよび環境クエリの伝送を制御するためのカスタマイズポイントの追加
  • delegatee schedulerを取得するためのget_delegatee_scheduler()を追加
    • delegatee schedulerとは、アルゴリズムもしくはschedulerが作業を委任して実行するために使用するschedulerのこと
  • schedule_result_tschedulerからそのsender型を取得する)の追加
  • stopped_as_errorsenderのエラーチャネルが指定したエラーを返すように変換するsenderアダプタ)をCPOとして再指定
  • tag_invokeの診断を改善
    • tag_invokeの呼び出し失敗がSFINAE可能であることを規定(これは通常のCPOと同様)

などです。

このリビジョンでの大きな変更は、Dependently-typed senderをサポートするようにしたことです。

stop_token、アロケータ、schedulerなどの現在の実行環境に関する情報はコンシューマからプロデューサーへ自然に伝達される必要があります。コルーチンではそれはpromise型を通して、呼び出したコルーチンから呼び出し側へ伝播します。sender/receiverモデルでもその伝達はされていますが、多くの情報はreceiverに関連づけられており、senderreceiverconnectされた後で、senderoperation_stateによって照会されます。これはすなわち、最終的に起動される非同期処理に関する情報はsenderreceiverで分割して保持されており、情報の一部はreceiverを通して遅れて提供されるということです。

senderアルゴリズムはそれらの情報をsender_traits機構(completion_signatures_of_t<Sender>)によって取得します。しかし、R3までのsender_traits機構はsender型しかパラメータに取らず、receiverによって決定される情報の一部を静的に引き出すことができませんでした。このため、senderによってはその情報を確定できず(typed senderとなれず)、コンパイルエラーを起こすことがありました。

namespace ex = std::execution;

// この段階ではreceiverが不定
ex::sender auto task =
  ex::let_value(
    ex::get_scheduler(), // receiverからschedulerを取得する
    [](auto current_sched) {
      // そのschedulerでネストした作業を開始する
      return ex::on(current_sched, nested work...);
    });

// sync_waitの用意するreceiverとconnectして処理を開始、完了を待機
std::this_thread::sync_wait(std::move(task));   // コンパイルエラー

get_scheduler()は接続されているreceiverからschedulerを取得するもので、引数なしでの呼び出しはその値チャネル(set_value)でschedulerを返すsenderを返します。そのsendervalue_typereceiverに接続されたschedulerの型です。この時、sender_traits<get_scheduler_sender>::value_typesも同じ型になって欲しいですが、receiverがないためその型は不定となります。

sync_wait()run_loop実行コンテキスト(現在のスレッドで処理を開始するシングルスレッド実行コンテキスト)で処理を開始するもので、その戻り値型の確定のために入力のsenderはtyped senderでなければなりません。しかし、let_value(sender, work)は入力のsenderがtypedではない場合得られるsenderもtypedではなく、それをsync_wait()に入力しようとするとコンパイルエラーとなります。get_scheduler()の返すsenderはtypedではない(sender_traits<get_scheduler_sender>::value_typesが決定できない)ため、上記例ではコンパイルエラーを起こしています。

この解決のための変更がDependently-typed senderであり、その内容は単純にsender_traits機構がreceiverの型も追加で受け取れるようになっただけです。アルゴリズム`sender_traits<Sender, Receiver>を介して、非同期処理に関する情報の全てを取得することができます。

ただし、このようなreceiver型の使用は、型のサイクルを引き起こしやすく、謎のエラーが多発するようで、sender_traits機構は実際にはreceiver型ではなく環境オブジェクト(environment object、key/valueペアの袋)を受け取ります。これは、コンテキスト情報をreceiverから環境へ移動させるための設計で、sender_traitstyped_sender(このリビジョンではsender)はreceiver型の代わりにこの環境オブジェクトを受け取ります。ただし、receiverと環境の分離は完全ではないようで、さらなる分離が可能なようですが、この提案ではそこに踏み込んではいません。

環境オブジェクトと呼ばれている値(実際には型っぽいですが)にsender(にまつわる環境情報)型が依存するので、Dependently-typed(依存型)senderという呼び名のようです。

この提案はC++23を目指して作業されていましたが、LEWGの最終投票をパスできなかったためC++23への導入は見送られました。C++26を目指すことになります。投票の反対意見では、C++23設計終了のギリギリのタイミングで巨大な提案を入れるべきではないという意見が目立ったようです。

P2302R3 std::ranges::contains

新しいアルゴリズムとしてstd::ranges::containsを追加する提案。

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

この提案はC++23を目指してLWGでのレビュー待ちをしています。

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

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

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

この提案はC++23に向けてEWGでの最終投票待ちをしていますが、すでにCWGのレビューと承認を終えており、EWGを通過すれば次の全体会議で投票にかけられます。

P2363R3 Extending associative containers with the remaining heterogeneous overloads

連想コンテナの透過的操作を、さらに広げる提案。

以前の記事を参照

このリビジョンでの変更は、at()に事前条件を追加したこととその説明を追記したことです。

この提案はC++23を目指してLWGでのレビュー待ちをしています。

P2370R2 Stacktrace from exception

現在投げられている例外オブジェクトに基づくスタックトレースを取得できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、P2490の内容についての議論を追記し、P2490のアプローチを実装可能とするように文言を修正したことです。

この提案の現在の設計はスレッドローカルなフラグを用いて例外キャッチ時のスタックトレース取得を制御していますが、それはTLSへのアクセスというオーバーヘッドを伴います。対してP2490では、catchに応じてそれをするかしないかを静的に判定するアプローチを提案しています。これを受けてこのリビジョンでは、P2490のアプローチには次のような短所があると指摘しています

  1. 実行時にスタックトレースキャプチャを無効化する方法が提供されていない
  2. 再スローするとスタックトレースが失われる
  3. ゼロオーバヘッド例外(P0709)では機能しない
    • P0709の例外機構はstd::expectedを返すのに近いことをしており、例外がキャッチブロックに到達した時にはスタックトレースは失われている
    • この提案では、例外発生元であらかじめスタックトレースを保存し、std::expected(的なもの)の内部に保存しておけば動作する

その上で、P2490のアプローチを受け入れ可能なように提案する文言を調整しています。その結果として、スタックトレースを保存する場所が実装定義とされたようです。

P2375R1 Generalisation of nth_element to a range of nths

std::nth_elementを一般化したnth_elementsの提案。

このリビジョンでの変更は、説明や背景を充実させたことと、アプリケーションやパフォーマンス調査について追記したこと、多くの質問への回答を追記したことなどです。

P2404R1 Move-only types for equality_comparable_with, totally_ordered_with, and three_way_comparable_with

P2404R2 Move-only types for equality_comparable_with, totally_ordered_with, and three_way_comparable_with

各種異種比較を定義するコンセプトのcommon_reference要件を緩和する提案。

以前の記事を参照

R1での変更は

  • コンパイル時間増大の回避のため、原始制約式にdisjunction(||)を用いるのを回避した
    • requires式とネスト要件に置き換え
  • 提案する文言のバグ修正
  • 実行時に共通型への変換が必要となる可能性のあるmonomorphic functionへの対応
  • 問題を一部しか解決しないソリューションに関する説明を削除
  • commom_reference要件を削除することを提案しない理由を拡充
  • 実装経験の拡充

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

  • 機能テストマクロの追加
  • 提案する説明専用コンセプトcommon-comparison-supertype-withからremove_cvref_tおよびcommon_reference_t<const T&, const U&>を取り除いた(可読性向上のため)

などです。

この提案はC++23を目指してLWGでのレビュー待ちをしています。

P2424R0 Abbreviated Parameters

関数引数の簡易宣言構文の提案。

1行で済むような単純な関数をラムダ式を用いて定義して使用することが多く、そのためラムダ式を可能な限り簡潔に書くことができるようにしたいという要望が強くあります。それを受けて、短縮ラムダ提案(P0573R2)などいくつかの提案がされましたが、以下のような理由によってリジェクトされています。

  1. 通常のラムダ式と意味論が異なる。関数本体が同じでも、短縮形か否かによって戻り値型が異なる。
  2. 任意の先読みパースが必要となる。パーサーはラムダ式が短縮形かどうかを判定するためにラムダ式本体を先読みしなければならない。
  3. 後置戻り値型との不一致。ラムダ本体と後置戻り値型とでは解析が異なるため、短縮ラムダは意図通りに動作しない可能性がある
    • この問題はP2036R2で解決される(予定)

この問題のうち、1と3はラムダ本体の意味論に関することで、2はラムダの引数宣言に関する事です。そして、多くの場合ラムダ式の冗長性を最も高めているのは引数宣言周辺の構文です。

[](const auto& a, const auto& b) { return a < b; }

auto&&を用いるともう少し短縮できるという主張もありますが、auto&&constとなるのは実引数がすでにconstである場合のみです。ラムダ式では非constなものを渡して内部ではconstとして扱いたい場合が多く、auto&&は意味が異なってしまいます。

ここで、この引数宣言からこれらの型の指定を取り除くことができれば、大きく冗長性を低下させることができることが見えます。そこで、この提案では上記に列挙した2つ目の問題(引数宣言周りの問題)に的を絞り、省略された仮引数宣言(Abbreviated Parameters)という構文の導入によりこの解決を図ります。

// ラムダ式
[]((a, b)) { return a < b; }
// 関数
auto less_then((a, b)) { return a < b; }

二重かっこ(())で囲まれているものが省略された仮引数リストです。この内部の識別子は型名ではなく変数名(引数名)として扱われます

// string型、vector型の仮引数宣言、引数名なし
auto something(string, vector);

// string、vectorという名前の仮引数宣言、型指定なし(この提案)
auto something((string, vector));

この時、カンマ区切りリストの各要素に2つ以上の識別子が指定された場合、それは通常の仮引数宣言と同じものとして扱われます。

// この2つは同じ宣言となる
auto something(vector v);
auto something((vector v));

// この2つも同じ宣言となる
auto something(const vector& v);
auto something((const vector& v));

言い換えると、省略された仮引数宣言において型名の省略はオプションです。これは当然混在することができます。

auto something((string, const vector& v));  // ok

省略された仮引数宣言(のうち型名指定がないもの)はテンプレートパラメータを伴う引数宣言とみなされ、通常のテンプレート実引数推論に従って型が推論されます。つまりは、ラムダ式の引数宣言からさらにautoを省略しているような感じです。

// これらは(ほぼ)同じ宣言となる
template<typename T>
auto something(T v);

auto something(auto v);

auto something((v));

ただし、修飾(&&とかconstなど)は異なり、この提案ではconst auto&と同じ推論を行うことを提案しています。

// これらは同じ宣言となる
template<typename T>
auto something(const T& v);

auto something(const auto& v);

auto something((v));

auto&&はデフォルトが可変参照でありそれが見えなくなることで危険性が増すとともに注意すべきことや前提知識が増えるため、この提案ではデフォルトimmutableが最適だとしています。また、デフォルトが参照となるため型によっては非効率となり得ますが、そのような型に対してオプティマイザがこれをコピーに置き換えることができるようにすることも提案されています。

std::string s;
std::vector<std::string> v;

std::for_each(s.begin(), s.end(), []((c)){ ... }); // decltype(c) is `const char`
std::for_each(v.begin(), v.end(), []((s)){ ... }); // decltype(s) is `const std::sting&`

このような振る舞いはユーザーが引数のアドレスを取得する場合に問題となる可能性がありますが、引数をポインタ型で宣言しないのに実引数の参照元のアドレスを取得するようなケースは考えづらいため、この提案では考慮しないことにしています。

// 問題となりうるケースだが
void func((arg)) { persistAddrOf(&arg); ... }

// 普通はこう書かれるはず?
void func(const auto* arg)) { persistAddrOf(arg); ... }

P2437R1 Support for #warning

警告を表示するための#warningプリプロセッシングディレクティブを追加する提案。

以前の記事を参照

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

この提案はC++23に向けてEWGでの最終投票待ちをしていますが、すでにCWGのレビューと承認を終えており、EWGを通過すれば次の全体会議で投票にかけられます。

P2447R2 std::span and the missing constructor

std::spaninitializer_listを受け取るコンストラクタを追加する提案。

以前の記事を参照

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

P2448R1 Relaxing some constexpr restrictions

constexpr関数がすべての引数について定数実行不可能となる場合でも、コンパイルエラーにしないようにする提案。

以前の記事を参照

このリビジョンでの変更は、CWGの指摘に従って修正(削除)する文言を拡大(取りこぼしていたものを追加した)ことです。

この提案はC++23に向けてEWGでの最終投票待ちをしていますが、すでにCWGのレビューと承認を終えており、EWGを通過すれば次の全体会議で投票にかけられます。

P2457R0 2021 December Library Evolution Poll Outcomes

2021年12月に行われたLEWGにおける全体投票の結果。

次の12の提案が投票にかけられ、否決されたものはありません(全てLWGへ転送されています)。また、投票に当たって寄せられた賛否のコメントが記載されています。

P2458R0 2022 January Library Evolution Polls

2022年の1月に予定されている、LEWGでの全体投票の予定表。

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

P2468R2 The Equality Operator You Are Looking For

==から導出される!=演算子とユーザー定義!=演算子が衝突してしまう問題を解消する提案。

以前の記事を参照

このリビジョンでの変更は、CWGのフィードバックに従って提案する文言を調整したこと、この提案がC++20へのDRであることを明示したことなどです。

この提案はC++23に向けてEWGでの最終投票待ちをしていますが、すでにCWGのレビューと承認を終えており、EWGを通過すれば次の全体会議で投票にかけられます。

P2474R1 views::repeat

指定された値の繰り返しによる範囲を生成するRangeファクトリである、views::repeatの提案。

以前の記事を参照

このリビジョンでの変更は、モチベーションについて追記したこと、repeat_viewイテレータ型の比較演算子<=>, ==だけにしたこと、SG9のレビューに従ってコンストラクタの数を減らしたこと、提案する文言の修正などです。

この提案はC++23に向けて、LEWGの最終投票を通過しLWGへ転送されています。

P2494R1 Relaxing range adaptors to allow for move only types

何かを保持する必要があるタイプのview型について、保持するものの型に対する要件をcopy_constructibleからmove_constructibleに弱める提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加したこと、P2474R1に対する変更を追記したことです。

この提案はC++23に向けて、LEWGの最終投票を通過しLWGへ転送されています。

P2498R1 Forward compatibility of text_encoding with additional encoding registries

std::text_encodingP1855)がIANA以外のレジストリに対応できるようにしておく提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の追加と調整などです。

この提案はSG16でのレビューを終えており、そこではこの提案をP1855へのバグフィックスとしてLEWGへ転送することに合意が取れています。ただし、std::text_encodingがIANA以外のレジストリを考慮できるように変更することや、名前をiana_text_encodingに改名することについてのコンセンサスは得られていません。

P2507R1 Limit [[assume]] to conditional-expressions

P1774で提案されている[[assume(expr)]]について、exprの部分に条件式(conditional-expression)だけを書けるようにする提案。

以前の記事を参照

このリビジョンでの変更は、タイトルの変更とP2461への参照を追加した事です。

P2508R1 Exposing std::basic-format-string

説明専用のstd::basic-format-string<charT, Args...>をユーザーが利用できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、std::basic_format_stringクラスにget()メンバ関数を追加した事です。これは例えば、最終的にstd::vformatにフォーマット文字列を渡したい場合などに文字列を引き出す手段を提供するためです。

この提案はすでにLWGとLEWGの両方でレビューを終え、それぞれでの最終投票を通過しています。次の全体会議で投票にかけられる予定です。

P2511R0 Beyond operator(): NTTP callables in type-erased call wrappers

std::move_only_fuctionを、呼び出し可能なNTTP値を型消去するCallable Wrapperへ拡張する提案。

move_only_fuctionにユーザー定義のオブジェクトを渡すには、それが関数呼び出し演算子によって次のように呼び出し可能である必要があります。

obj(some, args);

しかし、operator()は全てのクラス型で実装されるわけではなく、ユーザー定義のクラス型では例えば次のようにメンバ関数によって呼び出すことができるようになっています。

obj.send(some, args);

この提案は、このようにメンバ関数によって呼び出しを行うものをmove_only_fuctionで扱えるようにしようとする提案です。つまりは、このようなメンバ関数による呼び出しをoperator()によってラップする機能をmove_only_fuctionに入れ込もうとしています。

このようなラッピングは現在、ラムダ式を用いて行われます。

auto wrap = [obj = std::move(obj)]<typename... Args>(Args&&... args) mutable {
  return obj.send(obj, std::forward<Args>(args)...);
};

しかし見て分かる通り記述が冗長でボイラープレートが多すぎます。また、完全転送やthis(obj)の値カテゴリなど、気にすべき事項も多いです。ラムダ式はこの用途にはあまり適していません。

ほかには、std::bind_frontを使用することもできます。

auto wrap = std::bind_front(&Obj::send, std::move(obj));

かなりシンプルに書くことができますが、この場合は追加のコスト(特にテンプレートの実体化コスト)がかかります。

ここで当初の目的を思い出すと、このようなラッパーは最終的にmovo_only_functionへ入れ込みたいのでした。そして、このようなラッパーの対象となっている呼び出し可能なものとはメンバ関数ポインタ(あるいは関数ポインタ)であり、それはコンパイル時に値が定まるものです(ここではこれをNTTP値と呼ぶことにします)。

NTTP値の保存に当たっては追加のストレージ領域を必要としない(テンプレートパラメータとして保持することができ、実体的にはストレージの型名に埋め込まれる)ため、本来Callableオブジェクトを保持する領域を別の事に使用することができます。すなわち、その領域に束縛対象の*thisオブジェクトを保存することができます。従って、movo_only_functionにそれを受け取れるインターフェースを追加するだけで上記のようなラッピングができそうです。

この提案では、std::nontype_t<V>を標準へ導入し、それを用いてstd::move_only_fuctionを構築できるようにします。

std::move_only_function<void(Some, Args)> wrap{std::nontype<&Obj::send>, std::move(obj)});

また、std::nontype_<V>だけから変換するコンストラクタも用意されます。

move_only_function<int(const char*, const char*)> fn = strcmp;
fn = nontype<strcasecmp>;

これらの事は同時にstd::fuction_refについても提案されていますが、そちらはまだ標準への文言が固まっていないため、ここでも文言は保留中です。一方で、これらの事は既存のもの(std::function, std::pckaged_task)には提案されていません。

サンプルコード

// DB::connect()がこのようなシグネチャだとして
std::vector<std::move_only_function<bool(std::string, double, double)>> q{};

// C++11(std::bindによるthisの束縛)
q.push(std::bind(&DB::connect, std::move(db), _1, _2, _3));

// C++14(ジェネリックラムダによるthisの束縛)
q.push([db{std::move(db)}] (auto &&... args) mutable
       {
           return db.connect(std::forward<decltype(args)>(args)...);
       });

// C++20(std::bind_frontによるthisの束縛)
q.push(std::bind_front(&DB::connect, std::move(db)));
// あるいは(ジェネリックラムダ+ラムダのテンプレートパラメータ指定)
q.push([db{std::move(db)}] <class ...T>(T &&... args) mutable
       {
           return db.connect(std::forward<T>(args)...);
       });

// この提案
q.emplace(nontype<&DB::connect>, std::move(db));

このstd::nontype_t<V>によって、std::move_only_functionをはじめとする型消去ラッパーは、関数ポインタなどのNTTP値を型消去するようになったと見ることもできます。型消去(Type erasure)は異なる型に依存しているコードをそれらの型に依存しないようにすることで、この場合はそのようなコードが型ではなくコンパイル時の値(関数ポインタ)に依存していたわけです。

P2512R0 SG16: Unicode meeting summaries 2021-06-09 through 2021-12-15

2021年6月から12月の半年間のSG16(Unicode Study Group)のミーティング議事録。

例えばP2093R6: Formatted output等の提案に関する議論と投票の様子が詳しく記されています。

P2513R0 char8_t Compatibility and Portability Fixes

char8_tの非互換性を緩和する提案。

C++20で導入されたchar8_tUTF-8文字を表す専用の型であり、そのエンコーディングは常にUTF-8であることが保証されます(変なことしなければ)。ただし、u8文字/文字列リテラルの型を変更しており、C++17以前に対する壊的変更を伴っています。

const char* a = u8"a";            // C++17まではok、C++20からはng
const char b[] = u8"b";           // C++17まではok、C++20からはng
const unsigned char c[] = u8"c";  // C++17まではok、C++20からはng

char8_tによる文字/文字列はchar, const char*, const char[]などに暗黙変換することができません。これはUTF-8の文字を表すためにchar8_tを導入するという観点からは当然の変更ではありますが、既存のコードへの影響が小さかったわけでもありません。このような影響を緩和するために-fno-char8_t(GCC)とか/Zc:char8_t(MSVC)等の、C++20モードでchar8_tを無効化するコンパイラオプションが提供されています。

さらに、この非互換性はC++17以前だけにとどまらず、Cとの互換性も壊しています。Cにはchar8_tはありませんがu8リテラルは使用可能で、その型はchar[N]です。すなわち同じ問題がCとC++20の間で起こります。さらに、C23以降にCにもchar8_tが導入される可能性があり(N2653)、そこではchar16_t/char32_tと同じくchar8_tunsigned chartypedefとして提案されており、u8""リテラルの戻り値はcharchar8_tの配列どちらにも変換できるように規定されます。すなわち、CはC++の様な非互換を導入しておらず、結局この互換性問題は残り続けます。

extern const unsigned char d[] = u8"d"; // N2653後のCでもok、C++20はng

このような非互換性を緩和するために、この提案ではu8""文字列リテラルからunsgined charの配列が初期化できるようにすることを提案しています。

extern const unsigned char d[] = u8"d"; // この提案の後、C++でもok

これは変換ではなく配列の初期化時に特別扱いするものです(CのN2653のu8""文字列リテラルもほぼ同様です)。従ってchar8_tの型付けが弱くなることはなく、これ以外の特別扱いや変換は提案されていません。

// この提案の後
const char* a = u8"a";            // ng
const char b[] = u8"b";           // ok
const unsigned char c[] = u8"c";  // ok

この提案によって、直接的にu8""リテラルの非互換性が解消されるわけではありませんが(多くの場合、charの文字列はconst char*で扱われるため)、char8_t文字列をconst char*に渡すときに、変なことをする必要のない経路を作ることができます。

// UTF-8文字列を受け取る関数
void input_u8str(const char* str);

int main() {
  // C++17まではok、この提案の後でもC++20以降はng
  input_u8str(u8"UTF-8 string.");

  // C++20の回避策
  const char8_t* u8str = u8"UTF-8 string.";
  input_u8str(reinterpret_cast<const char*>(u8str));

  // この提案の回避経路
  const char tmp_str[] = u8"UTF-8 string.";
  input_u8str(tmp_str);
}

特に、reinterpret_castを使用しなくてもよくなるのでこのような回避策は定数式でも使用可能になります。また、世の中ではcharの文字列とUTF-8文字列を意味的に区別するためにtypedef std::basic_string<unsigned char, my_u8_traits> u8string;のようなものを定義するのが一般的なテクニックとなっているらしく、この提案はこのようなテクニックがC++20に移行するための一助にもなります。

このように、この提案では単純で小さな変更によって、C++20以前のコードをC++20に対応するための経路を提供し、Cコードとの互換性を確保することができます。そして、この提案はC++20への欠陥報告とすべく作業されているようです。

P2514R0 std::breakpoint

C++コード上からブレークポイントを設定するためのstd::breakpoint()の提案。

コードのデバッグ時にブレークポイントは重宝しますが、ブレークポイントそのものはC++コードの外の概念です。慣れれば大したことない作業ではありますが、初学者にとっては混乱の元で、C++の外のIDEやデバッガのインターフェースや構文を学習しなければなりません。

std::breakpoint()があることによってブレークポイントの配置をコードで表現でき、特に実行時の条件によるブレークポイントの制御がより簡単に行えるようになります。

#include <debugging>

// なんかの処理
auto f() -> double;

int main() {
  double d = f();

  if (std::isnan(d)) {
    std::breakpoint();  // dがNaNの時にブレークする
  }

  ...

}

std::breakpoint()はプログラムの実行中にデバッガがアタッチされている時だけ動作し、それが呼び出されると通常のブレークポイントがあるかのようにプログラムの実行を一時停止します。この振る舞いは現在でもほぼ全てのプラットフォームとデバッガでサポートされているようです。

  • MSVC : __debugbreak
  • GCC : __builtin_trap
  • Clang : __builtin_debugtrap
  • Keil armcc : __breakpoint
  • Unreal Engine 4 : マクロによる同様の実装

std::breakpoint()はこれらの実装定義のものを標準化するものでもあります。

P2515R0 std::is_debugger_present

プログラムがデバッグ中であるかを知るための関数std::is_debugger_present()の提案。

プログラムがデバッガにアタッチされて実行されている時だけ特別なこと(追加のメッセージの出力、追加のテストコードの実行、デバッグに有用なユーザーへの表示など)をしたい場合、それは簡単ではなく実行環境に関する深い知識が求められます。この提案は、ユーザーがそのような負担を負うことなく、デバッグ中であることを判定できるようにするものです。

追加するのはstd::is_debugger_present()と言う関数で、これはデバッガがアタッチされている場合にtrueを返す引数なしの関数です。

namespace std {
    bool is_debugger_present() noexcept;
}

一部のプラットフォームではIsDebuggerPresent()IsDebuggerActive()としてこれらの機能を提供しており、これはそれを標準化するものでもあります。

P2516R0 string_view is implicitly convertible from what?

std::basic_string_viewからRangeコンストラクタを削除する提案。

std::basic_string_viewに対するRangeコンストラクタ(同じ文字型による任意のcontiguous_rangeから構築するコンストラクタは)C++23に対してP1391で導入されました。一見するとこれは便利でありいいアイデアに思えますが、実際に使用してみるといくつかの罠が潜んでいます。

template <typename Container>
auto print(const Container& c)
    -> std::enable_if_t<!std::is_convertible_v<Container, std::string_view>> {
  std::cout << '[';
  const char* sep = "";
  for (const auto& item: c) {
    std::cout << sep << item;
    sep = ", ";
  }
  std::cout << ']';
}

void print(std::string_view s) {
  std::cout << '"' << s << '"';
}

この関数はコンテナに対しては[a, b, ...]の形式で出力し、文字列likeな型(std::string_viewに変換可能な型)に対しては文字列として"ab..."のように出力する関数です。これをprint(std::vector{'a', 'b'})のように使用した時、C++20までは[a, b]と出力されますが、C++23からは"ab"と出力されるようになります。これはC++23で導入されたRangeコンストラクタを通して暗黙変換が行われているために起きていることです。

std::string_viewは文字列の参照となるもので、その名前やインターフェースにもそれが現れています。Rangeコンストラクタによる暗黙変換はその仮定を破壊し、std::string_viewを文字列への参照ではなく任意のcontiguous_rangeの参照へと意味を変更してしまっています。この変換の問題点は文字列であるためのプロクシに連続性を使用して、文字列の実表現と意味論を混同してしまっていることにあります。

C++17 C++20 C++23
print(std::list{'a', 'b'}); [a, b] [a, b] [a, b]
print(std::deque{'a', 'b'}); [a, b] [a, b] [a, b]
print(std::vector{'a', 'b'}); [a, b] [a, b] "ab"

このように比較した時、std::vectorが他のコンテナと異なるところはどこなのでしょうか?そしてこれは本当に文字列likeなものなのでしょうか?contiguous_rangeを参照する型が欲しい場合、すでにstd::span<const T>があります。

このことは{fmt}ライブラリで報告されたことで発見され、問題を回避するためにprintのような関数の定義を変更するのは根本的な解決にはなりません。文字列とコンテナを区別する必要のある他のテキスト処理やシリアル化処理でも問題となる可能性があります。また、charには2つの意味(文字とバイトの単位)があり、バイト列として使用されているstd::vector<char>std::span<char>std::string_viewとして暗黙変換可能になってしまうと型安全性を破壊する可能性があります。

これらの理由から、std::basic_string_viewのRangeコンストラクタはメリット(主に利便性の向上)よりもデメリットの方が大きいため、P1391R4の内容を全て削除しようとする提案です。

P2517R0 Add a conditional noexcept specification to std::apply

std::applynoexcept指定を行う提案。

C++23に導入される予定のviews::zipとその仲間は複数のシーケンスを纏めて一つのシーケンスとするviewで、そのoperator*の結果は元のシーケンスの各要素を纏めたstd::tupleオブジェクトで返されます。zip_transform_viewなど、その要素に何か関数適用するタイプのviewでは、その効果を指定するためにstd::applyが使用されます。

// zip_transform_view::iterator::operator*()の効果の指定
return apply([&](const auto&... iters) -> decltype(auto) {
  return invoke(*parent_->fun_, *iters...);
}, inner_.current_);

しかし、このoperator*noexcept指定は次のように指定されています

// Isはzipしている入力範囲の数を指すstd::index_sequence<Is>
noexcept(invoke(*parent_->fun_, *std::get<Is>(inner_.current_)...))

ここでstd::applyが使用されていないのは、std::applynoexceptの指定が無いためです。

std::applyの効果は次のようなapply-implを用いて指定されています。

template<class F, class Tuple, size_t... I>
constexpr decltype(auto) apply-impl(F&& f, Tuple&& t, index_sequence<I...>) {
  return INVOKE(std::forward<F>(f), get<I>(std::forward<Tuple>(t))...)
}

std::getranges::subrangeのものを除いて例外を投げることはありません(tuple等の要素の参照を取り出すだけのため)。従って、例外を投げるとすればfを呼び出したときだけだとみなすことができます。std::invokeは既に条件付きnoexceptであるため、std::applyを同様にすることができるはずで、この提案はそれを提案するものです。

この提案の内容はすでにlibstdc++とlibc++では実装されているようです。

P2520R0 move_iterator<T*> should be a random access iterator

std::move_iterator<T*>random_access_iteratorとなるようにする提案。

std::move_iterator<T*>C++17イテレータとしてはランダムアクセスイテレータになりますが、C++20イテレータとしてはinput_iteratorにしかなりません。

C++17以前にこれについて議論された際に、パフォーマンス上の理由によりmove_iterator<I>Iの性質を継承することになりました。しかしC++20にて<ranges>追加と共にイテレータライブラリが改修された際、move_iterator<I>::iterator_conceptは常にinput_iteratorであるようにされました。これによって、C++20move_iteratorは常にinput_iteratorとなります。

vector<A> v;
vector<A> temp = ...;

using MI = move_iterator<vector<A>::iterator>;

// MIがランダムアクセスイテレータであることで、ここでのアロケーションを一回で済ませられる
v.insert(v.begin() + N, MI(temp.begin()), MI(temp.end()));

vector::insertでは、挿入される要素数が予めわかっていればvectorの領域をあらかじめ拡張しておくことでアロケーションの回数を最小にできます。それができない場合、挿入要素数push_back()するのと同じことになります。

C++20ではイテレータ間距離を求められるかどうかはイテレータカテゴリから分離され、sized_sentinel_for(あるいはsized_range)コンセプトによって別に指定されます。move_iterator<I>は自身のカテゴリとは関係なく、Isized_sentinel_for<I, I>であれば-によって距離を定数時間で求めることができるため、move_iteratorrandom_access_iteratorである必要はありません。

一方、このようなコードはC++23では次のように書くこともでき、ここではmove_iteratorのカテゴリが問題となってきます。

// アロケーションは一回
some_sized_range | views::move | ranges::to<vector>();

// アロケーションは要素数による
some_unsized_forward_range | views::move | ranges::to<vector>();

views::moveは入力のrangeの各要素をムーブするviewで、ranges::toはそれをコンテナに変換するものです。views::moveを通すことで、入力要素をムーブしてvectorを構築することができます。

ここで、views::movemove_iteratorを内部で用いているため、move_viewのカテゴリは常にinput_iteratorとなってしまいます。それでも入力rangesized_rangeであれば範囲としての長さを求めることができるため、上記1つ目のコードはranges::toによるvector構築時にも要素数をあらかじめ求めることができます。しかし、入力がrangesized_rangeでない場合はそのイテレータが何であれmove_viewsized_rangeではないinput_rangeでしかなく、上記の2つ目のコードではranges::toによるvector構築時にその要素数をあらかじめ利用できません。

これは例えば、move_viewがそういう性質のものであると了解したうえで、そこから元のrange.base()によって取り出して次に渡すみたいなことをすれば回避できますが、それは明らかに間違っています。この提案は、この問題を回避するためにユーザーがそのようなことをしなくてもいいように、move_iterator<I>Iの性質を継承するように変更するものです。

move_viewinput_rangeではなくなることで、input_rangeでは利用できないアルゴリズムで利用できるようになります。move_viewの性質上、ranges::minなど、アルゴリズムの中には意図通りに動作しないものがあります。

void f(vector<string> words) {
  // この提案以前はinput_range
  auto r1 = words | views::move;

  // 他の全ての要素もムーブされている
  auto min_str = std::ranges::min(r1);
}

とはいえこのことは、move_viewの問題を回避するためにviews::transformを使用したときにも起こります。

void f(vector<string> words) {
  // 現在でもrandom_access_range
  auto r2 = words | views::transform([](string& s) -> string&& { return std::move(s); });
  
  // 他の全ての要素もムーブされている
  auto min_str = std::ranges::min(r2);
}

つまりは、move_viewのカテゴリを変更したことによって生じる問題は、move_viewのカテゴリがinput_rangeであることから生じる問題を回避する場合にも生じます(これは現在でも起こり得ます)。ここでの一番の問題は、このような回避策を適用した方が一部の場合でパフォーマンスが良いため、このような構文が推奨されてしまう事です。この問題は標準ライブラリで解決されるべきです。

P2521R0 Contract support -- Working Paper

C++に最小の契約プログラミングサポートを追加する提案。

この提案は、現在アクティブな契約関連の提案であるP2388R4とP2461R1をベースとして、SG21でコンセンサスが得られているものとそうでないものとを明確にするためのものです。

従って、P2388R4とP2461R1で議論されていない事は含まれておらず、多くの部分がP2388R4の内容に基づいています。その上で次の部分を未解決の問題としています

  1. 構文の選択
  2. 契約術後の副作用除去と重複
  3. 事後条件で参照される非参照引数の扱い

これらのことをシンプルに書き下していく事で、SG21におけるコンセンサスを文書化しています。

P2523R0 Request for re-inclusion of std::hive proposal in C++23

std::hiveC++23に含めるよう求める提案。

std::hive(P0447)はC++26への導入を目指して作業されることになっており、C++23に導入されないことが決定されています。この提案はこれを覆そうとするものです。

しかし、C++23のdesign freezeがすぐそこ(太平洋時間の2022/2/7)に迫っており、P0447が大きいこともあってこれはすでに否決(LEWG議長の裁量による?)されています。

P2524R0 SG14: Low Latency/Games/Embedded/Finance/Simulation 2020/12/09-2022/01/12

SG14(Low Latency/Games Dev/Embedded/Financial/Trading/Banking/Simulation等に関するStudy Group)の2020年12月から2022年1月までの間の9回のミーティングの議事録。

P2525R0 SG19: Machine Learning Meeting Minutes 2020/12/10-2022/01/13

SG19(機械学習に関するStudy Group)の2020年12月から2022年1月までの間の10回のミーティングの議事録。

P2527R0 std::variant_alternative_index and std::variant_alternative_index_v

std::variantに対して、型からそのインデックスを取得するための方法を追加する提案。

std::variantに対するstd::get()/std::get_if()はインデックスもしくは型を指定して値を取り出すことができ、std::variant_alternativeによってインデックスに対応する型を取得することができます。しかし、型に対応するインデックスを取得する方法は用意されていません。

この提案は、std::variant_alternative_indexとしてそれを追加しようとするものです。

インデックス指定に従うシリアライズ/デシリアライズを行うサンプル。

struct Reset { };
struct Close { };
struct RunCommand { std::string command; };

using Action = std::variant<Reset, Close, RunCommand>;

void serializeAction(const Action& action, std::vector<uint8_t>& buffer) {
  // 現在のvariantのインデックスを先頭に保存
  buffer.push_back(action.index());

  if (auto* runCommand = std::get_if<RunCommand>(&action); runCommand != nullptr) {
    serializeString(runCommand->command, buffer);
  }
}

std::optional<Action> deserializeAction(std::span<const uint8_t> source) {
  if (source.size() == 0) {
    return std::nullopt;
  }

  // 先頭にあるインデックスによるswitch分岐
  switch (source[0]) {
  case std::variant_alternative_index_v<Reset, Action>:
    return Reset { };
  case std::variant_alternative_index_v<Close, Action>:
    return Close { };
  case std::variant_alternative_index_v<RunCommand, Action>:
    return RunCommand { deserializeString(source.subspan(1)) };
  }

  return std::nullopt;
}

このサンプルは、Webkitにおいてstd::variant_alternative_index相当のものを実装するきっかけとなったもののようです。

おわり

この記事のMarkdownソース