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

文書の一覧

SG22のWG14からのものを除いて、全部で102本あります。

P0290R3 apply() for synchronized_value

P0290R4 apply() for synchronized_value

ミューテックスを用いた値への同期アクセスをラップするユーティリティの提案。

この提案のsynchronized_value<T>Tの値とミューテックスをペアで保持する型で、保持するTの値へのアクセスを保持するミューテックスによって同期化するものです。

// synchronized_valueの宣言例
namespace std::experimental::inline concurrency_v2 {

  template<class T>
  class synchronized_value {
  public:
      synchronized_value(synchronized_value const&) = delete;
      synchronized_value& operator=(synchronized_value const&) = delete;

      template<class ... Args>
      synchronized_value(Args&& ... args);

  private:
      T value;   // exposition only
      mutex mut; // exposition only
  };

  template<class T>
  synchronized_value(T) -> synchronized_value<T>;
}

読み出しと書き込みを直接サポートしていませんが、それがあったとしてもそれだけならstd::atomicで十分であり、存在理由がありません。

ミューテックスを用いた値アクセスの同期化がアトミックアクセスと異なるところは、ミューテックスのロックと解放によって1度のアクセスを超えた範囲のクリティカルセクションを確保できることにあります。synchronized_value<T>はそのためにapply()メンバ関数を提供します。

// apply()の宣言例
namespace std::experimental::inline concurrency_v2 {

  template<class F,class ... ValueTypes>
  invoke_result_t<F, ValueTypes&...> apply(F&& f, synchronized_value<ValueTypes>&... values);
}

apply()は、1つ以上のsynchronized_value<T>とそれと同じ数のT...の値から呼び出し可能なfを受けて、f(T...)の呼び出し前後でvaluesの全てのミューテックスのロックと解放を自動でかつ適切に行うことでT...の値に対するクリティカルセクション内でfを実行します。

synchronized_value<std::string> s;

// 単純な読み出しの例
std::string read_value() {
  // apply()に渡した関数はsのmutexによるクリティカルセクション内で実行される
  return apply([](auto& x){ return x; }, s);
}

// 単純な書き込みの例
void set_value(const std::string& new_val) {
  // apply()に渡した関数はsのmutexによるクリティカルセクション内で実行される
  apply([&](auto& x){x=new_val;}, s);
}

synchronized_value<T>の保持する値へのアクセスはこのapply()を通してのみ行うことができ、使用間違いを防ぐために構築以外の操作は提供されていません。

ミューテックスを用いたアクセスの同期化においては同期対象の値とミューテックスオブジェクトがセットで扱われることになることが多いですが、コード上での記述はどうしても複数の変数宣言に分かれてしまうためセットは意味的なものとしてしか表現できません。また、実際のクリティカルセクションの作成においても、std::lock_guardなどである程度自動化できるとはいえ、少なくともロックは手動で行う必要があり、その際に使用するミューテックスも明示的に指定しなければなりません。

synchronized_value<T>apply()を用いると、同期対象の値とそのためのミューテックスのペアを型によって表現することができ、クリティカルセクションの作成においても手動でミューテックスを触る必要がなくなります。これによって、コードの可読性向上や記述ミスの防止などを図ることができます。

提案文書よりサンプルコード

より複雑な処理の例

// 何かメッセージのキューを同期化する
synchronized_value<std::queue<message_type>> queue;

void process_message(){
  std::optional<message_type> local_message;

  // グローバルなキューからメッセージを1つ読み出してくる
  apply([&](std::queue<message_type>& q) {
      if(!q.empty()) {
        // 先頭メッセージ取り出し(クリティカルセクション)
        local_message.emplace(std::move(q.front()));
        q.pop_front();
      }
  }, queue);
  
  // 読み出しに成功していたら、それを使って何かする
  if(local_message) {
    do_processing(local_message.value());
  }
}

複数の値を処理する例

// 口座間でお金を転送する例
void transfer_money(synchronized_value<account>& from_, // 転送元
                    synchronized_value<account>& to_,   // 転送先
                    money_value amount)                 // お金オブジェクト
{
  apply([=](auto& from, auto& to) {
    // 引き出して
    from.withdraw(amount);
    // 預け入れ
    to.deposit(amount);
  }, from_, to_);
}

このような複数のsynchronized_value<T>に対する操作では特に、複数のミューテックスを用いたアクセスにおけるデッドロックを回避できるというメリットもあります。

このように、複数のsynchronized_value<T>に対して何か関数を適用するという形は、std::tupleに対するstd::apply()とよく似たものなので、名前もそこから取っています。

この提案はConcurrency TS v2向けに提案されており、2月のIssaquah会議でConcurrency TS v2に採択されています。

P0447R21 Introduction of std::hive to the standard library

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

以前の記事を参照

このリビジョンでの変更は、ブロック容量の制限がstd::hiveオブジェクト間でコピーされる条件についてDesign Decisionsセクションに追記し提案する文言に正式に記載した、Appendix Fの修正、Design Decisionsセクションのタイトルを修正、などです。

P0493R4 Atomic maximum/minimum

std::atomicに対して、指定した値と現在の値の大小関係によって値を書き換えるmaximum/minimum操作であるfetch_max()/fetch_min()を追加する提案。

以前の記事を参照

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

  • 使用していなかったベンチマークの削除
  • メンバ関数についてフリースタンディングであることを明記
  • fetch_max(), fetch_min()remarkを追加
  • ポインタの比較についてnoteを追加
  • ポインタの操作について説明を追記

などです。

この提案はC++26をターゲットして、LWGによるレビューを終えています。次の全体会議で投票にかけられる予定です。

P0792R13 function_ref: a non-owning reference to a Callable

P0792R14 function_ref: a non-owning reference to a Callable

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

以前の記事を参照

このリビジョンおよびR13での変更は、LWGのフィードバックによる文言の調整と、フリースタンディング指定の修正などです。

この提案はすでにLWGのレビューをパスして、次の全体会議にかけられることが決まっています(C++26ターゲットです)。

P0870R5 A proposal for a type trait to detect narrowing conversions

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

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックの反映、変換元が定数式であることを考慮しないという意図的な選択についての解説を追記したことなどです。

この提案はLEWGのレビューをパスしたLWGに転送されています。

P0876R12 fiber_context - fibers without scheduler

スタックフルコルーチンのためのコンテキストスイッチを担うクラス、fiber_contextの提案。

以前の記事を参照

このリビジョンでの変更は、fiber_contextからstop_tokenサポートを取り除いたこと、呼び出し側の提供する未初期化メモリ領域をファイバーのコールスタックとして使用するためのコンストラクタを追加したことです。

stop_tokenサポートが取り除かれたのはそれについて実装の懸念が生じたためのようです。各ファイバー(そのコールスタック)自体はfiber_contextの寿命とは無関係な永続的なエンティティですが、fiber_contextはそうではありません。fiber_contextの新しいオブジェクトは常に中断状態で生成され、これによってファイバーを一時停止するコードは関連するstop_source共有状態を見つけられなくなります。

stop_tokenを使用したいユーザーは、自身でstop_sourceを管理した上でそこから取得したstop_tokenfiber_contextに渡すラムダ式に渡しておけばよく、fiber_contextで直接サポートする必要はない、とのことです。

// fiber_contextの宣言例
namespace std::experimental::inline concurrency_v2 {
  class fiber_context {
  public:
    fiber_context() noexcept;

    template<typename F>
    explicit fiber_context(F&& entry);

    // コールスタック配置に使用するメモリ領域を受け取るコンストラクタ
    template<typename F, size_t N>
    explicit fiber_context(F&& entry, span<byte, N> stack);

    ~fiber_context();

    fiber_context(fiber_context&& other) noexcept;
    fiber_context& operator=(fiber_context&& other) noexcept;
    fiber_context(const fiber_context& other) noexcept = delete;
    fiber_context& operator=(const fiber_context& other) noexcept = delete;

    fiber_context resume() &&;

    template<typename Fn>
    fiber_context resume_with(Fn&& fn) &&;

    bool can_resume() noexcept;
    explicit operator bool() const noexcept;
    bool empty() const noexcept;

    void swap(fiber_context& other) noexcept;
  };
}

P1061R4 Structured Bindings can introduce a Pack

構造化束縛可能なオブジェクトをパラメータパックに変換可能にする提案。

以前の記事を参照

このリビジョンでの変更は、CWGのレビューに伴うフィードバックを反映したことです。

P1708R7 Basic Statistics

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

以前の記事を参照

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

  • オーバーロードを活用して、重み付きと重みなしの関数を呼び分けるようにした
  • 導出の表示を簡略化
  • 歪度と尖度の導出を追加
  • 文言の調整

などです。

P1715R1 Loosen restrictions on "t" typedefs and "v" values.

std::conditional_tの定義を修正する提案。

C++14で導入されたconditional_tは、std::conditional<B, T, F>::typeに簡易にアクセスするためのものです。それは次のような実装になるように指定されています

namespace std {
  template <bool B, class T, class F>
  struct conditional {
    using type = …;
  };

  template <bool B, class T, class F>
  using conditional_t = typename conditional<B, T, F>::type; // C++14
}

conditional_tconditional<B, T, F>::typeエイリアスでなくてはならないわけですが、このように指定していることがconditional_tのより効率的な実装を妨げています。

conditional_t<B, T, F>の現在の実装では、テンプレートパラメータB, T, F毎にstd::conditionalインスタンス化が必要となります。3つのパラメータのうちいずれか1つが異なっているだけで、std::conditionalの新しいインスタンス化が必要となります。これは、conditional_tを多用する環境において、コンパイル時間の増大やデバッグ情報の肥大化を招きます。

例えば、conditional_tの実装を次のように変更したとすると

template<bool _Bp>
struct __select;

template<>
struct __select<true>  {
  template<typename _TrueT, typename _FalseT>
  using type = _TrueT;
};

template<>
struct __select<false> {
  template<typename _TrueT, typename _FalseT>
  using type = _FalseT;
};

template <bool _Bp, class _TrueT, class _FalseT>
using conditional_t = typename __select<_Bp>::template type<_TrueT, _FalseT>;

この実装では、conditional_t<B, T, F>が異なるパラメータの組み合わせで何度使用されても、インスタンス化されるのは__select<true>__select<false>の2つのクラステンプレートだけです(エイリアステンプレートはインスタンス化されないため)。conditional_tがどれだけ多用されようともこの2つのクラステンプレートがインスタンス化された後はその定義を使いまわすことができ、最終的な型の決定においてはエイリアステンプレートの実引数による置換だけしか発生しません。これによって、コンパイラのメモリ使用量を抑えるだけでなく、デバッグのために出力するデバッグ情報に記録される型情報も削減することができます。

筆者の方の(Googleにおける)調査では、特にTMPが多用されているファイルに対してclangが出力するデバッグ情報の1部として記録されているクラス名の約1/6がstd::conditionalインスタンス化で占められていたそうです。

この提案は、これらの理由から、conditional_tの実装をstd::conditionalから切り離し、より効率的な実装を選択可能にするものです。

ただし、Google社内でこのような変更を行ったところ、この変更は観測可能であることが判明しています。

// 最初にこのように宣言され
template<bool B>
long to_long(conditional_t<B, int, long> param);

...

// その後でこのように定義されている(おそらく記述ミス)
template<bool B>
long to_long(typename conditional<B, int, long>::type param) {
  return param;
}

この時、conditional_tstd::conditionalによって定義されていない場合、この2つの関数宣言は異なるシグネチャを持つことになり、to_long()の呼び出しは2つのオーバーロードの間で曖昧となりコンパイルエラーを起こします。

ただし、この例が記述ミスを含むものであるように、このような例はかなり稀であるため実際の影響は非常に小さいと思われます。

P1759R5 Native handles and file streams

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

以前の記事を参照

このリビジョンでの変更は、ほぼ設計と提案する文言のみに文書を絞ったこと、対象の型(.native_handle())を持つ型)としてstd::stacktrace_entryを考慮し、それを他のものと比較する記述を追記した事です。

C++23で追加されたスタックトレースの1行を表す型であるstd::stacktrace_entryもまた、その実装のハンドルを取得するために.native_handle()を持っています。ここから得られるネイティブハンドル型とstd::threadのそれとを比較して、ネイティブハンドル型について次のような要求を追加することを提案しています

  • native_handle_typesemiregularでありトリビアルコピー可能かつstandard_layout
  • ファイルのネイティブハンドルが何を意味しどのように動作するかを定義する

このことは、この提案の対象のファイルハンドルのnative_handle_typeに対してのみ要求されています。

P1854R4 Making non-encodable string literals ill-formed

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

以前の記事を参照

このリビジョンでの変更は、CWGのフィードバックを適用した事です。

この提案は、CWGのレビューを終えて次の全体会議で投票にかけられる予定です。

P1928R3 Merge data-parallel types from the Parallelism TS 2

std::simd<T>をParallelism TS v2から標準ライブラリへ移す提案。

以前の記事を参照

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

  • hmin()/hmax()の代替案を提案
  • <bit>との一貫性のために、simd_maskの削減を議論。曖昧さを避けるためによりよい名前を募集
  • some_ofを削除
  • simd_maskに単項~を追加
  • マスク付きオーバーロードの名前と引数順序について議論と回答を追加
  • fixed_size/resize_simdのNTTPをintからsize_tへ変更
  • ロード/ストアの変換について議論を追加
  • P2509R0を関連提案として追加
  • ロード/ストアをポインタからcontiguous_iteratorへと一般化
  • element_referenceの過剰な制約についてOpen questionsに移動

などです。

この提案は、LEWGのレビューを通過し、LWGに転送するための投票待ちをしています。

P2022R0 Rangified version of lexicographical_compare_three_way

std::lexicographical_compare_three_wayのRange版を追加する提案。

std::lexicographical_compare_three_wayは、与えられた2つのイテレータ範囲を辞書式順序で三方比較するイテレータアルゴリズムです。この関数はC++20で一貫比較とともに導入されたこともあり、対応するRangeアルゴリズムは用意されていませんでした。

この提案は、それを追加するものです。

// Rangeを受け取るものの宣言例
namespace std::ranges {

  template<
    ranges::input_range R1,
    ranges::input_range R2,
    class Comp = compare_three_way,
    class Proj1 = identity,
    class Proj2 = identity
  >
    requires is-lexicographical-compare-three-way-result-ordering<
               iterator_t<R1>, iterator_t<R2>, Comp, Proj1, Proj2
             >
  constexpr auto ranges::lexicographical_compare_three_way(
    R1&& r1,
    R2&& r2,
    Comp comp = {},
    Proj1 proj1 = {},
    Proj2 proj2 = {}
  ) -> common_comparison_category_t<
         decltype(
         comp(proj1(ranges::begin(r1)), proj2( ranges::begin(r2)))
         ),
         strong_ordering
       >;

}

他のRangeアルゴリズムと同様に、イテレータ範囲を受け取るものとそれをrangeで受け取るものの2種類が用意され、射影操作をサポートしています。is-lexicographical-compare-three-way-result-orderingというのは説明専用のbool定数の変数テンプレートで、それぞれの範囲の要素と比較関数オブジェクトcompによる比較結果が比較カテゴリ型を返すことを調べるものです。

P2047R6 An allocator-aware optional type

Allocator Awarestd::optionalである、std::pmr::optionalを追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言やHTMLの調整です。

この提案はこれ以上議論されません。

P2159R1 A Big Decimal Type

Numbers TS (P1889R1)に対して、10進多倍長浮動小数点型std::decimalを追加する提案。

以前の記事を参照

このリビジョンでの変更はよくわかりませんが、SG6のレビューではこの提案の主張するユースケースに関心がないとして、これ以上議論しないことになったようです。

P2300R6 std::execution

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

以前の記事を参照

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

  • 修正
    • get_completion_signaturesは一貫性のために、connectで使用されるものと同様のpromise型で待機可能性をテストするようにした
    • コルーチンpromise型は直接クエリするものではなく、環境プロバイダ(environment provider)(get_env()を実装するもの)であることを明確化
  • 機能拡張
    • senderクエリはsenderget_attrs()に渡すことによってアクセスされる、個別のクエリ可能な属性オブジェクトに移動される
      • senderコンセプトは、get_attrs()を必要とするように再表現され、ある型が特定の実行環境内でsenderであるかをチェックするsender_in<Snd, Env>コンセプトから分離されている
    • プレースホルダno_envdependent_completion_signatures<>は不要になったため削除された
    • 入力senderget_attrs()を呼び出した結果を永続化するために、ensure_startedsplitを変更
    • schedulerreceiverコンセプトの定義を制約の再帰を回避するように修正
    • sender_ofコンセプトをより人間工学的で汎用なものに再表現
    • エイリアステンプレートvalue_types_of_terror_types_of_tの指定、及び変数テンプレートsends_doneを、新しい説明専用エイリアステンプレートgather-signaturesを使用することで簡潔にした

などです。

この提案での大きな変更は、senderが直接クエリ可能ではなくなり、get_attrs()を介して個別のクエリCPOによってクエリを行うように変更されたことです。以前は、schedulerreceiversenderの3つのものは全て直接クエリ可能でした(クエリCPOに直接渡せた)。R4でreceiverのクエリは別の環境オブジェクトを介する形に変更され、それはreceiverget_env()に渡して取得できます。環境オブジェクトを介するようにしたのは、型の再帰が起こるのを回避するためでした。

このリビジョンでは、senderに関しても同様に属性オブジェクトを介して各種クエリCPOに渡して各種特性をクエリするように変更されました。これは、splitsecure_startedアルゴリズムの設計上の問題解決のためのようです。

クエリCPOをQsenderオブジェクトをsreceiverオブジェクトをr、クエリのための追加の引数をargs...とすると、sender/receiverに対するクエリは次のように行えます

// senderのクエリ
Q(get_attrs(s), args...);

// receiverのクエリ
Q(get_env(r), args...);

Qとしては、std::get_allocator(関連づけられたアロケータを取得)やstd::get_stop_token(関連づけられたstop_tokenを取得)、std::execution::get_scheduler(関連づけられたschedulerを取得)などがあります。クエリとは、schedulerreceiversenderなどに対してその実行環境に関する情報や実行時に使用するものなどを問い合わせ、取得するための操作です。

このリビジョンでもまだschedulerは直接クエリ可能であり、それを変更しようとする動機は今のところ無いようです。またそのほかに、operation_statereceiversenderconnectして得られるもの)もこのリビジョンで直接クエリ可能とされています。

この提案は現在LWGでのレビュー中です。

P2308R0 Template parameter initialization

非型テンプレートパラメータの初期化に関しての規定を充実させる提案。

現在に至るまで、非型テンプレートパラメータ(NTTP)の初期化に関しての規定は、指定された初期化子がNTTPの型に変換可能であること、及び、その変換は定数式であること、くらいしか指定されていませんでした。それでも、C++17まではNTTPに取れるのは一部の組み込み型の値に限られていたたためあまり問題にはならなかったようです。

しかし、C++20から非型テンプレートパラメータとしてクラス型のオブジェクトを扱うことができるようになりました。NTTPとして扱えるクラス型には制限があるものの、コンストラクタを持つことができる他ポインタ型のメンバを持つこともできます。すると、左辺値NTTPをとるクラステンプレートの初期化時にそのアドレスを調べることができ、それによってある種のパラドックスが発生します。

template<auto n>
struct B { /* ... */ };

struct J1 {
  J1* self = this;
};

B<J1{}> j1; // ??

このJ1自体はNTTPで使用可能なクラス型で、その初期化も問題なさそうに思えます。しかし、J1::selfthisによってデフォルト初期化されており、J1{}とすると初期化にあたって自身のアドレスを要求します。普通の変数としておいた場合などではこれは問題にはならないのですが、ことNTTPだとこれが深刻な問題となります。これは簡単に言えば、J1thisを決めるためにはまずそのNTTPを持っているテンプレートがインスタンス化されなければならず、テンプレートがインスタンス化するためには全てのNTTPの初期化が完了しなければなりません。

これは、テンプレートはインスタンス化に際して(そのオーバーロードの適切な処理、あるいはODRのために)テンプレートパラメータ毎の同一性を判定する必要があり、NTTPの場合はその値の同一性によって判定され、クラス型のNTTPの場合その型名及び全てのメンバの値によって同一性が判定され、ポインタ型の同一性はそのアドレスによって判定されるためです。

現在の(C++20時点の)規定はこのようなことを考慮しておらず、このNTTP初期化に伴う矛盾を解決することができません。

この提案は、この問題を含むNTTPの初期化に関する規定を適切に書き直すことで、いくつかのコア言語Issueを解決するものです。上記問題の他にも、{}初期化がクラス型NTTPで使えるのかどうか不透明な問題も解決を図っています。

この提案によるアプローチではまず、テンプレート実引数で使用可能な構文(template-argument)として{}初期化子(braced-init-list)を許可します。

その上で、プレースホルダ型(auto)あるいはテンプレートパラメータを推論する必要のある形で宣言(C++17 CTAD)されているNTTPの型の推定について次のように変更します。そのような推論を必要とする型名/プレースホルダauto)を仮にDとすると

// 現在
D x = template-argument;

// この提案
D x = E;

ここで、Etemplate-argumentかデフォルト引数に指定されている{}初期化子のいずれかの式です。このような仮のxの初期化式を構成し、この時にxの型として推論される型をそのNTTPの型(仮にTとする)として推定します。

このようにすることで、NTTPのデフォルト引数も含めてNTTPの実引数として{}初期化子が使用できることを明示的にしています。

次に、NTTPの初期化においては、まず模範(exemplar)となる値をその初期化式(NTTPの実引数A)から決定します。模範となる値の型UTもしくはTが参照型ならその参照される型として

  • Uがクラス型ではなく、Aが波括弧初期化ではない場合
    • 模範となる値は、定数式でATへ変換した値
  • それ以外の場合
    • const U v = A;と初期化される一時変数vを導入して
    • 模範となる値は、v

そして、NTTPは模範となる値からコピー初期化(copy-initialization)されます。

Uがクラス型の場合、NTTPの同一性は模範となる値vによって決定されます。

このように、NTTPの初期化のための一時変数(模範となる値)を初期化して、それを用いてNTTPの同一性を判定し、またNTTPの値はそこからコピーして初期化することで、まずテンプレートの同一性が判定されてから初期化が起こるようにするとともに、上記J1メンバselfのような例では一時オブジェクトのアドレスを保持してしまうためエラーとなるようになります。

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

template<int i>
struct C { /* ... */ };

C<{ 42 }> c1;  // OK、波括弧初期化の許可

struct J1 {
  J1* self = this;
};

B<J1{}> j1;  // error: J1::selfが一時オブジェクトのアドレスを取っている

struct J2 {
  J2* self=this;
  constexpr J2() {}
  constexpr J2(const J2&) {}
};

B<J2{}> j2;  // error: NTTPの初期化後に模範となる値と異なる値が生成される(コピーコンストラクタの呼び出しによる)

P2338R4 Freestanding Library: Character primitives and the C library

<charconv>std::char_traitsをはじめとするいくつかのヘッダをフリースタンディングライブラリ指定する提案。

以前の記事を参照

このリビジョンでの変更は、非推奨とされたerrc/errnoを取り除いたことです。

この提案は既にLWGでのレビューを終えており、次の全体会議で投票にかけられる予定です。

P2355R1 Postfix fold expressions

可変長テンプレートの畳み込み式において、() []の2つの演算子を使用可能にする提案。

以前の記事を参照

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

  • pack[...[abc]]のような一貫性のない畳み込みを禁止
  • インデックスの例の構文を修正
  • pack[...][expr]exprにおける式としてassign-or-braced-init-listの代わりにinitializer-clauseを指定

などです。

P2361R6 Unevaluated strings

コンパイル時にのみ使用され、実行時まで残らない文字列リテラルについての扱いを明確化する提案。

以前の記事を参照

このリビジョンでの変更は、CWGレビューに伴うフィードバックの反映です。

この提案は既にCWGのレビューを終えており、次の全体会議で投票にかけられる予定です。

P2363R5 Extending associative containers with the remaining heterogeneous overloads

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

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを反映した事です。

この提案はLWGのレビューを完了しており、次の全体会議で投票にかけられる予定です。

P2406R3 Add lazy_counted_iterator

P2406R4 Add lazy_counted_iterator

P2406R5 Add lazy_counted_iterator

std::counted_iteratorを安全に使用可能にする提案。

以前の記事を参照

R3での変更は

  • counted_iteratorとは異なり、iterator_concept/iterator_categoryを定義した

R4での変更は

  • input_or_output_iteratorからinput_iteratorへの変更忘れを適用
  • 2つのイテレータが同じシーケンスを参照する場合の単純化
  • 後置++の定義を単純化
  • 追加の設計や疑問点をまとめたセクションを追加
  • 実装経験リンクの追加

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

  • LEWGでの投票結果の追記
  • 代替設計について追記
  • 機能テストマクロの追加

などです。

この提案は、P2799のソリューションによって置き換えられるようで、議論は停止されています。

P2495R2 Interfacing stringstreams with string_view

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

以前の記事を参照

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

  • LWGのガイダンスに従って、クラスごとにオーバロードされたコンストラクタの文言をマージした
  • typenameの代わりにclassを使用する
  • 文言のEffects節のスタイルの調整

などです。

この提案は現在C++26をターゲットとしてLWGのレビュー中です。

P2497R0 Testing for success or failure of charconv functions

std::to_chars_result/std::from_chars_resultに成否を簡単に問い合わせるためのboolインターフェースを追加する提案。

std::to_chars_result/std::from_chars_resultstd::to_chars()/std::from_chars()の結果型で、std::errcとポインタの2つのメンバを持っています。

多くの場合、それらの結果を構造化束縛で受けて、メンバのerrcオブジェクトをstd::errc{}(デフォルト値、成功を表す)と比較することで処理の成否を判断するコードが書かれます。

// 42を文字列へ変換し範囲[p, last)へ書き込む
auto [ptr, ec] = std::to_chars(p, last, 42);

if (ec == std::errc{}) {
  // 成功時の処理
  ...
}

std::errcは単なるスコープ付き列挙型(enum class)でしかなく、これ以上に良い書き方は現状ありません。しかし、この比較は少し冗長かつ煩雑で、より読みやすい成功判定方法が求められました。

この提案はそのために、両結果型にoperator bool()を追加して改善を図るものです。

// 42を文字列へ変換し範囲[p, last)へ書き込む
auto [ptr, ec] = std::to_chars(p, last, 42);

if (ec) {
  // 成功時の処理
  ...
}

// あるいは
if (std::to_chars(p, last, 42)) {
  // 成功時の処理
  ...
}

// form_chars()も同様
if (int v; std::from_chars(p, last, v)) {
  // 成功時の処理
  ...
}
namespace std {
  struct to_chars_result {
    char* ptr;
    errc ec;
    
    friend bool operator==(const to_chars_result&, const to_chars_result&) = default;
    
    // 追加
    constexpr explicit operator bool() const noexcept { return ec == errc{}; }
  };

  struct from_chars_result {
    const char* ptr;
    errc ec;

    friend bool operator==(const from_chars_result&, const from_chars_result&) = default;
    
    // 追加
    constexpr explicit operator bool() const noexcept { return ec == errc{}; }
  };
}

この提案はすでにLWGのレビューを終えており、C++26ターゲットとして次の全体会議で投票にかけられる予定です(事務手続きのミスによりC++23にまにあわなかったとのこと・・・)

P2521R3 Contract support -- Record of SG21 consensus

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

以前の記事を参照

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

  • タイトルの変更
  • SG21における副作用の許可に関する決定を追記
  • 契約条件式からの例外送出についての問題を追記

などです。

2月のIssaquah会議においてSG21は、契約条件に含まれる副作用を認めるとともに、プログラムがそれに依存しないように、契約条件は0回以上呼ばれる可能性があるとすることを決定したようです。これによって、これ以外の副作用の方針(副作用の完全禁止、評価の内側に止まっているもののみ許可、など)は否決されました。

また、現在のところ、契約条件がその評価時に例外を投げた場合にどう扱うかは決まっていません。ある提案ではstd::terminate()を呼び出して終了することが提案されていましたが否決されており、別の提案では例外送出を契約違反として扱うことが提案されています。

P2527R2 std::variant_alternative_index and std::tuple_element_index

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

以前の記事を参照

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

P2545R3 Why RCU Should be in C++26

標準ライブラリにRead-Copy-Update(RCU)サポートを追加する提案。

以前の記事を参照

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

  • メモリリーク検出機の使用についてユーザーガイドを追加
  • 非同期コードとの対話のためのユーザーガイドを追加
  • rcu_obj_baseの説明から「クライアントが提供するテンプレート引数」という言葉を削除
  • rcu_obj_baseに5つのコンストラクタ、代入演算子、デストラクタを追加
  • rcu_obj_baseのテンプレートパラメータはrcu_obj_baseが参照される前に完全型でなければならないようにした
  • .retire(), .unlock(), rcu_retire()に関するコメントをNoteに移動
  • そのた文言の調整や改善

などです。

この提案はLEWGでのレビューを終えて、LWGに転送されています。

P2558R2 Add @, $, and ` to the basic character set

@ $ `の3種類の文字をソースコードの基本文字集合に追加する提案。

以前の記事を参照

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

この提案は既にCWGのレビューを終えて、次の全体会議で投票にかけられる予定です(C++26ターゲットです)。

P2572R1 std::format() fill character allowances

std::formatにおいて、文字列のアライメント(左寄せ、中央寄せ、右寄せ)の際に空白を埋める文字として使用可能な文字を制限する提案。

以前の記事を参照

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

  • 既存実装の振舞いとして、GCC13での<format>実装による出力(エラー)の例を追加
  • フィールド幅の単位(field width unit)、最小フィールド幅(minimum field width)、推定フィールド幅(estimated field width)、パディング幅(padding width)の正式な定義の追加
  • ↑のための調整
  • alignオプションとの一貫性向上のため、0オプションの文言の調整
  • std-format-specという文法要素を参照するための一貫した用語の導入
  • 22.14.2.2 [format.string.std]/11に対して変更していた、コードポイントという用語のUCSスカラ値への変更を削除
  • 文言変更の意図を明示的にするためにドラフトメモを追加
  • その他フィードバックの適用

などです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2588R3 Relax std::barrier phase completion step guarantees

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

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックに基づく文言の修正、機能テストマクロの追加、Annex Cセクション(C++20との非互換)の追記、などです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2591R3 Concatenation of strings and string views

std::stringstd::string_view+で結合できるようにする提案。

以前の記事を参照

このリビジョンでの変更は読みやすさの改善のみです。

P2592R3 Hashing support for std::chrono value classes

<chrono>の時間や日付を表す型に対してハッシュサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのレビュー受けての文言の修正です。

この提案はLWGのレビューを終えて、次の全体会議で投票にかけられる予定です。

P2593R1 Allowing static_assert(false)

static_assert(false)がテンプレートの実体化前にエラーとならないようにする提案。

以前の記事を参照

このリビジョンでの変更は、これを許可するためのセマンティクスとして考えられる他の方法を追記したことです。

この提案は、2月のIssaquah会議で全体投票をパスしてWDに適用されており、C++11へのDRとなっています。

P2594R1 Slides: Allow programmer to control coroutine elision (P2477R3 Presentation))

P2477(コルーチンの動的メモリ確保章竜最適化の制御のための機能の提案)の解説スライド

このリビジョンでの変更はおそらく、P2477の更新に伴う内容の更新です。

P2609R2 Relaxing Ranges Just A Smidge

P2609R3 Relaxing Ranges Just A Smidge

射影(プロジェクション)を取るアルゴリズムについて、その制約を緩和する提案。

以前の記事を参照

R2での変更は、indirect_value_tがネストした射影をハンドルできることを明確にしたことなどです。

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

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2616R4 Making std::atomic notification/wait operations usable in more situations

std::atomicnotify_one()wait()操作を使いづらくしている問題を解消する提案。

以前の記事を参照

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

  • 既存のnotification/wait操作の非推奨化をやめた
  • notify_token::notify_one/allの生存期間に関する言及をNoteに移動
  • P2689R1の文言を含めるようにした(採択される場合)

などです。

P2621R2 UB? In my Lexer?

字句解析するだけで未定義動作を引き起こすものについて、未定義ではなくする提案。

以前の記事を参照

このリビジョンでの変更は、CWGのレビューによるフィードバックの適用です。

この提案は既にCWGのレビューを終えており、C++26に向けて次の全体会議で投票にかけられる予定です。

P2641R2 Checking if a union alternative is active

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

以前の記事を参照

このリビジョンでの変更は、提案する機能をunionのアクティブメンバ検出に限ったものではなく、定数式であるオブジェクトが生存期間内にあるかどうかを調べるstd::is_within_lifetime()に変更されたことです。

unionの特定のメンバがアクティブメンバであるかを問い合わせるということは、オブジェクトが生存期間内にあるかを問い合わせることの特殊なケースです。以前のstd::is_active_member()をそこまで一般化させても実装可能性やこの提案の元の同期に対してデメリットもないと判断されたため、アクティブメンバのチェックから特定オブジェクトの生存期間チェックに対象を広げ、関数名もstd::is_within_lifetime()に変更されました。

とはいえ、使用感はほぼ同じで宣言も変化しません。

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

提案にあるOptBoolの例も以前とこの関数名以外変わりありません。

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_within_lifetime(&b); // 定数式ではbがアクティブメンバであるか(生存期間内にあるか)を問い合わせる
    } else {
      return c != 2;  // 実行時は今まで通り
    }
  }

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

P2652R2 Disallow user specialization of allocator_traits

std::allocator_traitsのユーザーによる特殊化を禁止する提案。

以前の記事を参照

このリビジョンでの変更は、allocate_at_least()allocator_traitsの非テンプレートメンバではなくネストしたテンプレートメンバとなっていたのを修正したことです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2655R2 common_reference_t of reference_wrapper Should Be a Reference Type

P2655R3 common_reference_t of reference_wrapper Should Be a Reference Type

std::reference_wrapper<T>T&の間のcommon_referenceT&になるようにする提案。

以前の記事を参照

R2での変更は、CV修飾されたプロクシ型のcommon_referenceに関する問題を修正したことなどです。

このリビジョン(R3)での変更は、新しい機能テストマクロ(__cpp_lib_common_reference)を追加し、古いものをリネーム(__cpp_lib_common_reference_wrapper)したことです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2656R2 C++ Ecosystem International Standard

C++実装(コンパイラ)と周辺ツールの相互のやり取りのための国際規格を発効する提案。

以前の記事を参照

このリビジョンでの変更は、SG15での投票を受けて、最初のISの目標を絞り、それらについての解説を追記したことです。

このリビジョンでの最初のISの目標は次の6項目です

  1. 定義
    • 標準の仕様を記述し、またその範囲を制限するための言葉や概念などの定義
  2. ビルドシステムとパッケージマネージャの相互運用のための基盤
    • ビルドシステムとパッケージマネージャが相互に対話するためのメッセージのフォーマットとインターフェースの仕様
  3. ファイル拡張子
    • ツールが認識し理解する必要のあるファイル拡張子の最小セットと、その役割
  4. イントロスペクション
    • ツールがサポートするEcosystem ISのバージョンを問い合わせ、回答するためのフォーマットとインターフェースの仕様
  5. ポータブルな診断メッセージのフォーマット
    • ツールが出力するエラーメッセージ等のユーザーに出力するメッセージのフォーマット
    • SARIFという形式を取り込むことを目指す
  6. コマンドラインの移植性
    • 異なるツールやプラットフォームの間で認識可能な、ツールコマンドを表現する共通言語
    • C++コンパイルの際の最適な通信手段となる、標準的な構造化応答ファイル形式を定義することを目標とする

これによって、示されているタイムラインの第一段階(計画の策定)はクリアしたことになりそうです。

P2663R1 Proposal to support interleaved complex values in std::simd

std::simdstd::complexをサポートできるようにする提案。

以前の記事を参照

このリビジョンでの変更は、SG1での関連する提案の投票結果を追記したことです。

P2664R1 Proposal to extend std::simd with permutation API

Parallelism TS v2にあるstd::simdに、permute操作のサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、SG1での関連する提案の投票結果を追記したことです。

P2670R1 Non-transient constexpr allocation

定数式で確保したメモリ領域を実行時に持ち越すことをできるようにする提案。

以前の記事を参照

このリビジョンでの変更は、propconst修飾子の代わりにpropconst指定子を説明し、それをメインの提案としたことです。

以前に提案されていたpropconst修飾子(qualifier)は、現在メンバ変数宣言に対してconstを指定する場所に置くことができ、そのポインタのconst性がdeep constとなるようにするものです。対して、propconst指定子(specifier)は、現在メンバ変数宣言に対してmutableを指定する場所に置くことができ、意味は同じになります。

指定子であることの利点は、多重ポインタに対してどこがpropconstなのかを指定できるようになることです。

propconst修飾子の場合、int propconst**int propconst* propconst*のように指定することができます。このとき、propconst int** pという宣言はpconstとしてアクセスされる際の意味として、次のような候補があります(propconstの役割から、const性は参照先のみを考慮する)

  • int const* const* (全てのレイヤでconst
  • int const** (1層目のみconst
  • int* const* (2層目のみconst

しかし、int const**int**に変換できないため、最後の選択肢のみが残ります。

その上で、std::vector<T>の実装を考えてみます。std::vector<T>::data() constT const *を返しますが、T自体がポインタ型(T*)の場合はT* const *を返します。これはT* constへのポインタであり、std::vector<T*>の要素であるポインタへのポインタかつ、要素がconst修飾されています。複雑ではありますが、意図通りになっています。

std::vector<T>の所有するストレージへのポインタ(T* begin_とする)をpropconst指定(propconst T* begin_)したとき、全てのレイヤにconstを付加するとすると、T const* const*が得られ、これは本来のT* cosnt*に変換できません。これによって、std::vector<T*> const後方互換を破壊し、実質的に使用できなくなってしまいます。

このことは、propconstconstを適用するのは1番外側のみであることを示唆しています。例えば、propconst int** pint* const*になり、propconst int*** qint** const*になります。

この場合に、簡単な行列型の実装を考えてみます。

struct Matrix3D {
  propconst int*** p;
  int n;

  constexpr ~Matrix3D() {
    for (int i = 0; i != n; ++i) {
      for (int j = 0; j != n; ++j) {
        delete [] p[i][j];
      }
      delete [] p[i];
    }
    delete [] p;
  }
};

この場合、Matrix3D::pint** const*として扱われます。すると、pそのものやp[i]は変化できませんが、真ん中のレイヤは変更可能になります(例えば、p[0][0] = new int(42);)。これによって、このクラスは定数式で使用することができません。

この例の場合は、外側のレイヤのみではなく一番内側を除く全てのレイヤでconstが必要になります。

宣言 外側のみconst 一番内側以外const
propconst int* int const* int const*
propconst int** int* const* int* const*
propconst int*** int** const* int* const* const*
propconst int**** int*** const* int* const* const* const*

しかし、std::vector<T**>ではT** const*となる必要があり、この方法もうまくいかないことがわかります。

結局、propconst1つでこれらのユースケースを全て満足することはできません。

そこで、propconst指定子によって、追加するconstのレイヤ数を指定できるようにすれば、ほぼ同じ構文によってどこまでconstを追加するのかを選択できるようになります。

宣言 意味
propconst int int
propconst int* int const*
propconst(1) int* int const*
propconst(2) int* int const*
propconst int** int const* const*
propconst(1) int** int* const*
propconst(2) int** int const* const*
propconst(3) int** int const* const*
propconst int*** int const* const* const*
propconst(1) int*** int** const*
propconst(2) int*** int* const* const*
propconst(3) int*** int const* const* const*

このpropconst指定子を使用する場合、std::vectorpropconst(1)を常に使用し、先ほどのMatrixNDのようなクラスはpropconst(N)(もしくは単にpropconst)を使用することで、const付加位置の問題を解決し、定数式でも使用可能となります。

また、propconst修飾子の場合、std::unique_ptr<propconst T>のような宣言が行えてしまい、この時にメンバ関数が正しく動作するためにはメンバの宣言を変更する必要があります。これは、std::tupleで使用すると、const伝播tupleとして使用できるメリットもありますが、より広い部分での後方互換の考慮と既存ライブラリの変更が必要になります。

propconst指定子の場合そのような使い方はできず、現在のmutableとほぼ同様の使い方しかできません。しかし、そのメリット(本当にメリットかどうかはわかりませんが)が得られないものの、propconstの本来の目標である定数式で確保したメモリの実行時への持ち越し(非一時的なメモリ割り当て)の許可の大部分を達成できており、その導入に伴って考慮すべきスコープを狭めています。

これらの理由から、この提案では、R0のstd::mark_immutable_if_constexpr()でもpropconst修飾子でもなく、propconst指定子を非一時的なメモリ割り当ての許可のための言語機能として提案しています。

P2679R2 Fixing std::start_lifetime_as and std::start_lifetime_as_array

std::start_lifetime_asの既存機能等との非一貫性を正す提案。

以前の記事を参照

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

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2681R1 More Basic Statistics

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

以前の記事を参照

このリビジョンでの変更は、線形回帰が将来の別の提案へ遅延された(より大きな、回帰分析ファミリーの一部であるため)ことです。

P2693R1 Formatting thread::id and stacktrace

std::thread::idstd::stacktracestd::format()及びstd::print()で出力できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、LWGからのフィードバックの適用と、stacktrace_entryフォーマットのfillオプションの必要性についてのLEWGへの質問を追記した事です。

以前のリビジョンから、stacktrace_entryのフォーマットではfillオプションが有効とされていました。これは有用である可能性があり、後から追加すると(std::formatterに状態を追加する必要があるため)ABI破壊を招くことから、そのままにすることがLEWGによって決定されています。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2695R1 A proposed plan for contracts in C++

C++ Contracts(契約プログラミング)のC++26導入へ向けた予定表。

以前の記事を参照

このリビジョンでの変更は、契約機能の構文決定(R0では2023.02)と契約違反時のサポートするビルドモードの決定(R0では2023.03)の予定を入れ替えたことです。

P2724R1 constant dangling

現在ダングリング参照を生成しているものについて、定数初期化可能なものを暗黙的/明示的に定数初期化する提案。

以前の記事を参照

このリビジョンでの変更は、関連する提案へのリンクを追加したことです。

P2727R1 std::iterator_interface

イテレータを簡単に書くためのヘルパクラスの提案。

以前の記事を参照

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

  • 基底クラスの入れ子型が派生クラスでも定義されているかに関する議論を追加
  • 名前の候補を追加

などです。

P2730R1 variable scope

ローカルスコープの一時オブジェクトの寿命を文からスコープまで延長する提案。

以前の記事を参照

このリビジョンでの変更は、関連する提案へのリンクを追加したこと、TemporariesDesign Alternativesのセクションを追加したことです。

P2733R1 Fix handling of empty specifiers in std::format

P2733R2 Fix handling of empty specifiers in std::format

P2733R3 Fix handling of empty specifiers in std::format

std::format()のフォーマット文字列内の置換フィールドが空である場合に、パースを行わなくてもよくする提案。

以前の記事を参照

R1での変更は、ネストしたrange/tupleのフォーマットバグ解消のための代替案の比較を追記したことです。

R2での変更は

  • オプションなし({}{:})の場合にフォーマット文字列解析をスキップする許可を削除
  • tuple要素のフォーマッターのためのparse()呼び出しを追加
  • フォーマット文字列({}:の右側)は{から始まることができないことを明確化
  • set_debug_formatの文言改善

このリビジョン(R3)での変更は

  • 実装可能性の問題の発覚のため、ネストしたrange/tupleのフォーマットバグ解消の解決方法を変更した事です。

ネストしたrange/tupleのフォーマットバグというのは、rangerangetupletupleなどに対するフォーマットにおいて、その要素(内側のrange/tuple)に対してデバッグ出力を有効化することができない(本来はするべきだったが実装不可能になっていた)問題の事です。

auto s = fmt::format("{}", std::make_tuple(std::make_tuple('a')));
// Before : ((a))
// Aftter : (('a'))

以前のリビジョンではこの解決のために、ネストした要素型でデバッグ出力が可能な場合にset_debug_format()を正しく呼ぶように規定、のようなことをしていましたが、その実装が難しいことから、rangetupleのフォーマッターにset_debug_format()を追加し、そのふparse()set_debug_format()を常に呼ぶ、のような基底に変更されました。

この提案は、元の目的(空のフォーマット文字列解析の省略)を失っているためこれ以上追及されないようです。ネストしたrange/tupleのフォーマットバグについては、ここでの知見を基にした別のIssue報告によって解決するようです。

P2736R2 Referencing the Unicode Standard

ISO 10646(UCS)の代わりにユニコード標準を参照するようにする提案。

以前の記事を参照

R1での変更は、SG16フィードバックによる文言の改善です。

このリビジョンでの変更は、SG16フィードバックによる文言の改善とP2713R1との文言衝突をマージした事です。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2738R1 constexpr cast from void*: towards constexpr type-erasure

定数式において、void*からポインタ型への変換を許可する提案。

以前の記事を参照

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

この提案は現在、CWGでレビュー中です。

P2740R1 Simpler implicit dangling resolution

P2740R2 Simpler implicit dangling resolution

関数からローカル変数の参照を返してしまうケースをコンパイルエラーにする提案。

以前の記事を参照

R1での変更は、文章の修正です。

このリビジョンでの変更は、関連する提案へのリンクを追加したことです。

この提案は、SG23のレビューでこれ以上時間をかけないことが決定されています。

P2741R1 user-generated static_assert messages

static_assertの診断メッセージ(第二引数)に、コンパイル時に生成した文字列を指定できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、コンパイル時の文字エンコーディングに関する議論の拡充と、volatile型に関する文言の削除です。

P2742R1 indirect dangling identification

P2742R2 indirect dangling identification

戻り値の参照やポインタの有効期間が別の変数の生存期間に依存していることを表明する属性の提案。

以前の記事を参照

R1での変更は、文章の改善とTooling Opportunitiesセクションを追加したことです。

このリビジョンでの変更は、関連する提案へのリンクを追加したことです。

この提案は、SG23のレビューでこれ以上時間をかけないことが決定されています。

P2749R0 Down with "character"

規格署中で使用されるcharacterという言葉を正しく使用しなおす提案。

規格署中で使用されるcharacterという言葉は多くの場所で不正確かつ曖昧な用法となっており、また、translation setというC++の規格署でしか使用されない言葉の使用にも問題があります。この提案は、適切な技術用語を使用してそれらを置き換えることで、表現を明確にするとともに解釈を容易にし、誤った意味にとられないようにするものです。

この提案では主に、単体あるいは近い形で使われているcharacterという言葉をUnicodeの用語(Unicode scalar value/Unicode code point)によって置き換えようとしています。一方、文字リテラルや文字型などのC++の要素を指す言葉や、改行文字など既に明確なものは変更しようとはしていません。

この変更は言葉遣いの正確さを向上させるためのもので、何か動作の変更を意図したものではありません。そのため、ほとんどのプログラマには影響はないでしょう。

P2750R1 C Dangling Reduction

P2750R2 C Dangling Reduction

Cの言語機能の範囲で発生するダングリングポインタを抑制する提案。

以前の記事を参照

R1での変更は、文章の修正です。

このリビジョン(R2)での変更は、サンプルコードの修正です。

この提案は、SG23のレビューでこれ以上時間をかけないことが決定されています。

P2751R1 Evaluation of Checked Contracts

契約条件のチェックに関する細かいルールの提案。

以前の記事を参照

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

  • 提案の2.3、2.4、3.4を明確化

    • 2.3 : 契約条件式の評価時に例外が投げられる場合は契約違反が発生している
      • 契約条件評価時に例外が伝播することを許可すると、noexceptが付加されている関数が契約条件をもつ場合、その関数評価を含むような式を渡したnoexcept演算子は何を返すべきか?
      • 想定される振る舞いには、様々な影響があり、それを考慮しなければならない
    • 2.4 : 契約条件式がUBを含む場合、欠陥が発生する
      • UBによるタイムトラベル(制御フローパスの破棄)が契約条件内で発生する場合、違反ハンドラの呼び出しに置き換えることを意図している
    • 3.4 : 複数の契約条件が連続して評価される場合、それらは相互に並べ替えて評価される可能性がある
      • 並べ替えられた契約条件の評価は、0ではない回数評価される可能性がある
  • P2751 進行状況

P2754R0 Deconstructing Avoiding Uninitialized Reads of Auto Variables

未初期化自動変数のゼロ初期化に関するいくつかのソリューションをまとめ、比較する提案。

この提案は、P2723R0の提出をうけて寄せられたいくつものフィードバックから特にその代替手段について調査し、それらの特徴や利点を比べることで、P2723とその問題の議論を促進しようとするものです。

P2723R0については以前の記事を参照

P2723R0の目標は、非クラス型の自動変数を初期化する前に不定の値を読み取ることによるセキュリティリスクを排除することです。P2723でもこの提案でも対象は未初期化の自動変数のみであり、その他のもの(動的確保したメモリや構造体のパディングなど)については考慮していません。

提案では、問題を理解するために、ごく簡単な未初期化変数読み取りの例を記載しています

// 無条件でUB
void f1() {
  int p;
  int q = p + 1;  // UB
}

// 条件次第でUB
void f2() {
  int y;
  int z = b ? y + 1 : 0;
}

// 未初期化変数を別の関数に渡す
void g3(int);

void f3() {
  int x;
  g3(x);  // likely a bug
}

// 未初期化である可能性のある変数を別の関数に渡す
void g4(int);
void f4() {
  int s;
  if (c) s = 0;

  g4(s);  // likely a bug
}

// 未初期化である可能性のある変数を別の関数に参照渡しする
void g5(int*);
void f5() {
  int t;

  g5(&t);  // possibly a bug
           // g5()がtに出力のみを行うならUBではない
}

// 意図的な初期化の遅延
void f6() {
  // スタック領域を使用するアロケータ
  char buffer[1000];
  BufferAllocator a(buffer, sizeof buffer);
  // vectorで使用されることで初期化される
  std::vector v(&a);

  // すぐに別の値を書き込むことが分かっているため初期化しない
  char buffer2[1000];
  snprintf(buffer2, sizeof buffer2, "cstring");
}

// テンプレートパラメータ型の変数宣言
template <typename T>
void f7() {
  
  T t;  // クラス型の場合は初期化され、組み込み型の場合は未初期化

  cout << t;  // 組み込み型の場合UB
}

この提案で挙げられているこのような問題の解決策の候補は次の7つです

  1. 常にゼロ初期化
    • 非クラス型の自動変数が初期化されない場合、常にゼロ初期化される
  2. ゼロ初期化もしくは診断
    • 無条件に不定値を読む場合は診断(コンパイルエラー)
    • 条件次第で不定値を読む可能性がある場合はゼロ初期化
  3. ソースでの初期化を強制
    • 非クラス型の未初期化変数はill-formed
  4. 後から初期化されることを考慮しつっつ、ソースでの初期化を強制
    • 注釈なしの非クラス型の未初期化変数はill-formed
    • 未初期化変数は明示する
  5. 実装定義の値で初期化するものの、書き込み前の読み取りは未定義動作
  6. 実装定義の値で初期化するものの、書き込み前の読み取りは誤った動作
    • 書き込み前の値の読み取りは誤っているものの、UBではない
    • コンパイラフラグなどによって、テストのために検出しやすい値で初期化したり、実運用のために安全な値で初期化したりする
    • あるいは、誤った動作を未定義動作として扱うこともできる
  7. 値初期化に一本化
    • 仕様からデフォルト初期化を削除する
    • これによって初期化は常に値初期化となり、仕様が単純化され、未初期化を含む初期化周りの問題が解決される

この提案では、これらを実現可能性、下位互換性、表現可能性の3つの観点から比較しています

  • 実現可能性 : そのソリューションが既存のC++標準に対して一貫しているかどうか。つまりは、C++標準に適用可能であるかどうか
    • 実現可能
    • 実現不可能
    • 不透明 : 現時点では判断できない
  • 下位互換性 : そのソリューションが採用された場合に、既存のコードを壊すことが無いかどうか。
    • 互換性がある : 以前にコンパイル可能なコードは引き続きコンパイル可能であり、UBの場合のみ動作が変更される
    • 正しいコードと互換性がある : 以前にコンパイル可能でUBを含まないものは引き続きコンパイル可能だが、UBを含むコードはコンパイルエラーとなる場合がある
    • 互換性がない : 以前に正しいコードもコンパイルが通らなくなる
    • 不透明 : 現時点では判断できない
  • 表現可能性 : そのソリューションが採用された場合に、既存コードの意味が変更されるかどうか。
    • 良い : 初期化を遅らせる意図を明示、あるいはロジックエラー(初期化忘れ)を修正するためにコードを更新する必要がある
    • 悪い : 意図的な初期化遅延とロジックエラー以外の可能性が発生することで、現在よりも状況が悪くなる
    • 変わらない : 意図的な初期化遅延もしくはロジックエラーを含むような(未初期化変数を含む)既存コードが曖昧ではなくなる
    • 不透明 : 現時点では判断できない

次の表は、先程の7つのソリューションに対してこれを比較したものです

ソリューション 実現可能性 下位互換性 表現可能性
1. 常にゼロ初期化 実現可能 互換性がある 悪い
2. ゼロ初期化/診断 不透明 正しいコードと互換性がある 変わらない
3. 初期化の強制 実現可能 互換性がない 良い
4. 遅延初期化を考慮した初期化の強制 実現可能 互換性がない 良い
5. 実装定義の値で初期化+その読み取りは未定義動作 実現不可能 互換性がある 変わらない
6. 実装定義の値で初期化+その読み取りは誤った動作 実現可能 互換性がある 変わらない
7. 値初期化に一本化 不透明 不透明 不透明

この表から、次のようなことが分かります

  1. 未初期化変数にまつわるセキュリティホールを塞ぐのに有効であるが、表現可能性が悪くなる
    • 現在の初期化されていないローカル変数は、初期化忘れ(ロジックエラー)か意図的な未初期化(遅延初期化のため)のどちらかですが、このソリューションの後での初期化されていない変数は、初期化忘れと意図的なゼロ初期化の区別がつかなくなるため
  2. ゼロ初期化とコンパイルエラーを組み合わせることは、コンパイラ間で拒否されるコードが変化しうることにつながる
  3. 意図的な遅延初期化のための注釈(ソリューション4)の有無にかかわらず、既存のコードを修正する多大な努力を強いることになる
    • ともすれば、未初期化変数を初期化するようにするスクリプトの安易な使用を招き、それによって元のコードの意図を見失う恐れがある
    • 既存のコードベースが抵抗する可能性がある
  4. 同上
  5. UBに定義された意味と動作を要求することは不可能
  6. 欠点が無く最も利得が高いが、誤った動作(erroneous behavior)という新しい概念を標準に導入する必要がある
    • 未初期化変数読み出しをEBとして指定することで、ソリューション7のような将来の変更をさたまげない
  7. より詳細な調査が必要

この提案は、あくまで代替案を含めたソリューションをまとめ比較するもので、どれを推しているわけでもありません。提案というよりも、議論を促すための文書です。

P2759R1 DG Opinion on Safety for ISO C++

WG21 Direction Group (DG)の安全性についての見解を説明する文書。

以前の記事を参照

このリビジョンでの変更は、寄せられたフィードバックを適用した事です。

P2763R1 layout_stride static extents default constructor fix

std::layout_strideのデフォルトコンストラクタの生成するレイアウトマッピングを修正する提案。

以前の記事を参照

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

  • LWGの要請により、全てのエクステントが静的であるかにかかわらずデフォルトコンストラクタが同じ振る舞いをするようにした
    • デフォルトコンストラクタは常に、std::layout_right::mappingと同じ方法でストライドを初期化する
  • 必要なスパンサイズがインデックス型で表現可能であることを保証するための事前条件の追加

などです。

この提案は、2月のIssaquah会議で全体投票をパスしてC++23に適用されています。

P2770R0 Stashing stashing iterators for proper flattening

stashing iteratorというイテレータカテゴリに属している標準ライブラリ内イテレータについて、そのカテゴリや振る舞いを修正する提案。

stashing iteratorとは、その間接参照がイテレータ自身の内部にあるもの(そのイテレータの生存期間と関連付けられているもの)を返すイテレータのことです。stashing iteratorinput_iteratorにしかならず、forward_iteratorではstashingは認められません。

stashing iteratorである標準ライブラリのイテレータにはstd::regex_iteratorstd::regex_token_iteratorがありますが、これらイテレータのカテゴリは現在Forward Iteratorになっており、カテゴリ指定が間違っています。

このイテレータカテゴリの間違いは、C++20のRangeアダプタで使用したときに深刻な問題を起こすことがあります。

#include <ranges>
#include <regex>
#include <iostream>

int main() {
  char const text[] = "Hello";
  std::regex regex{"[a-z]"};  // 小文字アルファベット1文字にマッチング

  // 範囲(sub_match)の範囲(match_results)
  std::ranges::subrange regex_range(std::cregex_iterator(
            std::ranges::begin(text),
            std::ranges::end(text),
            regex),
        std::cregex_iterator{}
      );

  // string_viewの範囲
  auto lower = regex_range
    | std::views::join  // sub_matchの範囲へと平坦化
    | std::views::transform([](auto const& sm) {
        // sub_matchオブジェクトが参照する文字列範囲をstring_viewへ変換
        return std::string_view(sm.first, sm.second);
    });

  // elloを1文字づつ改行して出力する(はず
  for (auto const& sv : lower) {
    std::cout << sv << '\n';
  }
}

このコードは一見すると問題なさそうに見えますが、実際にはダングリングイテレータが静かに生成されており、アドレスサニタイザーを使用しているとtransform_viewイテレータの最初の間接参照でheap-use-after-freeが発生します。

std::regex_iteratorはそのメンバとしてstd::match_resultsというサブマッチオブジェクトの範囲(std::vector<std::submatch>のようなもの)を所有しており、間接参照はそのメンバへの参照を返し、イテレータのインクリメントのたびにそのメンバは次のマッチ結果で上書きされます。

std::match_results自体も範囲であり、これはstd::submatchの配列のようなものです。そのため、上記のragex_rangeは範囲の範囲となっており、それをviews::joinに通すと平坦化によって直接std::submatchの範囲を得ることができます。

join_viewイテレータは、入力viewの外側イテレータstd::regex_iterator)と内側イテレータstd::match_resultsイテレータ)をコピーして保持しています。

// join_view::iteratorの概略
class iterator {
  // 入力範囲の外側範囲のイテレータ
  iterator_t<R> outer;

  // 入力範囲の内側範囲(*outer)のイテレータ
  iterator_t<range_reference_t<R>> inner;
};

// この場合、次のようになる
class iterator {
  // std::match_resultsオブジェクトを保持している
  std::regex_iterator outer;

  // std::match_results::iterator、例えばsubmatchのポインタ
  std::submatch* inner;
};

このようになっている時にjoin_viewイテレータをコピーすると、outer(及び内部のmatch_resultsオブジェクト)もコピーされますが、innerはコピー元のouterの要素を参照し続けます。コピー元イテレータのインクリメントや破棄によってそれが寿命を終えると、コピー後のjoin_viewイテレータはダングリングイテレータとなります。

コピーが起きなければ問題にならないのですが、残念ながら上記例ではtransform_viewイテレータ構築時にコピーが起きるとともにコピー元イテレータが即座に破棄されており、それによってダングリングイテレータが生成されてしまっています(とのことですが、正直どこでコピーが起きているのかわかりませんでした・・・)。

この問題はregex_iteratorがカテゴリについて嘘をついてるだけではなく、join_viewstashing iteratorを正しく扱えないことから起こっています。

この提案はこの問題の解決のために、それらに次のような変更を行います

  • regex_iteratorregex_token_iteratoriterator_conceptの変更
    • iterator_categoryの変更は破壊的かつ影響が大きいので行わない
  • join_viewjoin_with_viewinput_itetarorinput_range)に対して外側イテレータをそのviewオブジェクト内部にキャッシュするように変更
    • stashing iteratorを判定する方法が無いため、全てのinput_iteratorに対して適用

同時に、LWG Issue 3700(begin()が内側範囲に対してconstを考慮していない)とLWG Issue 3791(--内部で内側範囲が右辺値で得られる場合のハンドリング)の2つのjoin_view/join_with_viewにまつわる小さなイシューも解決しています。

この提案は既に2月のIssaquah会議で全体投票をパスしてC++23に採択されています。

P2771R0 Towards memory safety in C++

依存関係を追跡することによる時間的なメモリ安全性保証を言語に導入する提案。

時間的なメモリ安全性保証とは、破棄済みのオブジェクトへのアクセスや無効な参照の使用などのことで、例えば次のようなものです

int main() {
  std::string s1 = "abcdefg";
  std::string_view s2 = s1;

  std::cout << s2.length() << " " << s2 << std::endl;
  
  // 参照元を変更
  s1.erase(2,2);

  // s2の使用は危険
  std::cout << s2.length() << " " << s2 << std::endl;
}

対して、バッファオーバーランなど有効ではないメモリ領域へのアクセスは空間的なメモリ安全性の問題と言えます。空間的なメモリ安全性はランタイムのチェックや危険な機能(生ポインタやreinterpret_castなど)の使用禁止などの措置によって比較的簡単に防止することができます。

この提案の目的は、Rustのborrow checkerの導入(C++の現在のプログラミングモデルにそぐわない)やライフタイム注釈のような不十分なものの導入をしなくても、C++をメモリ安全にすることが可能であり、その議論を促すことです。

この提案のアプローチは、オブジェクトの依存関係を追跡することにより時間的なメモリ安全性を保証するものです。例えば、オブジェクトA(reference_wrapperなど)がオブジェクトBに依存するとき、Bが破棄された後にAを使用してはならないこと、あるいは、オブジェクトA(string_viewなど)がオブジェクトBのコンテンツに依存している時、Bの変更後にAを使用してはならないこと、を追跡し、それを静的に解析することで時間的なメモリ安全性を保証します。

この提案による安全性保証メカニズムは、何かしらの注釈によってスコープ単位で有効にするオプトインなもので、たとえば次のような構文によります

namespace oldcode {
  // 既存のC++コード
}

namespace newcode {
  [[memorysafety]];
  // このスコープのコードはメモリ安全性を強制する
}

namespace newcode {
  // 注釈がないので、メモリ安全性はチェックされない
  // newcodeとoldcodeから任意のものを参照できる
}

このmemorysafetyな領域内では、同じくmemorysafetyなコードを呼び出す限り、コンパイラは時間的なメモリ安全性を保証します。ただし非メモリ安全なコードを呼び出すとそのチェックは無効化されます。

memorysafetyな領域ではまず、メモリ安全性のために参照(ポインタ含む)のエイリアシングに制約を加えます。例えば次のような普通なコードはエイリアシングを考慮すると安全ではありません

void foo(vector<int>& a, const vector<int>& b) {
  if (!b.empty()) {
     // aとbが同じオブジェクトを指している場合危険(foo(x, x)のように
     a.push_back(b.front());
  }
}

memorysafetyな領域内ではデフォルトで安全にするために、あるオブジェクトの非const参照を関数に渡す際には他の参照はそのオブジェクトをエイリアスできなくします。この違反はコンパイラによってチェックされます。

// memorysafety内にあるとして
void bar(int x, int y) {
  std::vector<int> a, b;
  std::array<std::vector<int>, 2> c;

  foo(a,b); // ok
  foo(a,a); // error
  foo(c[x], c[y]); // error, x != yを証明できない

  auto& r1 = c[x];
  auto& r2 = c[y];
  if (&r1 != &r2) {
    foo(r1, r2); // safe now
  }
}

エイリアシングを許可したい場合は明示的にコンパイラに表明します。例えば

void swap(auto& a, [[mayalias(a)]] auto& b) { ... }

このswap()はオブジェクトに対して呼び出されても安全であるとみなされます。

多くのコードは引数がこのようなエイリアシングを起こしていると安全ではないため、エイリアシングを許可するのはオプトインである必要があります。

ただし、この制約は直接のエイリアスにのみ作用し、間接的なエイリアスの問題は依存関係の追跡によって対処されます。

void foo(vector<string>& a, const string& b) {
  a.push_back(b);
}

void bar(vector<string>& a) {
  // 直接のエイリアスではないものの、依存関係が発生している
  foo(a, a.front());
}

オブジェクトが破棄された後にその参照からのアクセスが発生してはなりません。参照がローカル変数のスコープの外側にある場合などはすべての用途について安全であると確信が持てない場合は参照はオブジェクトよりも長く存在してはならない。このチェックのために、オブジェクトと参照の生存期間を追跡する必要があります。

生存期間は依存関係としてモデル化され、グローバルなオブジェクトは(とりあえず)無限の寿命を持ち、ローカルなオブジェクトはそのスコープに応じた寿命を持ちます。ローカルオブジェクトが破棄された時、それに依存する全てのオブジェクトは

  1. トリビアルデストラクタを持つ
  2. そのオブジェクトを用いてメンバ関数呼び出しがなされない

必要があります。

void foo() {
  int* a;
  {
     int d = 2;
     a = &d; // aはdに依存
     *a = 3; // ok, dは生存期間内
  }
  // aはダングリング参照となるが、アクセスされなければ存在は許される

  // これはエラー、dは破棄済
  *a = 5;
}

関数呼び出し時に何かを渡す時、明示的な生存期間に関するアノテーションが必要になる場合があります。

void bar(int* x),

void foo(int& x, int* y) {
  int* a = &x;  // ok, aはxに依存
  bar(a);       // ok, xは生存期間内

  // error: yはxよりも長く生存しうる
  y = a;
}

ここでは、xが破棄された後でyが使用されないことを証明できないため、yへの参照の代入は禁止しなければなりません。許可するためには、依存関係を伝播するために注釈が必要になります

void foo(int& x, [[maycapture(x)]] int* y) {
  // ok, 呼び出し側はxとyの寿命をチェックできる
  y = &x;
}

メンバ関数の場合、thisに参照を保存する場合は同様の注釈を関数そのものに行います。また、戻り値で参照を返す場合も関数そのものに注釈が必要です。

[[maycapture(x)]] void Foo::bar(int* x) {
  this->y = x;
}

[[dependson(x,y)]] char* foo(char* x, char* y) {
  return x < y ? x : y;
}

これらの注釈によって破棄後のオブジェクトにアクセスしている場合を検出することができますが、最初のstring_viewの例のようにコンテンツの変更を検出することはできません。

このために、オブジェクトのコンテンツのキャプチャを明示する注釈を行います

[[dependson(*x)]] std::string_view foo(std::string& x) {
  return x;
}

これによって、次のようなルールを確立することができます

  • オブジェクトAがオブジェクトBのコンテンツに依存する場合、Bの非const関数が呼び出された後でAを使用してはならない

これは、オブジェクトの状態を変更するのは非const関数のみであり、あるオブジェクトの非const関数が呼ばれたらそのオブジェクトに依存する全てのオブジェクトは無効になる、という単純なルールです。しかし実際には、非constでありながらオブジェクトの状態を変更しない関数が存在してるためこのルールは少し厳しいものです。例えば、コンテナのbegin()/end()などがあり、明らかにbegin()/end()の呼び出しでその時点で取得されているイテレータを無効にしたくはありません。

そのため、ここでもそれらの関数がnon-mutatingであることをマークする注釈が必要になります。これによって、コンテンツに依存するオブジェクトはそのような非const関数の呼び出しの後でも有効なままでいることができるようになり、コンパイラはそのような関数が他の注釈なしの非const関数を呼び出さないように強制する必要があります。

template <class T>
[[dependson(*this), nonmutating]] myvec<T>::iterator myvec<T>::begin() {
  return iterator(this->ptr);
}

この上で、次のルールを確立することで、a.push_back(a.front())のような例を検出することができます

  • あるオブジェクトによる関数呼び出しの引数は、そのオブジェクトに対する他の非const関数引数に依存してはならない

a.front()aのコンテンツに依存しているので、a.push_back()に渡すことはできません。

ここまでの形式はほとんどのユースケースに対応していますが、標準ライブラリでも使用されている用例で禁止されているものが1つあります。例えばstd::vector::insert()の次の様な呼び出しです

a.insert(a.end(), b.begin(), b.end());

ここまでのルールによって、b.begin()b.end()aに依存してはならないことが要求され、それは正当なものです。しかし、isnert()の最初の引数は異なり、依存関係を受け入れる必要があるほかイテレータが同じコンテナのものであることを要求しています。

多くの場合、これを静的に証明することはできず、insert()に伴うイテレータ無効化によって実装自体も厄介です。ここでは、insert()の実装には安全ではないコード(およびイテレータがそのコンテナのものであることを検証するアサート)が必要になることを受け入れ、依存関係を許可するためのアノテーションを導入するにとどめています。

template<class InputIt>
[[dependson(*this)]] iterator insert([[maydependon(this)]] const_iterator pos, InputIt first, InputIt last);

ここまでのルールによって、多くの時間的なメモリ安全性を壊すコードを検出することができるようになります。しかし、コンパイル時にすべてのバグを検出するには十分ではありません。エイリアスによって、まだ時間的な安全性を損ねる可能性が残されています

void f1() {
  A a;
  B b;
  C c;
  a.push_back(123);
  f2(a, b, c);

  f3(b);  // b.iがaから取得される
  f4(c);  // b.iが無効化される
  f5(b);  // b.iが使用される
}
void f2(A& a, [[maycapture(a)]] B& b, [[maycapture(a)]] C& c) {
  b.a=&a;
  c.a=&a;
}
void f3(B& b) {
  b.i = b.a->begin(); // bにaのイテレータを保存
}
void f4(C& c) {
  c.a->clear(); // c経由でaのイテレータを無効化
}
void f5(B& b) {
  b.e = *b.i; // aのイテレータの使用
}

生存期間の制約は満たされていますが、aエイリアスとそれを介した操作によってダングリングイテレータが発生しています。これらの関数が別々の翻訳単位に定義されている場合、これを検出することはできません。この挙動を検出するために非常に精巧なアノテーションを導入することもできるかもしれませんが、それはあまり現実的ではないようです。

その代わりに、この問題は実行時のチェック(サニタイザーのようなもの)によって検出することにしており、このチェックは例えばデバッグ時のみとすることができるようにすることを想定しています。

これらのようにして、コンパイル時になるべく多くの問題を発見しつつ、それが難しい部分は実行時チェックに任せることで、時間的なメモリ安全性を保証します。

P2772R0 std::integral_constant literals do not suffice - constexpr_t?

コンパイル時定数オブジェクトを生成するクラスとリテラルの提案。

この提案は、P2725のstd::integral_constantリテラルの提案を受けてのもので、P2725のソリューションではカバーしきれないユーズケースに対応可能なようにP2725の提案を拡張するものです。P2725に関しては以前の記事を参照

P2725がカバーできていないユーズケースとは、次のように、非整数のNTTPをconstexpr引数として関数に渡したい場合です

// P2725のintegral_constantリテラルに対応する変数テンプレート
template<auto N>
inline constexpr std::integral_constant<decltype(N), N> Const = {};

template<typename T>
struct my_complex {
  T re , im;
};

template<typename T>
struct X {
  void f(auto c) {
    // cからNTTP値を引き出して定数式で使用可能
  }
};

inline constexpr short foo = 2;

template<typename T>
void g(X<T> x) {
  x.f(Const<1>);
  x.f(Const<2uz>);
  x.f(Const<3.0>);
  x.f(Const<4.f>);

  // P2725の提案ではこれらができない
  x.f(Const<foo>);
  x.f(Const<my_complex(1.f,1.f)>);
}

この例の最後の関数g()内部での最初の4つのx.f()呼び出しはP2725のstd::integral_constantリテラル1icなど)でも可能ですが、最後の2つの呼び出しはサポートされていません。P2725はあくまで整数定数のNTTPを同様の形で渡すためのリテラルを提案しているだけで、より一般的なNTTP(特に、C++20で許可されたクラス型)を渡すことができません。

関数にcosntexpr引数を渡したいケースは多くC++20以降は特にそれは整数型に限りません。従って、P2725が解決を目指している問題空間はより広いものであり、P2725の内容だけでは不完全です。また、このようなNTTP渡しにおいてはより簡単に行えることが望ましく、1ic(P2725のリテラル)とstd::cnst<1>(上記例のConst)の両方が必要です。

また、上記例では整数以外のものもstd::integral_constantに渡していますが、これは現在の定義でも行うことができます。その場合、このソリューションのためにintegral_constantを引き続き使用すべきではなく、そのための新しい型が必要になります。提案では、次のような型を例示しています

template<auto Value>
struct constexpr_t {
  using value_type = decltype(Value);
  using type = constexpr_t;

  static inline constexpr value_type value = Value;
  
  constexpr operator value_type() const noexcept { return Value; }
  static constexpr value_type operator()() noexcept { return Value; }
};

新しい型を追加すれば、P2725で問題となっている単項-std::integral_constantに追加する破壊的変更についても解決され、また一貫性のために他の演算子も追加することができます。

この提案ではさらに、これらの機能によって可能となるAPIについて記載しています。たとえば

  • std::arrayや固定サイズstd::span(及び提案中のstd::simd)などのsize()メンバ関数をこの定数型(std::integral_constant/constexpr_t)に置き換える
    • これらの型がoperator()を持つことによって、APIレベルでは破壊的変更にならない
    • メンバポインタを取られている場合のみ破壊的となるが、それは禁止されている
  • 非メンバoperator[]によって、contiguousな範囲からスライスを取得するAPI

この提案はまだ問題提起に留まっていますが、LEWGのレビューではこの提案の方向性が支持されているようで、この提案を勘案しながらP2725のソリューションを検討していくようです。

P2773R0 Considerations for Unicode algorithms

P2728/P2729で提案されているユニコードアルゴリズム(変換・正規化)について、推奨事項等のフィードバックの提案。

P2728/P2729については以前の記事を参照

この提案は、P2728/P2729の提案するユニコードアルゴリズム(文字列範囲に対するアルゴリズム)について、筆者の方の実装経験や考えをもとに推奨事項を記述するものです。

この提案の推奨事項の要旨は次のようなものです

  • ほとんどのユニコードアルゴリズムviewとして公開する必要がある
    • <ranges>の要件を満たす
  • ユニコードアルゴリズムは、標準の他のviewアルゴリズムとうまく構成できる必要がある
  • 正規化・クラスタ化・ケーシング(大文字小文字変換)を最初の作業の焦点とするべき
    • UTF相互変換を前提とする(それがなければ何もできない)
  • ユニコードアルゴリズムはコードポイント(ユニコード空間上の32bit整数値)で動作する
  • 調整済と未調整(Tailored and non-tailored)のアルゴリズムではそれぞれ異なる要件と実装上の課題があるため、類似しつつも別々のインターフェースで公開されるべき
  • 調整済アルゴリズムに取り組む前に、ロケール表現についてよく理解しておく必要がある
  • Rangeアダプタオブジェクト(|構文)は暗黙的なUTFデコード/エンコード手順を導入するのに最も適した場所。このような暗黙の手順はユーザビリティのために必要
  • char32_tはコードポイントを表現するのに適切な型である
  • charstd::byteの消費も可能だが、明示的であるべき
  • コードユニット(UTFエンコーディングの1単位の整数値)のシーケンスはデフォルトで検証すべき
  • ICUを使用した実装を可能とするために、未調整アルゴリズムを制約すべきではない
  • ICU4xは調整済アルゴリズムに対する長期的な最善の答えである
  • 将来のユニコードバージョンで変更されうる仮定を公開するのは避けるべき
  • プロパティルックアップを最適化し非sized_rangeのメモリを巧妙に確保することで、既存のフレームワークと遜色ないパフォーマンスを実現できる
  • ユニコードアルゴリズムは、インプレースの変換や文字列コンテナの恩恵を受けられない
  • UTFのデコード/エンコードを回避しても、明確にパフォーマンスが向上するとは限らない

この提案はP2728/P2729の内容を否定したり批判したりするものではなく、ユーザビリティユニコード固有事情などの面からC++の標準ライブラリとして望ましい方向性を示すものです。

P2779R0 Make basic_string_view's range construction conditionally explicit

std::string_viewrangeコンストラクタのexplicitを条件付きに緩和する提案。

std::string_viewstd::basic_string_view)に対する文字の範囲から構築するrangeコンストラクタは、C++23で追加されており、そのコンストラクタはexplicit指定されています。その目的は、文字の範囲を常に文字列として変換することには問題があるためで、文字の範囲からの構築時にその意図を確認するために明示的なものとするためです。

なお、std:stringstd::string_viewへの暗黙変換演算子を備えているため、このコンストラクタと関係なくstd::string_viewへ暗黙変換できます。

しかしこのことによって、独自定義された文字列型をstd::string_viewへ暗黙変換することも禁止されています。

void use(std::string_view v);

std::string str1;
use(str1);  // OK

// 自分で作成したものだったり、どこかのライブラリのものだったり
my::string str2;
use(str2);  // ERROR

このような在野のstring(あるいは同様のstring_view)型は必ずしも自分が定義したものではなく、別のライブラリに属するものかもしれません。その場合、自分で暗黙変換を提供することもできません。また、この問題は逆に、独自定義のstring_viewをライブラリのインターフェースとしているようなライブラリにおいて、std::stringで問題になる可能性もあります。

void lib::very_useful_algorithm(lib::string_view v);

std::string str;
lib::very_useful_algorithm(str);  // ERROR

これらのコードのコンパイルが妨げられる理由はなく、この提案は、現在のrangeコンストラクタのexplicitにこれらの文字列型を検出する条件を指定することでこの問題を解決しようとするものです。

その際問題となるのは、どのようにしてそれら在野の文字列型を識別するかという点です。

しかし残念なことに、std::stringstd::vector<char>が名前以外ほとんど同じクラスであるように、在野の文字列型を識別することは困難です。そのため、必然的に何かしらの方法でオプトインする手段が必要となります。そのために、次の2つが提案されています

  1. std::ranges::enable_view<T>またはstd::ranges::view_baseのように、特殊化/継承して有効化する特性の導入
  2. 文字列型に共通する、何かしらのユニークな特性を利用する
    • 入れ子traits_typeを活用する(P2499で以前に提案されていたが採用されなかった)

どちらにも

  • どちらのオプションでも既存コードからのオプトインのための作業が必要となる。
    • ただし、オプション1を実装するコードは存在していないが、オプション2は既に実装しているものが存在する
  • traits_typeは必ずしも文字列型に固有のものではなく、全く異なる意味で同じ名前を使用している可能性がある
  • オプション1は、それが適用され利用可能になるまでに時間がかかり過ぎる
    • それが採択され、実装されて利用できるようになるまでに1つのC++リリースサイクルの間問題が解決しない

オプション2の欠点については、std::string_viewrangeコンストラクタは現在でも制約によって厳密に文字の範囲を判定しており、その判定をパスしたうえでtraits_typeを持つようなクラスというのは実際にはほぼ存在しえないと思われます。この提案では在野のライブラリを調査することで、traits_typeを持たない文字列型はあっても、traits_type持ちながら(std::string_viewrangeコンストラクタの制約をパスして)文字列型であると認識される型は見つからなかったようです。

この提案はオプション2を推しており、それをC++23へのDRとすることを提案しています。

P2782R0 A proposal for a type trait to detect if value initialization can be achieved by zero-filling

値初期化をゼロフィルに置き換えることが安全な型を検出するための型特性を追加する提案。

値初期化とは型の値を初期化するときにゼロ相当の値で初期化することです。値初期化は初期化対象の領域にゼロ相当の値を代入しなければならないため、std::uninitialized_value_constructアルゴリズムに見られるように、たくさんの要素を値初期化する場合にパフォーマンスが低下する可能性があります。

// std::uninitialized_value_construct()の実装例
template<class ForwardIt>
void uninitialized_value_construct(ForwardIt first, ForwardIt last) {
  using Value = typename std::iterator_traits<ForwardIt>::value_type;
  ForwardIt current = first;
  
  try {
    // 範囲[first, last)の要素を1つづつ値初期化
    for (; current != last; ++current)
        ::new std::addressof(*current) Value();
  } catch (...) {
    std::destroy(first, current);
    throw;
  }
}

初期化対象領域が連続している(contiguousである)時で、初期化対象の型(Value)が単純なデータ型の場合、この初期化ループはmemset(ptr, 0, bytes_size)のようなコードで置き換えることができます。これは初期化する領域サイズが大きければかなりのパフォーマンス向上につながることがあり、コンパイラはこのようなコードに対してそのような最適化を行うことがあります。

単純なデータ型とは例えばint型のような数値型の事ですが、クラス型の場合はメンバやコンストラクタの宣言の仕方などによって変化するため簡単には判断できません。また、組み込み型でも浮動小数点数型やポインタ型等のそのオブジェクト表現が標準で義務付けられていない型の場合も(ゼロ相当の値がゼロフィルによって生成される値と同等であるかが分からないため)、単純なゼロフィルによって初期化することが適切であるかは分かりづらいところがあります。それらの型がゼロフィルで値初期化可能であるかどうかは最終的には実装あるいはプラットフォームによって決まります。

そのため、コンパイラはこのような最適化が可能かどうかを適切に判断することができ、可能な場合には値初期化ループをゼロフィルに置き換えるコードを出力してくれるのですが、次のような理由によってコンパイラオプティマイザを信頼するには問題があります

  • コンパイラは時々最適化できるはずのコードを見逃すことがある
  • 最適化を有効にする必要がある(GCCなら-O2以上など)
  • 最適化によってデバッグ効率が低下する
    • 最適化を無効にすると、非効率的なコードを生成する
  • 最適化はコンパイル時間増大につながる

これらの理由から、標準ライブラリを含めた多くのライブラリ実装がそのような最適化を手動で実装しています。つまり、先ほどのuninitialized_value_constructの実装のようなコードでは、初期化対象の型がゼロフィルによって安全に初期化することができる(値初期化と同等になる)ことが検出できた場合にのみ、memsetを用いたコードへディスパッチするようにしています。

特に、そのようなコードはBoost.ContainerやFolly、Qtなどのライブラリで見ることができますが、現在のその判定は何かしらの間違いがあるようです。値初期化をゼロフィルに置き換えることが安全ではない型の値初期化をゼロフィルで行ってしまうと、ともすれば深刻で見つかりづらいバグにつながる可能性があります。

この提案は、そのような型の検出を行う型特性を追加することで、それが正しくかつ完全に検出可能となるようにしようとするものです。それによって、標準ライブラリ以外のライブラリにおけるこの手の最適化コードの正確さを向上させることができ、潜在的なバグを削減することができます。

提案されているのは、std::is_trivially_value_initializable_by_zero_filling<T>という型特性で、これはTの値で初期化をゼロフィルに置き換えることが安全である場合にtrueとなり(true_typeから派生し)ます。

// 宣言例
namespace std {
  // 型特性の本体
  template<class T>
  struct is_trivially_value_initializable_by_zero_filling;

  // 簡易アクセス用変数テンプレート
  template<class T>
  constexpr bool is_trivially_value_initializable_by_zero_filling_v
    = is_trivially_value_initializable_by_zero_filling<T>::value;
}

これをtrueにするTtrivially value-initializabile by zero-fillingとよばれる型で、次のいずれかに該当する型として指定されます

  • 整数型
  • 列挙型
  • そのほかのスカラ型で、trivially value-initializabile by zero-fillingである実装定義の型
  • trivially value-initializabile by zero-fillingな型の配列型
  • trivially value-initializabile by zero-fillingなクラス型
    • 資格のある(eligibleな)トリビアルデフォルトコンストラクタを持ち、かつ
    • 全ての非静的メンバおよび基底クラスは、trivially value-initializabile by zero-fillingな型

前述のように、浮動小数点数型やポインタ型などはその値初期化される値をゼロフィルによって生成可能であるかが実装定義であるため、この型特性はコンパイラマジックで実装される事になるでしょう。また、同様の理由によりその結果は対称のプラットフォームによって変化する可能性があります。

P2784R0 Not halting the program after detected contract violation

契約違反が発生した際にすぐに停止せずにプログラムの実行を継続する機能についての提案。

現在進行中の契約プログラミング機能においては、契約違反が起きた場合(契約条件がfalseを返した場合)にその時点でプログラムを停止させ、他の選択肢を提供していません。契約違反が起きたと言うことはその指定された意図から外れて実行されており、そのまま継続したとしても結果は予測できないものになります(クラッシュや未定義動作など)。このデフォルトは厳しいですが適切なもので、契約違反時の継続モードについてはC++20契約の際にも問題となったことの一つでもあります。

しかし、実際には必ずしもそのデフォルトが最適とは言えない場合があります。たとえば

  1. プログラム内の分離されたサブコンポーネントの1つで契約違反が起きた時でも、他の部分に影響を与えないことが確信できる場合
  2. 上記の特殊なケースとしてmain()を1つのサブモジュールと見做した時、1回目のmain()実行における契約違反が2回目のmain()実行の正しさに影響しないことは、どうにかしてそのプログラムを再起動させたときにわかる可能性がある
  3. 単体テストの場合、契約チェックのような安全策の存在を確認するために意図的に契約違反を起こす場合がある
  4. プログラムが本番環境で長い間使用されており、その制御パスのほぼ全てを使用している可能性が高い場合、プログラムは内部仕様(を表現した契約)に固執しなくてもユーザーの期待通り動作していると確信できる場合

最後の例はともかく初めの3つの場合には、プログラムの実行を止めたくないが、契約違反が検出された場所とは異なる場所からプログラムの実行を再開することができ、それが望ましくすらある、と言う点が共通しています。この提案はこのユースケースに焦点を当てて、これを満足させるために取れるソリューションについて検討するものです。

ユースケースは少し異なりますが、ほぼ同様のことはP2698R0でも提案されており、そこではその方法として例外を用いるEval_and_throwモードを提案しています。ただし、これについてこの提案では次のような問題点や疑問点を提示しています

  1. 例外送出によってスタック巻き戻しが発生しデストラクタが呼ばれるが、デストラクタもまた契約を持つ可能性があり、同様に契約が破られうる
    • スタック巻き戻し中のデストラクタで例外が発生すると、結局std::terminate()される
  2. 契約(特に事前事後条件)の存在とnoexceptについてが未解決
    • 例外を投げうる契約指定に対してnoexceptはどう言う意味を持つのか、あるいは持たせるのか?
  3. 例外を無効にしてコンパイルされているプログラムにおいて、契約違反後の継続モードを提供する方法がない
    • 例外を無効にしているプログラムは少なくはないが、そのようなプログラムにおいても同じ要求があるはず

この提案の契約違反後の継続モードは、スタック巻き戻しよりも厳しくstd::abort()よりも柔軟な機構を提案しています。それは、2つの標準ライブラリ関数から構成されます

// コンポーネント境界を指定する関数
template <invokable F>
void abortable_component(F&& f);

// プログラムを終了させずに、コンポーネントを終了させる関数
[[noreturn]]
void abort_component() noexcept;

abortable_component()は、渡された関数fをほとんど通常通りに実行しますが、その実行は別のコンポーネントで実行されているものとして扱われます。abortable_component()は例外中立であり、fの呼び出し中の全ての例外はここから送出されます。この関数は、プログラマが想定するコンポーネント境界をコンパイラに指示するためのものです。

abort_component()を呼び出すと、abortable_component()によって指定されたコンポーネント境界に到達するまで、スタック巻き戻し(それに伴うデストラクタ呼び出し)を伴わずに現在のコールスタックから離脱する処理が開始されます。その処理が完了すると到達したコンポーネント境界の直後、すなわちabort_component()の呼び出しが発生したabortable_component()呼び出しの直後の地点からプログラムの実行が再開されます。もしこのとき、対応するコンポーネント境界が見つからない(abortable_component()の内部ではない)場合は、単にstd::abort()が呼ばれます(これは、違反後即終了と同じ動作)。

すなわちこれらのものは、例外送出に伴う大域脱出を行いつつもスタック巻き戻しに伴うデストラクタ呼び出しや例外オブジェクトのコピーを回避し(それによってネストする契約違反の発生を回避し)、なおかつ大域脱出は指定されたコンポーネント境界で止まる、と言うことを実現するものです。

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

struct Guard {
  ~Guard() { std::printf("A"); }
};

int fun() {
  Guard g;
  std::abort_component();          // (2) 中断シーケンスの開始
  std::printf("B");                // (3) この行はスキップ。"B"は出力されない
}                                  // (4) デストラクタ呼び出しもスキップ。"A"は出力されない
 
int main() {
  std::abortable_component(&fun);  // (1) fun()をサブコンポーネントとして実行
  std::printf("C");                // (5) サブコンポーネントから離脱すると、"C"が出力される
}

これらの機構はどこでコンポーネントが中断したかの情報やコンポーネントが正しく完了したかについての情報を伝達する方法を持たないほか、abort_component()呼び出しによるコンポーネントの中断はデストラクタの実行を伴わないことから簡単にリソースリークを発生させ、プログラムの継続を危うくします。

とはいえこれは、契約違反が起きた場合を前提とした、既にプログラムの継続が危うい状況における被害を最小限に抑えるためのツールであり、その場合にやるべきことはプログラムを正しく動作させることではなく、契約違反の原因(おそらくバグ)の影響を最小限に抑える策をとることです。これは危険な機能であり、そのような状況以外で使用することは推奨されません。abortable_component()をプログラム中に配置することは、それによって呼び出された関数の任意の部分(RAIIも含めて)がスキップされた時でも(あるいはその処理の成否に関わらず)プログラムの実行を継続することが合理的に安全であると、プログラマが判断したことを意味します。

この機構は、現在の契約機能(MVP)に専用のビルドモードを追加する必要がなく、現在のEval_and_abortモードで契約条件違反が検出された際に呼び出されるstd::abort()abort_component()に置き換えるだけでサポートできます(abortable_component()の呼び出し内部でなければ、その呼び出しはstd::abort()と等価なため)。

この場合abort_component()は通常使用可能である関数である必要がない(任意の使用を抑制できる)ほか、MVPの正式な策定(C++26予定)の後から後方互換性を保ちながら導入することもできます。

P2786R0 Trivial relocatability options

trivially relocatableをサポートするための提案。

trivially relocatableとは、memcpyあるいはビット毎コピーによって再配置(relocation)することができる型の性質のことで、relocationとはムーブとムーブ元オブジェクトのデストラクタ呼び出しが複合したかのような操作のことです。この提案は、P1144などで以前に提案されていた同様の性質についてを筆者の方の実装経験をもとに変更・改善を加えるものです。

P1144及び再配置操作については以前の記事を参照

この提案のトリビアルな再配置操作(trivial relocation operation)とは、ソースオブジェクトのストレージが別のオブジェクトによって使用されたかのようにそのソースオブジェクトの生存期間を終了するようなビットコピー操作です。これはムーブ構築の直後にソースオブジェクトを破棄するのと意味的には等価ですが、実際にはソースオブジェクトに対して何も行わず、デストラクタは実行されません(例外は許可されています)。

trivially copyableな型はそれが可能なのはすぐに想像がつきますが、実際には非トリビアルなムーブコンストラクタ/デストラクタを持つような型であっても可能なものがあり、std::vectorstd::unique_ptrなどの多くのリソース所有型が該当します。トリビアルな再配置操作では、ソースオブジェクトのデストラクタ呼び出しをスキップすることでムーブ先オブジェクトがムーブコンストラクタで行うべきムーブ後操作をスキップすることができます。

現在のC++の仕様においては、trivially copyableではない型でこのようなビットコピーによるムーブは未定義動作となり、再配置操作を行うことができません。

この提案の目的はこのサポートを行うことにあり、次の3つを目標としています

  1. トリビアルな再配置操作をサポートするために、標準でそれを明確に定義する
  2. より多くのtrivially relocatableな型を暗黙的にサポートする
  3. トリビアルな再配置が誤用された場合によりより診断メッセージを出力すること

この提案のP1144との違いは

  • トリビアル性は構文ではなく意味から決まる
    • P1144は再配置可能という性質をアクセス可能なデストラクタとムーブコンストラクタから構文的に構成され、そのトリビアル性も同様
    • この提案の再配置可能性ではトリビアル性が重要であり、言語の他の部分と同様に、ある型のトリビアル性はその全てのメンバと規定クラスのトリビアル性から決定される
  • 実装品質に作用されない、予測可能な再配置可能性の指定
    • P1144は再配置操作のためにビットコピーを使用すること(トリビアルな再配置操作)を使用する許可を与えるが強制せず、それを使用することは実装品質の問題としている
    • また、再配置に適していない型に対する注釈をつける誤用は、UBや診断不要のill-formedとなる
    • この提案では、再配置操作に関わるセマンティクスを完全に指定するとともに、機能が誤用された場合に診断可能なill-formedとする
  • std::swap()を3つの再配置操作によって実装可能か?
    • P1144では、swap操作を3つの再配置操作によって実装可能な型のみをサポートするように制限されているが、これによってpmrコンテナをサポートできなくなっている
    • この提案では、そのような制約を要求しない
  • pmr型のサポート
    • この提案では、std::pmrの型に代表されるスコープアロケータモデルをサポートすることを強い動機としている
    • P1144では、通常のアロケータモデルしか考慮していない様子

この提案では、relocatetrivially relocatableと言った性質の意味を定義した上で、非trivially copyableなクラス型に対してそれを明示的に指定するために、trivially_relocatable(expr)という指定子を導入します。

struct Relocatable trivially_relocatable(true ) {}; // trivially relocatable
struct Alternative trivially_relocatable(false) {}; // not trivially relocatable

そして、トリビアルな再配置操作を行うライブラリ関数を導入します

template <class T>
  requires is_trivially_relocatable_v<T>
T* trivially_relocate(T* begin, T* end, T* new_location) noexcept;

この関数は、単なるmemcpymemmoveトリビアルな再配置操作を意図しているかをコンパイラが判断できないため、その意図をコンパイラに伝えることを目的としています。効果は、単にmemmoveを行うだけです。trivially_relocate(&src, &src + 1, &dst)srcオブジェクトの生存期間が終了しdstオブジェクトの生存期間が開始されたことを表し、コンパイラ等のツールはそれを認識することができます。

使用されているis_trivially_relocatable_vは型のtrivially relocatable性を検出するための型特性です。

提案より、診断の例

struct MyType trivially_relocatable : BaseType {
  
  // ユーザー定義ムーブコンストラクタの存在によってこの型は*trivially relocatable*ではないが
  // trivially_relocatable注釈によって*trivially relocatable*であることを指定する
  MyType(MyType&&); 
};

struct NotRelocatable : BaseType {
  
  // ユーザー定義ムーブコンストラクタの存在によってこの型は*trivially relocatable*ではない
  NotRelocatable(NotRelocatable&&);
};

struct Error trivially_relocatable : BaseType {
  NotRelocatable member;
  
  // trivially_relocatableと注釈されているが、*trivially relocatable*ではない型をメンバとして持つ
  // そのため、ill-formed。コンパイルエラーとなる
  // trivial relocationの最中にはムーブコンストラクタは呼び出されないため、この定義がそれに抗うことはできない
  Error(Error&&);
};

P2787R0 pmr::generator - Promise Types are not Values

P2787R1 pmr::generator - Promise Types are not Values

std::generatorpolymorphic_allocatorを使用するエイリアスpmr::generatorを追加する提案。

std::generatorは他のアロケータ対応(allocator aware)なコンテナ型等と異なり、生成する要素のためにアロケータを使用するのではなく、コルーチンフレームを保存しておく領域をコルーチンの初期化時に確保するためにアロケータを使用します。そのため、std::generatorは生成要素へのアロケータ伝播を行わず、それはstd::generatorの役割ではありません。

しかし、アロケータのカスタマイズは標準ライブラリの他の型と同じようにテンプレートパラメータで行い、使用可能なアロケータに関しても差異はありません(アロケータオブジェクトの渡し方はコルーチンの事情により少し異なりますが)。したがって、polymorphic_allocatormemory_resourceによってアロケータをカスタマイズすることもでき、std::generatorに対してそうしようとするのは自然な発想です。

しかし、std::generatorには3つのテンプレートパラメータがありアロケータは一番最後のパラメータによってカスタマイズしますが、その場合は3つのテンプレートパラメータを全て明示的に指定しなければならず、使いにくくなります。

std::pmr::monotonic_buffer_resource mbr;
std::pmr::polymorphic_allocator<> pa{&mbr};

// 型名が長くなる
std::generator<int, void, std::pmr::polymorphic_allocator<>> g = pmr_requiring_coroutine(std::allocator_arg, pa);

template<typename T>
using pmr_genterator = std::generator<T, void, std::pmr::polymorphic_allocator<>>;

// エイリアスがあれば使用感がかなり良くなる
pmr_genterator<int> g2 = pmr_requiring_coroutine(std::allocator_arg, pa);

std::pmr名前空間の意図は、アロケータをカスタマイズ可能な型についてpolymorphic_allocatorをデフォルトとした型名を提供するもので、これまではそういう型しかなかったとはいえコンテナ型などのアロケータ対応型のためだけのものではないはずです。また、std::generatorpolymorphic_allocatorを使用する場合は上記のようなエイリアスを作成することになるはずです。

この提案は、本来不必要な作業を強いることを回避するために、標準でstd::pmr::generatorエイリアスを用意しておくべきという提案です。

// 追加する宣言例
namespace std {

  // std::generator本体
  template<class Ref, class V = void, class Allocator = void>
  class generator;

  namespace pmr {
    // pmr::generatorエイリアス
    template<class R, class V = void>
    using generator = std::generator<R, V, polymorphic_allocator<>>;
  }
}

これによって、std::generatorとほぼ同じ使用感によって、polymorphic_allocatorを使用したgeneratorを使用できるようになります

std::pmr::monotonic_buffer_resource mbr;
std::pmr::polymorphic_allocator<> pa{&mbr};

// この提案後
std::pmr::generator<int> g = pmr_requiring_coroutine(std::allocator_arg, pa);

この提案は、すでに2月のIssaquah会議でC++23向けに採択されています。

P2788R0 Linkage for modular constants

名前付きモジュール内で定義されているconst変数のリンケージを通常の変数と同様に決定するようにする提案。

Cでは定数を宣言する場合にマクロが良く使われますが、C++ではその代わりにconst変数を使用することができます。

// Cでの定数
#define MAX_BUNNIES 57
struct bunny bunnies[MAX_BUNNIES];

// C++での定数
const int max_bunnies=57;
bunny bunnies[max_bunnies];

ただし、C++においてこのmax_bunniesのような定数がヘッダで宣言される場合、その定義は複数の翻訳単位に現れる可能性があります。この場合に多重定義の問題を回避するために、C++ではこのような変数に暗黙的に内部リンケージを与えています。ただし、ODR違反を起こす可能性が完全に排除されたわけではありません。

C++17では、inline変数によってこのような定数をinline constexprとすることでODR的に完全に安全な定数を宣言できるようになりました。

また、C++20モジュールでは、inlineはそのリンケージを変更する程度の意味しか持たず、inlineであったとしても定義は翻訳単位全体で1つである必要があるため、同様にODRの問題が解消されます(exportやモジュールリンケージによって参照することで、定義を各翻訳単位に用意する必要が無くなる)。

しかし、const変数に暗黙に内部リンケージを与える特別扱いはモジュールにおいても残っており、これによってモジュール内部からのそのような変数の参照が不可解な問題を起こします。

/// 翻訳単位 #1 モジュールAの実装パーティション
module A:B;

// 内部リンケージ
const int dimensions = 3;

/// 翻訳単位 #2 モジュールAの実装単位
module A;

import std;
import :B;

using vector = std::array<double, dimensions>;  // error: dimensionsは内部リンケージ

dimensionsexportを付加すると外部リンケージが与えられるため、この問題は同じモジュールの内側に閉じています。

また、このような変数は非内部リンケージの関数から使用することは禁止されています

/// 翻訳単位 #1 モジュールAのプライマリインターフェース単位
export module A;

// 内部リンケージ
const double delta = 0.01;

// 外部リンケージ
template<class F>
export double derivative(F&& f, double x) {
  return (f(x+delta)-f(x))/delta; // インスタンス化するまでエラーにならない
}

/// 翻訳単位 #2 モジュールAの実装単位
import A;

double d = derivative([](double x) {return x*x;},2);  // error: derivative()の定義は内部リンケージ名deltaを参照している

モジュールにおいては、明示的にinline, extern, exportのいずれかを付加することでリンケージを変更することができますが、これらはそれぞれ少しずつ意味が異なるため、どれを使用するべきかは微妙です

  1. inlineは(標準的には)リンケージの変更のみを行う
  2. externは、定義に適用する必要があり、モジュールリンケージを与える
  3. exportは、異なる状況でexportを取り除いた時だとモジュール内部では影響がない
    • 外部リンケージからモジュールリンケージになるだけのはずでは・・・

これらのいずれかの指定を明示的に変更しなければならないということは、非inlineconst変数を使用している従来のヘッダーファイルベースのライブラリをモジュールに移行する際の障害となります。

この提案ではこの問題を根本的に解決するために、インポート可能なモジュール本文内では名前空間スコープのconst変数に暗黙的に内部リンケージを与える仕様を無効化し、通常の変数と同じ方法でリンケージを決定するようにします。これによって、exportが付加されない場合は通常これらの変数はモジュールリンケージが与えられるはずです。

ただし、インポート可能なモジュール本文内とあるように、モジュール実装単位やグローバルモジュール(特にヘッダユニット)ではその扱いは従来と同じとなります。グローバルモジュールは破壊的変更になるため当然として、モジュール実装単位は他の翻訳単位からインポートされることが無くこの変更を適用する意味がないためです。

この提案はC++20へのDRとすることを提案しています。

この提案はNBコメントの解決であることもあり、すでに2月のIssaquah会議でC++20のDRとして採択されています。

P2789R0 C++ Standard Library Ready Issues to be moved in Issaquah, Feb. 2023

2月に行われたIssaquah会議でWDに適用されたライブラリに対するIssue報告の一覧

P2790R0 C++ Standard Library Immediate Issues to be moved in Issaquah, Feb. 2023

2月に行われたIssaquah会議でWDに適用されたライブラリに対するIssue報告の一覧。こちらはC++23で新規追加されたライブラリ機能に対するものか、NBコメントを受けてのIssue報告です。

P2791R0 mandate concepts for new features

新しいライブラリ機能がテンプレートを使用する場合、その制約をコンセプトによって指定するようにする提案。

C++20でコンセプトが導入されて以降もいくつかの新しいライブラリ機能が標準に導入されていますが、そのテンプレートパラメータの制約は必ずしもコンセプトが使用されているわけではありません。レビューの最中にコンセプトを使うように訂正が入ることもあれば、適格要件(Mandate)で文章と式によって指定されることもあります。

この提案は、今後のライブラリ機能はテンプレートパラメータの制約に必ずコンセプトを使用するように提案するとともに、なぜ現在LWG/LEWGがそうしていないのかを明らかにしようとするものでもあります。

この提案は仮に採択されたとしても、標準のプロセスに適用される問題であり、規格書そのものに何か記述が追加されたりするものではありません。

P2796R0 Core Language Working Group "ready" Issues for the February, 2023 meeting

11月に行われたIssaquah会議でWDに適用されたコア言語に対するIssue報告の一覧。

P2797R0 Proposed resolution for CWG2692 Static and explicit object member functions with the same par

明示的オブジェクトパラメータを持つメンバ関数staticメンバ関数の曖昧さを解消する提案。

明示的オブジェクトパラメータとは、C++23で導入されたDeducing thisという機能のことで、thisに相当する引数を明示的に記述してメンバ関数を宣言できる構文のことです。

struct S {
  int n;

  // 明示的オブジェクトパラメータによるメンバ関数宣言
  void f(this S& self, int m) {
    self.n = m;
  }
}

こうして宣言した関数は、普通に使用する分にはメンバ関数のように使用できますが、規格的にはどちらかというと非メンバ関数のような扱いをされています。特に、そのアドレスはメンバ関数ポインタではなく普通の関数ポインタとして取得され、メンバポインタ特有の少し変わった関数呼び出しではなく通常の関数ポインタの用法によって呼び出しができます。

この扱いはstaticメンバ関数と同様であり、現在の仕様のもとでは関数ポインタ経由で呼び出しを行った際のstaticメンバ関数との間の振る舞いに仕様の空白地帯が存在しているようです

struct A {
  static void f(A);
  void f(this A);

  void g();
};

void A::g() {
  // C++23からの問題
  (&A::f)(A()); // #1 ?
  (&A::f)();    // #2 ill-formed

  // 通常の非修飾名関数呼び出し
  f(A());       // ok、(*this).f(A())のような呼び出しになり、static void f(A)を呼び出す
  f();          // ok、(*this).f()のような呼び出しになり、void f(this A)を呼び出す
  // ここでのオーバーロード解決では、次の2つの候補が上がっている
  // static void f(T, A) : 静的メンバ関数(Tは任意のオブジェクトに無変換でマッチする型名)
  // void f(this A)      : 明示的オブジェクトパラメータを持つ関数
  // そして、f(args...)に対して、f(*this, args...)のように探索とオーバーロード解決が行われる
}

この例の#1は適切な候補が見つからないためエラーとなりますが、#2がどうなるのかは規定されていないようです。

この例はクラス定義内(非staticメンバ関数内)からの呼び出し例であり、関数ポインタ経由の呼び出しを普通の関数呼び出しに直した場合は、(thisが見えていることから)明示的オブジェクトパラメータを持たないオーバーロード候補のメンバ関数static/非static)はその引数列の先頭に暗黙のオブジェクトパラメータを受け取るかのように(先頭に引数を1つ追加したシグネチャを持つかのように)扱われます。

これはメンバ関数staticに関わらず統一的に扱ってオーバーロード解決するための仕組み(おそらくは処理や規格の記述の共通化のため)ですが、staticメンバ関数の場合はこの暗黙の第一引数には同じく暗黙的にあてがわれているフェイクのthis引数だけが当てはまり、それを明示的に指定することも参照することもできません。オーバーロード解決においては、staticメンバ関数の暗黙の第一引数はあらゆる型のオブジェクトを無変換で受けられるような型となり、そのマッチングはオーバーロード順位に影響を及ぼしません。

ただしこの扱いは、関数ポインタから呼び出した時には行われず、(&A::f)(A())A::fオーバーロードされていない場合にのみ適切に呼び出すことができるはずです。ただし、現在の規定ではそれすらも曖昧となっているようです。

C++20までは、上記のA::f()が(static問わず)オーバーロードされている場合でも、そもそも関数ポインタを取る時は取得対象の関数が既に確定している必要があり、オーバーロードされている場合はアドレス取得の段階でエラーになるため問題となることはありませんでした。

// C++20までのコードとする

struct A {
  static void f(A) {}

  static void g(A) {}
  void g() {}

  static void h(A) {}
  static void h() {}

  void i() {}

  void call();
};

void A::call() {
  f(A{});       // ok
  (&A::f)(A{}); // ok

  g(A{});       // ok static
  g();          // ok 非static
  (&A::g)(A{}); // ok ただし実装によってはng

  h(A{});       // ok
  h();          // ok
  (&A::h)(A{}); // ng、オーバーロードされておりアドレス取得対象が確定しない

  i();                // ok
  (&A::i)();          // ng、メンバポインタ呼び出しが必要
  (this->*(&A::i))(); // ok、メンバポインタ呼び出し
}

しかし、Deducing thisの導入によって異なる構文を持ちながら実質的に同じようなシグネチャとなりうるメンバ関数宣言構文が追加されたことでこの辺りのことが曖昧になり、その関数ポインタを取得して呼び出そうとしたときにそれらの宣言が衝突するのかどうか(オーバーロードとみなされるのかどうか)が不透明になってしまっているようです。最初の例では、2つのfオーバーロードとみなされるのかが不透明であるため、&A::fからの呼び出しがどうなるのかが不透明となっています((&A::f)()は、仮に呼び出し可能だったとしても適切な候補がないのでどちらにしてもエラー)。

これらの問題はコア言語のIssueとして報告され、この提案はその解決のためのものです。

提案では、次のようなコードを例に、可能な解決として2つのオプションを提示しています。

struct A {
  static void f(A); // #A staticメンバ関数
  void f(this A);   // #B 明治的オブジェクトパラメータを持つメンバ関数

  static void e(A const&); // #C staticメンバ関数
  void e() const&;         // #D 通常メンバ関数


  // クラス外内からの呼び出し
  // 非修飾名呼び出しはメンバ関数も探索する
  void g() {
    // static + 明治的オブジェクトパラメータを持つメンバ関数
    (&A::f)(A()); // #1
    f(A());       // #2 非修飾名呼び出し、暗黙のオブジェクト引数補完が行われる
    (&A::f)();    // #3

    // static + 通常メンバ関数
    (&A::e)(A()); // #4
    e(A());       // #5 非修飾名呼び出し、暗黙のオブジェクト引数補完が行われる
    (&A::e)();    // #6
  }
};

// クラス外部からの呼び出し
// 非修飾名呼び出しは非メンバ関数のみを探索する
void h() {
  // static + 明治的オブジェクトパラメータを持つメンバ関数
  (&A::f)(A()); // #7
  f(A());       // ill-formed、非メンバf()は宣言されていない
  (&A::f)();    // #8

  // static + 通常メンバ関数
  (&A::e)(A()); // #9
  e(A());       // ill-formed、非メンバe()は宣言されていない
  (&A::e)();    // #10
}
  1. #1 #2及び#4 #5はそれぞれ同じ振る舞いをし(staticメンバ関数を呼び出し)、#3 #6はill-formed
    • 非修飾名による関数呼び出し(&A::f)の実際の引数は((A&)*this, A{})であるため、#Aだけが#1#2の有効な候補となる
    • 関数ポインタからの呼び出し時も、thisが見えてれば非修飾名呼び出しと同じ扱いをする
    • staticメンバ関数と明治的オブジェクトパラメータを持つメンバ関数の間でオーバーロード成立を回避する
  2. #1は曖昧、#2staticメンバ関数を呼び出し、#4は曖昧、#5staticメンバ関数を呼び出す
    • #3(候補がない) #6(メンバポインタ呼び出しが必要)はill-formed
    • (&A::e)という式でオーバーロード解決するのではなく、まずオーバーロード候補全てのアドレスを取得し、そのポインタで呼び出し式を解決する
    • #1 #4は曖昧になり、#2#3は異なる結果となる
    • staticメンバ関数と明治的オブジェクトパラメータを持つメンバ関数の間でオーバーロードが成立し、衝突しうる

どちらのオプションでも、現在は行なっていない関数ポインタの取得対象の自動解決を行うようになります。

EWGにおける議論の結果としてはオプション2が選択されたようです。これによって(&function-id-expression)(expr-list)というような関数ポインタによる呼び出し式では、そのポインタ取得先の最適候補を選択する前に、function-id-expressionでの探索結果によるオーバーロード候補集合を、その要素のポインタに減衰させてからオーバーロード解決を行い、その結果でもって&function-id-expressionがどの関数のポインタを取得するのかを決定します。

オプション2では現在に引き続いて、thisが見えているスコープでも関数ポインタからの呼び出しで暗黙のオブジェクトパラメータの自動補完のようなことをしないため、その呼び出し結果はthisが見えているかに関わらず(クラススコープ内外に関わらず)同じになります。よって、#7 #9は曖昧となり、#8 #10はill-formedとなります。

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

struct C {
  void c(this const C&);   // #1
  void c()&;               // #2 暗黙のオブジェクトパラメータを持つ
  static void c(int = 0);  // #3

  void d() {
    c();               // error: #2と#3の間で曖昧
    (C::c)();          // error: 同様
    (&(C::c))();       // error: オーバーロード候補(this->C::c)のアドレスを解決できない(メンバポインタの構文ではないため)
    (&C::c)(C{});      // #1を選択
    (&C::c)(*this);    // error: #2が選択されるがill-formed(メンバポインタ呼び出しが必要)
    (&C::c)();         // #3を選択
  }
};

c()の呼び出し候補は#1~#3全てですが、thisが非const左辺値であることから#2と#3が最適候補となり(staticメンバ関数に補われた暗黙のオブジェクトパラメータはあらゆる型の値を無変換で受け入れオーバーロード順位に影響しない)、両者の順位がつかないため曖昧となります。(C::c)()c()等価な呼び出しになります。

&(C::c)はメンバポインタ取得の構文として不正なのでエラーになります。

(&C::c)(C{})は#1~#3全ての関数ポインタを取得してからオーバーロード解決を行い(それをpとするとp(C{}))、#2も考慮対象となりますが、右辺値を受けられるのは#1のみとなります。

(&C::c)(*this)も上記とほぼ同様の手順を辿り、非const左辺値にベストマッチするのは#2ですが、この場合に非staticの明示的オブジェクトパラメータを持たない関数が選択されるとill-formedと規定されているためエラーになります。

(&C::c)()は取得される関数ポインタをpとするとp()という呼び出しになり、マッチするのは#3のみです。

この提案は、Issue解決であることもあり、すでに2月のIssaquah会議でC++23へ採択されています。

P2798R0 Fix layout mappings all static extent default constructor

std::mdspanのレイアウトマッピングクラスのデフォルトコンストラクタの事前条件を修正する提案。

現在用意されているstd::mdspanのレイアウトマッピングクラスは3種類(std::layout_left, std::layout_right, std::layout_stride)あり、それらの::mapping型はdefault実装のデフォルトコンストラクタを持っています。そして、このデフォルトコンストラクタは何ら事前条件や制約を持っていません。

例えば次のような極端な例を考えてみると

constexpr size_t N = 4'000'000;
std::layout_left::mapping<std::extents<int, N, N>> map;

レイアウトマッピングクラスは多次元インデックスを1次元配列上のインデックスに変換するようなことを行いますが、その際のインデックスの型はExtents::index_typeが使用され、これはstd::mdspanExtentsテンプレートパラメータから与えられます。この型には通常std::extents<I, N, ...>が使用され、レイアウトマッピングクラスが使用するインデックスの型はここのIから取得されます。

上記の例ではインデックスの型はintであり、各次元の静的な要素数Nint型に収まっているものの、それを1次元のインデックスにマッピングすると最大でN * N + Nのような計算を行うことになり、これはオーバーフローします。

非デフォルトのコンストラクタでは事前条件によってこの問題に対処しており、この提案はデフォルトコンストラクタも同様に事前条件を追加することによってこの問題に対処するようにしようとするものです。

ただし、各次元のextent(次元ごとの要素数)に1つでも動的なもの(std::dynamic_extent)を含む場合、それをデフォルト構築するとその次元のextentは0になるため全体の要素数も0、つまり空になるため問題とならず、デフォルトコンストラクタでこの問題があるのはすべてのextentが静的に定まっている場合のみです。

そこで、レイアウトマッピングクラスに指定されたExtentsが全て静的に定まっている(Extents::rank_dynamic() == 0の)場合、その多次元インデックス値はExtents::index_typeで表現可能であること、が適格要件(Mandates)として指定されるようにします。これは3つのレイアウトマッピングクラスすべてに対して指定されます。

これによって、上記のような例はコンパイルエラーになるようになります。

P2799R0 Closed ranges may be a problem; breaking counted_iterator is not the solution

P2406で報告されている問題について、counted_iteratorの変更による解決は間違っていると指摘する提案。

P2406については以前の記事を参照

P2406で報告されているのは、counted_iteratorを特定の範囲に対して使用するとその終端で意図しない振る舞いをする可能性があり、それを修正するためにlazy_counted_iteratorを提案(あるいはそれをcounted_iteratorに適用)しようとしています。

この提案は、それらの問題の本質はC++イテレータモデルにそぐわない閉区間の範囲を半開区間の範囲を扱うために設計されたcounted_iteratorで使用しようとしていることにあり、counted_iteratorは半開区間の範囲に対して適切に設計されているため、閉区間の範囲のためにcounted_iteratorを壊す(あるいはlazy_counted_iteratorを追加する)のは間違っている、とするものです。

C++イテレータは任意の要素からなる半開区間[first, last))を表現するものであり、N個の要素からなる範囲にはN+1個のイテレータの値が対応しています。この内N個はN個の要素に対応し、残りの1つは終端(番兵)値に対応します。C++20の範囲(range)は、このようなイテレータによる範囲の先頭イテレータと終端イテレータのペアとなるもののことであり、これもやはり半開区間の範囲を表現しています。

counted_iteratorは整数カウントをイテレータに結びつけただけのものです。

template <input_or_output_iterator It>
struct counted_iterator {
  It it;
  int count;
};

counted_iteratorの移動と共にカウントは増減するため、同じ範囲への2つのイテレータ間の距離を簡単に計算できます。

counted_iteratorがカウントダウン(進行するとカウンタを減らす、後退する場合は逆)によってカウントを管理しているのは単にその番兵をステートレスにするためで、終端チェック(番兵値との比較)においてはカウンタが0かどうかをチェックするだけで済むためです。

ここでP2406で提起されている問題に戻ると、これらの問題を引き起こしている入力の範囲は半開区間ではなく閉区間を想定するものであることに気付けます

iota | filter | takeの例

for (auto i  : std::views::iota(0)
             | std::views::filter([](auto i) { return i < 10; })
             | std::views::take(10))
{
  std::cout << i << '\n';
}

ここでiota(0) | filter([](auto i) { return i < 10; })によって生成される範囲はちょうど10個の要素を持ちますが、番兵値に対応する11個目の要素がありません。したがって、その終端を得るために11個目の要素(イテレータ)を計算しようとしていることが問題の根本的な原因です。

istream_viewの例

auto iss = std::istringstream("0 1 2");
for (auto i : std::ranges::istream_view<int>(iss)
            | std::views::take(1))
{
  std::cout << i << '\n';
}

auto i = 0;
iss >> i;
assert(i == 1); // FAILS, i == 2

istream_viewtake(1)によってストリームから1つ値を読み取り(0)それを要素とする範囲を生成しますが、ここでもやはり終端値としてその次の値(1)を必要とするため、このイテレーションの終了までの間にストリームから2つの値を読み出すことになります。

ここでの問題は、istream_viewがその入力ストリームへの全てのアクセスがそのイテレータを介してしか行われないことを前提としていることから起きており、イテレータを一貫して使用している限り問題は起きません。つまり、元のストリームを直接触りに行く利用者が、istream_view | take(1)による範囲が半開区間(1要素+1番兵)ではなく閉区間(1要素)だと思ってしまっていることから起きています。前述のようにこの期待は間違っています。

ただ、閉区間による範囲を考慮すると便利な場合もあり、例えばiota_viewはその要素型の最大値を範囲内に含めることができません(番兵値として必要となるため)。ですが、閉区間による範囲は現在のC++イテレータモデルにはそぐわないものであり、counted_iteratorがそのために設計されていないことは当然のことです。

とはいえ、P2406で提起されている問題にもあるように、閉区間の範囲も出現しうるものであり、それをC++イテレータに適合させて使用しようとするのも自然なことではあります。その場合に必要となるのは、そのようなちょうどN個の要素からなる閉区間をN+1個の要素による半開区間として扱うための方法です。つまり、番兵となる何かの値を添加する必要があります。

その方法としては、例えばvariant<OriginalIterator, PastTheEndSentinel>のような型の値を使用するなどの方法が考えられ、カウントを使用するのもその方法の一つです。ただし、カウントを使用するのはそのための唯一の方法ではありません。例えば、整数の閉区間の範囲[first, last]を表すrange-v3ライブラリのclosed_iotaイテレータは次のようなものになっており

struct iterator {
  I current;
  I last;
  bool past_the_end;
};

このイテレータは進行によってcurrent == lastとなる場合にpast_the_endtrueに設定し、currentをインクリメントしないようにしています。

区間の範囲を表現し、それを既存のイテレータ/rangeとして使用できるようにすることには価値がある可能性があります。しかし、counted_iteratorがそのための方法になるべきではありません。特に、そのためにオーバーヘッドを増やし、コンパイラの最適化を阻害し、機能を制限するような変更をC++23作業完了の直前に行うことは閉区間の範囲のサポート方法としては完全に誤っています。

counted_iteratorは半開区間の範囲に対してカウントを結びつけるように設計されており、その想定されるユースケースと設計に問題はなく、閉区間の範囲のサポートのためにはC++26で別の機能として追加することができます。

P2802R0 Presentation of P1385R7 to LEWG at Issaquah 2023

P1385R7をLEWGのメンバにプレゼンするためのスライド。

P1385R7は行列型をはじめとする線形代数関連のクラス型を標準ライブラリに用意しようとする提案です。詳しくは以前の記事を参照

R0からR7の変遷や、今後の展望などが簡単にまとめられています。

P2803R0 std::simd Intro slides

C++26に向けて提案中のstd::simdクラス型の紹介スライド。

std::simdについては以前の記事を参照

データ並列型としてのstd::simdのコンセプトから、基本的な使用方法まで非常にわかりやすく紹介されています。

P2805R0 fiber_context: fibers without scheduler - LEWG slides

P0876で提案中のスタックフルコルーチンの中核となるfiber_contextの紹介スライド。

P876については以前の記事を参照

fiber_contextの役割や必要性を紹介し、用意されているコンストラクタやメンバ関数について解説が行われています。

P2806R0 do expressions

値を返せるスコープを導入するdo式の提案。

C++の構文は文(statement)に基づいて構築されており、ifforなどは文であり式(expression)ではありません。文は基本的に値を返すことができず、単一の式は条件分岐やループなどを含むことができません。

変数の初期化時など、単一の式以上のことが必要になる場合はそれを関数にまとめて関数呼び出しに置き換えることで近いことを達成でき、特に即時呼び出しするラムダ式を使用するとかなり通常のブロックに近い形で書くことができます。

int main() {
  // 実行時に決定される値とする
  bool flag = ...;

  // 変数をconstで宣言したいが、初期化はflagによって分岐する
  const int n = [&] {
    if (flag) {
      ...
      return -10;
    } else {
      ...
      return 10;
    }
  }();

}

ただし、この方法も問題があり、追加の関数スコープを導入してしまうことで制御フローを複雑化させています。このようなラムダ式forループの内部に現れている場合、ラムダ内部から外側のループをbreak/continueすることはできず、関数内部で現れている場合はラムダを囲む関数から直接returnすることはできないほか、コルーチン内部で現れている場合はラムダ内部から外側コルーチンのco_yield/co_await/co_returnを行うことができません。このような制約を理解するには追加のC++に関する知識が求められるなど、この方法は完璧とは言い難いものです。

この問題はまた、提案中のパターンマッチング(P1371R3)においても問題となる可能性があります。パターンマッチングにおいてはinspect式のブロック中にpattern => expression;の形でパターンに対する処理を記述していきますが、ここでも=>の右辺に指定できるのは単一の式であり、先ほど同様に1つの式以上のことが必要になった時に関数呼び出しに置き換えるなどする必要があります。

P1371ではそのために、=>の右辺に現れる{ statement }を特別扱いして、この式をvoid型の式として評価することで1つの式以上のことを書けるようにしています。ただしこれは言語の他の部分と一貫性がなく、パターンマッチングが{}初期化式を使用する将来の拡張を妨げることになります。

auto f() -> std::pair<int, int> {
  // これはできる
  return {1, 2};

  // P1371(P2688)の方向性だとこれはできない
  return true match -> std::pair<int, int> {
      _ => {1, 2}
  };
}

このパターンマッチングにおける問題を解決するには、ステートメント式(statement-expression)が必要です。そして、そのようなものはパターンマッチングだけではなく、最初の例のように広く有用なものになります。

このように、現在及び将来の機能でもステートメント式が必要とされていることから、パターンマッチングにおける文法を単純化できるような構文でステートメント式をサポートする直交性の高い言語機能を導入しようとするのがこの提案の目的です。

この提案によるステートメント式は、do { statement }のような構文で、do式と呼ばれます。do式は式なので型と値を持ち、とても単純には次のように使用できます

int x = do { do return 42; };

do式のブロックが導入するのは単なるブロックスコープであり、そこから値を返すにはdo returnというreturn文を使用します。ブロックスコープ中には他のブロック同様に任意のC++コードを記述することができ、これは関数スコープではないため新しいスタックフレームを導入せず、その外側の制御フローの一部であり続けます。

do returnは、do式から値を返すという点を除いてreturnと同じ振る舞いをし、コピー省略などの戻り値最適化も適用されます。

std::string s = do {
  std::string r = "hello";
  r += "world";
  do return r;  // rは暗黙ムーブされる
};

do式の型と値カテゴリは、その内部のすべてのdo return文から推定されます。これはautoで戻り値型を宣言した関数/ラムダ式と同じルールによります。また、それらと同様に後置戻り値型を書くことで戻り値型を明示的に指定することもできます。

do -> long { do return 42; }  // do式の戻り値型はlong、値カテゴリはprvalue

do式内でdo returnが使用されていないか、do return;のようにしか使用されていない場合、式の型はvoidpravalueになります。

P2688R0で提案されているパターンマッチング構文(P1371の進化版)にこのdo式を組み込むことができ、それによって次のような記述が変化します

P2688 この提案
x match {
  0 => { cout << "got zero"; };
  1 => { cout << "got one"; };
  _ => { cout << "don't care"; };
}
x match {
  0 => do { cout << "got zero"; };
  1 => do { cout << "got one"; };
  _ => do { cout << "don't care"; };
}

どちらの場合も、このmatch式の結果はvoidです。

これによって、=>の右辺での{}の特別扱いを回避し、{}初期化をサポートすることができるようになります

P2688 この提案
auto f(int i) {
  return i match -> std::pair<int, int> {
    0 => {1, 2};          // ill-formed
    _ => std::pair{3, 4}; // ok
  }
}
auto f(int i) {
  return i match -> std::pair<int, int> {
    0 => {1, 2};          // ok
    _ => std::pair{3, 4}; // ok
  }
}

do式の導入するスコープは関数スコープではなく、制御フローはその外側のフローの一部です。従って、do式からのreturnbreakはその外側の制御フローに対して効果を持ちます。

int outer() {
  int g = do {
    if (cond) {
      do return 1;  // do式からのreturn
    }

    return 3; // outer()からのreturn
  };
}

void func() {
  for (;;) {
    int j = do {
      if (cond) {
        break;  // do式およびその外側のループから脱出
      }

      for (something) {
        if (cond) {
          do return 1;  // このループから脱出し、do式からreturn
        }
      }

      do return 2;  // do式からのreturn
    };
  }
}

このdo式の導入するスコープは関数スコープ(関数・ラムダ・コルーチン)と異なる点があり、do式がvoid型の式ではなくdo returnが現れる前にその終端にたどり着いた場合はill-formed とされます。関数スコープの場合はこれは未定義動作とされていました。これによって、ユーザーはdo式内の制御パスを全てカバーするように注意する必要があり、それができていないとコンパイラに怒られます。

int i = do {
  if (cond) {
    do return 0;
  }

  // error
};

do式の型がvoidの場合は暗黙的にdo return;が補われエラーにはなりません。

また、do return以外にも、do式終端にたどり着かないことがわかっているものが全ての制御パスに現れていればエラーにはなりません。それは例えば

  • 外側の制御フローへ戻るもの
    • return
    • breake
    • continue
    • co_return
  • throw
  • [[noreturn]]関数
    • 現在でも[[noreturn]]とマークされた関数から制御が戻ると未定義動作となる
    • この未定義動作を利用して、新しい未定義動作を導入することなく[[noreturn]]関数の呼び出しを制御フローからの脱出と見做せる
enum Color {
  Red,
  Green,
  Blue
};

void func(Color c) {
  // error
  std::string_view name = do {
    switch (c) {
      case Red:   do return "Red"sv;
      case Green: do return "Green"sv;
      case Blue:  do return "Blue"sv;
    }
    // ここに到達しうる
  };
}


int main() {
  // ok
  auto a = do {
    if (cond) {
      do return 1;
    } else {
      do return 2;
    }
    // ここにはこない
  };

  // ok
  int f = do {
    if (cond) {
      do return 1;
    }

    throw 2;
    // ここにはこない
  };
  
  // ok
  int h = do {
    if (cond) {
        do return 1;
    }

    std::abort(); // [[noreturn]]関数
    // ここにはこない
  };
}

また、goto文の使用はdo式からの脱出のみ許可され、do式内のラベルへのジャンプは禁止されます。

この提案はEWGでの最初のレビューにおいて引き続き議論していくことにコンセンサスが取れています。

P2807R0 Issaquah Slides for Intel response to std::simd

std::simdに対するintelの経験に基づくフィードバック提案の解説スライド。

対象の提案については以前の記事を参照

かく提案のうち既に解決済みの問題や、未解決のものの理由や利点欠点などを解説しています。

P2808R0 Internal linkage in the global module

グローバルモジュールにある内部リンケージを持つエンティティの曝露(exposure)を許容するようにする提案。

static inline関数のような、内部リンケージを持つエンティティを名前付きモジュールの本文内で使用する場合、それをその翻訳単位の外部に曝露しないように注意しなければなりません。内部リンケージを持つものの曝露とは、exportしたinline関数などから内部リンケージを持つものが翻訳単位外部から参照される可能性がある場合を言います。曝露が起きている場合はコンパイルエラーとなります。

曝露については以前の記事を参照

ただ、static inline関数のような内部リンケージエンティティは特に、次のような理由からCヘッダで一般的に使用されています

  • CのinlineセマンティクスはC++のそれとは異なり、どこかに非inline定義が必要となる
  • 内部リンケージを持つことから、コンパイラが認識したものと異なるものを取得することがなく、ABIの分離を実現できる
    • これによって、ABIの破損を気にすることなくその動作を変更できる

このようなCヘッダはC/C++間のコード共有のためのものでもあり、そのままモジュールに移行されることはなく、おそらくヘッダユニットのインポートやグローバルモジュールフラグメントでのインクルードなどによって名前付きモジュール内から利用されることになるでしょう。

その場合、ヘッダで定義されている内部リンケージエンティティ(特にstatic inline関数)はグローバルモジュールに属する内部リンケージエンティティとなりますが、名前付きモジュールのインターフェースからの暴露に関する制約は名前付きモジュールに属する内部リンケージエンティティと同じ扱いとなります。従って、そのようなものを普通に使う感覚でinline関数や関数テンプレートから参照してしまうとコンパイルエラーを引き起こします。

/// myheader.h

// 内部リンケージ
static inline int f(int n) {
  ...
}
/// mymodule1.cpp
module;
// グローバルモジュールでインポート
#include "myheader.h"
export module mymodule1;

export inline int func1(int n) {
  return f(n);  // ng、内部リンケージエンティティf()を曝露している
}

inline int func2(int n) {
  return f(n);  // ng、内部リンケージエンティティf()を曝露している
}

static inline int func3(int n) {
  return f(n);  // ok、func3()は内部リンケージ
}
/// mymodule2.cpp
export module mymodule2;

import std;

// ヘッダユニットでインポート
import "myheader.h"

template<std::integral I>
export inline int func1(I n) {
  return f(n);  // ng、内部リンケージエンティティf()を曝露している
}

// 以下同様

重要なのは、このようなものはC/C++のコード共有地点で現れるもので、ABI分離等の利点があり、C++の都合のみで変更できるものではなく、また名前付きモジュールに属していない(名前付きモジュールの一部ではない)ということです。さらに、そのようなC/C++共有ヘッダの内部リンケージエンティティはGithubで公開されているいくつかの大規模なプロジェクトだけでも数千件も発見でき、潜在的にはさらに多く利用されていることが予想されます。

P2691R0ではヘッダユニットのstatic inline関数に限って同様の問題を報告しており、そこではこの問題は以前に予想されたよりも影響が大きく、深刻なモジュール採用の障害になっていることを報告しています。

この問題を要約すると、Cヘッダにある内部リンケージエンティティをグローバルモジュールを介して名前付きモジュールのインターフェースから使用する際、曝露を回避して使用しなければならない(あるいはそれが困難)、ということです。この提案は、そのようなものを含む既存コードーベースの円滑なモジュールへの移行のために、この問題を解決しようとするものです。

この問題の解決のためには、次のことを達成する必要があります

  • 問題となっているエンティティを他の翻訳単位から参照できるようにする
  • 既存コードを壊さない
  • UB(すなわちODR違反)を増加させない
  • 実装可能であること

その上でこの提案では、次のような変更によってグローバルモジュールにある内部リンケージエンティティが名前付きモジュールインターフェースから曝露されるのを許可します

  1. インポート可能なヘッダをインポートするすべての翻訳単位は、そのヘッダの独自のヘッダユニットを取得する
    • これはモジュールであるかに関わらない
  2. 各翻訳単位のグローバルモジュールフラグメントとすべてのヘッダユニットに対して、次の変換を適用する
    • 内部リンケージを持つすべてのエンティティは、インポート先の翻訳単位に属するモジュールリンケージが与えられる
    • 内部リンケージを持つすべてのエンティティは、この変換が適用されない場合のエンティティと区別される
      • 何かしらのタグを用いて名前マングルされる
    • 内部リンケージを持つすべての関数と変数はinline化される

1つ目の変更によって、ヘッダユニットのインポートは翻訳単位ごとに異なるヘッダユニットを生成し使用するようになります。これによって、翻訳単位が異なれば同じヘッダを示すヘッダユニットをインポートしていても、異なるヘッダユニット(翻訳単位)を使用することになります。

2つ目の変更は、主にモジュールのインターフェース単位において行われ、Cヘッダからの内部リンケージエンティティはグローバルモジュールではなくそのモジュールに属するモジュールリンケージを持つエンティティとして扱われ(これによって同じモジュール内の別の翻訳単位から使用できるようになり)、かつマングル名(モジュール内部でのみ有効な)レベルで明確に区別されるとともに、関数と変数はインライン展開されることで直接定義を参照することを回避します。ただし、この変換はどうやら非モジュールにおいても行われるようで、その場合はその翻訳単位とインポートするヘッダユニットを含む匿名モジュールが生成されたかのような扱いをされるようです。

この解決策は、先ほどの4つの要件を全て満たしています。例えば、内部リンケージのエンティティはモジュールのインターフェースでモジュールリンケージを持つようになり曝露の制限対象から外れますが、翻訳単位ごとの実体生成とインライン化によりABI分離も保たれています。実装に関しては、少し問題があるものの主要なコンパイラの開発者から実装可能であるとの確認を取れているようです。

P2810R0 is_debugger_present is_replaceable

P2546で提案されているis_debugger_present()をユーザーが置換可能にする提案。

std::is_debugger_present()は実行時にデバッガがアタッチされている場合にtrueを返す関数で、これはフリースタンディング環境でもサポートされることを目指しています。しかし、組み込み環境などの一部のフリースタンディング環境ではこの実装が困難となる場合があります。

この機能はフリースタンディング環境でも有用である可能性があり、フリースタンディング環境で削除してしまうとその判定と代替手段のためにプリプロセッサが使用されることになり、C++エコシステムのCPP依存を高めます。

そのため、そのような環境でもこの関数を動作させるための方法が必要であり、この提案はその方法としてこの関数をユーザーが置き換えることを許可することを提案しています。

これによって、ユーザーはフリースタンディング環境以外の環境においても、そのユースケースに従ってstd::is_debugger_present()を柔軟にカスタマイズすることができるようになります。例えば

  • アプリケーションの検証ビルドではtrueを返すようにしておく
  • 外部入力によって結果を制御する
    • キー入力やその他の外部信号、シグナルハンドラのシグナルなど

ここでの置き換えとは、std::is_debugger_present()という関数シグネチャを衝突させる形でユーザーコードで定義し、実装はそれを検出したらデフォルトの実装をユーザー定義のものに置き換える、のようなことです。

P2812R0 P1673R11 LEWG presentation

P1673で提案中の線形代数ライブラリの解説を行う文書。

主に、LEWGのレビューにおいてその設計がどのように変化したかを記述しています。

P2815R0 Slides for presentation on P2188R1

P2188R1の解説スライド。

P2188R1で主張されているポインタの保証や意味論(必ずしも現在のC++が保証していないもの)についての詳しい解説がなされています。

P2188R1については以前の記事を参照

P2816R0 Safety Profiles: Type-and-resource Safe programming in ISO Standard C++

C++を安全なプログラミング言語へと進化させることについて、その必要性及び方法について解説したスライド。

主に、P2687で提案されていることのベースとなっている考えについて詳細に説明されています。

おわり

この記事のMarkdownソース

[C++] constexpr ifとコンセプトと短絡評価と

constexpr if(構文としてはif constexpr)の条件にはboolに変換可能な任意の定数式を使用できます。複数の条件によって分岐させたい場合、自然に&&もしくは||によって複数の条件式をつなげることになるでしょう。そしてその場合、条件式には左から右への評価順序と短絡評価を期待するはずです。

auto func(const auto& v) {
  return v; // コピーされる
}

template<typename T>
void f(T&&) {
  // Tが整数型かつfuncで呼び出し可能
  // Tが整数型ではない場合は右辺の条件(funcの呼び出し可能性)はチェックされないことが期待される
  if constexpr (std::integral<T> && std::invocable<decltype(func<T>), T>) {
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ng
}

エラーはfunc()returnvがコピーできないために起きており、ここでのvstd::unique_ptrconst左辺値なので当然コピーはできません。しかし、コードのどこでもfunc()std::unique_ptrで呼んではいません、なぜこれはエラーになるのでしょうか?

起きていること

コードをよく見ると、直接的ではないもののfunc()std::unique_ptrで呼んでいる箇所が1つだけあります。それは、std::invocable<decltype(func<T>), T>という式で、これはstd::invocableコンセプトによってTによってfunc<T>()が呼び出し可能かを調べています。関数f()の引数型T経由で、ここでstd::unique_ptrfunc()にわたっています。

(コンセプトはこの様に使用した時にそれ単体で1つの式になり、bool型のprvalueを生成する定数式となります)

とはいえ、その直前(左辺)の条件ではstd::integral<T>によってTが整数型であることを要求しており、std::unique_ptrは当然整数型ではないのでその条件はfalseとなり、短絡評価によってstd::invocable<decltype(func<T>), T>は評価されないことが期待されます。しかし、ここでfunc()std::unique_ptrがわたってエラーが起きているということは、どうやらその期待は裏切られている様です。

このことは、std::integral<T>を恒偽式に置き換えてやるとよりわかりやすくなります。

template<typename T>
void f(T&&) {
  if constexpr (false && std::invocable<decltype(func<T>), T>) {
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
  }
}

どうやらconstexpr ifの条件式では短絡評価が起きていない様に見えます。

そしてこのことは主要な3コンパイラで共通していることから、どうやら仕様に則った振る舞いの様です。

if constexpr (A && B)としたら、A -> Bの順番で評価されてほしいしA = falseならBは評価されないでほしい気持ちがあります(これは||でも同様でしょう)。そうならないのが自然な振る舞いなはずはありません・・・

constexpr ifの扱い

constexpr ifC++17で導入された新しめの言語機能であり、ユーザーフレンドリーになってることが期待されます。しかし実際にはconstexpr ifif文の一種でしかなく、構文的にはconstexprがあるかないか程度の違いであり、その条件式の扱いはifのそれに準じています(そのため、実はcostexpr ifにも初期化式が書けます)。では、ifの条件式の扱いがそうなっているのでしょうか?

ifの条件式に関しては、文脈的にbool変換可能な式という程度にしか指定されておらず、constexpr ifに関してはそれが定数式であることが追加で要求されるくらいです。つまり、if/if constexprの条件式で短絡評価が起こるのかにはifは関与していません。

文脈的にbool変換可能な式というのはめちゃくちゃ広いですが、今回問題となるのはその中でも&&||の2つだけです(前述のように演算子オーバーロードは考慮しません)。実は&&||の組み込み演算子が短絡評価するかどうかは実装定義だったりするのでしょうか?

これもそんなはずはなく、(A && BもしくはA || Bとすると)どちらの演算子もそのオペランドの評価順序はA -> Bの順で評価されることが規定され、なおかつ短絡評価に関してもきちんと規定されており未規定や実装定義などではありません。C++適合実装は、場所がifであるかどうかに関係なく、A && BもしくはA || Bという式の実行において、式Aの結果に応じた短絡評価を行わなければなりません。

さて、ここまで掘り返しても冒頭のエラーの原因がわかりません。一体何が起きているのか・・・?

コンセプトはテンプレート(重要!!)

回り道をしましたが、実はここで起きていることはそれら以前の問題です。

template<typename T>
void f(T&&) {
  // Tが整数型かつfuncで呼び出し可能
  // Tが整数型ではない場合は右辺の条件(funcの呼び出し可能性)はチェックされないことが期待される
  if constexpr (std::integral<T> && std::invocable<decltype(func<T>), T>) {
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
  }
}

この局所的なコードのコンパイルにおいて、コンパイル時には次の順番で処理が実行されます

  1. f()インスタンス
  2. cosntexpr ifの条件式のインスタンス
  3. constexpr ifの条件式の評価
  4. constexpr ifの分岐

この時、2番目の条件式のインスタンス化においては主に、テンプレートパラメータTに依存するもののインスタンス化が行われます。インスタンス化が必要なものとはつまりテンプレートのことであり、そこにはコンセプトも含まれています。そう、コンセプトはテンプレートの一種なので、単体の定数式として評価する前にコンセプトそのもののインスタンス化が必要となります。

この例でのコンセプトのインスタンス化では、std::integral<T>std::invocable<decltype(func<T>), T>の2つのコンセプトのインスタンス化が行われます。前者は今回関係ないことが分かっているので、後者を詳しく見てみます。

まず、std::invocableの定義は次のようになっています

template<class F, class... Args>
concept invocable = requires(F&& f, Args&&... args) {
  invoke(std::forward<F>(f), std::forward<Args>(args)...);
};

コンセプトの定義は基本的にはこのrequires式内に求める要件式を並べて行い(また、複数のrequires式をつなげることもでき)、コンセプトのインスタンス化に伴って、その字句順(requires式の並び順)(たぶん)にテンプレートパラメータの置換(substitution)とテンプレートのインスタンス化(以降単にインスタンス化)が行われていきます。その際、requires式内部ではインスタンス化に伴ってill-formed(コンパイルエラー/ハードエラー)になるようなことが起きたとしても、そのrequires式がその式の評価時にfalseとなるだけでハードエラーを起こしません。

requires式のインスタンス化とその式の値の評価も、注意深く規格署の記述を読むと、テンプレートパラメータ置換およびインスタンス化と評価は段階が異なることが読み取れ(る気がし)ます。

この時、次のような記述([expr.prim.req]/5)によって、インスタンス化された式がハードエラーを起こす場合がある事が指定されています

requires式内の要件式に無効な型や式を含むものが含まれていて
それがtemplated entityの宣言内に表示されない場合
プログラムはill-formed

templated entityというのはテンプレート定義内の何かの事で、ここではrequires式に含まれる色々なものの事です。その宣言内に含まれない場合というのは要するに、コンセプト定義に直接見えていないような場合ということで、requires式に書かれた各要件式の内部で、別の式等が何かハードエラーを起こす場合のことを言っています(とおもいます・・・)。

さて、std::invocableは1つのrequires式だけからなるコンセプトで、その1つのrequires式は1つの単純要件だけを持ち、それはstd::invokeによってFArgsで呼び出し可能かどうかを調べています。

std::invocable<decltype(func<T>), T>の場合、最終的にはfunc<std::unique_ptr>()が呼び出し可能かが問われることになります。そして、そのチェックは宣言のみのチェックではなく、invoke(std::forward<F>(f), std::forward<Args>(args)...);という式の有効性のチェックとなるため、テンプレートパラメータの置換とそのインスタンス化が発生します。

func<std::unique_ptr>()インスタンス化されると、そのreturn文でコピーができないことからハードエラーを起こしますが、それはまさにinvocableコンセプト定義のrequires式の要件式に直接現れずにその呼び出し先の関数本体内で発生するため、これはrequires式の範囲外となりそのままハードエラーになります。

冒頭の例の謎のエラーはまさに、この場合に起こるエラーです。つまりは、式として短絡評価されるか以前のところ(テンプレートのインスタンス化)でエラーが起きています。

この場合にconstexpr ifに求めるべきだったのは、その条件式においてその評価順序に応じたインスタンス化と短絡評価によるインスタンス化そのもののスキップだったわけです。当然そんなことはC++17でも20でも要求されておらず、現在のコンパイラはそのようなことはしてくれません。想像ですが、インスタンス化そのものがスキップされると定義の存在有無が変化し、それはひいてはODRに関係してくる気がします。

回避手段

とはいえ、constexpr ifコンパイル時の条件によってインスタンス化の抑制を行うものなので、その条件式はほとんどの場合に何かしらのテンプレートパラメータに依存することになるでしょう。テンプレートのインスタンス化に伴うハードエラー回避のために短絡評価を期待するのはある種自然な発想であるため、この問題は割と深刻かもしれません(それでも、実行時ifと異なり問題はコンパイル時の謎のエラーとして報告されるのでマシではあります)。

そのため、短絡評価を期待通りに行ってもらう方法を考えてみます。

関数に制約をかける

この場合の例にアドホックな解決策ですが、呼び出そうとする関数が適切に制約されていることによってstd::invokeの呼び出し先がなくなれば、このような場合にstd::invokeの内部の呼び出し先内でのエラー発生を回避できます。

template<typename T, typename F>
void f(T&&, F&&) {
  // Tが整数型かつfuncで呼び出し可能
  // Tが整数型ではない場合は右辺の条件(funcの呼び出し可能性)はチェックされないことが期待される
  if constexpr (std::integral<T> && std::invocable<F, T>) {
    
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up), [](const std::copyable auto& v) { return v; }); // ok
  f(std::move(up), [](const               auto& v) { return v; }); // ng
}

例示のために少し書き換えています。

この場合、std::invocableコンセプト定義内のrequires式内のstd::invokeによる制約式では、呼び出すべき関数が見つからない(std::unique_ptrstd::copyableではない)ことからstd::invokeそのものがエラーになり、それはrequires式の範囲内なのでハードエラーになる代わりにそのrequires式の式としての評価結果がfalseになり、std::invocable<F, T>falseに評価されます。

とはいえ、このような回避手段はこの問題を理解したうえで適用可能であるかを調べる必要があり、いつでも可能な汎用的なソリューションではありません。

条件式を分ける

&&でつながれた条件式であれば複数のif constexpr文に分割することができるかもしれません。

auto func(const auto& v) {
  return v; // コピーされる
}

template<typename T>
void f(T&&) {

  if constexpr (std::integral<T>) {
    // Tがunique_ptrの場合はここには来ない
    if constexpr (std::invocable<decltype(func<T>), T>) {
      ...
    }
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ok
}

costexpr iffalseとなる(選ばれなかった)ステートメントインスタンス化されません。そのため、costexpr ifがネストしていれば疑似的に短絡評価のようになります。

この場合は、分岐が増えることによって考慮すべきパスが増加することに注意が必要です

template<typename T>
void f(T&&) {

  if constexpr (std::integral<T>) {
    // Tがunique_ptrの場合はここには来ない
    if constexpr (std::invocable<decltype(func<T>), T>) {
      ...
      return;
    }
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
    ...
    return;
  }

  // ここに来る場合がある
  std::unreachable();
}

あと||はどうしようもありません。ド・モルガンで頑張れる可能性はありますが・・・

requires式と入れ後要件を使う

@yohhoyさんご提供の方法です。

auto func(const auto& v) {
  return v; // コピーされる
}

template<typename T>
void f(T&&) {

  if constexpr (requires { requires std::is_integral_v<T>; requires std::invocable<decltype(func<T>), T>; }) {
    // Tがunique_ptrの場合はここには来ない
    ...
  } else {
    // Tがunique_ptrの場合はこちらに来る
    ...
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ok
}

実はrequires式はコンセプト定義やrequires節の外側でも書くことができて、その場合も内部の要件をチェックした結果のbool型のprvalueを生成する定数式になります。そのため、costexpr ifの条件式でも使用することができます。

上記のrequires式を取り出して見やすくすると

requires { 
  requires std::is_integral_v<T>;
  requires std::invocable<decltype(func<T>), T>;
}

requires式内部でrequires expr;のようにしているこの書き方は入れ子要件の制約と呼び、そのインスタンス化及び評価の順序は、字句順すなわちこのようにフォーマットした場合の上から順番に行われます。そのため、実質的にこれは&&条件を書いたのと同様になっており、なおかつコンセプトのrequires式はその要件を満たさない(falseに評価された)式が出現するとそこでインスタンス化と評価を停止するため短絡評価が期待できます。

ただ、短絡評価に関しては、「requires式の結果を決定する条件に出会うと、インスタンス化と評価を停止する」のように規定されており短絡評価を指定しているかは微妙です。実際、MSVCは短絡評価をしないような振る舞いをする場合があります(上記の例ではMSVCでも回避可能でしたが)。

また、これはも||で使用できません。ド・モルガンで頑張ることはできるかもしれませんが。

コンセプトに埋め込む

@yohhoyさんご提供の方法その2です。

前項の方法のrequires式を1つのコンセプトに纏めてしまう方法です。

#include <iostream>
#include <concepts>
#include <memory>

auto func(const auto& v) {
  return v; // コピーされる
}

// constecpr if条件を抽出したコンセプト定義
template<typename T>
concept C = std::integral<T> && std::invocable<decltype(func<T>), T>;

template<typename T>
void f(T&&) {

  if constexpr (C<T>) {
    // Tがunique_ptrの場合はここには来ない
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ok
}

コンセプト定義内でrequires式を用いずに直接bool型の定数式を指定することもできます。この場合のインスタンス化と評価の順序の規定は複雑ですが、コンセプトの定義内でこのように書いている場合は&&でも||でも同様に、字句順あるいは左から右にインスタンス化と評価が進行します(またこれは、requires式が複数ある場合のインスタンス化と評価の順序と同様です)。

さらに、requires式内部ではなくコンセプト定義で直接このように書いた場合は、&&||の結果に従った適切な短絡評価が規定されています。従って、この場合はstd::integral<T>falseと評価されればstd::invocable<decltype(func<T>), T>は評価もインスタンス化もされません。

前項のrequires式と入れ後要件による方法は移植可能性に懸念があるのと、||で使用できないのが問題でしたが、この方法であればそれらの問題点はすべて解消され、インスタンス化と評価時に短絡評価が保証されます。

std::conjunction/std::disjunctionを使用する

前項のコンセプトに切り出してしまう方法はほぼ完璧な方法ですが、コンセプトが名前空間内でしか定義できないことから、名前空間を汚すのが忌避されるかもしれません。短絡評価を行いつつ条件式はその場での使い捨てにしたい場合の方法として、古のTMPのユーティリティであるstd::conjunction/std::disjunctionを使用する方法があります。

#include <iostream>
#include <concepts>
#include <memory>

auto func(const auto& v) {
  return v; // コピーされる
}

// func<T>のインスタンス化を遅延させるラッパ型
template<typename T>
struct C {
  static constexpr value = std::invocable<decltype(func<T>), T>;
};

template<typename T>
void f(T&&) {

  if constexpr (std::conjunction_v< std::is_integral<T>, C<T> >) {
    // Tがunique_ptrの場合はここには来ない
  } else {
    // Tがunique_ptrの場合はこちらに来ることが期待される
  }
}

int main() {
  std::unique_ptr<int> up{};

  f(std::move(up)); // ok
}

std::conjunctionおよびstd::disjunctionは、T::valuebool型定数であるメタ関数を任意個数受けて、その&&/||を求めるメタ関数ですが、その評価に当たっては短絡評価が規定されています。そのため、条件をメタ関数に落とすことができるならば、この方法を用いてもインスタンス化を短絡評価させることができます。

ただ、この例はあまり適切ではない例で、func<T>の出現(インスタンス化)を遅延させるために別の型に埋め込まなければならなくなっており、どのみち名前空間に不要なものがばらまかれてしまっています・・・

最後にこの古のメタ関数にたどり着いたところで、冒頭の例の問題とはまさにこれらのメタ関数が導入された理由の一つでもある事に気づきます。つまり、T1::value && T2::value && T3::value && ...というようにすると、この式の評価よりも前にTn::valueインスタンス化が要求されてしまい、そのいずれかがハードエラーを起こす場合にまさに冒頭の例と同じ問題にぶつかります。std::conjunctionおよびstd::disjunctionはその内部で先頭から1つづつTn::valueを呼んで行き結果が確定したところでインスタンス化と評価を停止することで、インスタンス化の短絡評価を行うためのユーティリティです。

constexpr ifとコンセプトという真新しい言語機能に惑わされてしまっただけで、本質的な問題はTMPの時代から変わっていなかったわけでした・・・。

参考文献

この記事のMarkdownソース

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

文書の一覧

SG22のWG14からのものを除いて、全部で84本あります。

N4928 Working Draft, Standard for Programming Language C++

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

N4929 Editors' Report - Programming Languages - C++

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

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

N4933 WG21 November 2022 Kona Minutes of Meeting

2022年11月7-12日にハワイのKonaで行われた、WG21全体会議の議事録。

開催期間中の各グループの活動報告や、CWG/LWG/LEWGの投票の様子などが記載されています。

N4934 2023 WG21 admin telecon meetings

2023年(今年)のWG21管理者ミーティングの予定表。

N4935 2023 Varna Meeting Invitation and Information

2023年6月にブルガリアのヴェルナで開催される予定のWG21全体会議のインフォーメーション。

主に開催場所やその注意点などが記載されています。

N4936 2023-11 Kona meeting information

2023年11月にハワイのKonaで開催される予定のWG21全体会議のインフォーメーション。

同上。

N4937 Programming Languages — C++ Extensions for Library Fundamentals, Version 3

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

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

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

おそらく内容はN4937と同一です。

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

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

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

P0260R5 C++ Concurrent Queues

標準ライブラリに並行キューを追加するための設計を練る提案。

キューはシステムコンポーネント間のデータのやり取りの方法を提供する基礎的なものです。現在のC++標準ライブラリにもstd::dequeなどが用意されていますが、それらは全てシーケンシャルなデータ構造であり、その要素アクセスとキューの操作を並行に行うことができません。そのため、並行キューを導入するためには、それらとは別のものが必要となります。

さらに、並行性の要求はパフォーマンスとそのセマンティクスに新たな評価軸を追加し、並行キューにおいては競合しない操作のコスト、競合する操作のコスト、要素の順序保証をトレードオフにする必要があり、これによって既存のキューよりもセマンティクスが弱くなります。

そのような対立軸にはたとえば

  • 固定長 vs 可変長
  • ブロッキング vs 上書き
  • シングルエンド vs マルチエンド
  • 厳密なFIFOによる順序 vs 優先度による順序

などがあります。

この提案は今の所、そのような並行キューのインターフェースのベースとなる概念的なインターフェースの要件を定義しています。

基本操作

並行キューイングの問題に対する本質的な解決策は、参照ベースではなく値ベースの操作へ移行することです。そのために次の2種類の基本操作を定義しています

// 要素をキューイングする
void queue::push(const Element&);
void queue::push(Element&&);

// 要素をキューから取り出す
// 要素はコピーではなくムーブされる
void queue::pop(Element&);

ここでのqueueは特定のキューを示すものではなく、コンセプト的なプレースホルダです。

これらの操作はまたブロッキングを伴う操作でもあり、キューが満杯/空の場合に待機し、操作の競合を回避するためにブロックされる可能性があります。

即時操作

満杯/空のキューで待機すると操作が完了するまでにしばらく時間を要する可能性があり、機会費用がかかります。この待ち時間を回避することで、満杯/空のキューで操作の完了を待機する代わりに他の作業を行うことができます。そのために、次の2種類の即時操作(待機しない操作)を定義しています

// キューが満杯/クローズ状態の場合はその状態を返し、そうではない場合にキューイングしqueue_op_status::successを返す
queue_op_status queue::try_push(const Element&);

// キューが満杯/クローズ状態の場合はその状態を返し、第一引数を第二引数へムーブする
// そうではない場合にキューイングしqueue_op_status::successを返す
queue_op_status queue::try_push(Element&&, Element&);

// キューが空ならqueue_op_status::emptyを返し
// そうではない場合に要素をキューから取り出し(コピーではなくムーブされる)、queue_op_status::successを返す
queue_op_status queue::try_pop(Element&);

queue_op_statusは次のようなスコープ付き列挙型です

enum class queue_op_status { 
  success, 
  empty,
  full,
  closed
};

これらの操作はキューが満杯/空の場合にブロックしませんが、操作の競合を回避するためにブロックされる可能性があります。

キューのクローズ

通信にキューを使用しているスレッドでは、キューが不要になった場合にそのキューを使用している他のスレッドにそのことを通知するメカニズムが必要になる場合があります。典型的には、それはキューとは別の条件変数やアトミック変数などの帯域外信号が使用されます。ただし、このアプローチでは、そのキューで待機している他のスレッドを起床しなければならない問題があり、そのためにキューの満杯/空のブロッキングに使用される条件変数にアクセスする必要が出てくるなど、インターフェースの複雑さと危険性を増大させます。また、ミューテックスやアトミック変数を使用することでパフォーマンスに影響が及ぶ可能性もあります。

そのため、この提案ではそのようなシグナルをキュー自体でサポートすることを選択しており、これによってコーディングがかなり簡素化されます。

このシグナルのために、キューはクローズ(close())を行うことができます。あるスレッドでキューがクローズされると新しい要素をそのキューに挿入(push)することができなくなります。クローズ済キューに対する挿入操作はqueue_op_status::closedを返すか例外としてスローします。キューに存在する要素は取り出し(pop)が可能ですが、キューが空でクローズされている場合、取り出し操作はqueue_op_status::closedを返すか例外としてスローします。

// キューを閉じる
void queue::close() noexcept;

// キューが閉じられていればtrueを返す
bool queue::is_closed() const noexcept;

// キューが閉じられていればqueue_op_status::closedを返す
// そうでないならば、要素をキューイングする
queue_op_status queue::wait_push(const Element&);
queue_op_status queue::wait_push(Element&&);

// キューが空で閉じられていればqueue_op_status::closedを返す
// そうではなく、キューが空ならばqueue_op_status::emptyを返す
// それ以外の場合、キューから要素を取り出しqueue_op_status::successを返す
queue_op_status queue::wait_pop(Element&);

wait_とあるpush/pop操作は、キューが閉じられている場合に例外を回避するためのインターフェースです。この操作はキューが閉じられておらず満杯/空の場合に待機し、操作の競合を回避するためにブロックされる可能性があります。

クローズ後のキューを再開したいユースケースがあり、この提案ではそのためのインターフェースも定義しています

// キューをオープンする
void queue::open();

キューを再開する機能が困難になる実装は現在把握されてはいませんが、存在する可能性があります。また、キューの再開は通常キューが閉じていて空の場合にのみ呼び出すことができ、これによってクリーンな同期ポイントを提供することができます。ただし、空でないキューでopen()を呼び出すことは可能です。

is_closed()falseを返す場合でも、他のスレッドがキューを同時にクローズする可能性があるため、後続の操作でキューが閉じられている保証はありません。

オープン操作が利用できない場合、キューが閉じられるとキューは閉じたままになるという保証があります。したがってその場合、プログラマが他のすべてのスレッドがキューを閉じないように細心の注意を払わない限りは、is_closed()の戻り値はtrueのみが意味を持ちます。

キューの再開にはこれらの問題があるため、この提案ではこのインターフェースを提示するにとどめ提案していません。

要素型の要件

上記の操作のためには、要素型にはコピー/ムーブコンストラクタ、コピー/ムーブ代入演算子、及びデストラクタが必要になります。

コンストラクタと代入演算子は例外を投げる可能性がありますが、後続の操作のためにはオブジェクトを有効な状態のままにしておく必要があります。

例外ハンドリング

基本操作の2つの操作(push()/pop())はキューの状態によって例外を投げる可能性があります。その例外オブジェクトはstd::exceptionの派生クラスであり、queue_op_stateの値を含んでいる必要があります。

他のスレッドがキューの状態を監視している時に変更を透過的に元に戻すことができないため、並行キューは要素型がスローした例外の影響を完全に隠すことはできません。そのような例外は要素型のコピー/ムーブコンストラクタ及びコピー/ムーブ代入演算子から投げられる可能性があります。

それ以外の場合、キューは、メモリ確保、ミューテックス、条件変数から例外を再スローする可能性があります。

要素のコピー/ムーブが例外を投げる可能性がある場合、一部のキュー操作には追加の動作が定義されています

  • 構築時は例外を再スローし、構築しようとしていた要素を破棄する
  • 挿入操作は再スローし、キューの状態は変化しない
  • 取り出し操作は再スローし、取り出そうとしていた要素はキューから取り除かれる(要素は実質的に失われる)

この提案ではこれらの要件に沿った具体的なキューを提案してはいませんが、P1958R0でその一つであるbuffer_queueが提案されている他、google-concurrency-libraryにこの提案の初期のインターフェースをベースとした実装があります。

この提案は、フィードバックを得るためにConcurrency TS v2入りを目指しています。

P0342R1 What does "current time" mean?

<chrono>の時計型のnow()が最適化によって並べ替えられないようにする提案。

<chrono>の時計型(steady_clockなど)はその静的メンバ関数now()によってその時計の示す現在の時刻(current point in time)を取得することができます。しかし、この現在の時刻が何を指すのか不明瞭であり、必ずしもコードに記述した実行地点での現在時刻を取得しないことがあります。

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

#include <chrono>
#include <atomic>
#include <iostream>

std::size_t fib(std::size_t n) {
  if (n == 0)
    return n;
  if (n == 1)
    return 1;
  return fib(n - 1) + fib(n - 2);
}
int const which{42};

int main() {
  // fib()の実行にかかる時間を計測する
  auto start = std::chrono::high_resolution_clock::now(); // #1
  auto result = fib(which);                               // #2
  auto end = std::chrono::high_resolution_clock::now();   // #3

  std::cout << "fib(" << which << ") is " << result << std::endl;
  std::cout << "Elapsed time is "
            << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
            << "ms" << endl;

  return 0;
}

godbolt

このようなコードはかなり基本的なものですが、少なくともMSVCは最適化を有効にすると#2 #3の順番を入れ替えて、#1 -> #3 -> #2のように実行してしまい、結果0msが出力されます。このような最適化は標準の範囲内で許可されているため、この最適化自体は合法です。

これはシングルスレッドプログラムにおける実行順序の並べ替えであるため、標準の範囲内で回避するのは難しいようです。ファイルを分割することで回避できるようですが、それもプログラム全体の最適化やリンク時最適化を考慮すると確実なものとは言えません。また、この問題はコンパイラによっては起こらないかもしれず、回避策を含めたこのようなコードの移植性を損ねています。

プログラム中で現在のタイムスタンプを取得するという単純な処理にすらこのような罠が潜んでいて回避が難しいというのは大きな問題であり、この提案はその改善のためのものです。

この提案ではこの問題の解決のためにいくつかの方法を挙げています

  • 標準を変更はしないが、ガイダンスを充実させる
    • SG20で配布可能なガイダンスを作成するだけでも教育者には大きな助けになる
  • 編集上の変更を加える
    • ↑のガイダンスを標準に記述する
  • now()の並べ替えを禁止する
    • このアプローチはR0の議論において実装可能性について懸念があった
  • シングルスレッドフェンスを導入する
    • この問題が発生するのは時刻取得に止まらないと考えられるため、このユースケースに応えるためにより一般的なソリューションを提供する
    • このアプローチはR0で提案していたものだったが、実装可能性について懸念があった

ただし、現在のところどれかを選択してはいません。

この提案のR0ではこの問題の解決のためのシングルスレッドフェンスを提案していましたが、2016年にレビューされた際にはその実装可能性の懸念などから受け入れられず、提案の追求はストップしていました。しかし、2022年のKona会議におけるSG1のミーティング中にこの問題が取り上げられ、この提案の改訂版が望まれたことで、とりあえず問題を整理したR1(このリビジョン)が再度提出されました。

著者の方やSG1のメンバは、現在の時刻の取得という単純なタスクでプログラマが直面するこの問題は、現状の改善をより広く検討するのに十分に深刻だと考えているようです。

P0792R12 function_ref: a non-owning reference to a Callable

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

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックによる文言の調整と、メンバ変数ポインタを誤って処理していた推論補助の修正です。

この提案は次のリビジョン(未公開)がLWGのレビューをパスして、次の全体会議にかけられることが決まっています(C++26ターゲットです)。

P1383R1 More constexpr for <cmath> and <complex>

<cmath><complex>の数学関数をconstexprにする提案。

<cmath>の数学関数をはじめとする浮動小数点数を扱うものをconstexpr対応させるにあたって問題となっているのは、同じ浮動小数点数値に対するある関数の結果がコンパイラの設定やプラットフォーム、実行タイミング等のコンテキストによって等しい必要があるのか?という点です。明らかにそうなって欲しいのですが、浮動小数点数の特性などの事情によってそれは実際には困難であり、そうするとそれら関数の実行結果についてどのように規定するのか、あるいはどのような保証を与えるのか?が問題となります。

C++23におけるP0533R9による<cmath>等の関数のconstexpr対応にあたってもそのような問題の議論を回避するために、四則演算(+ - * /)よりも(その事情の下では)複雑でないとみなされる関数のみがconstexpr対応されました。

この提案は次のような設計指針によって、<cmath><complex>にあるほぼすべての数学関数をconstexpr対応させようとしています

  1. 数学関数の実行結果が実行時とコンパイル時で異なることを許容する
  2. 数学関数の定数実行が異なるプラットフォームで異なることを許容する
  3. <cmath>内の既存関数に正確な動作を義務付けるのではなく、QoIの定量化を奨励することが望ましい

そもそも実行時における現在の<cmath>の数学関数や浮動小数点演算は、異なるコンパイラやプラットフォームの間、あるいは異なるコンパイラオプション(-ffast-mathなど)の間で結果が一致しないことは長い間許容されています。また、定数式では数学関数を呼び出さない浮動小数点演算は可能であり、規格も利用者もその結果の実行時とコンパイル時の差異を許容しています(実行時における丸めモードの変更やFMAの利用など)。

costexprな数学関数にのみ過剰な正確性や結果の一貫性を要求することは、実装の困難さを高めるとともにそれそのものが実行時とコンパイル時の出力差の原因となります。そのためこの提案では、許容されている現在の実行時の振る舞いをベースとした設計指針によって数学関数をconstexpr対応させる方針を押しています。

P1673R11 A free function linear algebra interface based on the BLAS

標準ライブラリに、BLASをベースとした密行列のための線形代数ライブラリを追加する提案。

以前の記事を参照

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

  • in_{vector,matrix,object}*_tがユニークなレイアウトを持つという要件を削除
  • in_{vector,matrix,object}*_tに対する名前付き要件を説明専用コンセプトに変更
  • 対称なHermitian updateアルゴリズムを制約するために、説明専用コンセプトpossibly-packed-inout-matrixを追加
  • 新しい説明専用コンセプトと重複する制約を削除
  • 全てのアルゴリズムの制約から、mdspanがユニークなレイアウトを持つという制約を削除
  • ベクトル/行列オブジェクトのテンプレートパラメータがmdspanへのconst左辺値参照もしくは非const右辺値参照を推論する可能性があるという要件を削除
  • 両方のベクタ型を含めるようにdotの要件を修正
  • mdspanelement_typeエイリアスの代わりにvalue_typeエイリアスを使用するように、いくつかの関数の規定を修正
  • matrix_vector_productのテンプレートパラメータ順序と仮引数の順序を合わせた
  • LEWGのガイダンスに従って、効果と制約を数学的に記述するようにした
  • matrix_one_normの事前条件(要素のabs()の結果がTに変換可能であること)を制約に変更
  • vector_abs_sumの事前条件(init + abs(v[i])の結果がTに変換可能であること)を制約に変更
  • Bikeshedの代わりにPandocを使用するようにした
  • conjugated-scalarconjugatable<ReferenceValue>を適格要件ではなく制約に変更
  • “If an algorithm in [linalg.algs] accesses the elements of an out-vector, out-matrix, or out-object, it will do so in write-only fashion.”という文言を削除
  • P2642の内容をR2にアップデート
  • 全てのタグ型のdefault実装デフォルトコンストラクタにexplicitを付加
  • givens_rotation_setupを出力パラメータではなく新しいgivens_rotation_setup_result構造体の値を返すように変更

などです。

P1684R4 mdarray: An Owning Multidimensional Array Analog of mdspan

多次元配列クラスmdarrayの提案。

このリビジョンでの変更は、size constructible container要件を削除し、関連するコンストラクタでは事前条件を使用するようにしたことです。

P1883R2 file_handle and mapped_file_handle

ファイルI/Oライブラリの提案。

この提案のファイルI/Oライブラリはllfioというライブラリをベースとしたもので、llfioは現代の高性能ストレージに対するI/Oで理論性能値に迫るパフォーマンスを引き出すことができることを謳っています。また、llfioはPOSIXLinux/Mac等)とWindowsにおけるI/Oを抽象化して扱うクロスプラットフォームなライブラリでもあります。

この提案では、そのllfioからfile_handlemapped_file_handleを中心としたファイル毎のI/O機能を標準ライブラリへ導入しようとしています(llfio自体はネットワークI/Oやファイルシステム操作などより広い機能を持っています)。

llfioではファイルやファイルシステムの要素などをハンドルという次のような7階層の階層構造によって抽象化しています

  1. handle
    • オープン/クローズ、パスの取得、クローン、追加のみのset/unset、キャッシュの変更、などの特性を提供する
  2. fs_handle
    • inode番号を持つhandle
  3. path_handle
  4. directory_handle
  5. io_handle
    • 同期スキャッタ/ギャザーI/O、バイト範囲ロックを提供する
  6. file_handle
    • ファイルのオープン/クローズ、最大サイズの設定/取得を提供する
  7. mapped_file_handle
    • メモリマップされたファイルに対する低レインテンシのスキャッタ/ギャザーI/Oを提供する

この階層構造はこのままクラスの継承関係に対応しています。

file_handlemapped_file_handleは6,7階層に位置するもので、ファイルという対象に対する実際のI/O操作を提供します。主役がこの2つであるだけで、下の5階層のクラスも一緒に導入されます。

提案文書より、スキャッタ書き込みのサンプルコード

// 上記階層構造によって、file_handleでもmapped_file_handleでも使用可能
void write_and_close(file_handle &&fh) {
  // ファイルの最大サイズを設定
  // mapped_file_handleはファイルサイズが固定のために必要
  fh.truncate(12).value();

  // 書き込むデータのバッファ
  const char a[] = "hel";
  const char b[] = "l";
  const char c[] = "lo w";
  const char d[] = "orld";

  // ギャザー書き込み(バラバラのバッファからのスキャッタ書き込み)を行う
  // file_handleの場合 : max_buffers() >= 4ならばこの書き込みは並行する読み取りに対してアトミック
  //                    そのような読み取りは何も読まないか、完了した結果を読むかのどちらか(破損はない)
  // mapped_file_handleの場合 : 書き込みに伴う同期は行われず、読み書きは並行するリーダ/ライターに対して競合するため、追加の同期が必要になる
  fh.write(
    // ギャザーリストの指定
    { // 入力はstd::byteで行われるためキャストが必要
      { reinterpret_cast<const byte*>(a), sizeof(a) - 1 },
      { reinterpret_cast<const byte*>(b), sizeof(b) - 1 },
      { reinterpret_cast<const byte*>(c), sizeof(c) - 1 },
      { reinterpret_cast<const byte*>(d), sizeof(d) - 1 },
    }
    // デフォルトのタイムアウトは無限
  ).value(); // 失敗した場合、filesystem_error例外をスローする

  // ファイルのクローズに失敗する場合に備えて、明示的にファイルをクローズする
  fh.close().value();
}


// 書き込み用にファイルをオープン
// 必要に応じてファイルを作成、キャッシュをスルーして書き込み
write_and_close(file(
  {},                                       // 子要素(↓)を探索するベースディレクトリのpath_handle(この場合はカレントディレクトリを意味する)
  "hello",                                  // ベースディレクトリ(↑)からの相対的なパスフラグメントへのpath_view(ファイル名)
  file_handle::mode::write,                 // 書き込みアクセスを要求
  file_handle::creation::if_needed,         // 必要ならファイルを新規作成
  file_handle::caching::reads_and_metadata  // ストレージに到達するまで書き込み(デフォルトはnone
).value());                                 // 失敗した場合、filesystem_error例外をスローする


const path_handle& somewhere;

// メモリマップを使用して既存ファイルを更新する例
write_and_close(mapped_file(
  somewhere,                            // 子要素(↓)を探索するベースディレクトリのpath_handle
  "hello2",                             // ベースディレクトリ(↑)からの相対的なパスフラグメントへのpath_view(ファイル名)
  file_handle::mode::write,             // 書き込みアクセスを要求
  file_handle::creation::open_existing  // 既存ファイルを開くのみ、ファイルがない場合に失敗する
                                        // デフォルトは全てキャッシュを使用する
).value());                             // 失敗した場合、filesystem_error例外をスローする

mapped_file_handleはメモリにマップされたファイルの抽象であり、ファイル書き込みに伴ってファイルサイズの自動延長が行われないなどの制約があります。一方で、file_handleはもっと広いファイルの抽象であり、通常ファイルの自動延長機能を持ちます。

各I/O操作やハンドル作成の結果でvalue()を呼んでいるのは、それぞれの操作がresult<T>型を返しており、その成功結果を取得するためです。result<T>はエラー型がstd::errorP1028R4)に固定されたstd::expectedのような型で、同じようなインターフェースを持っています。

file_handlemapped_file_handlefileとはUNIXにおけるファイルという概念のようなもので、必ずしもファイルシステム上のファイルだけを意味するのではなく、ファイルとして扱えるもの全体を指しています。例えば、ASIOがソケットベースのI/Oライブラリであるとすると、このライブラリはファイルベースのI/Oライブラリです。

このライブラリは次のような設計原則を謳っています

  1. デフォルトパラメータや設定はパフォーマンスよりもセキュリティを重視する。
    • これはいつでも明示的にオプトアウトできる
  2. 実行されるI/Oの種別に関係なく、基礎となるシステムコールのラインタイムオーバーヘッドを超える(ライブラリの)統計的に計測可能なラインタイムオーバーヘッドはない
  3. ホストOSの並行I/Oに関する保証を可能な限りそのまま提供する
  4. POSIXのrace free filesystem path lookup拡張を中心として設計されている
  5. システム内でのどこでも、C++I/Oと最終ストレージデバイスの間の全てのメモリコピーを常に回避可能である必要がある
  6. カーネルI/Oキャッシュ制御および仮想メモリ制御機能を提供する
  7. ファイルシステムの競合を検知して回避する機能を提供し、少なくともホストOSが許可する範囲で第三者によるファイルシステム同時変更によって導入される競合が完全にないコードを記述可能にする

筆者の方(llfio開発者の方)によれば、llfioの中でfile_handlemapped_file_handle周りのAPIおよびABIは2020年ごろから安定しており、市場取引のデータ処理において数年間の使用実績があり、現在も1TB/日のデータを処理している、とのことです。

この提案は現在のところ機能や設計についてのレビューを受けている段階であり、具体的な文言はありません。

P1928R2 Merge data-parallel types from the Parallelism TS 2

std::simd<T>をParallelism TS v2から標準ライブラリへ移す提案。

以前の記事を参照

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

  • 浮動小数点数ランクに対応したコンストラクタのexplicit指定の追加
  • 新しいABIタグの同じもしくは異なるセマンティクスについて追記
  • セクション4導入段落を修正
  • simd::sizestd::integral_constant<size_t, N>型のstatic constexpr変数に変更
  • APIconstexpr対応についてドキュメントを追記
  • constexprを提案する文言に追加
  • ABI境界を越えてstd::simdをやり取りするためのABIタグを削除
  • キャストインターフェースの変更を適用

などです。

P1967R10 #embed - a simple, scannable preprocessor-based resource acquisition method

コンパイル時(プリプロセス時)にバイナリデータをインクルードするためのプリプロセッシングディレクティブ#embedの提案。

以前の記事を参照

このリビジョンでの変更は、ファイルが空であることを検知するための__has_embedsuffix/prefix/if_emptyの指定をオプショナルな機能から提案する機能に移動したことなどです。

P2013R5 Freestanding Language: Optional ::operator new

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

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを反映したことです。

この提案はすでにLWGのレビューを終えており、次の全体会議で投票にかけられる予定です。

P2047R5 An allocator-aware optional type

Allocator Awarestd::optionalである、std::pmr::optionalを追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整、デフォルトのアロケータ型の引数にstd::remove_cv_tを通すようにしたこと、make_~関数の削除(文言からは消えてない?)などです。

この提案はどうやら、これ以上議論に時間を費やさないことになったようです。

P2075R2 Philox as an extension of the C++ RNG engines

<random>に新しい疑似乱数生成エンジンを追加する提案。

以前の記事を参照

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

  • Philoxだけに着目したAPIの文言をシンプルにした
  • カウンタベースエンジンのAPIを拡張した
  • Design considerationsを追加
  • エンジン型にset_counter()メンバ関数を追加
  • counter_based_enginecテンプレートパラメータの削除

などです。

P2164R9 views::enumerate

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

以前の記事を参照

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

この提案は2023年2月のIssaquah会議でC++23に向けて採択されています。

P2169R3 A Nice Placeholder With No Name

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

以前の記事を参照

このリビジョンでの変更は、提案する文言を追加したこと、名前空間スコープでは複数回の使用を禁止した(以前は名前付モジュール本文の名前空間スコープでのみ複数回使用可能としていた)ことなどです。

P2198R6 Freestanding Feature-Test Macros and Implementation-Defined Extensions

P2198R7 Freestanding Feature-Test Macros and Implementation-Defined Extensions

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

以前の記事を参照

R6での変更は、

  • 最新のワーキングドラフトへの追随
  • 次のC++23機能テストマクロをフリースタンディング指定
    • __cpp_lib_forward_like
    • __cpp_lib_modules
    • __cpp_lib_move_iterator_concept
    • __cpp_lib_ranges_as_const
    • __cpp_lib_ranges_as_rvalue
    • __cpp_lib_ranges_cartesian_product
    • __cpp_lib_ranges_repeat
    • __cpp_lib_ranges_stride
    • __cpp_lib_start_lifetime_as

このリビジョン(R7)での変更は、LWGのフィードバックの反映のみです。

この提案は既にLWGのレビューを終えており、次の全体会議で投票にかけられる予定です(C++26ターゲットです)。

P2338R3 Freestanding Library: Character primitives and the C library

<charconv>std::char_traitsをはじめとするいくつかのヘッダをフリースタンディングライブラリ指定する提案。

以前の記事を参照

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

この提案は、C++26をターゲットとしてLEWGからLWGへ転送されています。

P2363R4 Extending associative containers with the remaining heterogeneous overloads

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

以前の記事を参照

このリビジョンでの変更は、set/unordered_set.insert()に追加するオーバーロードが曖昧にならないように制約を追加したことです。

この提案はLWGでレビュー中ですが、C++26をターゲットにしています。

P2406R1 Fix counted_iterator interaction with input iterators

P2406R2 Add lazy_counted_iterator

std::counted_iteratorを安全に使用可能にする提案。

以前の記事を参照

R1での変更は、フィードバックに基づく提案全体の修正などです。

このリビジョン(R2)での変更は

  • P2578への参照を削除
  • 提案する設計について修正
  • いくつかの代替案の提示

この提案はR1以降、問題を解決したcounted_iteratorlazy_counted_iteratorとして追加し、それを使用するバージョンのviews::take/views::countedとしてlazy_take/lazy_countedを追加することを提案しています。

その上で、その設計について提示されているオプションは次のものです

  1. lazy_counted_iteratorを可能な限りcounted_iteratorに近づける
    • random_access_iteratorに対する振る舞いは変更しない
    • lazy_counted_iteratorをインクリメントする時、カウントが0になる場合は基底のイテレータをインクリメントしない
    • lazy_counted_iteratorをデクリメントする時、カウントが0の場合は基底のイテレータをデクリメントしない
    • 実装は0カウントの構築を正しくハンドリングしなければならない
  2. lazy_counted_iteratorは最も強くてもforward_iteratorとする
    • これによって、デクリメントを考慮しなくても良くなる
  3. 0カウントで構築された時は、あらゆる読み取りを許可しない
    • 可能なのは、カウント値の読み取り(.count())と終端チェック(==)のみ
  4. 間接参照時に基底のイテレータをインクリメントする
    • これによって、逆参照がconst操作ではなくなり、イテレータをコピーしてからインクリメントするとコピー元が無効になる
    • 対策として、lazy_counted_iteratorをコピー不可とする

また、lazy_counted_iteratorではそのカウント値に依存して基底のイテレータの状態が非線形に変化することから、基底イテレータを取得するbase()をどうするかも問題となり、そのためのオプションを提示されています

  1. .base()を提供しない
  2. lazy_counted_iteratorをコピー不可とする
    • 実装は、.base()の呼び出し時に必要に応じて(0カウントの時)イテレータをインクリメントして返す
    • .base()の呼び出しは遅延操作ではない
    • lazy_counted_iteratorをコピー不可とすることで、lazy_counted_iteratorの無効化について考慮しなくて良くなる
  3. .base()を提供するが、それによって他のコピーが無効となることを指定する
    • 実装は、.base()の呼び出し時に必要に応じて(0カウントの時)イテレータをインクリメントして返す

SG9の投票では、lazy_counted_iteratorそのものについてはオプション1と2が選択され、.base()に関してはオプション1が選択されたため、R2の文言はそれを反映したものになっています。

LEWGのレビューと投票では、lazy_counted_iteratorに対する変更をcounted_iteratorに直接適用することに弱いながらもコンセンサスが得られている様です。

P2407R2 Freestanding Library: Partial Classes

一部の有用な標準ライブラリのクラス型をフリースタンディング処理系で使用可能とする提案。

以前の記事を参照

このリビジョンでの変更は、提案をHTMLにしたこと、フリースタンディング対象外のメンバ関数deleteされると注釈をつけた(対象のものには注釈しないようにした)こと、std::forward_likeへの言及、フリースタンディング指定をまとめて行うような文言の追加、などです。

この提案は、LWGのレビュー中で、C++26をターゲットにしています。

P2508R2 Exposing std::basic-format-string

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

以前の記事を参照

このリビジョンでの変更は提案する文言の調整のみです。

この提案は2022年11月のkona会議で全体投票をパスしており、C++23ワーキングドラフトに含まれています。

P2530R2 Why Hazard Pointers should be in C++26

標準ライブラリにハザードポインタサポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、使用例をtony-tableにして、使用しない時としたときの比較ができるようにしたことです。

この提案は、LEWGの投票をパスしてLWGに転送されています(C++26ターゲットです)。

P2537R2 Relax va_start Requirements to Match C

可変長引数関数を0個の引数で宣言できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、C23の仕様(N2975、C23に導入済み)に合わせた2つの文言を用意したことと、CWG/EWGによって選択されなかった方の文言を巻末に移動させたことです。

この提案は、C++26をターゲットとしてCWGでレビュー中です。

P2545R2 Why RCU Should be in C++26

標準ライブラリにRead-Copy-Update(RCU)サポートを追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善、使用例をtony-tableにして、使用しない時としたときの比較ができるようにしたこと、将来的に想定される機能の拡張方向についてを記述したことなどです。

この提案では、RCU機能の将来的な拡張や改善について想定されるものとして次のものを挙げています

  • 削除処理が呼び出されるコンテキストとそのタイミングを制御する方法の提供
  • RCUを介した型安全メモリ
    • LinuxSLAB_TYPESAFE_BY_RCUのようなもの
  • std::thread以外のスレッドの手動登録方法の提供
  • rcu_retire()メタデータ処理に関する、実装者とユーザーに対するアドバイスの提供
  • メモリリーク検出器との相互作用について

この提案は、LEWGの投票をパスしてLWGに転送されています(C++26ターゲットです)。

P2570R2 Contract predicates that are not predicates

コントラクト注釈に指定された条件式が副作用を持つ場合にどうするかについて、議論をまとめた文書。

以前の記事を参照

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

  • 設計基準セクションを拡張。特に、UBが無いことを安全と同義としていないことを強調
  • P2680R0のアプローチが正しいコードを壊す例を追記
  • 契約条件が副作用を持つことがなぜ問題なのかを説明するセクションを追記
  • 副作用を評価することを保証することが、将来考えられるビルドモード(事前条件のみ評価)を妨げる可能性がある事を追記
  • 副作用が排除されるC++の他の場所の例を追記

などです。

P2591R2 Concatenation of strings and string views

std::stringstd::string_view+で結合できるようにする提案。

以前の記事を参照

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

  • 提案するoperator+シグネチャを変更
    • std::string_viewに変換可能なものを取るのではなく、std::string_viewそのものを取るようにした
  • 提案するoperator+hidden friendsにした

  • P2591 進行状況

P2616R3 Making std::atomic notification/wait operations usable in more situations

std::atomicnotify_one()wait()操作を使いづらくしている問題を解消する提案。

以前の記事を参照

このリビジョンでの変更は、atomic_notify_tokennotify_tokenに名前を変更した事、notify_tokenのテンプレートパラメータを任意の型からatomic型に変更したことなどです。

P2630R2 Submdspan

std::mdspanの部分スライスを取得する関数submdspan()の提案。

以前の記事を参照

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

  • カスタマイゼーションポイント選択に関する議論を追加
  • strided_index_rangestrided_sliceにリネーム
  • submdspan_mappingの戻り値型のために、マッピングとオフセットを持つ名前付き構造体を導入
  • mandatesの冗長な制約を削除し、よりよりエラーメッセージを得られるようにした
  • レイアウトポリシー型の保存ロジックを修正
    • 特に、ランク0マッピングのレイアウトを保存し、スライス指定のいずれかがstride_index_rangeである場合にlayout strideを作成するようにした
  • エクステントがstrideより小さい場合のstrided_sliceのサブマッピングエクステントの計算を修正
  • strided_slice指定のサブマッピングストライド計算を修正
  • strided_sliceのstrideが0の場合に対処するために事前条件を追加

などです。

この提案は現在LEWGのレビュー中で、C++26ターゲットに変更されています。

P2636R2 References to ranges should always be viewable

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

以前の記事を参照

このリビジョンでの変更は主に説明や例の拡充や修正ですが、この提案はLEWGでコンセンサスを得られなかったため、追求は停止されています。

P2642R2 Padded mdspan layouts

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

以前の記事を参照

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

  • layout_{left,right}_paddedの特殊化は、レイアウトマッピングポリシー要件を満たし、トリビアル型であるという規定を追加
  • layout_{left,right}_padded::mappingのコンストラクタの事前条件の単純化
  • layout_{left,right}_padded::mappingのデフォルトコンストラクタを追加
  • layout_{left,right}_padded::mappinglayout_(left,right)::mappingからの変換コンストラクタを追加
  • layout_{left,right}_padded::mappinglayout_stride::mappingからの変換コンストラクタを追加
  • layout_{left,right}_padded::mappingoperator==を追加
  • 既存のコンストラクlayout_stride::mapping(const StridedLayoutMapping&)explicit条件内のレイアウトマッピング型のリストにlayout_{left,right}_paddedを追加
    • layout_{left,right}_padded::mappingからlayout_stride::mappingへの変換はexplicitではない

などです。

P2656R1 C++ Ecosystem International Standard

C++実装(コンパイラ)と周辺ツールの相互のやり取りのための国際規格を発効する提案。

以前の記事を参照

このリビジョンでの変更は、IS発行のためのタイムラインとプロセスを追加したことなどです。

Ecosystem ISは2023年から始まる2年ごとのリリースサイクルで発効していくことを目指していて、最初のISを発効するためのスケジュールを次のようにしています

  • 2023/02 : 計画
    • 最初のISの開発計画の完了
    • 初版に何が含まれるかを検討
  • 2023/03 : 最初のドラフト作成
    • 最初の最小限のドラフトを作成
    • このドラフトには、最低1つの提案が統合されていて、のこりのものも概要が記載されている
    • 作業のチェックポイントとするために、EWGの承認を得る
  • 2024/02 : 提案
    • 最初のIS発効のための正式な提案を作成する
    • そこには、最初のISのドラフトのほぼ完全なものが含まれる
  • 2025/01 : CD作成完了
    • NBコメント募集のためのCommittee Draftを承認する
  • 2025/02 : DIS作成完了
    • 寄せられたNBコメントを解決したFinal Draft International Standardを承認する

この後、2年ごとに同様のスケジュールでEcosystem ISを改善していく予定です。C++本体と比較すると次のようになります

この作業のためのWG21におけるプロセスは、WG21の現在のプロセスに親和するように次の2つのプロセスを提案しています

  • ブートストラップ
    • Tooling Study Group (SG15)で初期開発及びレビュー
    • その後、EWG/LEWGでのレビューと承認
    • そこから、文言プロセスの定期的なレビューと承認が続く
  • 並行
    • 開発及びレビューは、適宜既存のSGから開始できる
    • 続いて、Tooling Working Group (TWG)によるレビューと承認が行われる
    • TWGはIS事態の文言の検討も行い、WG21全体投票のための作業も担う

ブートストラッププロセスは最初のISを作成するために使用し、その後は並行プロセスに切り替えます。

並行プロセスは、現在のWG21作業フローをベースに次のようなものになります

P2659R2 A Proposal to Publish a Technical Specification for Contracts

契約プログラミング機能に関するTechnical Specificationを発効する提案。

以前の記事を参照

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

  • GCCが契約機能を実装したのを受けて、MotivationとAppendixを更新
  • タイムラインのアップデート

などです。

P2675R1 LWG3780: The Paper (format's width estimation is too approximate and not forward compatible)

std::formatの文字幅の規定を改善する提案。

以前の記事を参照

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

  • 標準がワイドとして予約するコードポイントはそのように扱わないことを明示
  • サンプルとして示した結果の生成に使用したファイルへのリンクを追加
  • 関連する、既存のターミナルにおける実装へのリンク追加
  • 有用ではなかったスクリーンショットを削除

などです。

P2677R2 Reconsidering concepts in-place syntax

autoによる簡易関数宣言構文において、型名を取得可能にする提案。

以前の記事を参照

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

  • 推奨するシンボルを~から:に変更
  • 可能な表記方法についての追加の議論を追記
  • ユニバーサル参照(フォワーディングリファレンス)についての議論の追記

この提案のR1は公開されていませんが、どうやらそこではautoから型名を取得する構文としてauto~Tのようなものを提案していたようです。これはR0で提案していたauto{T}がdecay-copy構文(auto{x})と衝突することによるもので、このリビジョンではそれを:に置き換えています。

// R0(auto{x}、decay-copyと衝突する)
[](auto{T}&& x) { return f(std::forward<T>(x)); }

// R1
[](auto~T && x) { return f(std::forward<T>(x)); }

// R2
[](auto:T && x) { return f(std::forward<T>(x)); }

まだ確定ではなく、この提案では他の選択肢として$や@、ダッシュのほか、auto<T>T:autoなどを候補として挙げています。

P2680R1 Contracts for C++: Prioritizing Safety

C++契約プログラミング機能は安全第一で設計されるべき、という提言。

以前の記事を参照

このリビジョンは、基本的なところは変わっていないようですがほとんどR0と別物になっています。特に、巻末にP2700の問に対する回答が記されています。

P2689R1 atomic_accessor

アトミック操作を適用した参照を返すmdspanのアクセッサである、atomic_accessorの提案。

以前の記事を参照

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

  • 特定のメモリオーダーを指定するstd::atomic_refであるatomic-ref-boundedを説明専用テンプレートとして追加
  • atomic_ref_relaxed, atomic_ref_acq_rel, atomic_ref_seq_cstの3つのエイリアスを追加
  • basic-atomic-accessor説明専用テンプレートを追加
  • atomic_accessor_relaxed, atomic_accessor_acq_rel, atomic_accessor_seq_cstの3つのテンプレートを追加

R0では、atomic_accessorで使用するstd::atomic_refのデフォルトのメモリオーダーが過剰で、それが変更できないことが問題になっていました。

このリビジョンではその対応のために、メモリオーダー指定をテンプレートパラメータで受け取るバージョンのstd::atomic_refatomic-ref-boundedとして説明専用で追加し、その設定済みエイリアスとしてatomic_ref_relaxed, atomic_ref_acq_rel, atomic_ref_seq_cstを追加します。

そして、使用するatomic_refを追加のテンプレートパラメータとして受け取ることでメモリオーダーを指定するbasic-atomic-accessorを追加し、atomic_accessor及びatomic_accessor_relaxed, atomic_accessor_acq_rel, atomic_accessor_seq_cstはその設定済みエイリアステンプレートとして定義されます。

namespace std {
  
  // メモリオーダー指定を受けることのできるatomic_ref(説明専用)
  template <class T, memory_order MemoryOrder>
  struct atomic-ref-bounded {
    ...
  };

  // 特定のメモリオーダーによるatomic_ref(これらは説明専用ではない)
  template<class T>
  using atomic_ref_relaxed = atomic-ref-bounded<T, memory_order_relaxed>;

  template<class T>
  using atomic_ref_acq_rel = atomic-ref-bounded<T, memory_order_acq_rel>;

  template<class T>
  using atomic_ref_seq_cst = atomic-ref-bounded<T, memory_order_seq_cst>;

  // atomic_accessorのベース型(説明専用)
  template <class ElementType, class ReferenceType>
  struct basic-atomic-accessor {
    ...
  };

  // デフォルトのatomic_accessor、std::atomic_refを使用
  template <class ElementType>
  using atomic_accessor = basic-atomic-accessor<ElementType, atomic_ref<ElementType>>;

  // メモリオーダーをカスタムしたatomic_accessor
  template <class ElementType>
  using atomic_accessor_relaxed = basic-atomic-accessor<ElementType, atomic_ref_relaxed<ElementType>>;

  template <class ElementType>
  using atomic_accessor_acq_rel = basic-atomic-accessor<ElementType, atomic_ref_acq_rel<ElementType>>;

  template <class ElementType>
  using atomic_accessor_seq_cst = basic-atomic-accessor<ElementType, atomic_ref_seq_cst<ElementType>>;
}

atomic_ref_relaxed, atomic_ref_acq_rel, atomic_ref_seq_cstの3つは、メモリオーダーが変更されたstd::atomic_refとして追加され、ユーザーが使用することができます。

P2700R1 Questions on P2680 "Contracts for C++: Prioritizing Safety"

P2680R0で提唱されている契約条件の副作用の扱いについて、幾つかの疑問点を提出する文書。

以前の記事を参照

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

  • 緩和された契約条件とそうでないものの両方の存在を説明するための修正
  • Q1.4, Q5.1, Q5.2, Q5.9について、明確化したり質問を拡張
  • Q5.10に新しい質問を追加

などです。

これ(多分R0)に対する回答は、少し上のP2680R1でなされています。

P2713R0 Escaping improvements in std::format

P2713R1 Escaping improvements in std::format

std::formatエスケープ出力(?)時の出力方法を修正する提案。

std::format?指定は、C++23でrange出力サポート(P2286R8)と同時に、ロギングやデバッグ時の出力のために追加されたものです。

?による指定は特に文字/文字列の出力時に有効なもので、入力の文字列をそのまま(人間の視認性を優先して)出力します。出力はまず、出力文字列全体が"にくくられて出力され、入力された文字で対応するエスケープシーケンスがある文字(\t \n \r " \の5つ)は対応するエスケープシーケンスに置換されて出力されます。また、ユニコード文字の中でも出力できない(見えない)ものなどや、無効な文字となるものもエスケープされて(\u{xxxx}の形で)出力されます。

int main() {
  std::cout << std::format("{:?}", "hello") << std::endl;
  // "hello"と出力
  std::cout << std::format("{:?}", R"("hello"\n)") << std::endl;
  // "\"hello\"\n"と出力
}

このフォーマット時のエスケープの振る舞いは、入力文字列を1文字づつ見ていって、文字が置換条件に合致した場合に対応する表現に置換(エスケープ)されて出力する、ような形で記述されています。

ただ、このフォーマット方法は、何を目的としてフォーマットするのかが不明瞭だったようです。つまり、このエスケープは、入力文字列を人間にとって読みやすい形で出力することを意図しているのか、元のエンコーディングにおける文字表現を視認可能な形で出力することを意図しているのか、が不透明で、それによって一部の文字の最適なエスケープが変化します。これはC++23 CDに対するアメリカ/フランスからのNBコメントによって指摘されました。

この提案は、?によってエスケープされた出力文字列の意図が、入力文字/文字列をC++コード上で文字/文字列リテラルとして記述した場合の文字列を再現することにあることを明確化するために、エスケープ方法を修正するものです。

この提案はこの問題に関するSG16の投票によって確認された事項に対して標準の文言を提供するもので、SG16では次のことが確認されました

  1. ?によってエスケープされた文字列はそのまま文字/文字列リテラルとして使用できる
    • エスケープされた出力文字列は、文字列リテラルとして使用されたときに入力文字列を再現することのできる文字列となる
  2. エスケープされた文字列は視覚的に明確な(人間にとって読み取りやすい)文字列を生成しない
  3. 区切り文字や表示できない文字は引き続きエスケープされる

これによる主な変更は、次のようになります

  • ユニコードの結合文字は、出力文字列の直前に結合可能な文字が現れている場合にエスケープされない
    • 結合文字とは、Grapheme_­Extend=Yesというユニコードプロパティを持つ文字
    • 結合可能な文字とは、エスケープされない文字もしくは、それに結合している結合文字のこと

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

string s0 = format("[{}]", "h\tllo");               // s0の結果文字列は [h    llo] (非エスケープ出力)
string s1 = format("[{:?}]", "h\tllo");             // s1の結果文字列は ["h\tllo"] (タブ文字が\tにエスケープ)
string s3 = format("[{:?}, {:?}]", '\'', '"');      // s3の結果文字列は ['\'', '"'] (\ と " はエスケープされて出力される)

// 次の例ではUTF-8エンコーディングを仮定
string s4 = format("[{:?}]", string("\0 \n \t \x02 \x1b", 9));  // s4の結果文字列は ["\u{0} \n \t \u{2} \u{1b}"]
string s5 = format("[{:?}]", "\xc3\x28");           // s5の結果文字列は ["\x{c3}("] (不正なUTF-8文字列のケース)
string s6 = format("[{:?}]", "\u0301");             // s6の結果文字列は ["\u{301}"]
string s7 = format("[{:?}]", "\\\u0301");           // s7の結果文字列は ["\\\u{301}"]
string s8 = format("[{:?}]", "e\u0301\u0323");      // s8の結果文字列は ["ẹ́"]

この提案において振る舞いが明確化されたのは、下の3つの例(s6, s7, s8)です。まず、\u0301\u0323ダイアクリティカルマークという結合文字(アルファベットの上下につく文字)です。

s6は、結合文字に非結合文字(結合対称)が先行していないため、エスケープされて出力されています。s7は、エスケープされた文字が先行しているため、エスケープされて出力されています。s8エスケープされない文字(e)が先行しているため\u0301(上付きの,)が結合し、結合した結合文字が先行しているため\u0323(下付きの.)も結合して出力され、結果は見た目1文字になり、エスケープされる文字はありません。

P2714R0 Bind front and back to NTTP callables

std::bind_frontstd::bind_backにNTTPとして呼び出し可能なものを渡すオーバーロードを追加する提案。

これは、P2511で提案されていたstd::nontypeのアプローチ(メンバポインタをNTTPとして渡す)をstd::bind_frontstd::bind_backでできるようにしようとするもので、バインドするCallableオブジェクトを第一引数からテンプレートパラメータへ移動させます。

struct S {
  int func(int, char, std::string_view);
};

int main() {
  // 現在(C++20
  auto bf1 = std::bind_front(&S::func, 20, 'a');
  // この提案
  auto bf2 = std::bind_front<&S::func>(26, 'a');
}

このテンプレートパラメータにはメンバポインタだけでなくCPOのようなものも使用できます。

#include <compare>

int main() {
  // 現在(C++20
  auto bf1 = std::bind_front(std::strong_order, 20);
  // この提案
  auto bf2 = std::bind_front<std::strong_order>(26);
}

この提案によるメリットは

  1. ストレージ容量の削減
    • メンバ関数とその対象オブジェクトをバインドする場合、その結果オブジェクトは元のオブジェクトよりも大きくなり、std::function等のSBOのサイズを超えてしまう
    • 呼び出したいメンバ関数等はコンパイル時にわかっていることがほとんどだが、現在の実装では回避できない
    • NTTPとしてメンバポインタを転送することで、そのストレージサイズを削減できる
  2. 実装の単純化デバッグ情報のスリム化
    • ステートレスラムダ式のような空のcallableオブジェクトを効率的に保存するために複雑なこと(EBOの有効化)をする必要がなくなる
    • std::move()等をデバッグ時に掘り下げない場合、束縛されたエンティティにアクセスする際にデバッグ情報を肥大化させる中間層が無くなる
  3. std::bind_frontstd::bind_backの可読性向上
    • invoke(func, a, b, ...)のような呼び出しはC++において一般的になりつつあるが、依然としてfunc(a, b, ...)の方がより関数呼び出しの表現として自然だと感じる人が多いと思われる
    • そのため、bind_front(func, a, b)よりもbind_front<func>(a, b)の方が自然であると感じるかもしれない
    • また、callableターゲットとそこに束縛する引数が視覚的に分離されているため、見やすくなる

この提案では、同様の変更をstd::not_fnに対しても行うことを提案しています。

P2717R0 Tool Introspection

C++周辺ツールが、Ecosystem ISにどれほど準拠しているのかを互いに通信する手段を標準化する提案。

Ecosystem ISはC++のツール(コンパイラ、ビルドシステムやパッケージマネージャ、静的解析ツールなど)環境を整備するための独立した標準規格で、P2656で方向性が提案されています。Ecosystem ISにWG21として取り組んでいくことは合意されているようです。

Ecosystem ISは既存のツールを否定しWG21でツールを作り上げるものではなく、ツールとツールの間の相互通信に必要な部分を標準化しようとするものです。

Ecosystem ISもC++そのものと同様に徐々に進化していくことを考えると、Ecosystem ISにもバージョンが生まれ、Ecosystem ISに準拠したC++ツールはある時点では特定バージョンのEcosystem ISを実装することになるでしょう。Ecosystem ISの目的はそのようなツールの相互のやり取りの標準化にあるので、互いが想定するEcosystem ISバージョンが異なる場合にそれを知る手段がないことは問題となります。

この提案は、ツールが実装するEcosystem ISのバージョン情報を取得できるメカニズムをEcosystem ISに最初から組み込んでおくことを提案するとともに、あるツールが別のツールのEcosystem ISの機能を使用する場合にどのバージョンを想定しているのかを同時に伝達できるメカニズムについても提案しています。

この提案は次の2つの機能を提案しています

  1. イントロスペクション(Introspection
    • 実装するバージョンについて相手に報告する
  2. 宣言(Declaration
    • 必要とするバージョンとエディションを相手に指定する

イントロスペクションによって、ツールは相手のツールが特定の機能を実装しているかを問い合わせることができます。問い合わせを受けたツールはサポートしている機能の範囲で応答するか何も応答しません。応答があった場合、その情報を使用して、Ecosystem IS標準に従って相手ツールとのさらなるやり取りを進めることができます。

宣言によって、ツールは特定の機能によってやり取りする時のその機能についてのバージョンを指定できます。相手ツールがその宣言を受け入れた場合は、その機能を使用してやり取りを進めることができます。

どちらの機能の場合も、これはツールのコマンドライン引数で渡し、返答はJSONで返します。

イントロスペクションの場合は--std-infoというコマンドライン引数を使用します

tool --std-info

これに対する応答は例えば次のようになります

{
  "$schema": "https://raw.githubusercontent.com/cplusplus/ecosystem-is/release/schema/std_info-1.0.0.json",
  "std:info": "[1.0.0,2.5.0]"
}

この問い合わせはEcosystem ISの全ての機能について行われ、結果もすべての機能について帰ってきます。しかし、場合によっては特定の機能についてだけ問い合わせをした方が効率的な場合がり、そのための制限付きの問い合わせをサポートしています

制限付き問い合わせも--std-infoで行いますが、機能を示す名前とバージョンを同時に指定します

tool "--std-info=std:info==[1.0.0,2.1.0)"

これは--std-infoそのもののバージョンの問い合わせです

これに対する応答は例えば次のようになります

{
  "$schema": "https://raw.githubusercontent.com/cplusplus/ecosystem-is/release/schema/std_info-1.0.0.json",
  "std:info": "[1.5.0,2.0.0]"
}

この場合、相手のツールは要求されたバージョン(1.0.0 ~ 2.1.0)のサブセット(1.5.0 ~ 2.0.0)しか実装していないといっています。

また複数同時に問い合わせることもできます

tool "--std-info=std:info=[1.0.0,2.1.0)" "--std-info=gcc:extra[2.0.0,2.1.0]"

この結果は例えば次のようになります

{
  "$schema": "https://raw.githubusercontent.com/cplusplus/ecosystem-is/release/schema/std_info-1.0.0.json",
  "std:info": "[1.0.0,2.0.0)",
  "gcc:extra": "2.1.0"
}

この結果を受けて、使用する機能のバージョンを指定(宣言)するには、使用する機能と共に--std-declにその機能と要求バージョンを指定するようにします。

tool "--std-decl=std:info=2.0.0" "--std-decl=gcc:extra=2.1.0" --std:info --gcc:extra...

ここでは、--std:infoは2.0.0を使用し、--gcc:extraは2.1.0を使用するように相手ツールに要求しています。

これらの返答のフォーマットやバージョン指定のフォーマットなどは、この提案ではJSON Schemaの形で規定することを提案しています。

P2723R1 Zero-initialize objects of automatic storage duration

自動記憶域期間の変数が初期化されない場合に常にゼロ初期化されるようにする提案。

以前の記事を参照

このリビジョンは、基本的なところは変わっていませんがR0から全体的に大きく書き直されています。特に、オプトアウト(未初期化のままにしておく)の方法についての検討や言語の変更ではない別の手段によって達成することについてなどが追加されています。

2月のイサクア会議におけるEWGのレビューでは、オプトアウトの方法としてはstd::uninitializedという特別なライブラリ変数によるものが好まれているようです。

P2724R0 constant dangling

現在ダングリング参照を生成しているものについて、定数初期化可能なものを暗黙的/明示的に定数初期化する提案。

この提案は、以前のP2658P2623の提案から、定数初期化関連の部分をマージするものです。それぞれについては以前の記事を参照

ほとんど定数と同等でありながら定数ではないためにふとした使い方の間違いでダングリング参照を生成し安全性を損ねているケースがC++のあちこちで見られます。そのような定数とみなせるものを定数初期化(コンパイル時に初期化)することで静的ストレージに配置し、ダングリング参照を回避するのがこの提案の目的です。

std::string_view sv1 = "hello world";  // ok

// 共にダングリング参照となる
std::string_view sv2 = "hello world"s;           // UB、この提案によって暗黙定数初期化
std::string_view sv3 = constinit "hello world"s; // 明示的に定数初期化を行う
struct X {
  int a, b;
};

const int& get_a(const X& x) {
  // 入力によってはダングリング参照を返す
  return x.a;
}

const int& a = get_a({4, 2}); // UB、aはダングリング参照となる
a; // この提案によって修正されると4を保持
std::generator<char> each_char(const std::string& s) {
  for (char ch : s) {
      co_yield ch;
  }
}

int main() {
  auto ec = each_char("hello world"); // コルーチンから制御が戻った時点でダングリング参照となる。この提案によって暗黙定数初期化

  for (char ch : ec) {
      std::print(ch);
  }
}

この例で問題となっている個所は全て、この提案による暗黙定数初期化によってダングリング参照を生成しなくなります。

この提案はこれらの例を防止できるようになる一方で、同じようにダングリング参照を生成する全ての場合にそれを防止できるわけではありません。あくまで、一部のケースでダングリング参照の生成を抑止し、ダングリング参照の出現機会を減少させるものです。

また、この提案による暗黙/明示的定数初期化は定数初期化そのものは既存の仕組みを使用します。すなわち、クラス型の構築に使用されるコンストラクタがconstexprコンストラクタである(かつ、そのメンバをすべて定数式で初期化する)場合はこの提案の恩恵を受けられるようになります。それはすなわちconstexprの利点として安全性の向上が加わるようになり、クラスの初期化やそれに伴う処理をconstexpr対応させておくことの重要性が高まることになります。

P2725R1 std::integral_constant Literals

std::integral_constantの値を生成するユーザー定義リテラルの提案。

以前の記事を参照

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

  • 二進リテラルプリフィックス0Bの見落としの修正
  • integral_constantに単項-を追加することと、Tへの暗黙変換の相互作用についての議論を追記

などです。

この提案は、constexpr_tというより堅牢な定数型を追加して、それに対してユーザー定義リテラルを提供する方向(P2725R0)が好まれたため、そちらと統合された別の提案(P2781)に作業が引き継がれます。

P2728R0 Unicode in the Library, Part 1: UTF Transcoding

標準ライブラリにユニコード文字列の相互変換サポートを追加する提案。

ユニコード文字コードの規格としてデファクトスタンダードとなっており、日常的にソフトウェアを使用する非常に多くのユーザーにとって重要なものです。しかし、C/C++は本質的にユニコードをサポートしていない数少ない主要なプログラミング言語となっています。

この提案はその状況の修正のために、まずUTF(ユニコードにおける文字表現)の相互変換機能を標準ライブラリに追加することを目指すものです。

この提案による変換インターフェースは次のような設計を選択しています

  • <ranges>との親和性。番兵や遅延ビューのサポートなど
  • (レガシーな)イテレータのサポート。任意のイテレータ型を考慮する
  • ポインタのサポート。SIMDを用いるような最速の変換方法はポインタを介して行われる
  • 変換はブラックボックスではない。入力文字列中の、エンコーディングの切れ目や壊れたエンコーディング個所などの状態をユーザーが調査できるユーティリティを提供する
  • null終端された文字列を特別扱いしない
  • 同じテキストを異なるタイミングでコードユニット(UTFエンコーディングの1単位の整数値)として表示したい場合とコードポイント(ユニコード空間上の32bit整数値)として表示したいことがよくある。そのため、変換イテレータは変換済みのコードユニットの元のデータにアクセスする簡便な方法を提供する。

この提案はそこそこ巨大ですが主に次のものから構成されています

  1. APIを制約するためのコンセプト
  2. null終端された文字列のための番兵型
  3. UTFシーケンスの状態を照会する定数と関数
  4. 変換アルゴリズム
  5. 変換イテレータ
  6. 変換view

また、これらのものは基本的にstd::uc名前空間に定義されています

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

バッファからバッファへのパフォーマンス重視の変換

// UTF-8 -> UTF-16 へ変換する

// バッファのサイズを同じにすることで、変換に余裕があるようにする
char utf8_buf[buf_size];
char utf16_buf[buf_size];

char * read_first = utf8_buf;
while (true) {
  // UTF-8シーケンスを読み取る(ネットワークなど)
  // 末尾に部分的なUTF-8シーケンスが現れうる
  char* buf_last = read_into_utf8_buffer(read_first, utf8_buf + buf_size);

  if (buf_last == read_first)
    continue;

  // 有効なUTF-8シーケンスを特定する(部分的なシーケンスを除外する)
  char* last = buf_last;
  auto const last_lead = std::ranges::find_last_if(utf8_buf, buf_last, std::uc::lead_code_unit);

  if (!last_lead.empty()) {
    auto const dist_from_end = buf_last - last_lead.begin();
    
    assert(dist_from_end <= 4);

    if (std::uc::utf8_code_units(*last_lead.begin()) != dist_from_end) {
      last = last_lead.begin();
    }
  }

  // std::ranges::copy()と同じインターフェースで、変換しながらコピーする
  auto const result = std::uc::transcode_to_utf16(utf8_buf, last, utf16_buf);

  // 変換結果(UTF-16)を使って何かする
  send_utf16_somewhere(utf16_buf, result.out);

  // 末尾にあった部分的なシーケンスを次の処理バッファの先頭へ移動
  read_first = std::ranges::copy_backward(last, buf_last, utf8_buf).out;
}

オブジェクトからのなるべく高速な変換

struct my_string; // ポインタインターフェースを持たない文字列型

// UTF-8文字列の取得
my_string input = get_utf8_input();

// 出力バッファ
std::vector<uint16_t> input_as_utf16(input.size()); // Reserve some space.

// UTF-16 -> UTF-8 変換
auto const result = std::uc::transcode_to_utf16(input, input_as_utf16.data());

input_as_utf16.resize(result.out - input_as_utf16.data()); // Trim unused space.

オブジェクトからのなるべく簡単な変換

struct my_string; // ポインタインターフェースを持たない文字列型

// UTF-8文字列の取得
my_string input = get_utf8_input();

// 出力バッファ
std::vector<uint16_t> input_as_utf16;

// UTF-16 -> UTF-8 変換
std::ranges::copy(input, std::uc::from_utf8_back_inserter(input_as_utf16));

既存のイテレータAPIへのアダプト

// UTF-16シーケンスを受ける関数テンプレート
template<class UTF16Iter>
void process_input(UTF16Iter first, UTF16Iter last);

// UTF-8文字列の取得
std::string input = get_utf8_input(); // std::stringにUTF-8文字列を詰めて使用

// UTF-8 -> UTF-16 変換しつつ、関数に渡す(遅延評価)
process_input(std::uc::utf_8_to_16_iterator(input.begin(), input.begin(), input.end()),
              std::uc::utf_8_to_16_iterator(input.begin(), input.end(), input.end()));


// viewの利用によるさらなる簡単化
auto const utf16_view = std::uc::as_utf16(input);
process_input(utf16_view.begin(), utf16.end());

既存のRange APIへのアダプト

// UTF-16シーケンスを受ける関数テンプレート
template<class UTF16Range>
void process_input(UTF16Range && r);

// UTF-8文字列の取得
std::string input = get_utf8_input(); // std::stringにUTF-8文字列を詰めて使用

// UTF-8 -> UTF-16変換しつつ、関数に渡す(遅延評価)
process_input(std::uc::as_utf16(input));

この例では(おそらくWindows環境で頻出する)UTF-8からUTF-16への変換のみを扱っていますが、提案としてはUTF-8/16/32の全ての相互変換を含んでいます。

この提案の提供する機能は、Boost.TextとしてBoostに提案中のライブラリの一部として5年ほどの実装経験があります。

P2729R0 Unicode in the Library, Part 2: Normalization

標準ライブラリにユニコード文字列の正規化サポートを追加する提案。

この提案は↑の提案に引き続いて、ユニコードサポート改善のためにユニコード正規化機能を標準ライブラリに追加しようとするものです。

ユニコードの正規化とは、同じ意味を持つ複数の文字(コードポイント)をある1つの文字(コードポイント)に変換することを言います。例えば、半角全角や、丸文字などを対応する通常の文字へ変換します。

この提案による正規化インターフェースは次のような設計を選択しています

  • <ranges>との親和性。番兵や遅延ビューのサポートなど
  • (レガシーな)イテレータのサポート。任意のイテレータ型を考慮する
  • ポインタのサポート。SIMDを用いるような最速の変換方法はポインタを介して行われる
  • 変換はブラックボックスではない。入力文字列中の、エンコーディングの切れ目や壊れたエンコーディング個所などの状態をユーザーが調査できるユーティリティを提供する
  • null終端された文字列を特別扱いしない
  • UTFの形式ごとに最適なアルゴリズムへのディスパッチ
    • パフォーマンスのため
    • 使用するUTF形式が変わった時でも、同じコードを利用することができるようにする
  • 煩雑なユニコードの詳細を隠蔽する、より高レベルの抽象化を提供する

この提案は主に次のものから構成されています

  1. ユニコードバージョン定数
  2. ストリームセーフな操作
  3. ストリームセーフなアルゴリズム
  4. ストリームセーフなイテレータ
  5. ストリームセーフなview
  6. APIを制約するためのコンセプト
  7. サポートする正規化形式の列挙型
  8. 汎用の正規化アルゴリズム
  9. 一度に複数の出力を行うアルゴリズム
  10. std::stringのための正規化操作

また、これらのものは基本的にstd::uc名前空間に定義されています

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

コードポイントのシーケンスをNFCに正規化する

std::string s = /* ... */; // std::stringにUTF-8文字列を詰めて使用

// 正規化されていないことを確認
assert(!std::uc::is_normalized(std::uc::as_utf32(s)));

// 出力バッファ
char * nfc_s = new char[s.size() * 2];

// 正規化はコードポイントで動作するため、まずUTF32に変換する(as_utf32()によって)必要がある
auto out = std::uc::normalize<std::uc::nf::c>(std::uc::as_utf32(s), nfc_s);

// null終端
*out = '\0';

// 正規化されていることを確認
assert(std::uc::is_normalized(nfc_s, out));

string-likeなコンテナへ出力する

std::string s = /* ... */; // std::stringにUTF-8文字列を詰めて使用

// 正規化されていないことを確認
assert(!std::uc::is_normalized(std::uc::as_utf32(s)));

// 出力先
std::string nfc_s;
nfc_s.reserve(s.size());

// 正規化はコードポイントで動作するため、まずUTF32に変換する(as_utf32()によって)必要がある
std::uc::normalize_append<std::uc::nf::c>(std::uc::as_utf32(s), nfc_s);

// 正規化されていることを確認
assert(std::uc::is_normalized(std::uc::as_utf32(nfc_s)));

この場合、normalize_append()によってある程度纏めて出力されることで、より高速になる可能性があります。

正規化を維持したまま正規化済み文字列を更新する

std::string s = /* ... */;// std::stringにUTF-8文字列を詰めて使用

// 正規化されていることを確認
assert(std::uc::is_normalized(std::uc::as_utf32(s)));

// 挿入したい文字列(正規化されていないかもしれない
std::string insertion = /* ... */;

// 正規化しながら挿入
normalize_insert<std::uc::nf::c>(s, s.begin() + 2, std::uc::as_utf32(insertion));

// 正規化されていることを確認
assert(std::uc::is_normalized(std::uc::as_utf32(nfc_s)));

この提案の提供する機能もまた、Boost.TextとしてBoostに提案中のライブラリの一部として5年ほどの実装経験があります。

P2730R0 variable scope

ローカルスコープの一時オブジェクトの寿命を文からスコープまで延長する提案。

この提案は、以前のP2658P2623の提案から、ローカル一時オブジェクトの寿命延長関連の部分を抽出したものです。それぞれについては以前の記事を参照

ただし、この提案ではP2658に含まれていたvariable_scopeなどの指定は含まれておらず。P2623に加えてP2658に含まれていたvariable_scopeと同等の寿命延長を提案しています。

次のような関数がああったとき

const std::string& f(const std::string& defaultValue) {
  return defaultValue;
}

この関数の内部からは、引数のdefaultValueがどういう寿命を持つのかは分かりません。それの参照を戻り値で返すことには問題がありますが、それは必ずしもおかしいことでもありません。

このf()に一時オブジェクトを渡して呼び出すと、暗黙的なスコープが追加される形になります。

int main() {
  // 先ほどのf()のこのような呼び出しは
  {
    f("Hello World"s);
  }

  // このような呼び出しとなる
  {
    {
      auto anonymous = "Hello World"s;
      f(anonymous);
    }
  }
}

この時、戻り値を受けていると

int main() {
  // 先ほどのf()のこのような呼び出しは
  {
    const std::string& value = f("Hello World"s);
  }

  // このような呼び出しとなる
  {
    const std::string& value; // 実際には初期化子が必要
    {
      auto anonymous = "Hello World"s;
      value = f(anonymous);
    }
  }
}

コンパイラは通常のC++としてはかけないコードに書き換えたかのようにコンパイルします。これによって、一時オブジェクトanonymousのスコープは他の変数とは異なることになり、プログラマの期待とも一致しなくなります。

この時、最も自然なのはanonymousのスコープが戻り値を受けているvalueと同じになることです。この場合のvalueのスコープのことをこの提案ではブロックスコープと呼び、関数呼び出し時のこのような一時オブジェクトの寿命を現在の文からブロックスコープまで延長することを提案しています。

これによって、参照セマンティクスを持つクラスによっておこるダングリング参照も抑制されます。

int main() {
  std::string_view sv = "Hello World"s;

  ...

  std::cout << sv;  // ok
}

ただしこのような場合には、一時オブジェクトのブロックスコープへの寿命延長が機能しない場所があります

std::string_view sv = "initial value"s;

if (randomBool()) {
  sv = "if value"s;
} else {
  sv = "else value"s;
}

この場合、"if value"s"else value"sの寿命はif節とelse節のそれぞれのブロックスコープまでは延長していますが、svに束縛することによってその外側に持ち出されてしまうと依然としてダングリング参照となります。

この場合に、これらの一時オブジェクトが変数svと同じスコープを持っていればこの問題は解決されます。この提案ではそのようなスコープのことを変数スコープ(variable scope)と呼び、一時オブジェクトが直接別の変数に代入される場合にその寿命を代入先の変変数スコープまで延長することを提案しています。

これによって、関数内部で発生しうる直接的なダングリング参照の発生を抑制します。

結局、この提案は次の2つのことを提案します

  • ローカル変数の一時変数は変数スコープを取得する
  • 関数引数の一時変数はブロックスコープを取得する

これと同等なことは、例えばCの複合リテラルに見ることができますが、C++においてもconst auto&auto&&による一時オブジェクト寿命延長時に行われているため、実装にあたって大きな問題があるわけではないと思われます。

P2732R0 WG21 November 2022 Kona meeting Record of Discussion

2022年11月7-12日にハワイのKonaで行われた、WG21全体会議の議事録。

N4933との違いはよく分かりません。

P2733R0 Fix handling of empty specifiers in std::format

std::format()のフォーマット文字列内の置換フィールドが空である場合に、パースを行わなくてもよくする提案。

フォーマット文字列内の置換フィールド{}には{:+3}などのように出力を調整するためのオプション(フォーマット引数)を指定することができます。この解析のためにはstd::formatter<T>::parse()が使用され、ユーザー定義型に対するフォーマットのカスタマイズではこの関数でオリジナルオプションをパースすることで自作の型にオリジナルのオプションを指定してフォーマットすることができるようになります。

とはいえ、多くの場合はオプション無しの{}のまま使用されることが多いようで、その場合に、現在の規定はフォーマット引数の有無にかかわらずstd::formatter<T>::parse()を呼び出すことを要求しています。

struct S {};

// ↑のSをstd::format()可能にする
template <>
struct std::formatter<S> {
  auto parse(format_parse_context& ctx) { return ctx.begin(); }
  auto format(S, format_context& ctx) const { return ctx.out(); }
};

int main() {
  auto s1 = fmt::format("{}", S());  // (1) フォーマット引数指定なし
  auto s2 = fmt::format("{:}", S()); // (2) フォーマット引数は空
}

(1)と(2)の例はいずれもフォーマット引数は存在していないため、parse()を呼び出す意味がありません。{fmt}ライブラリでの実装経験によれば、このオーバーヘッドは実際にrangeのフォーマットにおいて悪影響を与えていることが確認できたようです。それはフォーマット文字列のコンパイル時検査等、parse()を呼び出す必要のある他のコンテキストにおいても同様である可能性があります。

また、C++23のrangeではフォーマットオプション中で:...のようにして要素型に対するオプション指定を行うことができ、そこでも同様の問題が発生します。

int main() {
  auto s3 = std::format("{}", std::vector<S>(2));     // vector : なし、S : なし
  auto s4 = std::format("{:}", std::vector<S>(2));    // vector : 空、  S : なし
  auto s5 = std::format("{::}", std::vector<S>(2));   // vector : 空、  S : 空
  //                       ^ 要素型Sに対する空のフォーマット引数指定
}

これはいずれのケースでも、外側のstd::vectorと要素型のSに対してフォーマットオプションを指定していません。現在の規定ではフォーマットオプションの指定なしと空は区別されていたようですが、rangeの要素型に対するフォーマットオプション指定においてはパースの都合上(ほぼ:の有無だけで要素型に対するフォーマットオプション有無が決まるので)この2つの違いを区別することができません。そのため、この提案ではこの2つのケースは同等に扱われるようになります。

この問題はLWG Issue 3776で指摘され、その解決のためには提案が必要になるということで、この提案が書かれました。この修正は、省略することを許可するものの必須ではない、となるようになっています。

また、類似の問題の解決として、std::tupleに対するフォーマット時に要素型に対するデバッグ出力を有効化できていなかったバグの修正も同時に行われます。

std::tupleに対するフォーマットでは要素型に対するオプション指定が無いため、要素型に対するオプションのparse()は常に省略されていました。一方で、要素型の文字/文字列型に対してデバッグ出力オプション(?)が常に有効になっており、そのために要素型のフォーマットオプションのparse()std::formatter::set_debug_format()の呼び出しが必要になる、というある種の矛盾状態に陥っていました(本来、set_debug_format()parse()内で?オプションをパースしたときに呼び出されることでデバッグ出力を有効化します)。

この提案ではこれを解決し、std::tupleに対するフォーマットではネストした要素型でデバッグ出力が可能な場合にset_debug_format()を正しく呼ぶように規定を修正します。

auto s = fmt::format("{}", std::make_tuple(std::make_tuple('a')));
// Before : ((a))
// Aftter : (('a'))

P2734R0 Adding the new 2022 SI prefixes

2022年に追加された新しいSI接頭辞に対応するstd::ratio特殊化を追加する提案。

<ratio>ヘッダにはコンパイル有理数std::ratioと、SI接頭辞(ミリとかマイクロ、キロやギガなど)に対応するその事前定義型エイリアスが定義されています。

SI接頭辞は長らく更新されていませんでしたが、2022年に新しく次の4つが追加されました

  • quecto : $10^{-30}$
  • ronto : $10^{-27}$
  • ronna : $10^{27}$
  • quetta : $10^{30}$

この提案は、この4つに対応する設定済みのstd::ratioエイリアスを追加するものです。名前はSI接頭辞名と同じで提案されています。

ただし、既存のstd::yoctostd::yotta等と同様に、itmax_t型で値を表現可能な場合(64bitを超える幅の整数型を提供していない場合)は定義しなくてもよいようにされています。

P2736R0 Referencing the Unicode Standard

ISO 10646(UCS)の代わりにユニコード標準を参照するようにする提案。

現在のC++ユニコードのサポートのためにユニコード標準とISO 10646の両方を参照しています。この2つはほとんど同じ内容ではあるものの別々の規格です。

ユニコード標準は単に文字コードを定めるだけでなく、正規化や比較、大文字と小文字の変換など、ユニコード文字を扱うためのアルゴリズムも含んでいるなど、ISO 10646に比べてより広い規格になっています。また、ユニコード標準の定め提供するそれらのものは、参照するユニコード標準のバージョンを厳密に指定しているため、異なるバージョンでは動作が保証されませんが、ISO 10646とユニコードのリリースサイクルが異なることからそれを厳密に一致させることが困難になります。

また、ISO 10646とユニコード標準では使用する用語にも微妙に差異があり、その違いや影響について評価するためにSG19(ユニコードに関する作業部会)の時間を無駄に消費しています。さらに、ユニコード標準は関連するユニコードデータをツールに使用しやすいようなフォーマットで提供していますが、ISO 10646はただのPDFであり、実装者にとってもISO 10646対応の検証などは重荷になっています。

C++では、言語とライブラリの両方でユニコードのサポートを進めており、そこではユニコードの参照が必要となります。今後それを進めていくにあたってISO 10646対応のために(規格化と実装の)時間を消費するのを回避し、標準の依存関係を減らして標準を明確化するために、ISO 10646への参照を完全にユニコードへの参照で置き換えようとする提案です。

この提案は標準の文書からISO 10646の参照を削除し、それに関連する文言をユニコード標準を参照するように書き換えるだけのものなので、実装には影響はありません。

ただ、__STDC_ISO_10646__という事前定義マクロだけは影響を受けますが、これについてはユニコードの任意のコードポイント値をwchar_tオブジェクトを格納できることのような定義にし(実質現在と変更はない)、その値を実装定義とすることでISO 10646への参照を回避するようにしています。

P2737R0 Proposal of Condition-centric Contracts Syntax

新しいキーワードによる条件中心な契約構文の提案。

現在検討中の契約プログラミングのための構文は、C++20で削除された時から変わらず属性を利用したものが主流です。ただし、これはまだ確定したものではなく、他にもクロージャを意識した構文(P2461R1)が提案されています。

属性 クロージャ
int select(int i, int j)
  [[pre: i >= 0]]
  [[pre: j >= 0]]
  [[post r: r >= 0]]
{
  [[assert: _state >= 0]];

  if (_state == 0)
    return i;
  else
    return j;
}

int pre;    // OK
int assert; // OK
int post;   // OK
int select(int i, int j)
  pre{i >= 0}
  pre{j >= 0}
  post(r){r >= 0}
{
  assert{_state >= 0};

  if (_state == 0)
    return i;
  else
    return j;
}

int pre;    // OK
int assert; // ???
int post;   // OK

この提案では3つ目の候補として、precond, postcond, incond(順に、事前条件、事後条件、アサート)の3つのキーワードを用いた条件中心な構文を提案するものです。

属性 この提案
int select(int i, int j)
  [[pre: i >= 0]]
  [[pre: j >= 0]]
  [[post r: r >= 0]]
{
  [[assert: _state >= 0]];

  if (_state == 0)
    return i;
  else
    return j;
}

int pre;    // OK
int assert; // OK
int post;   // OK
int select(int i, int j)
  precond(i >= 0)
  precond(j >= 0)
  postcond(result >= 0)
{
  incond(_state >= 0);

  if (_state == 0)
    return i;
  else
    return j;
}

int precond;  // ERROR
int incond;   // ERROR
int postcond; // ERROR

この提案はP2521R2にある契約機能のMVPについて次の変更を加えます

  1. アサーションの構文をassertからincondに変更
  2. precond, postcond, incondを完全な(文脈依存ではない)キーワードとして追加
  3. 条件指定は()の中に式を指定する
    • precond(expr), incond(expr), postcond(expr)
  4. 事後条件(postcond)では、定義済みの変数resultによって戻り値を参照する変数が暗黙的に導入される

incondは事前条件(処理の前に満たすべき条件)と事後条件(処理の後で満たすべき条件)からの類推で、処理の途中で満たすべき条件を表す造語です。これは、2分木の探索順序を表す3つの単語preorder, inorder, postorderを参考にして作られた言葉でもあります。

アサーションassert)を置き換えたい動機は次のようなものです

  1. assertionは他の2つ(preconditionpostcondition)と一貫していない
    • 条件は満たさないことを違反したと言うが、アサーションは失敗したと言われる
    • 3つの事柄の互いの関連性が明確であることが重要
  2. 既存のアサーションとの混同
    • C assert(assert()マクロ)とstatic_assertに加えて、アサーションのためのライブラリが多数存在する
    • 単にアサーションと言った時にどれを指しているかが曖昧
  3. assertはキーワードとして登録できない
    • 少なくともC assertと競合する

incondというワードはこの3つのいずれの問題もクリアしている新しい造語です。

この新しいキーワードはACTCD19というC/C++コードベース調査で1000件程度しかヒットしないため新しいキーワードとして使用できる、と主張しています(使われ方にもよりますが1000件は多いのではと思わないでもないですが)。

P2738R0 constexpr cast from void*: towards constexpr type-erasure

定数式において、void*からポインタ型への変換を許可する提案。

背景やモチベーションは後の方のP2747R0と共通するのでそちらをご覧ください。

この提案では主に、std::formatconstexpr対応のために、型消去ユーティリティをコンパイル時に使用可能とすることを目的としています。

#include <string_view>

struct Sheep {
  constexpr std::string_view speak() const noexcept { return "Baaaaaa"; }
};

struct Cow {
  constexpr std::string_view speak() const noexcept { return "Mooo"; }
};

// 型消去ユーティリティの実装例
// speak()メンバ関数をもつ任意の型のviewとなる
class Animal_View {
private:
  // オブジェクトの型を消去して保持するポインタ
  const void *animal;
  // 関数ポインタ
  std::string_view (*speak_function)(const void *);

public:

  template <typename Animal>
  constexpr Animal_View(const Animal &a)
    : animal{&a}  // オブジェクトポインタをvoid*へキャストし保存(これは定数式で行える)
    , speak_function{[](const void *object) {
        // 実際に渡されたAnimal型をラムダ式内に保存し、ラムダ式は関数ポインタへ変換して保存
        // 実際の型情報を用いてvoid*からAnimal*を復帰するので、常に正しいキャスト
        return static_cast<const Animal *>(object)->speak();  // ここをコンパイル時に実行できない
      }} {}

  constexpr std::string_view speak() const noexcept {
    // このクラスが正しく構築されていれば、この呼び出しは常に正しく元のオブジェクトをvoid*から復帰させてspeak()メンバを呼び出す
    return speak_function(animal);
  }
};

// Animal_Viewの要求するインターフェースを持つ任意の型のオブジェクトを渡すことができる
std::string_view do_speak(Animal_View av) { return av.speak(); }

int main() {
  constexpr Cow cow;

  // cannot be constexpr because of static_cast
  [[maybe_unused]] auto result = do_speak(cow);
  return static_cast<int>(result.size());
}

このような型消去ユーティリティがやっていることは、ほとんど仮想関数によるポリモルフィズムと同様です。その大きな違いは、要求するインターフェースに準拠するクラスは必ずしもこれにアダプトするための作業をする必要がなく(非侵入的であり)、個別のクラスごとに仮想関数テーブルを必要としないことにあります。そして、このような型消去は、テンプレートのインスタンス化を抑制しコンパイル時間を削減するなどのメリットがあります。

これと同様のアプローチは、std::format()の実装において使用されている(フォーマット対象引数の文字列化のために)ほか、std::function_refの実装においても使用されます。

ただし、この例を見てわかるように、このような型消去の肝はオブジェクトポインタをvoid*へ落とした後で必要になったタイミングでvoid*から復帰するところにありますが、void*からポインタ型へのキャストは現在定数式で明示的に禁止されているため、これらの型消去テクニックはコンパイル時に使用可能ではありません。

この提案は、この制限を取り除くことでこのような型消去をコンパイル時に使用可能とし、std::format()std::function_refconstexpr対応するための障害を取り除くものです。

ただし、ここで提案されているのは、void* -> T*の変換時にそのポインタが正しくTのオブジェクトを指している場合にのみ変換を許可することで、ポインタ相互互換性(pointer interconvertible)や全く異なるポインタへの変換を許可するものではありません。

現在のほとんどのコンパイラの定数式の実装においては、未定義動作排除などのためにポインタとそれが指すオブジェクトの状態をその実行に当たって追跡しています。そのためvoid*からのキャスト時にその正しさのチェックを行うことは可能であり、提案ではClang/GCC/MSVC/EDGの実装者からこの提案の実装が問題ない事を確認しています(筆者の方はClangで実装して確認しているようです)。

P2739R0 A call to action: Think seriously about "safety" then do something sensible about it

C++の安全性向上のための行動を呼びかける文書。

2022年11月ごろに出されたNSAアメリカ国家安全保障局)のレポート(Software Memory Safety - Department of Defense)において、C++はCとともにメモリ安全な言語ではなく他のメモリ安全な言語に移行することが望ましい、と名指しされました。この文書はそれを受けて、筆者の方(Bjarne Stroustrup先生)のこれまで及びこれからのC++の安全性向上のための取り組みを説明し、C++標準化委員会としても行動していくことを促すものです。

取り組みとしてはコアガイドラインの整備やそれをベースとした静的解析ツールの整備、コアガイドライン基準の静的解析アノテーションの提案(P2687R0)などが挙げられています。

NSAの文書は安全をメモリ安全性に限定していますが安全性とはそれだけではなく、安全性を実現するための方法も一通りではありません。また、安全性は重要ではありますが誰もが安全性だけを重視するわけではなく、安全性よりも他の事項(例えばパフォーマンス)が重視される場合もあります。また、モダンなコードだけを安全にしたとしても、現在の多くのC++コードがそうなるわけではなく、それらの過去のコードは安全なコード(あるいは安全なプログラミング言語)から呼び出されて使用されます。

安全性の問題を放置すればC++のコミュニティの大部分が損なわれ、委員会で行われている多くの作業が無駄になってしまいますが、安全性だけを重視しすぎても同様の結果を招くことになります。

この文書では、まず安全性の問題と考えられることのリストを作成し、P2687R0の枠組みの中でそれをどのように改善できるかを考えていくことを提案しています。これはまた、Bjarne先生の今後の方針でもあります。

P2740R0 Simpler implicit dangling resolution

関数からローカル変数の参照を返してしまうケースをコンパイルエラーにする提案。

C++23では、P2266R3の採択によって一部のローカル変数の参照を返す関数がコンパイルエラーとなるようになります。

// 参照を返す関数
int& f(bool b, int i) {
  static int s;

  if (b) {
    return s;  // OK
  } else {
    return i;  // error : C++23から
  }
}

// reference_wrapperを返す関数
std::reference_wrapper<int> g() {
  int w;
  
  return w;  // error : C++23から
}

この変更は画期的なものですが、P2266の主目的はあくまで暗黙のムーブ対象の拡大にあったのでこれは副次的な効果に過ぎず、完全なものではありません。例えば、上記それぞれの場合においての参照に当たるものをreturnで返す場合は、その参照先がローカル変数でもエラーになりません。

この提案はここからさらに進んで、追加でいくつかの場合もコンパイルエラーにしようとするものです。

1つ目は参照ではなくポインタを返す関数の場合です。

int* h(bool b, int i) {
  static int s;
  if (b) {
    return &s;  // OK
  } else {
    return &i;  // error: iは戻り値のポインタよりも先に寿命が尽きる
  }
}

(ここでのerrorとはエラーにすることを提案しているという意味で、以降も同様です)

これと同様のことは、関数の本体内でも起こり得ます。

void h(bool b, int i) {
  int* p = nullptr;

  if (b) {
    static int s;
    p = &s;  // OK
  } else {
    int i = 0; 
    p = &i;  // error: iは戻り値のポインタよりも先に寿命が尽きる
  }

  // ...
}

このために、この提案では次のようなルールを提案しています

ポインタまたは参照の寿命が尽きる前にその参照するオブジェクトの寿命が尽きる場合、オブジェクトのアドレスをポインタまたは参照に代入できない

ただしこれは、一段階の間接化だけを対象としています。つまり、すでに何かを参照している参照/ポインタを新しい参照/ポインタに代入する(returnする)ような場合はこのルールに該当しません。それをしようとすると、コンパイラはローカル変数と参照/ポインタの依存関係グラフを作成することになり、これはコンパイル時間増大と実装の複雑化を招きます。

次に、ラムダ式やコルーチンがローカル変数を参照キャプチャしていて、それを内包するオブジェクトを返す場合をコンパイルエラーとします。

auto lambda() {
  int i = 0;
  return [&i]() -> &int
      {
          return i;
      };  // error: iは戻り値のラムダよりも先に寿命が尽きる
}

auto coroutine() {
  int i = 0;
  return [&i]() -> generator<int>
      {
        co_return i;
      };  // error: instance `i` dies before the returned coroutine
}

このために、この提案では次のようなルールを提案しています

ローカル変数へのポインタ/参照をキャプチャするラムダ式またはコルーチンを関数から返すことはできない

これは前項の提案から一段階だけ進んだ間接化の中でも言語機能による特殊なケースを処理するものです。これ以上の間接化を処理しようとすることは、前項と同様の理由により提案していません。

コンパイラは、関数が戻り値としてオブジェクトを返すのかその参照(ポインタ)を返すのかを知っていて、その関数内でのローカル変数とそれを参照する(一段階の)ポインタや参照を認識しています。二段階以上の間接化を処理しようとするとローカル変数の依存関係グラフを構築する必要が出てきてしまいますが、一段階の間接化(と一部の特殊ケース)だけなら全てのコンパイラが簡単に検証できるはずです。

この提案によって、追加の静的解析ツールを必要とすることなくC++の言語内で多くのダングリング参照を防止できるようになります。完全なものではありませんが、この修正は大きな改善です。また、Cコードを(この提案を実装した)C++としてコンパイルすることで、Cのポインタの安全性を検証することもできるなど、C/C++エコシステム全体の改善に貢献することができます。

P2741R0 user-generated static_assert messages

static_assertの診断メッセージ(第二引数)に、コンパイル時に生成した文字列を指定できるようにする提案。

static_assertの診断メッセージには現在文字列リテラルを直接指定することしかできず、定数式で生成した任意の文字列を指定することができません。これによって、コンパイル時の診断メッセージの柔軟さが損なわれています。

この提案は、static_assertの診断メッセージとして定数式で生成された文字列を指定してエラーメッセージとして出力可能とすることで、static_assertによるコンパイル時のエラー報告を改善しようとするものです。

この提案が通れば、将来的にstd::format()を使用可能となるでしょう。

static_assert(sizeof(S) == 1, std::format("Unexpected sizeof: expected 1, got {}", sizeof(S)));

ただし、この提案ではstatic_assertの診断メッセージに指定可能なものを広げることをだけを念頭に置いていて、std::format()constexpr対応については提案していません。

また、この提案によって指定可能になる文字列とはstd::stringを指すのではなく、次のような特性を持つ型の値を文字列と見做して出力可能とすることを提案しています。

  • .size()メンバ関数を持つ
    • 戻り値型は整数型
  • .data()メンバ関数を持つ
    • 戻り値型はchar*char8_t*のどちらか(CV修飾はあってもいい)
  • 診断メッセージに指定されたオブジェクトをmsgとすると、[msg.data(), msg.data() + msg.size())は有効な範囲であること

std::stringだけでなく、std::string_viewstd::vector<char>なども使用可能です。

P2742R0 indirect dangling identification

戻り値の参照やポインタの有効期間が別の変数の生存期間に依存していることを表明する属性の提案。

この提案は、関数の戻り値で発生するダングリング参照の防止・削減を目的としており、上の方で出ていたP2724R0やP2730RO、P2740R0とよく似た目的で同じ作者による提案です。

この提案では、parameter_dependencyという新しい属性を提案していて、これはdependent引数として文字列でその有効期間が他に依存しているものを指定し、providers引数に文字列で依存先のものを指定します。"return"で戻り値を指定することができるほか、依存先として"this"*thisオブジェクトを指定できます。

[[parameter_dependency(dependent{"return"}, providers{"this", "left", "right", "first", "second", "last"})]]

providers引数は文字列の配列で、dependent1つにつき複数の依存先を指定できます。

parameter_dependency属性は関数(フリー関数及びメンバ関数)に指定するもので、これによってdependentに指定したものの有効期間がprovidersに指定したものの生存期間に依存していることを表明します。

// 戻り値の参照の有効期間は引数iに依存している
[[parameter_dependency(dependent{"return"}, providers{"i"})]]
int& f(bool b, int& i) {
  static int s;
  if (b) {
    return s;
  } else {
    return i;
  }
}

// 戻り値の参照の有効期間は引数left/rightに依存している
[[parameter_dependency(dependent{"return"}, providers{"left", "right"})]]
int& g(bool b, int& left, int& right) {
  if (b) {
    return left;
  } else {
    return right;
  }
}

class Car {
private:
  Wheel wheels[4];
public:
  // 戻り値の参照の有効期間は*thisオブジェクトに依存している
  [[parameter_dependency(dependent{"return"}, providers{"this"})]]
  const Wheel& getDriverWheel() const {
    return wheels[0];
  }
}

この属性は参照とオブジェクトの依存関係を手動で表明するものであって、その依存関係を確立し参照が参照しているオブジェクトの生存期間を延長するものではありません。

このような属性はライブラリのAPIにおいてドキュメントや仕様記述として使用できます。ヘッダオンリーなライブラリであれば静的解析など他の手段によってその依存関係を知ることができるかもしれませんが、翻訳単位が分かれている場合のABI境界の関数宣言においてはこのような属性の有効性は高まります。

また、コンパイラや静的解析ツールがこの属性を認識することで、ライフタイム解析の手助けをすることができます。例えば先ほどのCar::getDriverWheel()では

const Wheel& f() {
  Car local;
  return local.getDriverWheel();  // error?
}

この属性が無い(無視される)場合、getDriverWheel()から返される参照の有効期間は分からず、追加の解析なしではこの例が正しいのかどうかを判断できません(ABI境界の向こう側に実装がある場合、そのような解析は不可能かもしれない)。しかし、parameter_dependency属性によって戻り値は*thisの生存期間に依存していることがわかるため、*thisすなわちローカルのCarオブジェクトlocalの寿命に依存していることがわかり、コンパイラや静的解析器はこの関数の外側にその戻り値を持ち出すのは間違っていることを認識できるかもしれません。

上の方のP2740R0ではローカル変数依存関係の解析が必要となるため困難だった多段階の間接化によるダングリング参照生成も、この提案による属性指定によって手動ではありますがコンパイラが認識可能になるかもしれません。また、これはポインタでも機能し、C23からは属性構文がC++と同等になったため、この属性をCとの共通コードに書くこともでき、間接的にCコードの安全性向上にも役立つ可能性があります。

P2743R0 Contracts for C++: Prioritizing Safety - Presentation slides of P2680R0

P2680R0の紹介と解説スライド。

P2680R0は、C++をより安全な言語に進化させていくことの第一歩として契約プログラミングに焦点を当て、副作用やUBの扱いについて議論のある契約条件式の実行モデルをconstexprの実行モデルと同じものにしようとするものです。

このスライドはSG21のメンバに向けてその内容を紹介するとともに背景などを解説するもののようです。

P2746R0 Deprecate and Replace Fenv Rounding Modes

浮動小数点環境の丸めモード指定関数std::fesetround()を非推奨化して置き換える提案。

std::fesetround()には次のような問題があり、移植可能でも効果的でもないようです

  1. FENV_ACCESS#pragmaしていない場合、コンパイラは丸めモードを無視した最適化を実行する。しかし、そのマクロはC++ではサポートされていないため、C++ではfesetround()が正しく動作することを保証できない
  2. 丸めモードを変更した状態で、標準の数学関数やユーザー定義関数を呼び出した結果は一貫性が無く、予測可能ではない
  3. 丸めモードはコードの領域に対して指定するのではなく、それぞれの演算に対して指定する必要がある
  4. 様々な丸めモードを試すことで浮動小数点数の誤差をある程度把握することができるが、3と同じ理由によりそれはランダムに結果を擾乱するよりもわずかにマシ程度のもの
    • また、プログラムロジックが丸めモードに依存している場合やコードが丸めモードを変更している場合、このアプローチはうまくいかない
  5. コンパイラは定数式においては丸めモードを無視する傾向にあり、プログラマが実際の結果を予測することを困難にしている
  6. もし丸めモードを適用したうえで結果を確定させたいならば、定数式においても丸めモードを考慮しなければならない。しかし、これはCでは禁止されている
  7. 特にC++では、そもそも正しく丸められていない演算に対する丸めモードの意味が不明確。IEEE標準は正しい丸めを要求しているが、C++実装は適合していない

CのFENV_ROUNDstd::fesetround()よりは良い振る舞いをしますが、結局上記のいずれかは問題となります。

これらの理由により、std::fesetround()は実際にはほとんど使用されていません。想定されるユースケース区間演算や、それのように計算結果の上限と下限を得る必要がある場合ですが、そのような用途はあまり一般的ではありません。また、fenv.hC++と相性が悪く、Cの改訂に追いついておらず、Cでさえその有用性が疑問視されているようです。

特に、この機能は数学関数のconstexpr化の作業の際に多くの問題を引き起こしており、全ての数学関数はfesetround()による暗黙の丸めモード引数を持ってしまっており、この値はコンパイラによって予測不可能となるため、ユーザーが期待する最適化を混乱させることになります。C++では、数学ライブラリが丸めモードを尊重すべきかどうか、あるいは非標準の丸めモードにおいて合理的なことを行うべきかが不透明です。

このような理由から、この提案ではstd::fesetround()std::fegetround()を非推奨にしたうえで、IEEE標準の指定する正しい丸めに従った浮動小数点演算結果を生成する代替機能を追加することを提案しています。

提案する代替機能はcorrectly_roundedというクラスで、メンバ関数としてIEEE準拠の浮動小数点演算を行う各種関数が提供されます。

template<floating_point F>
class correctly_rounded {

  explicit correctly_rounded(F plain_value);
  // Other expected constructors, destructor, assignment;
  F to_plain() const;
   
  ...

  // IEEE準拠の四則演算
  correctly_rounded<F> operator+(correctly_rounded<F> y) const;

  template<float_round_style r = round_to_nearest>
  correctly_rounded<F> add(correctly_rounded<F> y) const;
  
  correctly_rounded<F> operator*(correctly_rounded<F> y) const;
  
  template<float_round_style r = round_to_nearest>
  correctly_rounded<F> multiply(correctly_rounded<F> y) const;
  
  ...

  // IEEE準拠の数学関数
  template<float_round_style r = round_to_nearest>
  correctly_rounded<F> sqrt() const;
  
  ...
};

correctly_rounded<double> operator"" _d_round_to_nearest(const char
*);
correctly_rounded<double> operator"" _d_round_toward_infinity(const
char *);

correctly_rounded<F>浮動小数点数Fの値をラップして、正しい丸めを行う各種操作を提供するクラスです。コンストラクタに値を渡すか、ユーザー定義リテラルを使用して生成し、各種演算の際に非型テンプレートパラメータとしてfloat_round_styleの値をしていすることで丸めモードをコンパイル時に指定します。

P2747R0 Limited support for constexpr void*

定数式において、void*からポインタ型への変換を許可する提案。

現在、定数式においてはvoid*型のポインタを任意のT*にキャストすることは明示的に禁止されており、仮にそのポインタが正しくTのオブジェクトを指している場合でも許可されません。

このルールは定数式において不正なポインタの読み替え(type punning)が起こらないようにするためであり、constexprC++の安全なサブセットとする方針に基づいたものです。ただし、void*からの変換がいつも危険というわけではなく、中には安全と分かっているものもあり、一律に禁止されていることによっていくつかの便利なツールをコンパイル時に使用できなく居しています。

  • 型消去ユーティリティ
    • 例えば、function_refは型消去して保持している呼び出し可能なものへのポインタを復帰するためにvoid*からの変換が必要
  • 配置new
    • 配置newはオブジェクトを構築する領域ポインタをvoid*で受ける
    • std::construct_at()ではnew式で可能な全ての初期化をカバーできておらず(デフォルト初期化できないなど)、コピー省略を妨げる
  • オブジェクトの遅延初期化(未初期化配列など)
    • 配置newが必要となる
    • 配置new回避のためには未初期化オブジェクトが問題となる
    • 現在、static_vectorconstexpr対応で問題となっている

これらのユースケースのサポートのために、この提案では現在定数式でvoid*からT*へのキャストを禁止している一文を削除することで、void*からのポインタの復帰を定数式でも可能とすることを提案しています。

ただし、void* -> T*への変換を行うポインタは予めT*からstatic_cast<void*>で得られたものである必要があり、他の場合は許可されません。すなわち、ポインタが実際に指すオブジェクトの型に応じたポインタ型への変換のみを許可し、type punningのようなことを可能にするわけではありません。

また、未初期化配列(遅延初期化可能なTの配列)サポートのために、unionのメンバとなっている配列に対する配置newが暗黙的に配列全体の生存期間を開始することも提案しています。

この提案の想定される実装上の懸念は、そのようなポインタ変換を検証するコストです。定数式におけるポインタの追跡コストが高くつくようになってしまうと、コンパイル時間の増大を招きます。しかし、void* -> T*への変換が禁止されていることによって型消去ユーティリティや配置newなどを直接的に利用できないために、それが必要となった時に個別にstd::construct_atのようなものを導入する必要があります。このコスト(規格化やコンパイラ実装の手間)に比べれば、この提案を実装するコストは低いと筆者の方は主張しています。

P2748R0 Disallow Binding a Returned glvalue to a Temporary

glvalueが暗黙変換によって一時オブジェクトとして参照に束縛される場合をコンパイルエラーとする提案。

次のコードには一箇所バグがあります。

struct X {
  const std::map<std::string, int> d_map;
  const std::pair<std::string, int>& d_first;

  X(const std::map<std::string, int>& map)
    : d_map(map)
    , d_first(*d_map.begin())
  {}
};

コンストラクタのmapは常に1要素以上あるとして、問題があるのは先頭要素の参照をd_firstに取っているところです。

std::map<K, V>の要素型はstd::pair<const K, V>なのでd_firstの型std::pair<std::string, int>は間違っています。ところがこのこと自体はコンパイルエラーにならず、暗黙変換によってstd::pair<std::string, int>の一時オブジェクトが生成され、その一時オブジェクトがd_firstに束縛されます。そして、その一時オブジェクトの寿命はコンストラクタ呼び出しと共に終了し、d_firstはダングリング参照となります。

とはいえこの例は、コンストラクタ初期化子リスト内で一時オブジェクトの参照への束縛が起こった場合はill-formed、というルールによってコンパイルエラーとなります。

ただし、コンストラクタの外で同じことが起こるとコンパイルエラーにはなりません。

struct Y {
  std::map<std::string, int> d_map;

  const std::pair<std::string, int>& first() const {
    return *d_map.begin();  // 一時オブジェクトが生成される
  }
};

このfirst()は常にダングリング参照を返します。先程と同様の理由によりstd::map<std::string, int>の先頭要素はstd::pair<std::string, int>に暗黙変換され、return文ではその一時オブジェクトが参照に束縛され、その参照がreturnされます。ところが、return文で生成された一時オブジェクトの寿命はそのreturn文の終了までと規定されているため、この場合に一時オブジェクトの寿命は延長されず、この関数は常にダングリング参照を返します。

このようなコードは必ずしもC++初学者だけが書くコードではなく、経験豊富なC++プログラマでも書いてしまう恐れがあります。そのため、コンストラクタの初期化子リスト(及びデフォルトメンバ初期化子)と同様に、このような変換による一時オブジェクトの参照への束縛が起こる場合をコンパイルエラーにしようとする提案です。

この提案では、参照を返す関数のreturn文で一時オブジェクトが参照に束縛される場合をill-formedとすることを提案しています。現在存在しているこのような関数は常にダングリング参照を生成しており、未定義動作に陥っています。従って、この提案による影響はUBをコンパイルエラーとするだけです。

コンストラクタ初期化子とデフォルトメンバ初期化子で同じことが起こる場合をill-formedにしたCWG 1696の議論では、同じ理由により論争が起こることなく採択されており、この提案の根拠はその時と同等以上に強いものであると思われます。

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

auto&& f1() {
  return 42;  // コンパイルエラー
}

const double& f2() {
  static int x = 42;
  return x;   // コンパイルエラー
}

auto&& id(auto&& r) {
  return static_cast<decltype(r)&&>(r);
}

auto&& f3() {
  return id(42);  // OK, ただしバグと思われる
}

P2750R0 C Dangling Reduction

Cの言語機能の範囲で発生するダングリングポインタを抑制する提案。

これは、上の方で出ていたP2724R0やP2730R0、P2740R0、P2742R0の内容を参照ではなくポインタに対して適用するものです。そちらの提案でも同様のソリューションはポインタに対しても提案しているので内容はほぼ同一です。この提案はそれらの提案からCのポインタ向けの部分をまとめたもののようです。

この提案で提案されている事をまとめると次のようなものです

  1. ポインタの寿命が尽きる前にその参照するオブジェクトの寿命が尽きる場合、オブジェクトのアドレスをポインタに代入できない
    • ポインタを返す関数から、ローカル変数のポインタを直接returnする場合をコンパイルエラーにする
    • 同様のことが関数内のローカルスコープで起こる場合をエラーにする
  2. 1段階の間接化を処理
    • ポインタを返す関数から、ローカル変数の構造体メンバのポインタを直接returnする場合をエラーにする
    • ポインタを返す関数から、ローカル変数へのポインタが代入されたポインタ変数をreturnする場合をエラーにする
  3. parameter_dependency属性の導入
  4. 一時オブジェクトの寿命を複合リテラルの寿命(variable scope)と同じにする
  5. ローカル定数一時オブジェクトが必ず定数化されるようにする
    • constな一時オブジェクトがコンパイラによって定数化されるかもしれない、となっているところを定数化される、と変更する

1の例

int* f() {
  return & 1;     // エラー
}

int* g() {
  int local = 1;
  return &local;  // エラー
}

struct Point {
  int x;
  int y;
};

Point* h() {
  Point local = {1, 3};
  return &local;  // エラー
}

void i(bool b, int i) {
  int* p = nullptr;

  if (b) {
    static int s = 0;
    p = &s;  // OK
  } else {
    int i = 0; 
    p = &i;  // エラー
  }
  
  ...
}

2の例

struct Point {
  int x;
  int y;
};

int* f() {
  Point local = {1, 3};
  return &local.y;  // エラー
}

Point* f() {
  Point local = {1, 3};
  Point* p = &local;
  return p;         // エラー
}

3の例

struct Point {
  int x;
  int y;
};

[[parameter_dependency(dependent{"return"}, providers{"point"})]]
Point* obfuscating_f(Point* point) {
  return point;
}

Point* f() {
  Point local = {1, 3};
  return obfuscating_f(&local); // エラー
}

4の例

void f() {
  int* i = &5;  // or uninitialized

  if (whatever) {
    i = &7;
  } else {
    i = &9;
  }

  // iを安全に使用できる(間接参照も含めて)
}

5の例

const int* f() {
  return & 1;   // ダングリングではない、グローバル変数と同等
}

const int* f() {
  const int local = 1;
  return &local;  // ダングリングではない、グローバル変数と同等
}

struct Point {
  int x;
  int y;
};

const Point* f() {
  const Point local = {1, 3};
  return &local;  // ダングリングではない、グローバル変数と同等
}

struct Point {
  int x;
  int y;
};

const int* f() {
  const Point local = {1, 3};
  return &local.y;  // ダングリングではない、グローバル変数と同等
}

P2751R0 Evaluation of Checked Contracts

契約条件のチェックに関する細かいルールの提案。

C++26に向けて契約機能を固めていくにあたって、契約条件の各式がどのように評価されるのかについての詳細は重要な事項であり、現在の議論の中心でもあります。この提案は、契約条件の評価をしっかりと規定し、契約違反がいつ発生したかを判断するためのルールを提案するものです。

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

  1. 契約条件は文脈的にbool変換可能な式であり、評価する際は式の評価に関するC++の通常のルールに従う
  2. 契約条件の評価が完了し、その結果がtrueでは無い時、契約違反が発生している
    1. 契約条件式の評価結果がtrueとなる場合は契約違反を意味せず、それ以上のアクションは発生しない
    2. 契約条件式の評価結果がfalseとなる場合は契約違反が発生している
    3. 契約条件式の評価時に例外が投げられる場合は契約違反が発生している
    4. 契約条件式がUBを含む場合、欠陥が発生する
      • 実装は、欠陥(UB)を契約違反として扱うことを推奨する
    5. 正常に評価されないその他の契約条件は正常に動作する
      • bool変換に失敗した場合など、式の評価に関するC++の通常のルールに従う
  3. 契約条件が評価される回数は未規定。違反の検出のために0回以上、違反の処理のために1回以上評価されうる
    1. 契約条件を1回だけ評価することは許可される
    2. 契約条件を0回だけ評価することは許可される
      • 条件式に副作用がない場合、その評価をスキップすることが許可されている
      • コンパイラが同じ違反を検出する同等の式を識別できる場合、その代替式の評価で条件式を置き換えることができる(as-ifルールに基づく)
    3. 契約条件を複数回評価することは許可される
    4. 複数の契約条件が連続して評価される場合、それらは相互に並べ替えて評価される可能性がある

特に3つ目の規則は、副作用を実質的に禁止しつつ、契約条件の他の利用法などまだ議論の最中にあるユースケースを許容することを意図しています。

P2752R0 Static storage for braced initializers

std::initializer_listの暗黙の配列がスタックではなく静的ストレージに配置されるようにする提案。

std::initializer_list<E>のオブジェクトは、実装によって生成された要素数Nconst Eの配列オブジェクトを参照するようなオブジェクトで、その暗黙の配列の各要素は初期化子の対応する要素からコピーされて初期化されます。std::initializer_listを初期化子に使用する場合、そのような暗黙配列の各要素からさらにコピーされて初期化されます。

struct X {
  X(std::initializer_list<double> v);
};

void f() {
  // このコードは
  X x{ 1,2,3 };

  // こう書いたかのように初期化される
  const double __a[3] = {double{1}, double{2}, double{3}};
  X x(std::initializer_list<double>(__a, __a+3));
}

標準では、実装はこの暗黙配列をリードオンリーメモリー(すなわち静的ストレージ)に配置して使い回すことができることを示唆しています。しかし、それは実質的に不可能となっているようです。

void f(std::initializer_list<int> il);

void g() {
  // この{1, 2, 3}の配列は静的ストレージに配置できるか?
  f({1,2,3});
}

int main() { g(); }

/// どこか別の翻訳単位で定義されているとする
void g();

void f(std::initializer_list<int> il) {
  static const int *ptr = nullptr;
  if (ptr == nullptr) {
    ptr = il.begin();
    g();
  } else {
    // C++準拠実装はこのアサートを常にパスする
    assert(ptr != il.begin());
  }
}

C++23に準拠した実装はこのコードで生成される2つの暗黙配列(1つはmain()g()呼び出しで生成されるもの、もう一つはネストしたf()の中でのg()呼び出しで生成されるもの)がそれぞれ異なるアドレスを持つようにコンパイルしなければなりません(実際、clang/GCC/MSVCはそのようにコンパイルします)。

これによって、std::initializer_listの暗黙の配列を静的ストレージに配置する最適化は実質的に不可能になっています。

std::initializer_listの暗黙の配列が静的ストレージに配置されることがないということは、std::initializer_listを初期化に使う場合には目には見えない2回のコピーが常に行われていることになります。

#include <vector>

int main() {
  // vectorのこのような初期化において、各要素は2回のコピーを伴う
  std::vector vec = {1, 2, 3, 4, ...};
}

1回目のコピーは定数({1, 2, 3, 4})からstd::initializer_listの暗黙の配列へのコピーで、この定数は即値であるか静的ストレージに配置されたものです。2回目のコピーは、std::initializer_listの暗黙の配列からstd::vectorの各要素へのコピーです。

#include <vector>

int main() {
  // このコードは次のようなコードと等しい
  std::vector vec = {1, 2, 3, 4, ...};

  // 1. 静的ストレージ(定数)から暗黙配列へのコピー
  const int __a[] = {int{1}, int{2}, int{3}, int{4}, ...};

  // 2. 暗黙配列からvectorのストレージへのコピー
  std::vector<int> vec(__a.begin(), __a.end());
}

このようなコピーは無駄でありできれば削減したいものですが、一方でリスト初期化を行う際はその要素数はそれほど大きくない場合が多く、実際にはそこまで気にしなくても良いかもしれません。

ところが、C++26では#embedによってこの問題は一層深刻になります

void f() {
  std::vector<char> v = {
    #embed "2mb-image.png"
  };
}

ファイル2mb-image.pngが2MBのサイズのファイルだとすると、このコードはまず2MBの暗黙配列を作成します。それによって関数のスタックが2MB消費されることになります。スタックオーバーフローを起こさなくても、2MB×2回のコピーは十分なオーバーヘッドとして観測されるでしょう。しかも、この問題はコード上から隠されており、std::initializer_listの振る舞いをよく知らないと気づくことが困難です。

この提案は、この問題の解決のために、std::initializer_listが導入する暗黙の配列を静的ストレージに配置する実装を明示的に許可するように標準の規定を調整するものです。

void f(std::initializer_list<double> il);

void g(float x) {
  f({1, x, 3});
}

void h() {
  f({1, 2, 3});
}

// これらの初期化は、おおよそ次のようなコードと等価になる

void g(float x) {
  const double __a[3] = {double{1}, double{x}, double{3}};  // スタックに配置
  f(std::initializer_list<double>(__a, __a+3));
}
void h() {
  static constexpr double __b[3] = {double{1}, double{2}, double{3}}; // 静的ストレージに配置
  f(std::initializer_list<double>(__b, __b+3));
}

スタックではなく静的ストレージに配置されるようになることでスタックの消費を回避することができ、定数->スタックへの1回目のコピーを消し去ることができます。

ただし、この提案ではこのような実装を強制するのではなく、あくまでこうする(h()の例のように実装する)ことを許可するに留めています。

この提案の方向性は、初期化子リスト({...})のセマンティクスを文字列リテラルと一致させることにあり、現在文字列リテラルで許可されている最適化を(コンパイラが実装可能であれば)初期化子リストでも許可しようとするものでもあります。

std::initializer_list<int> i1 = {
    #embed "very-large-file.png"  // OK
};

void f2(std::initializer_list<int> ia,
        std::initializer_list<int> ib) {
  PERMIT(ia.begin() == ib.begin()); // 静的ストレージに配置した同じは配列の使い回しを許可する
}
int main() {
  f2({1,2,3}, {1,2,3});
}
const char *p1 = "hello world";
const char *p2 = "world";
PERMIT(p2 == p1 + 6);  // 現在許可されている振る舞い

std::initializer_list<int> i1 = {1,2,3,4,5};
std::initializer_list<int> i2 = {2,3,4};
PERMIT(i1.begin() == i2.begin() + 1);  // この提案によって許可される振る舞い

P2756R0 Proposal of Simple Contract Side Effect Semantics

契約条件式に含まれる副作用の扱いについて、C++の他の場所と同じ扱いにする提案。

この提案は、P2521R0にあるMVP仕様をベースとして、現在議論の中心にある契約条件式に含まれる副作用の取り扱いについて他のC++の式と差別化しないことを提案するもので、「副作用の禁止」や「2回以上呼ばれる可能性を規定する(ことによる副作用への依存の禁止)」を提案しないものです。

この提案では

  1. 契約条件式はあらゆる種類の副作用を持ちうる
    • 副作用について、不適格・未定義動作・実装定義の動作、などとしない
    • 他のC++の式の標準的な動作と同じ動作をする
  2. 契約の2つの実行モードは、コンパイル前に実装定義の方法によって決定される
    • No_evalモードでは、契約条件は評価されないためそこに含まれる副作用も起こらない
    • Eval_and_abortモードでは、契約条件式は事前条件と事後条件付きの関数呼び出しごとに一回、アサーション実行ごとに一回呼び出される
      • 契約条件式の副作用は、式の1回の実行につき1回発生する
      • アサーションの場合、文(ステートメント)によってその順序が決定する
      • 事前条件の場合、関数呼び出しと関数本体実行の間に実行される
      • 事後条件の場合、関数本体実行終了後、呼び出し元の継続処理の開始前に実行される

この提案の下でも、as-ifルールに従って実装は観測可能な振る舞い(observable behavior)が変化しない限り契約条件式の実行を並べ替えたり省略したりすることは許可されます。この提案の意図は、そういったところも含めて、契約条件式を特別扱いしないことにあります。

int x = 0;

void f() PRE(++x) // 事前条件指定とする
{
  ...
}

int main() {
  f();
  return x;
}

この提案では、このプログラムはill-formedではなく未定義動作も含みません。そして、No_evalモードでは0を返し、Eval_and_abortモードでは1を返します。契約条件を特別扱いする他の提案の場合は、Eval_and_abortモードで0, 1, 2のいずれかを返します(結果が1つに定まることが標準によって保証されない)。

この提案による契約記述時の契約条件式のセマンティクスは、現在のC++における普通の式とほとんど同じ振る舞いをするため単純かつ直観的であり、ユーザーの期待と異なる振る舞いをしないため(他の提案と比べて)使いやすいものになっています。

P2757R0 Type checking format args

std::format()のフォーマット文字列構文について、幅/精度の動的な指定時の型の検証をコンパイル時に行うようにする提案。

std::format()ではフォーマット後文字列の幅の指定と、浮動小数点数フォーマット時の精度(小数点を除いた数値の桁数)を指定することができます。

int main() {
  // 文字幅の指定
  std::cout << std::format("|{:3}|\n", 10);
  
  // 精度の指定
  double v = 3.141592653589793;
  std::cout << std::format("{:.5}\n", v);
}

出力例

| 10|
3.1416

これはまた、実行時の動的指定を簡単に行うために、追加の引数によって指定することができます。

int main() {
  // 幅の指定を引数で行う
  std::cout << std::format("|{:{}}|\n", 10, 4); // 幅4が設定される

  // 精度の指定を引数で行う
  double v = 3.141592653589793;
  std::cout << std::format("{:.{}}\n", v, 3); // 精度3桁が設定される
}

出力例

|  10|
3.14

この置換フィールド({})内にネストした置換フィールド内のオプションには使用する引数のインデックス指定のみが有効で、それ以外のオプションは構文エラーとなります。また、このネストした置換フィールドには整数値の指定のみが有効です。

フォーマット文字列の妥当性はコンパイル時にチェックされますが、C++23時点においても、このネストした置換フィールドに対する型のチェックをコンパイル時に行うことができません。

std::format("{:>{}}", "hello", "10"); // 実行時エラー

この場合、2つ目の引数"10"が文字列リテラルになっているため(整数型でなければならず)構文エラーとなるはずですが、それはコンパイル時ではなく実行時エラーとして報告されます。

この問題は<format>の元になった{fmt}ライブラリでは既に修正済みで、コンパイル時にエラーを発することができています。この提案は、それをstd::format()に対しても適用するものです。

ユーザー定義型でも組み込み型でも、std::format()に指定された引数に対するフォーマット文字列のパースはstd::formatter<T>::parse()で行われます。フォーマット文字列中の置換フィールドごとに対応する引数を取得して、その型Tによってstd::formatter<T>オブジェクトが作成され、.parse()呼び出されます。.parse()の引数には、フォーマット文字列における現在位置や対応する引数の情報を持ったstd::basic_format_parse_contextのオブジェクトが渡されます。

置換フィールドにネストする置換フィールドの場合、外側の置換フィールドに対するparse()においてネストした置換フィールドを処理する必要がありますが、そこに渡っているstd::basic_format_parse_contextオブジェクトは、引数の実際の値および型情報にアクセスできません。できるのは、対応する引数が存在していることをチェックすることとそのインデックスを取得することくらいです。

これによって、ネストした置換フィールドのパース時には対応する引数に対する型のチェックをコンパイル時に行えなくなっています。

本家{fmt}でもこのあたりの構成はほぼ同じですが、C++20以降の更新によってこの問題に対処しています。そこでは、basic_format_parse_contextcheck_dynamic_spec()という関数を追加して、そこでこのネストした置換フィールドに対する型チェックを行います。そのために、コンパイル時のformatter::parse()呼び出しでは、basic_format_parse_contextオブジェクトの代わりにbasic_format_parse_contextから派生したcompile_parse_contextというクラスのオブジェクトを生成してそれをparse()に渡します。

parse()は先ほどと同様に対応する置換フィールド内をパースして行きますが、その際にネストした置換フィールドに当たったとき(組み込み型なのでこれは幅/精度の動的指定オプション)、その引数の存在チェックやインデックスチェックと同時に、basic_format_parse_context::check_dynamic_spec()を呼び出します。basic_format_parse_context::check_dynamic_spec()コンパイル時に呼ばれる場合は自身(*this)がcompile_parse_contextであることを知っているので、*thiscompile_parse_contextにアップキャストしてからcompile_parse_context::check_dynamic_spec()を呼び出します。

compile_parse_contextはその構築時に全てのフォーマット引数の型情報をインデックス情報と共に受けて保持しているため、compile_parse_context::check_dynamic_spec()にインデックスを渡してやればインデックスに対応した型情報が取得でき、それによって幅/精度の動的指定オプションの置換フィールドに対応する引数が整数型か否かをチェックすることができます。

実装イメージ

// 組み込み型の種類を表す列挙型
enum class type {
    none_type,
    // Integer types should go first,
    int_type,
    uint_type,
    long_long_type,
    ulong_long_type,
    int128_type,
    uint128_type,
    bool_type,
    char_type,
    last_integer_type = char_type,
    // followed by floating-point types.
    float_type,
    double_type,
    long_double_type,
    last_numeric_type = long_double_type,
    cstring_type,
    string_type,
    pointer_type,
    custom_type
};

// 列挙型typeの値が整数型であるかを判定
constexpr auto is_integral_type(type t) -> bool {
    return t > type::none_type && t <= type::last_integer_type;
}

// {fmt}の更新されたbasic_format_parse_context定義
template <typename Char, typename ErrorHandler>
class basic_format_parse_context : private ErrorHandler {
public:
    // この2つの関数はC++20と同じ
    constexpr auto next_arg_id() -> int;
    constexpr auto check_arg_id(int arg_id) -> void;

    // 幅/精度の動的指定オプションの型チェックを行う追加された関数
    constexpr auto check_dynamic_spec(int arg_id) -> void;
};

// コンパイル時のパースコンテキスト型 compile_parse_context定義
// コンパイル時にparse()に渡される前に構築され、basic_format_parse_contextにダウンキャストして渡される
template <typename Char, typename ErrorHandler>
class compile_parse_context : basic_format_parse_context<Char, ErrorHandler> {
    // format()に指定された引数の型情報配列
    std::span<type const> types_;

public:
    constexpr auto arg_type(int id) const -> type { return types_[id]; }

    // check_dynamic_spec()のコンパイル時用実装
    constexpr auto check_dynamic_spec(int arg_id) -> void {

        if (arg_id < types_.size() and not is_integral_type(types_[arg_id])) {
            // エラー報告
            this->on_error("width/precision is not an integer");
        }
    }
};

// parse()内からはこちらのcheck_dynamic_spec()が呼び出される
template <typename Char, typename ErrorHandler>
constexpr auto basic_format_parse_context<Char, ErrorHandler>::check_dynamic_spec(int arg_id) -> void {
    if consteval {
        // コンパイル時に呼ばれた場合は自身がcompile_parse_contextオブジェクトであることを知っているため、安全なキャスト
        using compile_context = compile_parse_context<Char, ErrorHandler>;
        static_cast<compile_context*>(this)->check_dynamic_spec(arg_id);  // compile_parse_contextの実装を呼び出す
    }
}

このようなcheck_dynamic_spec()のアプローチは整数型のみを考慮していますが、組み込み型のフォーマット指定においては十分です。この提案では一歩進んで、ユーザー定義型でネストした置換フィールドを扱う際に整数型以外の型を使用した場合でもコンパイル時に検証可能にしようとしています。

そのためにこの提案では、check_dynamic_spec()を関数テンプレートにして要求する型をテンプレートパラメータに指定しつつ、ネストした置換フィールドに対応する引数のインデックスを渡すことで、そのインデックスの引数の型をチェックします。

こうすることで、型を表す列挙型を標準に追加し公開することを避け、期待する型を直接指定するという使いやすいインターフェースになります。ただし、代償としてこの関数テンプレートを呼び出すにはtemplateキーワードが必要になります。

// ユーザー定義特殊化とする
template<typename T, typename charT>
struct formatter<T, char> {

  auto parse(auto& ctx) {
    // Tに対するparse()実装
    ...

    // check_dynamic_spec()の呼び出し
    ctx.template check_dynamic_spec<char>(id);
  }
};

変更例

namespace std {
  template<class charT>
  class basic_format_parse_context {
    ...

  public:
    // このコンストラクタは削除()
    constexpr explicit basic_format_parse_context(basic_string_view<charT> fmt,
                                                  size_t num_args = 0) noexcept;
    

    // 追加する型チェック関数
    // id番目の引数(フォーマット対象引数)に期待する型をTsに指定する
    template<class... Ts>
      constexpr void check_dynamic_spec(size_t id);

    // ↑を呼び出すユーティリティ関数
    constexpr void check_dynamic_spec_integral(size_t id);
    constexpr void check_dynamic_spec_arithmetic(size_t id);
    constexpr void check_dynamic_spec_string(size_t id);
  };
}

P2758R0 Emitting messages at compile time

コンパイル時に任意の診断メッセージを出力できるようにする提案。

現在のC++にはコンパイル時に診断メッセージを出力できる使いやすい方法がありません。例えばstatic_assertでは、そのメッセージをエラーに合わせてカスタマイズすることができません。

template <typename T>
void foo(T t) {
  static_assert(sizeof(T) == 8, "All types must have size 8");
  // ...
}

int main() {
  foo('c'); // error
}

このような場合にアサーションに関する情報(Tは何か、sizeof(T)はいくつか)を文字列リテラルに含めることはできません。幸い、コンパイラは長いエラーメッセージの中にそれらの情報を出力してくれる場合が多いですが、Tがメタ関数によって変換されている場合などには出力されなくなることもあります。

一般的に、コード中のアサーションにおいてはそのコードを書いているプログラマの方がそのアサーションのメッセージについて何が有用な情報なのかを知っているはずで、その方法さえあればコンパイラが頑張らなくてもより良い診断メッセージを出力することができるはずです。しかし、現在はstatic_assertの2つ目の引数は文字列リテラル限定であり、定数式で生成された任意の文字列を使用できません。

他のところでは、std::formatコンパイル時フォーマット文字列チェック時の診断メッセージがあります。

auto f() -> std::string {
  return std::format("{} {:d}", 5, "not a number");
}

例えばこの例では、:dconst char*のためのフォーマット指定子ではないためコンパイルエラーとなります。しかし、コンパイラのエラーメッセージは多くの場合何が原因でエラーが起きたのかを報告できません。

このエラー報告メカニズムはstatic_assertを使用しているわけではなく、consteval関数実行中に定数式で実行不可能なものが現れるとコンパイルエラーとなることを利用したもので、throw式の実行か未定義関数の呼び出しによってコンパイルエラーを発生させています。それは本来の用法ではないため、フォーマット文字列のパース中にコンパイルエラーを発生させることはできても、的確な診断メッセージを出力することはできません。

この場合のエラーメッセージが、例えば次のようなものになっていたとしたら、この原因を特定するのはかなり簡単になります

format("{} {:d}", int, const char*)
             ^         ^
'd' is an invalid type specifier for arguments of type 'const char*'

このメッセージは完璧ではないにしても、今日の診断メッセージよりははるかに優れています。

この提案は、次のように今日のコンパイル時診断メッセージ出力の現状改善を図るものです。

  • static_assertを拡張して、第二引数に文字列リテラルだけでなく文字列範囲を受け取れるようにする
  • 新しいコンパイル時診断APIの追加(コンパイル時出力を行う)
    • std::constexpr_print_str(msg)
    • std::constexpr_print_str(msg, len)
  • 新しいコンパイル時エラートリガーAPIの追加(コンパイルエラー発生と診断メッセージ出力を行う)
    • std::constexpr_error_str(msg)
    • std::constexpr_error_str(msg, len)
  • std::format()constexpr対応を追求すると、上記APIを拡張できる
    • std::constexpr_print(fmt_str, args...)
    • std::constexpr_error(fmt_str, args...)
    • std::format_parse_error(fmt_str, args...)
      • フォーマット文字列のコンパイル時チェックの為のユーティリティ。コンパイル時に評価されるとconstexpr_error()を呼び出し、実行時に評価されるとformat_error例外を投げる

static_assertの引数は、static_assert(cond, string-literal, expression-list...)のようにする(つまり、std::formatを埋め込む)事も可能ですが、この提案では単にstatic_assert(cond, string-range)のようにすることを提案しています。前者の形式だと、static_assert(cond, "T{} must be a valid expression.")のように3番目以降の引数無しで呼ばれた場合に曖昧になる為で、この場合に文字列リテラルをフォーマット文字列として扱わないようにするとその一貫性が失われます(このアプローチは以前のRustのpanic!()マクロで採用されていたようですが、2021年に見直されたようです)。

また、前者の形式を取るとライブラリ機能であるstd::format()を言語機能であるstatic_assertに実質的に埋め込んでしまう事になり、言語機能が複雑なライブラリ機能に依存することになったり、将来の拡張を妨げたりとあまり良いことがありません。

このような理由により後者の形式を採用し、static_assert+std::format()は明示的に書く必要があります。

static_assert(cond, std::format("The value is {}", 42));

冗長な記述にはなりますが、現在できないコンパイル時診断メッセージの柔軟化を達成できます。

提案されている新しい2種類のAPIstd::constexpr_print_str(), std::constexpr_error_str())はmanifestly constant evaluatedである時のみ効果を持つ、のように指定されます。これは、constexpr処理が投機的に実行される可能性があり、その場合にはメッセージを出力したりエラーを発生したりさせない為の規定です。例えば、constexpr関数の実行中にthrow式に出会った場合、その文脈が必ず定数式で実行されなければならないものでなければ、その処理はそこで中断され実行時まで延期されます。manifestly constant evaluatedとはその様な場合の実行ではないことを指定していて、constexpr処理のコンパイル時実行が確定した場合にのみその効果を発揮することを言っています。

P2759R0 DG Opinion on Safety for ISO C++

WG21 Direction Group (DG)の安全性についての見解を説明する文書。

プログラミング言語の安全性についての関心の高まりを受けて、C++の将来の方向性として安全性を追求していく事はほぼ固まっていますが、それをどのように進めていくのかについて意見をまとめ、C++標準化委員会の構造やプロセスを定義することを目的として書かれた文書です。

この文書では、それらを構築するにあたっての基本的な考え方を提示しています

  • 広く目に付きやすいフレームワークの確立を目指す
  • そのフレームワーク内で、安全性に関する変更を議論する
  • その変更を適用する場所(言語・ツール等)について合意をする
  • 後方互換性の扱いについての方向性に合意する
  • 最も重要なものに優先順位をつける

この文書ではまた、そのような仕組みづくりをどのように進めていくのかについて決まっておらず、そのために考慮すべきことや前提知識等を記述し、そのような仕組みづくりのために必要な作業等について説明しています。

安全性とセキュリティに関して2022年末に新設されたSG23を利用していく事になるのかもしれませんが、この文書はさらなる提案を募ってそれらを決めていくいことを求めています。

P2762R0 Sender/Receiver Interface For Networking

現在のNetworking TSにP2300のsender/receieverサポートを入れ込む提案。

Networking TSやP2300については以前の記事を参照

この提案は、Networking TSの現状の問題を解決するために、P2300で提案されているsender/receieverによる非同期フレームワークをNetworking TSに組み込むことで、Networking TSの非同期ネットワーク操作をP2300ベースで構築しようとするものです。

この提案では、Networking TSの同期操作については手を入れず現状の同期/非同期操作のインターフェースを基本として、P2300の非同期フレームワークにアダプトしようとしています。その際に、設計にいくつかの分岐が生じるところがあり、それぞれの選択肢について説明しています。説明のために、一部の選択肢を選択する形で後の機能を解説している部分もありますが、この提案の目的は選択肢を示してNetworking TSとP2300調和のための議論を促すところにあります。

スケジューラの取得方法について

非同期ネットワーク操作においては、非同期性を扱う適切なコンテキスト(実行コンテキスト)に基づいて操作を実行するものが必要です。P2300ではそれはschedulerであり、非同期ネットワークAPIでスケジューラをどのように取得するかについていくつか選択肢があります。

1つ目は、APIsenderファクトリとして、schedulerを引数としてそこに渡すものです。この方法では、非同期操作を作成する際にそのスケジューラを明示する必要があります。

auto make_accept(auto scheduler, auto& socket) {
  return async::accept(scheduler, socket);
}

ここで、make_accept()はユーザー定義関数(おそらくTCP通信などにおいて待ち受けを行う処理)であり、async::accept()はこの提案による非同期ネットワークAPIの一例です。

この場合の非同期APIは、schedulerと追加引数を受け取ってsenderを返すsenderファクトリとなります。

2つ目は、APIsenderアダプタとして、スケジューラは上流のsenderから取得するものです。

auto make_accept(auto scheduler, auto& socket) {
  return schedule(scheduler)
    | async::accept(socket);
}

ここで上流のsenderとはschedule(scheduler)のことですが、make_accept()はユーザー定義関数なのでAPIasync::accept)に|で合成されるsenderはユーザーが用意した任意のものです。schedulerはそれら上位のsenderに指定されたものを取得して使用します。

この場合の非同期APIは、処理に必要な引数を受け取ってその処理を表すsenderを返すsenderアダプタとなります。

3詰めは、APIsenderファクトリとして、スケジューラは下流senderから取得するものです。この方法では、処理グラフの使用側(下流)から使用するスケジューラを与えることができます。

auto make_accept(auto scheduler, auto& socket) {
  return on(scheduler, async::accept(socket));
}

on()はP2300のCPOの一つで、on(sc, se)のように呼んでsenderであるseの処理をschedulerであるscのコンテキストで実行するsenderを返します。この例ではonによってまずschedulerを与えていますが、それをせずにasync::accept()|で他の処理をチェーンした後からその呼び出し前にschedulerを与えて、そのコンテキストで実行することもできます。

この場合の非同期APIは、処理に必要な引数を受け取ってその処理を表すsenderを返すsenderファクトリとなります。

2と3の違いは、非同期APIが処理グラフの先頭に来るのか途中に来るのかの考え方の違いであり、それによってグラフの前方と後方のどちらから実行コンテキストであるschedulerを取得するのかが異なっています。

1と2の場合は、非同期APIを用いて非同期ネットワーク操作を構築する際に既に使用するスケジューラを知っている(用意している)必要があります。一方、3の場合はその必要はなく、非同期ネットワーク操作を構築した後で実際にそれを実行する場所で使用するスケジューラを指定することができます。

そのため、この提案では3のアプローチを仮採用し、以降のAPIを説明しています。

エラーハンドリング

Networking TSの非同期APIでは、そのコールバック関数のシグネチャstd::error_codeを用いる1つだけであり、他の選択肢はありません。P2300による非同期APIでは、エラーや成功といった情報はreceiverを通して3つのチャネルで通知されるため、エラーハンドリングインターフェースに複数の選択肢が生まれます。

1つ目は、set_valueチャネルを使用して他の追加引数と一緒にエラーを通知するものです。これはNetworking TSの非同期APIと近しい使用感になります。

auto sender
= async::read(socket, buffer)
    | then([](error_code const& ec, int n) { ... });

// コルーチンの場合
auto[ec, n] = co_await async::read(socket, buffer);

2つ目は、エラー有無によって異なるset_valueチャネルを使用することで、成功とエラーの経路を分けるものです。コルーチンでのシグネチャは1つに制限されるため、この場合はコルーチンで使用できません。

auto sender
= async::read(socket, buffer)
    | overload(
      [](int n){ /* success path */ },
      [](error_code const& ec){ /* error path */ }
    );

また、後続の処理はこの2つのパスの分岐に対応して同様にoverload()によって継続する必要がある可能性があります。

3つ目は、前の選択肢におけるエラーチャネルとしてset_valueではなくset_errorを使用するものです。

auto sender
= async::read(socket, buffer)
    | then([](int n) { /* success path */ })
    | upon_error([](error_code const& ec) { /* error path */ });

// コルーチンの場合
try {
  int n = co_await async::read(socket, buffer);
  // success path
} catch (error_code const& ec) {
  // error path
}

コルーチンの場合はエラーチャネルが例外になってしまいますが、成功経路と失敗経路を統合するようなsenderアルゴリズムを使用することで例外を回避することは可能です。

4つ目は、1~3の複合的なもので、エラーの重大度に応じてエラーチャネルを使い分けるものです。これは考えられるだけであまり有効性はないとみなされたのか、サンプルコードは記載されていません。

5つ目は、std::error_codeへの参照を非同期APIに渡して、エラー報告はそこで行うものです。この場合、それが渡されていないときは他の方法をとることができます。

error_code ec;

auto sender
= async::read(socket, buffer, ec)
    | then([](int n) { ... });

// コルーチンの場合
int n = co_await async::read(socket, buffer, ec);

if (!ec) {
  /* success path */;
} else {
  /* error path */
}

この場合、Networking TSの同期操作と使用感が近くなります。

メンバ関数と非メンバ関数

Networking TSでは、ある操作についてメンバ関数と非メンバ関数の両方を提供しています。socketというエンティティに対してそのメンバとして各種操作を提供することは他の言語でも一般的なもので、おそらくほかのオプションは存在しません。また、IDEによる入力補完も非メンバ関数よりもメンバ関数の方が効きやすい傾向にあります。

メンバ関数の操作を提供する場合、それはCPOとして定義されることになるかもしれませんが、CPOはメンバ関数にはなり得ません。また、あるクラスに対してメンバ関数として提供しておくと、時間の経過とともに他の多くの操作や役割がそのクラスに集約されていく可能性があります。非同期ネットワーク操作には潜在的に多くのバリエーション(オプション)があるため、非メンバ関数を用いる方が管理しやすいでしょう。

たとえば、senderを返す操作はasync、コルーチンで使用する場合はcoroのように、使用目的によって適切な名前空間に配置することができるかもしれません。

// senderの場合
auto sender = async::read(socket, buffer)
                | then([](auto&&...){ /* use result */ };

// コルーチンの場合
auto[ec, n] = co_await coro::read(socket, buffer);

P2300のCPOやsenderアルゴリズムでは、主体となるエンティティが無いためそれらは常に非メンバ関数として定義されます。Networking TSの場合はsocketというエンティティがあるため、そのメンバとして各種操作を定義する方が好まれるかもしれません。また、オプションの全種類を提供する非メンバ関数を提供したときでも、一部のユースケースのためにメンバ関数を提供することを妨げるわけではありません。

I/Oスケジューラのインターフェース

ネットワーク操作(もしくはより一般的なI/O操作)は、特殊なコンテキストでスケジューリングされ、対応するschedulerで実行されることが必要です。ネットワーク操作がスケジューラとやり取りするためのインターフェースにはいくつかのオプションがあります。

  1. ネットワーク操作とスケジューラは秘密のチャネルを使用する
    • もっとも簡単な仕様だが、ネットワーク操作がユーザー定義のスケジューラを使用できない場合があるか、基礎となるスケジューラを抽出する方法についてのプロトコルが必要になる
  2. 各種I/O操作を何らかの方法で公開/抽象化する
    • 仮想関数やCPOなどを使用する
    • より汎用的となるが、I/O操作のインターフェースはsenderが使用するものよりも低レベルになり、プラットフォーム固有となる可能性がある
    • 例えば、非同期read_some()がio_uring(2)を使用する場合、read_some()iovecへのポインタ(2つのリングバッファのポインタ)を提供しなければならず、なおかつそのポインタはそのI/O操作が完了バッファから消費されるまで存続する必要がある。これは、read_some()に渡されるバッファとはかなり異なるインターフェースとなる。
      • 非同期I/Oなので、read_some()の呼び出しはI/Oの完了を待たずに呼び出し側に戻るため、I/Oの完了までそこで使用するリソースをどこでどうやって保持しておくのかが問題となる
  3. スケジューラインターフェースは複数の契約(サポートするI/O操作に対して1つづつ)をモデル化し、ネットワーク操作はI/O操作の状態オブジェクトに埋め込まれるオブジェクトを生成する
    • 各I/Oインターフェースは、その状態オブジェクトを必要な形で受け入れ保存する
  4. P1031のLow level file I/Oライブラリで提案されているものをI/Oスケジューラとする
  5. Networking TS(Asio)のものを使用するか、それと統合する
    • io_contextは1の秘密チャネルを使用している様子

タイマークラスか、sender

Networking TSには、basic_waitable_timerというタイマークラスがあり、基礎となるクロック型や待機方法などのいくつかのタイマープロパティをエンコードしています。Networking TSにおけるこのタイマークラスは、エンティティ(socket等)がキャンセルをトリガーするために使用されます。操作がキャンセル可能であっても、キャンセルは明示的に指定する必要があります。

waitable_timer timer(/* timer settings */);
auto sender = wait_for(timer, 5s);  // 待機は5秒でキャンセル

// コルーチンの場合
co_await wait_for(timer, 5s);

sender/receiverでキャンセルを行う場合は、キャンセルを行えるsenderに接続されたreceiverstd::stop_tokenの使用によって行います。そのため、タイマークラスは必要なくなります。

タイマーを定義するには、適切なsenderを作成しそれを適切なschedulerでスケジューリングするのが合理的となります。

// 5秒待機するsenderを生成
auto sender = wait_for(5s);

// コルーチンの場合
co_await wait_for(5s);

ただし、タイマーのプロパティを指定したい場合などは、それらの調整を1つのオブジェクトにカプセル化して、このオブジェクトを使いまわす方が望ましい場合も考えられます。そのため、両方の選択肢をサポートすることが合理的かもしれません。

高レベルのツール

基本的なネットワーク操作はかなり単純であり、サポートすべき操作の数はそれほど多くはありません。そのため、ネットワーク操作に注力するNetworking TSでは基礎となるI/O操作(例えば、io_uring)が提供する全てのものを提供する必要はありません。むしろ、そのような基礎のI/O操作を超えて、より高いレベルのアルゴリズムを合理的に構成可能とすることに注力すべきです。

例えば、async::read_some()操作は部分的に書き込まれている可能性のあるバッファをうまく読むことができますが、ここから、常に完全なバッファを読むかさもなければ失敗するようなasync::read()操作を構成することができます。

問題は、そのようなアルゴリズムはどれが提案に含まれるべきか、にあるかもしれません。

async::read()async_write()は分かりやすい例ですが、async::resolve()のように非自明なアルゴリズムの例があります。そこでは、getaddrinfo(3)が使用されますが、これは同期操作であり、対応する非同期バージョンは提供されていません。非同期フレームワークを名乗る以上は、このような場合でも非同期バージョンを提供しなければならないでしょう。

senderアルゴリズム以外の部分でも、他の興味深いコンポーネントが考えられます

  • コルーチン内で使用される非同期ネットワーク操作にスケジューラを注入するコルーチンタスク(io_task
  • async_scope(cppcoroのもの)と似た、何らかのI/Oスケジューラに関連する適切なスコープを設定するio_scope
  • ソケットの全二重操作
    • 同時読み書き操作のスケジューリングには、バッファの生成と消費に対応するsenderインターフェースを持つリングバッファのようなものが有用と思われる

キャンセルについての懸念

ネットワーク操作は、操作が開始されてから完了するまでの間は非アクティブになります。そのため、このような操作をキャンセルするためには何らかのキャンセル操作を能動的にトリガーする必要があります。そのため、std::stop_token/std::stop_sourceの提供するようなatomic boolによる単純なテストは一般的にうまく機能しません(スレッドの処理をキャンセルするよりも複雑な処理が必要となりうる)。

そのため、キャンセルにstd::stop_token/std::stop_sourceを使用する(これはほぼ確定事項)場合、キャンセルシグナルを受信するstd::stop_tokenにコールバックを指定して、そこで具体的なキャンセル処理を行う必要があります。stop_tokenno_stop_tokenでない限り(つまり操作がキャンセルをサポートしない場合以外は)コールバックの登録と解除の両方で何らかの同期を行う必要があり、socketでのデータ処理中にその操作を繰り返すとパフォーマンスが低下する可能性があります。

ここの操作に対してキャンセルを設定するのではなく、socketのようなエンティティの単位でキャンセルをサポートする事もできるかもしれません。その場合は、ライブラリによるある程度のサポートが必要となります。例えば、ある操作をキャンセルするために、その操作をキャンセルする関数を用意するなどです。

その場合でも、パフォーマンス低下を避けるために、stop_tokenによるコールバック登録を禁止する必要があるかもしれません。例えば、when_all()アルゴリズムは、複数のsenderを受けていくつものstop_tokenno_stop_tokenではない)を使用するreceiverを使用することになりますが、その場合でも、受信した完了シグナルをすべて通過させつつ、no_stop_tokenを後続senderに公開するsenderアダプタを用意すると便利かもしれません。

システムによっては、一般的なユースケースに対応したキャンセル操作(タイムアウトなど)を備えている場合があります。この場合にそれらを簡単に利用するために、ネットワーク操作に対応するsenderと時間指定からタイムアウトsenderを取得することが合理的かもしれません。タイムアウトsenderは、sender(ネットワーク操作)の完了か指定時間の経過(タイマーのタイムアウト)が起こった時に、もう片方の操作をキャンセルします。

タイムアウトsenderは、利用可能であれば基礎となるシステムのタイムアウト機構を利用して機能を提供し、そうでなければタイマー+when_anyアルゴリズムの合成操作のように振舞い、どれか一つのsenderが完了した場合にstop_tokenによって残りのsenderにキャンセルをかけるような実装になるでしょう。

ただ、このネットワーク操作におけるキャンセル機構の扱いについてはP2300とNetworking TSの両方に経験がなく、実験と設計検討が必要となりそうです。P2300の主張するような、キャンセルを非同期操作にカプセル化する方法は興味深いものではありますが、潜在的なコストとそれを回避する方法が明確にはなっていません。

この提案は、仮の文言を含んでいるものの、その設計は確定したものではなく、さらなる議論によってこれらの選択肢の中から設計を選択していくことを意図しています。

P2763R0 layout_stride static extents default constructor fix

std::layout_strideのデフォルトコンストラクタの生成するレイアウトマッピングを修正する提案。

std::layout_stridestd::mdspanのレイアウトマッピングをカスタマイズするクラスで、次元毎にstrideを指定したレイアウトを扱う為のものです。このクラスにはデフォルトコンストラクタがありますが、layout_strideの要素数extents)の指定が完全に静的である場合に無効なマッピングを生成します。

std::layout_stride::mapping<std::extents<int, 4>> map;
// map.is_unique() == true;
// map(0) == 0; 
// map(1) == 0;
// map(2) == 0;
// map(3) == 0; 

レイアウトマッピングクラスはポリシークラスであり、その入れ子mapping<E>クラスによってレイアウトマッピングをカスタムします。テンプレートパラメータEには次元と要素数の情報をもつ型(std::extentsstd::dextents)を指定し、std::extents<I, N...>Iは整数型、N...は各次元に対応する要素数)は静的に次元と要素数を指定し、std::dextents<I, N...>は動的な指定も行える(std::dynamic_extentを含むことができる)ものです。

この例の場合のマッピングは1次元4要素の配列を表しており、次元と要素数は完全に静的に指定されていて、layout_stride::mappingをデフォルト構築しているためstrideは指定されていません(layout_strideは、コンストラクタ引数でstrideの情報を渡します)。layout_stride::mappingのデフォルトコンストラクタはdefault実装されており、それによって、layout_stride::mappingの持つstride情報(次元数Dとすると、std::array<std::size_t, D>のような配列)は全て0として初期化されます。

layout_strideによるマッピングlayout_stride::mapping::operator(i...))では、i...の先頭からのインデックスを次元数としてその次元に対応するstride値(コンストラクタで指定されたもの)をかけたもの、を足し上げた値をマッピングしたインデックスとして返します。そのため、layout_stride::mappingをデフォルト構築すると、それによるマッピングはあらゆるインデックスを0に写すものになり、これは明らかに間違ったマッピングになっています。

一方で、他のレイアウトマッピングクラス、std::layout_right(行優先レイアウト、C/C++の通常の多次元配列のレイアウト)、std::layout_left(列優先レイアウト、fortranmatlabの配列のレイアウト)ではこの問題は起こりません。

std::layout_left::mapping<std::extents<int, 4>> map;
// map.is_unique() == true;
// map(0) == 0; 
// map(1) == 1;
// map(2) == 2;
// map(3) == 3; 

この提案は、std::layout_strideのこの挙動を修正しようとするもので、次の2つの方法を提示しています。

  1. extentsが完全に静的である場合のstd::layout_strideのデフォルトコンストラクタを削除
  2. extentsが完全に静的である場合のstd::layout_strideのデフォルト構築は、std::layout_rightと同じマッピングを生成する
    • strideを指定しない場合のフォールバック先としては、C++のデフォルトのレイアウトである行優先、すなわちstd::layout_rightが望ましい
    • 可能な場合は、layout_stride::mappingトリビアルデフォルト構築を維持する
      • 次元数が0であるか、動的要素数が指定されている場合

他のレイアウトマッピングクラスとの一貫性やデフォルト構築をサポートした方が使いやすいと考えられることなどから、この提案では2番目の案を提案しています。また、この提案はC++23の欠陥報告(DR)とすることを推奨しています。

この提案は2023年2月のIssaquah会議でC++23に向けて採択されています(DRとなることは回避されています)。

P2764R0 SG14: Low Latency/Games/Embedded/Finance/Simulation virtual meeting minutes 2023/01/11

2022年11月のKona会議中に行われたSG14のハイブリッドミーティングの議事録。

どんなことを議論したかの概要だけが記されています。

P2765R0 SG19: Machine Learning Virtual Meeting Minutes 2022/12/08-2023/01/12

2022年12月8日から2023年1月12日の間に行われたSG19のミーティングの議事録。

どんなことを議論したかの概要だけが記されています。

P2766R0 SG16: Unicode meeting summaries 2022-10-12 through 2022-12-14

2022年10月12日から2022年12月24日の間に行われたSG16のミーティングの議事録。

NBコメントや提案のレビューや議論において、誰がどんなことを発言したか詳細に記録されています。

P2769R0 get_element customization point object

tuple-likeなオブジェクトから特定のインデックスの要素を抜き出すCPOの提案。

例えば、std::pairを要素とする範囲に対して何かRangeアルゴリズムを適用したいとき、std::pairそのものよりもむしろpairの2つの要素のどちらかに着目する場合が多いでしょう。Rangeアルゴリズムではそのためにプロジェクションが使用でき、入力範囲の要素からその1部を抽出したうえでその結果に対してアルゴリズムを適用することができます。

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

std::ranges::sort(v, std::less{}, [](auto& x) {
  return std::get<0>(x); // キーによってソートする
});

std::pairならばプロジェクションのためにメンバ変数ポインタを使用できますが、std::tupleはできないためこのようにラムダ式によって記述しなければならず、少し冗長です。さらには、入力範囲の要素が右辺値の場合などに正しく処理することを意識するとさらに冗長になってしまいます。

// 値カテゴリによらないtuple-likeオブジェクトに対するプロジェクション
[](auto&& x) -> auto&& {
  return std::get<0>(std::forward<decltype(x)>(x)); // key-based sorting
}

このような場合のプロジェクションは次のように書けると理想的です

std::ranges::sort(v, std::less{}, std::get<0>);

しかし、std::get<0>だけではまだテンプレートのインスタンス化が完了しないためこれはできません。

他の手段として、views::elementsが思いつくかもしれません。これは、tuple-likeオブジェクトを要素とする入力範囲を、指定インデックスの要素を抽出した範囲に変換するものです。ただ、これはプロジェクションを使用する場合とは異なりtuple-likeオブジェクトからなる範囲の一部の要素だけに着目するものであり、変換後の範囲に対する操作は、元の範囲に対して影響を与えるものではありません。

// views::keys(views::elements<0>)によって、1つ目の要素だけからなる範囲に変換してソート
std::ranges::sort(v | std::ranges::views::keys, std::less{});

for (auto& x : v) {
  auto [key, val] = x;
  std::cout << "Key = " << key << ", Value = " << val << std::endl;
}
                
// 出力例(キーのみがソートされてしまう)
// Key = 1, Value = 1
// Key = 2, Value = 4
// Key = 3, Value = 7

views::elements<I>は入力範囲の各要素からstd::get<I>によって要素の参照を抽出して、それを要素とした範囲を生成します。そのため、elements_viewの各要素に対する操作はその要素(元の範囲の各要素の一部)だけにしか影響を及ぼしません。そのため、この例のようにelements_view上でソートしようとすると、元の範囲の要素は動かずキーだけがswapされてしまいます。

このように、現在のところtuple-likeオブジェクトを要素とする範囲に対してRangeアルゴリズムを適用し正しく射影を行うには、ラムダ式による冗長なコードを適切に記述する以外に方法はありません。C++23で追加されたzip_viewではその要素は常にtuple-likeオブジェクトとなるため、この問題はより深刻になる可能性があります。

この提案は、上記のようなtuple-likeオブジェクトの射影を行うstd::ranges::get_element<I>というCPOを追加することで、この問題を解決しようとするものです。

// get_element CPOの宣言例
namespace ranges {
  inline namespace /* unspecified */ {
    template <size_t I>
    inline constexpr /* unspecified */ get_element = /* unspecified */;
  }

  inline constexpr auto get_key = get_element<0>;
  inline constexpr auto get_value = get_element<1>;
}

これを使用すると、冒頭のコードは次のように書くことができます

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

// pairの1つめのオブジェクトによるソート
std::ranges::sort(v, std::less{}, std::ranges::get_element<0>);

// あるいは
std::ranges::sort(v, std::less{}, std::ranges::get_key);

get_element<I>elements_viewとは異なりRangeアダプタではなくtuple-likeオブジェクトを引数に取るCPOであり、入力のオブジェクトからget<I>によってI番目の要素を抽出するものです。これはこのようにRangeアルゴリズムのプロジェクションにおいて使用することを意図しています。

std::ranges::getという名前を使用しないのは、std::ranges::subrangeのために提供されているstd::getオーバーロードがすでにその名前を使用しているためで、これを押しのけて導入しようとするとABI破壊を招くことを回避するためです。ただ、可能ならばstd::ranges::getを使用したいため、そのようなABi破壊が受け入れられるかを探ることも提案されています。

この提案に対するSG9のレビューでは、std::ranges::getという名前にするコンセンサスは得られていませんが、ranges名前空間の外に出してstd::get_elementのようにすることにコンセンサスがあるようです。

おわり

この記事のMarkdownソース

[C++]暗黙ムーブの副作用による安全性

C++23から、左辺値参照を返す関数においてローカル変数を直接返すケースがコンパイルエラーとなるようになります。

int& f() {
  int n = 10;

  return n; // ng
}

int main() {
  int& r = f();
}

これは意図された振る舞いであるとはいえ個別の提案によって導入されたものではなく、一見関係なさそうな別の提案の副作用として導入されました。それはP2266R3 Simpler implicit moveという提案で、これはreturn文における暗黙ムーブ仕様を簡素化するものです。

暗黙ムーブ

暗黙ムーブとはC++11で許可された戻り値最適化(Return value optimization)の一種で、ローカル変数がreturn文でコピーされて返される場合に暗黙的にムーブを行うことでコピーを回避する最適化のことです。

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

Widget one(Widget w) {
  return w;  // ローカル変数の暗黙ムーブ、C++11から
}

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

RRefTaker two(Widget w) {
  return w;  // ローカル変数の暗黙ムーブ、C++11(CWG1579)
}

C++11では関数のローカル変数のみが暗黙ムーブの対象でしたが、C++20(P1825R0 Merged wording for P0527R1 and P1155R3)では関数ローカルの右辺値参照も暗黙ムーブ対象になったほか、return文だけではなくthrow式でも起こるようになり、型変換演算子等の変換を考慮するようになりました。

RRefTaker three(Widget&& w) {
  return w;  // ローカル右辺値参照の暗黙ムーブ、C++20(P0527)
}

[[noreturn]]
void four(Widget w) {
  throw w;  // throw式での暗黙ムーブ、C++20(P1155)
}

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

struct To {
  operator Widget() const &;
  operator Widget() &&;
};

From five() {
  Widget w;
  return w;  // 暗黙ムーブ(コンストラクタによる変換)、C++11
}

Widget six() {
  To t;
  return t;  // 暗黙ムーブ(変換演算子による変換)、C++20(P1155)
}

struct Fowl {
  Fowl(Widget); // 値で受け取るコンストラクタ
};

Fowl seven() {
  Widget w;
  return w;  // 暗黙ムーブ、C++20(P1155)
}

// DerivedはBaseを公開継承しているとき
Base eight() {
  Derived result;
  return result;  // 暗黙ムーブ(基底クラスへの変換)、C++20(P1155)
}

C++20時点の暗黙ムーブ仕様の概要

まず、暗黙ムーブ可能なもの(implicitly movable entity)とは次のどちらかです

  • 自動記憶域期間の非volatileオブジェクト
  • 自動記憶域期間の非volatile型の右辺値参照

そして、次のどちらかのコンテキストでコピーによる初期化が行われる場合、コピーの代わりにムーブを使用して初期化することが許可されています(必須ではありません)

  • return/co_return
    • オペランドはid式(変数名を指定する式)であり(()で囲まれていても良い)
    • id式は、その文を囲む最も内側の関数(ラムダ式)の本体内もしくは関数引数宣言内の、暗黙ムーブ可能なものを指定している
  • throw
    • オペランドはid式であり(()で囲まれていても良い)
    • そのid式の指定するもののスコープは、囲む最も内側のtryブロックのスコープよりも長くなく
    • id式は暗黙ムーブ可能なものを指定している

これらの細かい条件は、暗黙ムーブが起きた後でアクセスされる可能性のある変数を除くための条件です。

これらのコンテキストにおいて、throwするオブジェクトを生成するためのコピーコンストラクタもしくは戻り値を生成するためのコンストラクタ、を選択するためのオーバーロード解決は次の順序で実行されます

  1. オペランドのid式をrvalueとみなしてオーバーロード解決を実行する
  2. 1が失敗した(もしくは行われなかった)場合、オペランドのid式をlvalueとしてオーバーロード解決を実行する

暗黙ムーブはこの最後の手順における1において起こっており、その対象はimplicitly movable entityとして指定されます。対象外のコンテキストや暗黙ムーブが行われない場合は2の手順だけが実行されます。

C++23 P2266の概要

C++20の仕様では、暗黙ムーブが起こるのは関数の戻り値型がオブジェクト型である場合のみであり、参照型の場合は暗黙ムーブ可能なものをreturnしていても暗黙ムーブは起こりません。

int&& four(int&& w) {
  return w;  // Error
}

なぜなら、暗黙ムーブが起こるコンテキストとはコピーによる初期化が行われる場合なので、参照戻り値型の関数のreturn文はそもそも対象外のコンテキストとなるためです。

また、C++20の暗黙ムーブの仕様は2段階のオーバーロード解決を含む複雑な処理になっており、実装が困難なことから実装による挙動の差異を生んでいました。

P2266R3ではこれらの問題の解決のために、return文におけるムーブする資格のあるid式(move-eligible id-expression)はxvalueである、と規定することによって暗黙ムーブ仕様を簡素化します。

P2266R3では、暗黙ムーブ可能なもの(implicitly movable entity)は次のコンテキスト

  • return/co_return
    • オペランドはid式(変数名を指定する式)であり
    • id式は、その文を囲む最も内側の関数(ラムダ式)の本体内もしくは関数引数宣言内の、暗黙ムーブ可能なものを指定している
  • throw
    • (略)

(ここは変更なし)

でid式によって指名される場合、そのid式はムーブする資格がある(move-eligible)とします。そして、ムーブする資格のあるid式の値カテゴリはxvalueであると規定されます。

return文でコピーによる初期化が行われるかどうかに関係なく、ムーブする資格のある変数名を指定したreturn文はそれをxvalueとして扱う(すなわちstd::move()したかのように扱う)ことで暗黙ムーブが行われます。また、return文に指定された式の値カテゴリを指定した後の工程は通常のreturn文の仕様に従うため、2段階のオーバーロード解決をする必要もなくなっています。

先程の例をもう一度見てみると

int&& four(int&& w) {
  return w;  // 暗黙ムーブ、C++23
  //return std::move(w); のような扱いになっている
}

wは暗黙ムーブ可能なもの(右辺値参照int&&)であり、return文ではid式wでそれを指定しています。wはこの関数の引数で宣言されているため(この関数スコープよりも寿命が長くはないため)このid式wはムーブする資格のあるid式であり、値カテゴリはxvalue(すなわち、int&&)となり、戻り値型と合うため特に変換されずにreturnされます。

また、このような仕様の単純化によって、暗黙ムーブはされる可能性があるから必須になっています(必須になったのはこの提案より前かもしれません)。

ダングリング参照生成の抑止

P2266R3の変更によって、return文における暗黙ムーブは(非volatile)ローカル変数をxvalueとして扱うだけのものになり、それは常に行われます。これは関数の戻り値型に関わらずいつも行われます。

int& f() {
  int n = 10;

  return n; // ng、暗黙ムーブが起こることで、型が一致しなくなる
}

すると、左辺値参照を返す関数内のreturn文でローカル変数を直接指定すると、それは常にxvalueとして(ムーブされたかのように)扱われることとなり、T&&T&で返そうとすることになる結果コンパイルエラーを起こすようになります。

これは同じメカニズムでstd:reference_wrapperでも有効です

std::reference_wrapper<int> f() {
  int w;
  
  return w;  // ng、C++23から
}

これはreturn文でint&& -> std::reference_wrapper<int>の変換が起こりますが、このような変換はstd::reference_wrapperのコンストラクタで禁止されているためです(禁止の方法はかなり複雑ですが・・・)。

ただし、間接化が1段階増えると、つまりローカル変数を参照している参照を返そうとすると、防ぐことができなくなります。

int& f() {
  int n = 10;
  int& r = n;

  return r; // UB
}

std::reference_wrapper<int> g() {
  int w;
  int& r = w;
  
  return r;  // UB
}

なぜかというと、どちらの場合もreturn文で指定されているrはローカルの左辺値参照であり、暗黙ムーブ可能なもの(implicitly movable entity)ではないためムーブする資格(move-eligible)はなく、return文での変換はその値カテゴリのまま行われ、int&を返そうとするためどちらの場合も問題なくコンパイルが通ってしまいます。

あくまで、左辺値参照を返す関数から直接ローカル変数を返そうとする場合にのみ保護が働きます。

提案より、その他サンプルコード

struct Weird {
  Weird();
  Weird(Weird&);
};

Weird g(bool b) {
  static Weird w1;
  Weird w2;

  if (b) {
    return w1;  // OK: Weird(Weird&)
  } else {
    return w2;  // error: w2はこのコンテキストでxvalue
  }
}
// 戻り値型推論の差異
auto f(int x) -> decltype((x)) { return (x); }   // 戻り値型は"int&"
auto g(int x) -> decltype(auto) { return (x); }  // 戻り値型は"int&&"
int& h(bool b, int i) {
  static int s;
  if (b) {
    return s;  // OK
  } else {
    return i;  // error: iはxvalue
  }
}

decltype(auto) h2(Thing t) {
  return t;  // OK: tはxvalue、戻り値型はThing
}

decltype(auto) h3(Thing t) {
  return (t);  // OK: (t)はxvalue、戻り値型はThing&&
}
// Annex CセクションのC++20との非互換性レポート
decltype(auto) f(int&& x) { return (x); }  // int&&を返す。以前は int&を返していた
int& g(int&& x) { return x; }  // ill-formed; 以前は well-formed

この変更の意味するところ

P2266R3の変更が実装された(執筆時点でもclang 13/gcc 13で実装済)場合、C++規格は、コンパイラが関数内でローカル変数とそうでないものを区別できること(あるいはその能力)を仮定するようになります。これはよく考えると当たり前のことかもしれません(自動変数というカテゴリが存在しているため)が、今までこの能力が明示的に仮定されて利用されてはいなかったと思います。

この能力を利用すると、さらなるダングリング参照の抑止方法を考えることができ、既にそのような提案が提出されています。

P2266R3やこれらの提案が導入されてもC++が完全に安全な言語になるわけではありませんが、その安全性はわずかでも確実に向上します。

参考文献

この記事のMarkdownソース

[C++]P0588R1を紐解く

C++20にひっそりと採択されているP0588R1 Simplifying implicit lambda captureという提案は、3度見したくらいでは何をしているのか、何がしたいのかさっぱりわかりません。一体これはなんなのでしょうか・・・

P0588R1のやっていること

眺めているとP0588R1ではラムダ式に関連したいくつかのこと・振る舞いを明確化しようとしていることが朧げながら見えてきます。それはおそらく次の4つです

  1. ラムダ式のキャプチャの振る舞いの明確化
  2. 構造化束縛をキャプチャできないことを明確化
  3. ラムダ式がクラスメンバ初期化子で使用された時の挙動の明確化 (CWG1632)
  4. ラムダ式の構文内でキャプチャした対象に対するdecltype((x))の振る舞いの明確化 (CWG1913)

これらの変更は全て、規格上でその振る舞いが不透明だったものをしっかりと記述し直そうとするもののようで、既存の振る舞いを変更することを意図するものではないようです。

そのため規格文書の言い回しを工夫する変更になっており、それが上記4要素分いっぺんに入っているので意味不明度を高めています。

言葉の定義

ここでは、P0588R1で導入されている用語や、それに使用されている用語などの定義をしておきます。なお、ここでの定義はP0588R1の変更を反映したものなので、それ以前の定義とは異なるものです。

エンティティ

エンティティ(entity)とは次のいずれかに該当するものです

  • オブジェクト
  • 参照
  • 構造化束縛
  • 関数
  • 列挙体
  • クラスメンバ
  • ビットフィールド
  • テンプレート
  • テンプレート特殊化
  • 名前空間
  • パラメータパック

C++のコード上で構文以上の何かしらの意味を持つもののことを総じてエンティティと呼びます。

ローカルエンティティ

ローカルエンティティ(local entity)とは、エンティティの中でも次のいずれかに該当するものです

  • 自動記憶域期間(automatic storage duration)にある変数
    • いわゆる自動変数(ローカル変数)
  • 分解対象(構造化束縛宣言の右辺に来るオブジェクト)がローカルエンティティである構造化束縛
  • *thisオブジェクト

ほぼほぼ、一般的にローカル変数と呼ばれるもののことを言っていると思って差し支えありません。

宣言領域

宣言領域(declarative region)とは、あるエンティティを指す名前が有効なコード上の最大の範囲(領域)のことです。

void f() {
  int n = 0;/*
nの宣言領域
*/{/*
nの宣言領域
*/int n = 1;/*
このスコープのnの宣言領域(外側nの宣言領域ではない)
*/}/*
nの宣言領域
*/
}

C++コード上の全ての名前(変数名やクラス名などなど)はそのコード上に宣言領域を持ちます。

スコープ

ある名前のスコープ(scope)とは、その名前が持つ宣言領域のことです。

例えば、変数名の宣言領域とは変数のスコープのことです。

ブロックスコープ

ブロックスコープ(block scope)とは、ブロック内で宣言された名前が持つスコープのことです。

その名前はブロック内でローカルであり、そのスコープは宣言された場所からブロックの終端までです。

ブロックとは構文定義でcompound-statementとして指定されるもので、関数定義の{...}ラムダ式の本体{...}はブロックですが、クラス定義の{...}名前空間定義の{...}はブロックではありません。

namespace N {
  // ブロックスコープではない
}

struct S {
  // ブロックスコープではない
};

export {
  // ブロックスコープではない
}

int main () {
  // ブロックスコープ

  {
    // ブロックスコープ
  }

  try {
    // ブロックスコープ
  } catch(...) {
    // ブロックスコープ
  }

  [] {
    // ブロックスコープ
  };
}

ブロックスコープで宣言された変数はローカル変数です。

関数パラメータスコープ

関数パラメータスコープ(function parameter scope)とは、関数の仮引数名の持つスコープのことです。

関数パラメータスコープは関数の仮引数が宣言された地点から始まるため、関数のローカル引数よりも少しだけ広いスコープを占めます。

void f(
  // 関数パラメータスコープ
  int a,
  int b
) {
  // 関数パラメータスコープ
  {
    // 関数パラメータスコープ
    int a = 1;
    // 仮引数aの関数パラメータスコープではない
    // 仮引数bの関数パラメータスコープではある
    // ローカル変数aの関数パラメータスコープでもある
  }
  // 関数パラメータスコープ
}

クラススコープ

クラススコープ(class scope)とは、クラスで宣言された名前の持つスコープです。

クラススコープはその定義内だけでなく、クラス定義外のメンバ関数定義スコープなども含まれます。

struct S {
  // Sのクラススコープ
  int n;

  void f() {
    // Sのクラススコープ
  }

  void g();
};

void S::g() {
  // Sのクラススコープ
}

odr-usable

odr-usableとはまず、ローカルエンティティに対する概念です。

あるローカルエンティティがその宣言領域内で参照される場合

  • そのエンティティは*thisではない、もしくは
  • その場所はクラススコープかラムダ式のものではない関数パラメータスコープに囲われている
    • そのスコープの最も内側のスコープが関数パラメータスコープであるならば、そのスコープは非静的メンバ関数のもの

のどちらかに該当しており(これを前段の条件と呼びます)

そのローカルエンティティが導入される地点とそのローカルエンティティが参照される領域との間に介在している全ての宣言領域について

  • 介在する宣言領域はブロックスコープである、もしくは
  • 介在する宣言領域はラムダ式の関数パラメータスコープであり
    • そのローカルエンティティを明示的にキャプチャしているか、デフォルトキャプチャを持っていて
    • そのラムダ式ブロックスコープ(本体)もまた、介在する宣言領域である

のどちらかに該当する場合に、そのローカルエンティティはodr-usableとなります(この条件を後段の条件と呼びます)。

介在する宣言領域というのは、ローカルエンティティの導入(宣言/定義)地点から、そのローカルエンティティを参照する地点の間に存在している宣言領域(スコープ)です。介在する(intervening)というのは、参照地点から導入地点の間でそのスコープが折り重なっている様を言っているのだと思われます。

そして、ローカルエンティティがodr-usableではない宣言領域でodr-usedとなる場合、プログラムはill-formedです。(odr-usedはとても難しい概念なので深入りはしませんが、ここでは定義が必要になる使われ方、のような意味だと思ってください)

規格書([basic.def.odr]/9)より、サンプルコード

void f(int n) {
  [] { n = 1; };                // #1 error: n is not odr-usable due to intervening lambda-expression

  struct A {
    void f() { n = 2; }         // #2 error: n is not odr-usable due to intervening function definition scope
  };

  void g(int = n);              // #3 error: n is not odr-usable due to intervening function parameter scope

  [=](int k = n) {};            // #4 error: n is not odr-usable due to being
                                // outside the block scope of the lambda-expression

  [&] { [n]{ return n; }; };    // #5 OK
}

この例の場合、ローカルエンティティnは関数fの関数パラメータスコープを宣言領域として導入されていて、*thisではないので、odr-usableの前段の条件はクリアしており、問題となるのは後段の条件のみです。

  1. ローカルエンティティnラムダ式の関数パラメータスコープに囲われて(介在して)いますが、そのラムダ式はキャプチャに何も指定していない(明示的にも暗黙的にもnをキャプチャしていない)ため、この場所でnはodr-usableではありません
  2. ローカルエンティティnA::f()の関数定義スコープ(ブロックスコープ)と関数パラメータスコープとAのクラススコープに囲われています。後2つはブロックスコープではないため(当然ラムダ式の関数パラメータスコープでもないため)、odr-usableではありません
  3. ローカルエンティティng()の関数パラメータスコープに囲われていますが、これも後段2条件のどちらに合致するスコープでもないため、odr-usableではありません
  4. ローカルエンティティnラムダ式の関 数パラメータスコープに囲われていて、そのラムダ式はデフォルトキャプチャを持っています。しかし、そのラムダ式の本体のスコープが介在していない(nが参照される地点は本体の外側の)ため、odr-usableではありません
  5. ローカルエンティティnは2つのラムダ式の関数パラメータスコープに囲われていて、いずれのラムダ式nをキャプチャしており(デフォルトキャプチャ->明示的キャプチャ)、nが参照される地点は2つのラムダ式の本体のブロックスコープの内部です。従って、これはodr-usableです。

このサンプルコードをよく見ると、いずれのケースでもこの関数f()の外側にローカルエンティティnを参照しているもの(関数やラムダ式、ローカルクラス)を持ち出すことができます。戻り値をautoにするとか関数ポインタにするとか、std::functionを使用するとか・・・

もしこのng例がokだったとすると、それら外に持ち出したものを介してこれらの関数が呼び出し可能となるため、f()のローカルエンティティnf()の外側から読み書きされることになります。それはあたかもラムダ式における参照キャプチャが暗黙的に行われているようなもので、当然それは正しい振る舞いでも標準が意図する振る舞いでもないため禁止されなければなりません。ng例はいずれもそれが起こる場合を指していることがわかると思います。

逆に、okの最後の例は外に持ち出した時にそのようなことは起こらないことがわかります。参照キャプチャだけを使用した場合は同様の問題がありますが、少なくともそれはコードに表れているため暗黙的には起こりません(そして、その問題を解決しようとすることはまた別の問題でもあります)。

ローカルラムダ式

ローカルラムダ式local lambda expression)とは、ラムダ式であって次のいずれかに該当するものです

  • 宣言された場所を囲む最も内側のスコープがブロックスコープである
  • クラスのデフォルトメンバ初期化子で現れており、囲む最も内側のスコープがそのメンバに対応するクラススコープである
struct S {
  int m = []{ return 0; }(); // ローカルラムダ式
};

int N = []{ return 0; }();  // ローカルラムダ式ではない

int main() {
  []{}; // ローカルラムダ式
}

ローカルラムダ式のみが、デフォルトキャプチャ(= &)と明示的キャプチャ(名前を指定するキャプチャ)を行うことができます。言い換えると、非ローカルなラムダ式では初期化キャプチャのみが行えます。

int N = 10;

// 非ローカルラムダ
int M1 = [=] { return 20; }();  // ng
int M2 = [&] { return 20; }();  // ng
int M3 = [N] { return 20; }();  // ng
int M4 = [n=N] { return n*2; }(); // ok

int main() {

  // ローカルラムダ
  [=] { return 20; };  // ok
  [&] { return 20; };  // ok
  [N] { return 20; };  // ok
}

これは、ラムダ式がキャプチャする(必要がある)ものは常にローカルエンティティであることを反映しています。

ラムダ式のキャプチャに伴うローカルエンティティの参照

ラムダ式のキャプチャのために、式はローカルエンティティを参照する可能性があり、それは次の場合です

  • 1つ以上の非静的クラスメンバを指定し、そのメンバへのポインタを形成するものではないid式(id-expression、単体の識別子名だけからなる式)は、*thisを参照する可能性がある
  • this(式)は、*thisを参照する可能性がある
  • ラムダ式はその明治的キャプチャに指定された名前のローカルエンティティを参照する可能性がある
struct S {
  int m;

  void f() {
    [=] {
      int n = m;  // メンバmを参照する式によって、*thisの参照が発生する
    };

    [=] {
      [this]{};  // this式の使用によって、*thisの参照が発生する
    };
  }

  void g(float) {}

  static void g(int) {}

  void h() {
    [=] {
      g(0); // 結果的に静的メンバ関数が選択されるが、メンバgを参照する式によって*thisの参照が発生する
    };
  }
};

int main() {
  int n = 0;

  [n] {
    int m = n;  // nの明示的キャプチャによるローカルエンティティnの参照が発生する
  };
}

可能性があるのような書き方をしているのは、おそらくそれに該当する場合でも参照されない場合があり得るためです。例えばこの例でも、S::f()の2つ目のラムダ式ではthisを結局使用していないので参照はされないですし、main()ラムダ式中でnを使用しなければキャプチャしていても参照されないでしょう。

暗黙的なキャプチャ

  • ある式が、odr-usableなローカルエンティティを参照する可能性があり
  • その式を囲んでいるtypeid式の効果が無視された場合に評価される可能性がある(potentially evaluated)とき

そのローカルエンティティは、そのローカルエンティティを明示的にキャプチャしないデフォルトキャプチャを持つ介在するラムダ式によって、暗黙的にキャプチャされて(implicitly captured)います。

ここでの介在するは、odr-usableの宣言領域に対する条件の場合と同様にラムダ式がネストしている様を表しています。

要するに、ラムダ式がデフォルトキャプチャ(&/=)を持っていて、その本体内で外側のエンティティを参照する場合に自動で行われるキャプチャの事です。

規格書([expr.prim.lambda.capture]/7)より、サンプルコード

void f(int, const int (&)[2] = {});         // #1
void f(const int&, const int (&)[1]);       // #2

void test() {
  const int x = 17;
  auto g = [](auto a) {
    f(x);                       // OK: #1を呼び出す、xをキャプチャしない
  };

  auto g1 = [=](auto a) {
    f(x);                       // OK: #1を呼び出す、xをキャプチャする
  };

  auto g2 = [=](auto a) {
    int selector[sizeof(a) == 1 ? 1 : 2]{};
    f(x, selector);             // OK: #1か#2のどちらかを呼び出す、xをキャプチャする
  };

  auto g3 = [=](auto a) {
    typeid(a + x);              // OK: a + xが評価されないオペランドであるかどうかに関わらず、xをキャプチャする
  };
}

ラムダ式gの例では、xが左辺値から右辺値への変換(lvalue-to-rvalue conversion)の対象となるためxの使用(参照)はodr-usedではなく(そのためodr-usableである必要がなく)、キャプチャしなくても参照可能になります。

g1~g3の例ではラムダ式内からのxの参照はodr-usableであり、xは暗黙キャプチャされています。

ただし、g1の場合はxが左辺値から右辺値への変換の対象であるため、xの参照はodr-usedではなく、それによって実装がxのキャプチャを最適化(キャプチャしないように)することが許可されています(どうやら、g2, g3の使用ではxは左辺値から右辺値への変換の対象とならないようです)。

また、この暗黙的なキャプチャはローカルエンティティが破棄されるステートメントから参照される場合でも発生することがあります

template<bool B>
void f(int n) {
  [=](auto a) {
    if constexpr (B && sizeof(a) > 4) {
      (void)n;  // Bとsizeof(int)の値に関係なく、ローカルエンティティnをキャプチャする
    }
  }(0);
}

これらの事は、暗黙的にキャプチャされるエンティティは構文的に決定されることを言っています。

また、この提案でAnnex Cに追加されている互換性レポート(破壊的変更を記録している章)によると、この提案の規定による暗黙的なキャプチャは以前(C++17)ではキャプチャしなかったローカルエンティティをキャプチャする可能性がある、としており、これはルールを単純化constexpr ifとの相互作用を解決するため、とされています。

これも、暗黙的にキャプチャされるエンティティは構文的に決定される事を意味しており、暗黙的なキャプチャはconstexpr ifの分岐によって変化しないということだと思われます。

ラムダ式のキャプチャの振る舞いの明確化

定義した概念の中で、この作業に関連するのは

  • odr-usable
  • ローカルラムダ式
  • ラムダ式のキャプチャに伴うローカルエンティティの参照
  • 暗黙的なキャプチャ

の4つです。

特に4つ目は、以前は暗黙的/明示的キャプチャの定義が曖昧でそれがいつ起きて何をするのか不透明だったところを、暗黙的なキャプチャ(と明示的なキャプチャ)がどういうものでいつ起こるのかを明確に定義するようになっています。

それに加えて、次のような規定によってラムダ式の明示的キャプチャに対応する名前の探索がローカルエンティティだけを発見することが明確化されます

明示的なキャプチャに指定された識別子は、非修飾名探索(unqualified name lookup)の通常のルールを使用して探索される。この探索では、識別子に対応するローカルエンティティを発見しなければならない

以前は単に、エンティティを発見する、のようになっていたためキャプチャ対象が不透明だったのをローカルエンティティという言葉を用いて明確化した形です。

また、ラムダ式のキャプチャは次のように定義されていました

エンティティは明示的または暗黙的にラムダ式によってキャプチャされたとき、エンティティはキャプチャされる(captured)。
ラムダ式にキャプチャされたエンティティは、そのラムダ式を含むスコープでodr-usedとなる。

(この定義に変更はありません)

そのうえで、次のような規定

ラムダ式がodr-usableではないエンティティを明示的にキャプチャする場合、プログラムはill-formed

を追加し

また、odr-usableのところで触れましたが

ローカルエンティティがodr-usableではない宣言領域でodr-usedとなる場合、プログラムはill-formed

という規定も追加しています。

結局、この提案ではこれらの変更によって

  • ラムダ式がいつローカルになるのかを明確化し、それによってラムダ式が非初期化キャプチャ(暗黙的/明示的キャプチャ)を行える場所を明確化
    • ローカルラムダ式のみが、暗黙的/明示的キャプチャを行える
  • 暗黙的なキャプチャがいつ起こるのかを明確化
    • 暗黙的なキャプチャはローカルエンティティに対してのみ起こる
  • 明示的なキャプチャにおける名前探索の対象を明確化
    • 明示的なキャプチャはローカルエンティティに対してのみ起こる
  • ローカルエンティティのodr-usableによって、ラムダ式がキャプチャできない(しない)ものを明確化
    • odr-usableではないローカルエンティティをodr-usedしようとするとill-formed
      • ラムダ式がキャプチャしたものはodr-usedとなるため、odr-usableではない明示的/暗黙的なキャプチャはill-formed
    • ラムダ式がキャプチャしないものは、odr-usableとならない
      • キャプチャしないものがラムダ式の宣言領域でodr-usedとなる場合ill-formed

このような、キャプチャ周りの挙動を明確になるように規定の修正を行っています。

エンティティがいつodr-usedになるかは難しいですが、評価されない文脈やインスタンス化していないテンプレートなどの内部を除いてほとんどの場合にodr-usedになると思っていいと思います。

thisのキャプチャ

odr-usableの後段の条件の2つ目

  • その場所はクラススコープかラムダ式のものではない関数パラメータスコープに囲われている
    • そのスコープの最も内側のスコープが関数パラメータスコープであるならば、そのスコープは非静的メンバ関数のもの

*thisがodr-usableとなる場合の前提条件を言っています。読み解けば、*thisがodr-usableとなるのはクラススコープ(デフォルトメンバ初期化子)か、非静的メンバ関数の関数パラメータスコープのどちらかです。

odr-usableでなければキャプチャできないので、*thisをキャプチャできるのはクラススコープ(デフォルトメンバ初期化子内)か、非静的メンバ関数内のどちらかです。

また、ラムダ式のキャプチャに伴うローカルエンティティの参照

  • メンバを参照する場合*thisを参照する
  • this*thisを参照する

の定義と、この提案で追加される規定

thisもしくは*thisの明示的なキャプチャは、ローカルエンティティ*thisを明示的にキャプチャする

より、thisのキャプチャも*thisのキャプチャもローカルエンティティ*thisをキャプチャします。

この*thisをコピーしてキャプチャするか参照キャプチャするのかは、この後でキャプチャの仕方(構文)から決定されます。

構造化束縛をキャプチャできないことを明確化

C++17導入時点では構造化束縛をラムダ式がキャプチャできるかどうかは不透明でした。この提案ではそれが明確に禁止されます。これは次のように規定されることによります

ラムダ式が明示的もしくは暗黙的に構造化束縛をキャプチャする場合、プログラムはill-formed

ただし、これはとりあえず振る舞いを明確化するのが目的であって禁止することが意図ではないようです。おそらく、それは別の問題でありここではなく別の提案で解決することを意図していたのでしょう。実際に、最終的にC++20では構造化束縛をラムダ式でキャプチャできるようになっています。

詳しくは以前の記事をご覧ください。

ローカルクラス

ラムダ式はローカルクラスの特殊な場合でもあり、外側のエンティティの参照に関してほぼ同じ問題があります。そのため、ラムダ式同様にローカルクラスはローカルエンティティを参照できません。

ローカルクラスはその宣言内で囲むスコープのローカルエンティティをodr-usedできない

と規定されます。より正確には、これはローカルクラススコープが介在することでローカルエンティティの参照がodr-usableではなくなるためにodr-used出来なくなります。

この提案の修正では、ローカルクラスが参照できないエンティティをローカルエンティティという言葉で規定し直すことで、構造化束縛の参照が禁止されるようになっています。

規格書([class.local]/1)より、サンプルコード

void f() {
  int x;
  const int N = 5;
  int arr[2];
  auto [y, z] = arr;

  struct local {
    int g() { return x; }       // error: odr-use of non-odr-usable variable x
    int m() { return N; }       // OK: not an odr-use
    int* n() { return &N; }     // error: odr-use of non-odr-usable variable N
    int p() { return y; }       // error: odr-use of non-odr-usable structured binding y
  };
}

(サンプルコードは一部省略しています)

この例はいずれも、ローカルエンティティをローカルクラス内から参照しようとしています。それにあたってはodr-usableであるかどうかが問題となり、いずれもクラススコープが介在していることからodr-usableではありません。

ただし、m()の例だけは、式Nが左辺値から右辺値への変換の対象となることからNの参照はodr-usedではなくなり、odr-usableであるかは問題とならなくなります。他の例は全てローカルエンティティをodr-usedしようとしています。

ラムダ式がクラスメンバ初期化子で使用された時の挙動の明確化

これは、CWG Issue 1632を解決するための作業です。

これは以前のローカルラムダ式の定義がクラススコープを考慮していなかったために起きている問題でしたが、この提案によってローカルラムダ式の定義には

クラスのデフォルトメンバ初期化子で現れており、囲む最も内側のスコープがそのメンバに対応するクラススコープである

という条件が追加されており、これによってクラスメンバ初期化子でラムダ式が使用可能であり、キャプチャもできることが明確になっています。

ただし、

  • ローカルエンティティの定義
    • *thisはローカルエンティティだがメンバ変数はそうではない
  • ラムダ式のキャプチャに伴うローカルエンティティの参照
    • メンバを参照する場合*thisを参照する
    • this*thisを参照する

の定義と、この提案で追加される規定

thisもしくは*thisの明示的なキャプチャは、ローカルエンティティ*thisを明示的にキャプチャする

などから、クラスのデフォルトメンバ初期化子内のラムダ式が行えるキャプチャは、デフォルトキャプチャかthis*this)の明示的キャプチャのどちらかに限られます。odr-usableの条件から、ローカルクラス内ラムダ式がその外側のエンティティを参照することもできません。

struct S {
  int n = 1;

  // これらはok
  int m1 = [=] { return n + 1; }();
  int m2 = [&] { return n + 1; }();
  int m3 = [this] { return n + 1; }();
  int m4 = [*this] { return n + 1; }();
  int m5 = [n = n] { return n + 1; }();

  // これはng
  int ng = [n] { return n + 1; }();
};

この場合、ラムダ式内部から参照されているnはローカルエンティティではなく、クラスメンバnの参照を介した*thisのキャプチャによって使用可能となっています。

ただし、thisをキャプチャできるため、初期化前に未初期化メンバを参照するコードが書けてしまいます。

struct S {
  int n = 1;
  int m = [this] { return n + l; }();  // 💀 UB!!
  int l = 3;
};

この場合、初期化前のメンバ変数はコンストラクタすら呼ばれていないので読み書き共に未定義動作になります。

ラムダ式の構文内でキャプチャした対象に対するdecltype((x))の振る舞いの明確化

これは、CWG Issue 1913を解決するための作業です。

問題となっているのはラムダ式内部でdecltype((x))した時の結果を指定するところで、キャプチャした変数xクロージャ型のメンバであるかのように扱って結果を求める、のようにしていました。しかしこれは、ラムダがxをキャプチャしていない場合やxを参照キャプチャしている場合を考慮できておらず、不正確な規定でした。

この提案ではラムダ式内部での特別扱いをやめ、非修飾名xを指定するid式の型について次のように規定することでこの問題を解決します。

id式の結果は、その識別子で示されるエンティティである。
エンティティがローカルエンティティで、その非修飾名が現れる宣言領域内の評価されない文脈の外側から(その名前を)指名すると、介在するラムダ式がコピーによってそのローカルエンティティをキャプチャする事になる場合
そのid式の型は、最も内側のラムダ式クロージャオブジェクトでそのキャプチャのために宣言されている非静的メンバ変数を指定したクラスメンバアクセス式の結果の型となる。
[Note: ラムダ式mutableと宣言されていない場合、そのid式の型は通常const修飾される。]
それ以外の場合、id式の型は式の結果(ローカルエンティティ)の型。
[Note: この型がCV修飾されているか参照型である場合、その型は[expr.type]で説明されているように調整される。]

1つ目のNoteの直前の文は、xをコピーキャプチャしている場合のラムダ式内でdecltype((x))した時の結果型を言っており、2つ目のNoteの直前の文は、xを参照キャプチャしている場合のラムダ式内でdecltype((x))した時の結果型を言っています。

2つ目のNoteにある[expr.type]で説明されている型調整とは、いわゆるdecayのようなもので、autoやテンプレートパラメータの推論において行われるCV・参照修飾の調整の事です。これはコピーキャプチャしている場合のid式の型には適用されません。

xをコピーキャプチャしている場合にのみクロージャ型のメンバアクセスとして扱って、クラスメンバアクセスの結果と一致させ、参照キャプチャの場合は特別扱いせずに通常と同様の結果を返します。

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

void f() {
  float x, &r = x;
  [=] {
    decltype(x) y1;             // y1の型はfloat
    decltype((x)) y2 = y1;      // y2の型はfloat const& (このラムダはmutableではなく、xはlvalueのため)
    decltype(r) r1 = y1;        // r1の型はfloat&
    decltype((r)) r2 = y2;      // r2の型はfloat const&
  };
}

decltype()オペランドが評価されない文脈であるため、この例のラムダ内ではx, rはodr-usedではなく、このラムダ式x, rをキャプチャしていません。にもかかわらず、decltype((expr))の型の決定においてはx, rはキャプチャされているかのように扱われます。それは

その非修飾名が現れる宣言領域内の評価されない文脈の外側から(その名前を)指名すると、介在するラムダ式がコピーによってそのローカルエンティティをキャプチャする事になる場合

というのによります。評価されない文脈の外から名前を参照したかのようにして型を求めるので、キャプチャされているかは関係なかったりするわけです。つまりは、キャプチャされてない場合でもキャプチャされているかのように扱います。

decltype((expr))では、まずオペランド(expr)の値カテゴリに応じて結果の値カテゴリ(参照修飾)が決まり、(expr)が左辺値式なら左辺値&(expr)が右辺値式なら右辺値&&となります。その修飾を付加する対象の型は式(expr)の型と指定されており、(expr)の型はexprの型であり、この例ではexprはid式xもしくはrです。

id式の型の決定過程は上に引用した通りで、前述のとおりこの例ではx, rは共にコピーキャプチャしている場合と同様に扱われるため、クロージャオブジェクトのクラスメンバx, rへのアクセスの結果型となり、このラムダ式は非mutableのため、id式x, rの型はconst floatになります。また、ラムダ式の関数呼び出し演算子は左辺値修飾であるので、id式x, rの値カテゴリは左辺値です。

よって、decltype((x))decltype((r))の結果型は、どちらもfloat const&const float&)となります。

一方、decltype(x)decltype(r)の型の決定はそれとはまるで異なり、decltypeの行う()で囲まれていないid式(unparenthesized id-expression)に対する型の決定過程に従い、そこではid式x, rの結果エンティティの型を取得します。id式の結果はその識別子が示すエンティティであり、このラムダ式内部において式x, rの示すエンティティとはラムダ式の外側にあるf()のローカル変数x, rです。

このエンティティの型は宣言されたとおりの型であり、decltype(x)floatdecltype(r)float&となります。

なお、この節に関連する規格の記述はC++23において少し厄介な変更が入っているため、C++20 DIS(N4861)を参照しています。

DR?

これらの変更はおそらく、規格で不透明だったものの最初から意図されていた挙動を明確化することを目的としており、既存の挙動を変更するようなものではありません。したがって、これらの変更は対応する機能が追加された時点(ほぼC++11、構造化束縛関連はC++17)に対するDRであると思われます。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で75本あります。(新規35本

N4924 WG21 2022-10 Admin telecon minutes

WG21の各作業部会の管理者ミーティングの議事録。

前回から今回の会議の間のアクティビティの報告がされています。

N4925 2023-02 Issaquah meeting information

2023年2月に行われる予定のWG21全体会議のインフォメーション。

次回は、アメリカのワシントン州イサクアで行われる予定で、内容は主に会場の案内についてです。

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

Library Fundamental TS v3のワーキングドラフト。

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

↑の差分を記した文書。

変更点は

  • P2705R0 : 報告されたIssueの解決
  • P0987R2 : 型消去の代わりにpolymorphic_allocator<>を使用するように変更
  • P2708R1 : LFTSをこれ以上更新しないことを明記

の内容を適用したことなどです。

P0901R10 Size feedback in operator new

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

以前の記事を参照

このリビジョンでの変更は、提案するnew式の設計の詳細を追記したこと、サイズを返さないoperator new()にフォールバックする振る舞いの削除、セクションの整理、などです。

P1018R18 C++ Language Evolution status 🦠 pandemic edition 🦠 2022/07–2022/11

2022年7月から11月にかけてのEWG活動報告書。

主にコア言語のIssueのレビューが主だったようです。

なお、C++23についての作業は終了しているらしく、これ以降コア言語の提案をC++23に向けて作業することはないようです。

P1018R19 C++ Language Evolution status

2022年11月のkona会議のEWG活動報告書。

コア言語のIssueやNBコメントのレビュー・投票が行われていたようです。

C++26に向けて、以下の提案がCWGに転送されました

次の提案はLEWGに転送されました

P1028R4 SG14 status_code and standard error object

現在の<sysytem_error>にあるものを置き換える、エラーコード/ステータス伝搬のためのライブラリ機能の提案。

<sysytem_error>ヘッダはstd::error_code関連のものを擁するヘッダで。C++11にて、当時のFilesystem TSから分離される形で先行導入されました。<filesystem>を使うとき以外はあまり使用されることはないようですが、これは標準の多くのヘッダファイルから参照されており、標準ヘッダの内部依存関係の一部を構成しています。

このエラーコードインターフェースの設計上の問題は近年(2018年ごろ)明らかになり、正しく使うことが難しく、エラー報告のためのインターフェースなのにエラーを起こしやすいなどの問題がありました。それはP0824R1で纏められて報告され、現在のstd::error_code周りの問題を改善した代替の機能が追求されました。

この提案は、そのようなライブラリ機能を実装するとともにBoost.Outcomeなどでの経験をベースとして、現在のエラーコードインターフェースの問題を解決し置き換えることを目指したライブラリ機能を提案するものです。

このライブラリの中核は、std::error_codeを置き換えるstd::system_codeというクラス型です。これはシステムの何かしらのコード(必ずしもエラーではない)を統一的に表現するためのクラスで、使用感はほぼstd::error_codeと同様です。

std::system_code sc;  // デフォルト構築は空(成功でもエラーでもない)
native_handle_type h = open_file(path, sc);

// 失敗しているかをチェック
if(sc.failure()) {
  // ファイルが見つからないため失敗したかをエラーコードとの比較によってチェック
  // この比較は値ベースではなく意味論的な比較となる
  if(sc != std::errc::no_such_file_or_directory) {
    std::cerr << "FATAL: " << sc.message().c_str() << std::endl;
    std::terminate();
  }
}

このopen_file()はプラットフォームによって次のように実装できます。

// POSIXシステムの場合
using native_handle_type = int;

native_handle_type open_file(const char *path, std::system_code &sc) noexcept {
  sc.clear();  // 非エラー状態にする
  
  // ファイルオープン
  native_handle_type h = ::open(path, O_RDONLY);

  // エラーチェック
  if (h == -1) {
    // errnoはsystem_codeに型消去される
    sc = std::posix_code(errno);
  }

  return h;
}
// Windowsの場合
using native_handle_type = HANDLE;

native_handle_type open_file(const wchar_t *path, std::system_code &sc) noexcept {
  sc.clear();  // 非エラー状態にする

  // ファイルオープン
  native_handle_type h = CreateFile(path, GENERIC_READ,
    FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
    nullptr,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    nullptr
  );

  // エラーチェック
  if (h == INVALID_HANDLE_VALUE) {
    // GetLastError()の結果はsystem_codeに型消去される
    sc = std::win32_code(GetLastError());
  }

  return h;
}

この新しいsystem_codeは、std::error_codeの次のような点を改善しています

  • <string>に依存しない
  • constexpr対応
  • std::error_categoryの、リンク時にランダムに比較が壊れる問題が起きない
  • bool変換時の曖昧さがない
    • std::error_codebool変換でtrueが帰るのはエラー状態の時とは限らない(値が非ゼロであることしか意味しない)
  • 上記と関連して、0を特別扱いせず、成功と失敗を任意に表現できる
  • 比較が意味ベース(エラーコードとの比較はその値の比較ではなく、意味するエラー状態の比較になる)
    • std::error_codeのように値ベースではない
    • std::error_conditionstd::error_codeのように分かりづらい関係性のクラスを必要としない
  • エラーコードの型が任意(std::error_codeint限定)
  • 複数のシステムエラーコードを扱うことができる
  • エラーカテゴリの厳密な区別

また、この提案には含まれていないように見えますが、P0709で提案されている静的例外クラスstd::errorを実装し、それをエラー状態とする戻り値型result<T>も提案しようとしているようです。これは、std::expected<T, std::error>と非常によく似ているクラスですが、エラー型がハードコーティングされていることによって若干異なるインターフェースを持っています。

P1202R5 Asymmetric Fences

非対称なフェンスの提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の明確化のみのようです。

この提案は11月のkona会議でConcurrency TS v2へ導入することが決まっています。

P1264R2 Revising the wording of stream input operations

<istream>の例外に関する規定を改善する提案。

現在の<istream>の規定は非常に複雑になっており、特にいつ例外を投げるのかが分かりづらく、それによって実装間で振る舞いに差異が生じています。

空でないストリームからの抽出に失敗する入力操作の例

#include <iostream>
#include <sstream>

int main () {
  std::stringbuf buf("not empty");
  std::istream is(&buf);

  // failbitがセットされたら例外を送出する
  is.exceptions(std::ios::failbit);

  bool threw = false;
  
  try {
    unsigned int tmp{};
    // 数値を読み取れないので失敗する
    is >> tmp;
  } catch (std::ios::failure const&) {
    threw = true;
  }

  std::cout << "bad = " << is.bad() << std::endl;
  std::cout << "fail = " << is.fail() << std::endl;
  std::cout << "eof = " << is.eof() << std::endl;
  std::cout << "threw = " << threw << std::endl;
}

この結果は、実装によって次のようになります

libstdc++ MSVC STL libc++
bad 0 0 1
fail 1 1 1
eof 0 0 0
threw 1 1 0

正しいのはlibstdc++/MSVC STLの振る舞いに思えますが、現在の複雑な規定によればlibc++の振る舞いも合法のようです。ただ、この振る舞いは有用ではなく、ほぼ無意味です。

空のストリームからの抽出に失敗する入力操作の例

#include <iostream>
#include <sstream>

int main () {
  std::stringbuf buf; // empty
  std::istream is(&buf);

  // failbitがセットされたら例外を送出する
  is.exceptions(std::ios::failbit);

  bool threw = false;
  
  try {
    unsigned int tmp{};
    // 数値を読み取れないので失敗する
    is >> tmp;
  } catch (std::ios::failure const&) {
    threw = true;
  }

  std::cout << "bad = " << is.bad() << std::endl;
  std::cout << "fail = " << is.fail() << std::endl;
  std::cout << "eof = " << is.eof() << std::endl;
  std::cout << "threw = " << threw << std::endl;
}

この結果は、実装によって次のようになります

libstdc++ MSVC STL libc++
bad 0 0 1
fail 1 1 1
eof 1 1 1
threw 1 1 0

正しいのはlibstdc++/MSVC STLの振る舞いに思えますが、やはりこれもlibc++の振る舞いが間違っているわけではないようです。

この提案は、このような状況を招いている複雑な規定を明確になるように修正し、libstdc++/MSVC STLの振る舞いを維持したままlibc++の振る舞いを修正しようとするものです。

P1478R8 Byte-wise atomic memcpy

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

以前の記事を参照

このリビジョンでの変更はP2396R1関するLEWGの要望を反映したことです。

この提案は11月のkona会議でConcurrency TS v2へ導入することが決まっています。

P1619R2 Functions for Testing Boundary Conditions on Integer Operations

整数演算の境界条件をチェックするためのライブラリ関数の提案。

C++の整数演算には誰もが遭遇する境界条件(最大値・最小値を跨ぐような演算)についての罠があります。これは初心者でも簡単に遭遇しうる一方で、その理解にはC++言語の知識や整数のハード上表現の知識、数学的な知識などを必要とし、アサーションのためにその判定を書くことはかなりの注意を必要とします。

提案より、整数の二項演算で行われることの流れ図

(提案によれば、この図は修正が必要な箇所があり、他の部分についても修正の必要がある可能性があり、それこそが整数演算の複雑さを現している、とのことです・・・)

この提案は、そのような境界条件に関するアサーションに使用するための、条件を簡単かつ直接的に命名及び表現したライブラリ関数を追加しようとするものです。

この提案による関数群は多岐に渡りますが、いずれもプリフィックスcan_~で始まり、bool値を返すconstexpr関数です。

#include <limits>

int add(std::integral auto lhs, std::integral auto rhs) {
  // 2つの整数値の足し算が問題なく行えるか
  assert(std::can_add(lhs, rhs));
  // 足し算の結果をintに正しく変換できるか
  assert(std::can_convert<int>(lhs + rhs));

  return lhs + rhs;
}

他にも、インクリメント等の単項演算子、四則演算と%などの2項演算子+= *=などの複合代入演算子_in_placeが後ろにつく)にそれぞれ対応した関数が用意され、全ての関数に_modularが後ろにつくバージョン(2^Nを法とするモジュロ演算によるもの)が用意されています。

この関数群は次の条件を全てパスする場合にtrueを返し、そうでなければfalseを返します。

  1. 各関数に指定された式が評価されると、その実行はwell-formed
  2. オペランドについて、整数昇格と変換が適用される前後の値は2^Nを法として合同。また、比較演算と/ /= % %= >> >>=の両オペランド<< <<=の右オペランドについて、整数昇格と変換の適用前後の値は等しくなる。
  3. 式の結果と数学演算の結果は2^Nを法として合同。また、ポストフィックスに_modularが付かない関数では、式の結果と数学演算の結果は等しくなる。

この提案による関数群は、C++26以降で導入されるContractにおいても有用であると思われます。

P2164R8 views::enumerate

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

以前の記事を参照

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

  • enumerate_resultを削除(SG9での投票により要素型をstd::tupleで一貫させることが決定)
  • 1引数コンストラクタにexplicitを付加
  • enable_borrowed_range特殊化を追加
    • 入力のview(元のシーケンス)のborrowed_range性を受け継ぐ
  • インデックスのみを取得する.index()enumerate_viewイテレータに追加
  • その他文言の改善や修正

などです。

このリビジョンで追加された.index()は、入力のviewoperator*()が重く(コストがかかり)、インデックスの値だけが必要な場合に、元の範囲のイテレータの間接参照を回避してインデックス数値を取得するためのものです。

#include <vector>
#include <ranges>

int main() {
  std::vector days{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};

  auto ev = days | std::views::enumerate;

  for (auto it = ev.begin(); it != ev.end(); ++it) {
    auto idx = it.index();  // 元のイテレータの間接参照を避け、インデックスのみを取得する

    ...
  }
}

この提案はLEWGのレビュー中ですが、先行してLWGのレビューが完了しています。

P2167R3 Improved Proposed Wording for LWG 2114 (contextually convertible to bool)

contextually convertible to boolと言う規格上の言葉を、C++20で定義されたboolean-testableコンセプトを使用して置き換える提案。

このリビジョンでの変更は、フィードバックに基づく提案する文言の修正のみです。

この提案は11月のKona会議でC++23WDへの導入が決定しています。

P2248R7 Enabling list-initialization for algorithms

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

以前の記事を参照

このリビジョンでの変更は、Rangeアルゴリズムのテンプレートパラメータにデフォルトを指定するために必要な実装によるテンプレートパラメータの並べ替えが許可されるのか?についての疑問を追記したこと、projected_value_tをフリースタンディング指定したことなどです。

P2396R1 Concurrency TS 2 fixes

Concurrency TSv2を目指しているいくつかの提案について、本題と関係の薄い微修正の提案。

以前の記事を参照

このリビジョンでの変更は、ハザードポインタとRCUに対して配置するヘッダの変更と機能テストマクロの追加を提案していることです。

この提案は11月のKona会議でConcurrency TS 2への適用が決定しています。

P2434R0 Nondeterministic pointer provenance

現在のC++のポインタ意味論をポインタのprovenanceモデルに対して整合させるための提案。

ポインタにおけるprovenanceという概念は、ポインタのprovenance(出自・由来・出所)を重視したポインタの意味論のことで、現在の整数アドレス的なポインタ意味論で行えてしまっていることを制限しようとするものです。これによって、コンパイラエイリアス解析においてより多くの仮定を行えるようになり、最適化が促進されます。そこでは特に、ポインタ値を整数にキャストした後で戻すという操作によってprovenanceがどこまで伝播するのかが問題となっています。

C/C++の規格にはこの概念はまだ導入されておらず、P2381R1にて提案段階にあります。そこでは、いくつかのprovenanceモデルが挙げられています

  • PNVI (provenance-not-via-integer)
    • (ポインタをキャストした)整数値を介してポインタのprovenanceの追跡をする代わりに、整数->ポインタのキャスト地点で指定されたアドレスが生存期間内にあるオブジェクトを指しているかをチェックし、問題がなければそこでprovenanceを再作成する
  • PNVI-ae (PNVI exposed-address)
    • PNVIを発展させたもので、以前に露出した(exposed)ストレージインスタンス(オブジェクトの配置されているストレージの実体、オブジェクトの配置されている場所のような概念、ほぼポインタのことと思って差し支えない)に対してのみ、整数->ポインタのキャストにおいてprovenanceの再作成を許可する
    • ストレージインスタンスは次のいずれかの場合に露出したことになります
      • (ストレージインスタンスの)ポインタ値の整数型へのキャスト
      • ポインタの表現の(非ポインタ型での)読み出し
      • %pによるポインタ値の出力
  • PNVI-ae-udi (PNVI exposed-address user-disambiguation):
    • PNVI-aeをさらに発展させたもので、ストレージインスタンスの直後の場所のポインタの、ポインタと整数間の往復キャスト(ポインタ<->整数の間を行き来するキャスト)もサポートする。
    • ストレージインスタンスの直後の場所のポインタとは、いわゆる配列の末尾のポインタ(配列のendイテレータ)や、それを発展させて許可されている任意のオブジェクトの1つ後ろを指すポインタの存在、のこと
  • PVI (provenance-via-integers)
    • 整数演算を介したときでもprovenanceを追跡するモデル。ポインタ値だけでなく全ての整数値に対してprovenanceを関連づけ、整数/ポインタのキャスト前後でprovenanceを保持し、整数とポインタの+ -整数演算の結果のprovenanceに関して特別な選択を行う。

Cのprovenance導入議論においての最有力候補はPNVI-ae-udiモデルです。おそらくこれはC++においても同様となるでしょう。

規格には取り込まれていなくても、現在のC++(最新のC++23ドラフト N4917)が規定するポインタセマンティクスは既に整数アドレス的なものではなく、ポインタのprovenanceが禁止することを目的とする多くの状況は現在も未定義動作となります。

int main() {
  int jenny=0;
  // std::cout << &jenny;
  *(int*)8675309=1;
  return jenny;
}

実装がこの整数値(8675309)に対するポインタを特別に定義していないとすると、このコードはPNVI-ae-udi及びPVIの両方で未定義動作となり(どちらも、整数値8675309provenanceを見出せない)、現在のC++においてもキャストが無効なポインタ値を生成する可能性があることから未定義動作になります。

int main() {
  int x,y=0;
  uintptr_t p=(uintptr_t)&x,q=(uintptr_t)&y;
  p^=q;
  q^=p;
  p^=q;
  *(int*)q=*(int*)p;
  return x;
}

PVIでは、p, qの操作の結果得られる値はそのprovenanceを継承しない(^=は特別扱い演算の対象外な)ためこれを許可しません。PNVI-ae-udiでは、main()内2行目でストレージインスタンスが露出しており、整数演算(xorによるswap)後のp, qが生存期間内にあるオブジェクトを指していることから許可されます。
C++では、x, yのアドレスがどのように選択されたとしてもq(p)は結局x(y)のアドレスになるため、ポインタへのキャストは交換されたポインタを生成し、これは許可されます。この解釈は任意の整数演算やI/O操作に及びます。

int main() {
  int *p=new int;
  uintptr_t i=(uintptr_t)p;
  delete p;

  p=new int(1);
  if((uintptr_t)p==i) *(int*)i=0;
  return *p;
}

PVIでは、新しいポインタpと以前のp由来の整数iを比較しただけではipprovenanceが継承されないため、これを許可しません。PNVI-ae-udiでは、新しいオブジェクトのアドレス(pのポインタ値)が比較(後段2行目(uintptr_t)p==i)時に露出するため、それと同じ値を持つiのポインタへのキャストは新しいpと同じ値になるため、これは許可されます。
C++では、iが新しいpと同じ値を持っていれば、キャストによって同じポインタに戻されるため、これは許可されます。

このように、現在のC++のポインタ意味論と規定はPNVI-ae-udiモデルによく整合しているように見えます。しかし、現在のC++のポインタ規定はudi(user-disambiguation)を実装していません。往復変換が許可されるポインタは元の値を持つと規定されている([expr.reinterpret.cast]/5)ため、オブジェクトへのポインタとメモリ内でその直後の場所を指すポインタに対して同じ整数値を生成する実際の実装を(誤って?)禁止しています(つまり、1つの整数値に対するポインタ値が複数存在する可能性があり、実際にそのような実装があるようです)。

同様に、[basic.types.general]/2–4は、バイト列から構成されたポインタかもしれないものについても同様に元の値を持つこと(値表現が一致すること)を要求しており、そのような状況を許可していません。

std::bit_castは同じ値表現を持つ複数の値の可能性を認めるものの、結果の値は未規定としています。

この提案は、udi(user-disambiguation)を実装(該当する場合を未定義動作ではなく)するために、これらに関連する規定を修正しようとするものです。

変更は、ポインタの値表現(ビット列)については、同じビット列が複数のポインタ値に対応する可能性があり、(memcpyなどによって)そのビット値が取得される(acquires)時は、その複数ある中からwell-definedとなる(未定義動作とならない)1つを選択する、のようにします。これによって、udi(user-disambiguation)が実装され、現在のC++のポインタ規定をPNVI-ae-udiモデルにより整合させることができます。

P2539R4 Should the output of std::print to a terminal be synchronized with the underlying stream?

提案中のstd::printP2093)が出力するストリームについて、同じストリームに対する他の出力との同期を取るようにする提案。

このリビジョンでの変更は、LWGのフィードバックに基づく文言の修正です。

この提案は11月のKona会議でC++23WDへの導入が決定しています。

P2546R3 Debugging Support

標準ライブラリにデバッグサポートの為のユーティリティを追加する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正とEWGでの投票の結果を追記した事です。

この提案はEWGでの承認が得られたためLEWGでのレビューが開始されます。

P2548R2 copyable_function

P2548R3 copyable_function

P2548R4 copyable_function

std::move_only_functionに対して、コピー可能なCallableラッパであるcopyable_functionの提案。

以前の記事を参照

R2での変更は、以前に適用していたP2511R2std::nontype)の内容を削除した事です(P2511がLEWGでコンセンサスを得られなかったため。

R3での変更は

  • デザインセクションのcallable要件を修正
  • std::functionの非推奨化についてのOpen Questionを削除
  • move_only_functionへの変換をmove_only_functionへ移動
    • move_only_functionの変換コンストラクタを利用する
  • 標準ライブラリのポリモルフィックCallableラッパ間の変換関係についてのセクションを追加
  • アロケータサポートの可能性についてのセクションを追加
    • この提案ではやらない

R4(このリビジョン)での変更は、move_only_functionの変換コンストラクタで変換時に例外を投げない規定を削除した事です。

P2552R1 On the ignorability of standard attributes

属性を無視できるという概念について、定義し直す提案。

以前の記事を参照

このリビジョンでの変更は明確ではありませんが、全体的に説明や例が拡充されています。おそらくこのリビジョンはR0の後に提出されたCWG Issue2538とNBコメントを受けての更新です。

この提案では、属性の無視について次の3つの観点から決定が必要だとして、それぞれにオプションを提示しています

  1. 実装が標準属性を構文的に無視できるかを明確にする
    1. 標準属性を構文的に無視できないことを明確にする
      • R0およびCWGの推奨
    2. 標準属性を構文的に無視できることを規定する
      • 引数などは構文チェックされず、無視される属性内から参照されるエンティティはODR-usedではない
    3. 標準属性が条件付きでサポートされないことを明確化する
      • 特定の標準属性を実装しない場合は文書化し、使用されたときは警告する
    4. 何もしない
  2. 標準属性が意味的に無視できるという時の意味を明確にする
    1. 標準属性が意味的に無視できることを規定し、これが正確に何を意味するかを標準で指定する
    2. 代わりに、これを別の新しいStanding Documentで指定する
    3. 何もしない
  3. 実装が無視する属性に対する__has_cpp_attributeの動作を明確にする
    1. __has_cpp_attributeが標準属性に対して正の値を返すのは、実装がそのセマンティクスの有用な実装を持っている場合のみ
    2. 有用なセマンティクスを実装していなくても、実装がその属性を認識できる場合は__has_cpp_attributeは正の値を返す
      • clang/ICCの実装
    3. 何もしない

11月のkona会議でNBコメントに関連してこれの選択に関する投票が行われたところ、1-1にコンセンサスが得られて採択された一方、2と3はどちらにもコンセンサスが得られませんでした。

CWG2538の採択によって、標準属性の無視とはその意味的な効果のみで属性の構文を無視することを意味しない、となったわけですが、投票に当たってはコンパイラ実装者が実装できないとして強く反対し続けていたようです。

P2559R1 Plan for Concurrency Technical Specification Version 2

Concurrency TS v2発効に向けた作業計画書。

以前の記事を参照

このリビジョンでの変更は、ハザードポインタ(P1121R3)とRCU(P1122R4)がすでにConcurrency TS v2に採択されたことを反映した事です。

P2564R1 consteval needs to propagate up

P2564R2 consteval needs to propagate up

P2564R3 consteval needs to propagate up

consteval関数をconstexpr関数の中で呼び出すことができない問題を軽減する提案。

以前の記事を参照

R1での変更は、提案する文言とサンプルの追加です。

R2での変更は、集成体初期化に関する文言を追加したことです。

R3(このリビジョン)での変更は、機能テストマクロを追加したことです。

この提案は11月のKona会議でC++23WDへの導入が決定しています。

P2570R1 Contract predicates that are not predicates

コントラクト注釈に指定された条件式が副作用を持つ場合にどうするかについて、議論をまとめた文書。

以前の記事を参照

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

  • 議論の範囲を拡張し、冪等性などの契約条件式の他のプロパティを含めた
  • 契約条件式の実行時評価から、契約注釈に基づく静的解析に議論をシフト
  • P2680R0で示唆された解決策に対処
  • 純粋な契約条件とそうではないものが共存できるソリューションについて解説を追記
  • N2956([[unsequenced]])が契約条件を制限するのに役立つ可能性についての議論を追加
  • 契約条件式で標準ライブラリ機能をどの程度使用できるかを判断するための基準について説明を追記

などです。

P2588R2 Relax std::barrier phase completion step guarantees

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

以前の記事を参照

このリビジョンでの変更は、バリアの完了関数が新しいスレッドで(スレッドを起動して)行われる事を許可しないようにしたことです。

P2589R1 static operator[]

添え字演算子operator[])を静的メンバ関数として定義できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロとして__cpp_multidimensional_subscriptの値を増やしたしたことです。

この提案は11月のKona会議でC++23WDへの導入が決定しています。

P2602R2 Poison Pills are Too Toxic

標準ライブラリから、Poison Pillと呼ばれるオーバーロードを削除する提案。

以前の記事を参照

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

この提案は11月のKona会議でC++23WDへの導入が決定しています。

P2614R2 Deprecate numeric_limits::has_denorm

std::numeric_limits::has_denorm関連の定数を非推奨化する提案。

以前の記事を参照

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

この提案はすでにC++23に向けてLWGの承認を得ていますが、LEWGでの作業が残っているようです。

P2615R1 Meaningful exports

無意味なexportを行えないようにする提案。

以前の記事を参照

このリビジョンでの変更は、EWGの要求に従って特定のexport(主に推論補助)を許可するように調整したことです。

P2616R1 Making std::atomic notification/wait operations usable in more situations

P2616R2 Making std::atomic notification/wait operations usable in more situations

std::atomicnotify_one()wait()操作を使いづらくしている問題を解消する提案。

以前の記事を参照

R1での変更は、R0で提案していた2つの解決策に加えて3つ目の解決案を追加、R0のオプション2の制限に関するフィードバックを反映。

このリビジョン(R2)での変更は

  • SG1の要請によりオプション3のみを提案に含めるようにした
  • abstractを書き換え
  • 設計ディスカッションを追加
  • 提案する文言を追加
  • 提案の対象をstd::atomic_flagに拡大

R1で追加されたOption3は、std::atomicオブジェクトから通知に使用できるトークンを取得できるようにするものです。このトークンは、アトミックオブジェクトが破棄された後でも、元のアトミックオブジェクトを待機しているエンティティに通知を行うために使用できます。

namespace std {

  // 提案するトークン型
  template<typename T>
  class atomic_notify_token;

  template<typename T>
  class atomic {
  public:
    // Existing members...
    
    // 提案するトークンを取得する関数
    atomic_notify_token<T> get_notify_token() noexcept;
    
  };
  
  template<typename T>
  class atomic_ref {
  public:
    // Existing members...
    
    atomic_notify_token<T> get_notify_token() noexcept;
  };
  
  // 提案するトークン型定義
  template<typename T>
  class atomic_notify_token {
  public:
    // Copyable
    atomic_notify_token(const atomic_notify_token&) noxcept = default;
    atomic_notify_token& operator=(const atomic_notify_token&) noxcept = default;
    
    // Perform notifications
    void notify_one() const noexcept;
    void notify_all() const noexcept;
  private:
    // exposition-only
    friend class atomic<T>;
    explicit atomic_notify_token(std::uintptr_t p) noexcept : address(p) {}
    std::uintptr_t address;
  };
  
}

この背後にあるアイデアは、アトミックオブジェクトが破棄される可能性がある場合に、未定義動作を起こす可能性のある事を回避して待機中のスレッドに通知を行えるだけの情報をトークンが保持できることです。

そして、std::atomic, std::atomic_ref, std::atomic_flagにある通知関数を非推奨化します。

この方法では、前回の記事のサンプルコードは次のようになります

#include <atomic>
#include <thread>

int main() {
  {
    // 同期用アトミックオブジェクト
    std::atomic<bool> sync = false;

    std::thread{[&sync]{
      // 破棄される前にトークンを取得
      auto token = sync.get_notify_token();
      // 値をtrueに更新
      sync.store(true);
      // 通知
      token.notify_one();
    }}.detach();

    // 値が更新(trueになる)されるまで待機
    sync.wait(false);
  }
}

おそらくSG1の選択はこのOption3のようですが、まだ明確に解決策が確定したわけではありません。

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

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

以前の記事を参照

このリビジョン(実質R1)での変更は

  • シンボルレベルの影響に関する説明を追加
  • 弱い所有権による実装を削除する提案を追加

などです。

この問題を引き起こしている物体についてリンケージを知る必要性があるのは、弱い所有権モデルを許可するためであるようです。強い所有権モデルであれば、外部リンケージとモジュールリンケージの区別をなくすことができるため、そのリンケージを知る必要性がなくなるためです。

ただし、その場合でも名前解決についての問題は解決されないようです。

P2644R1 Final Fix of Broken Range based for Loop Rev 1

範囲for文の13年間放置されているバグを修正する提案。

以前の記事を参照

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

この提案は11月のKona会議でC++23WDへの導入が決定しています。

P2647R1 Permitting static constexpr variables in constexpr functions

関数スコープのstatic constexpr変数をconstexpr関数内で使用可能にする提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の改善、例の追加、機能テストマクロの追加などです。

この提案は11月のKona会議でC++23WDへの導入が決定しています。

P2649R0 2022-10 Library Evolution Poll Outcomes

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

次の提案が投票にかけられ、一部の投票に当たって寄せられた賛否のコメントが記載されています。