[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に移動

などです。

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ソース