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

文書の一覧

全部で47本あります。

P0009R18 MDSPAN

多次元配列に対するstd::spanである、mdspanの提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを受けての文言調整のみです。

この提案は今回(2022/07)の全体会議で承認され、C++23入りしています。

P1018R17 C++ Language Evolution status 🦠 pandemic edition 🦠 2022/06–2022/07

2022年6月から7月にかけてのEWG活動報告書。

投票にかけられた提案は以下のものです

そのほかにも、いくつかのコア言語IssueがCWGに転送されています。

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

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

以前の記事を参照

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

  • max_align_v(最大アライメントサイズを表す定数)をinline constexpr変数として定義
  • aligned_raw_storage入れ子::typeを削除
  • aligned_raw_storagestd::aligned_storageの完全な代替ではないことを明確化
  • この提案には必須ではなかったため、aligned_object_storageを削除
  • C++26ターゲットへ変更

などです。

P1255R8 A view of 0 or 1 elements: views::maybe

std::optionalやポインタ等のmaybeモナドな対象を、その状態によって要素数0か1のシーケンスに変換するRangeアダプタviews::maybeの提案。

以前の記事を参照

このリビジョンでの変更はタイポ修正や見た目の調整のみです。

P1642R11 Freestanding Library: Easy [utilities], [ranges], and [iterators]

[utility]<ranges><iterator>から一部のものをフリースタンディングライブラリに追加する提案。

前回の記事を参照

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

この提案は今回(2022/07)の全体会議で承認され、C++23入りしています。

P1684R3 mdarray: An Owning Multidimensional Array Analog of mdspan

多次元配列クラスmdarrayの提案。

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

  • std::mdspanに適用された提案をこちらにも適用した
  • size constructible containerという要件を新設
    • 整数(もしくはrange/initilizer_list)からの構築によって、構築後のサイズを指定できるコンテナ
  • std::mdspanからの構築のための推論補助を追加
  • range/initilizer_listからのコンストラクタを削除
  • 文言の解説を更新

などです。

P1899R3 stride_view

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

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを受けての文言の修正と、stride_viewのデフォルトコンストラクタを削除した事です。

この提案は今回(2022/07)の全体会議で承認され、C++23入りしています。

P1967R8 #embed - a simple, scannable preprocessor-based resource acquisition method

コンパイル時(プリプロセス時)にバイナリデータをインクルードするためのプリプロセッシングディレクティブ#embedの提案。

以前の記事を参照

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

  • C++の提案からlimit/suffix/prefixに対する__を削除
  • 2022/6月のミーティングの議論とその反応を追記
  • limit(0)を使用したif_emptyの例を追加
  • ファイルが空であることを検知する二つの方法(__has_embedsuffix/prefix/if_empty)の違いについての説明の追加

などです。

この提案のC言語向けの部分はC23入りしましたが、C++ではまだEWGで議論中であるためもう少し時間がかかりそうです。

P2047R3 An allocator-aware optional type

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

このリビジョンでの変更は、提案する文言の改善と、pmr::basic_optional(アロケータの一般化)の方向性についての説明を拡充したことです。

この提案ではまだpmr::basic_optionalを含んではいませんが、LEWGのレビューではその方向性を採用することで合意されているようです。

P2079R3 System execution context

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

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

  • execute_allexecute_chunkを削除
    • 処理をメインスレッドでそのまま実行することができるようにするために、コンパイル時のカスタマイズを許可するようにするように文言を調整
    • (この2つの関数はそれを妨げていた)
  • カスタマイズのためのアプローチと、実行コンテキストを実装定義とする範囲についての議論の追加
  • system_contextクラスの設計についての議論の追加
  • 処理の優先度に関する設計の議論を追加

などです。

P2165R4 Compatibility between tuple, pair and tuple-like objects

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

前回の記事を参照

このリビジョンでの変更はLWGのレビューからのフィードバックを受けての修正がメインです。変更は1.5ページ分あるので転載はしませんが、大きな設計の変更はないはずです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2248R5 Enabling list-initialization for algorithms

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

以前の記事を参照

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

  • LEWGフィードバックを受けての修正
  • projected_valueprojected_value_tにリネーム
  • projected_value_tP2609ROとの関連について追記
  • ranges::fold()ranges::contains()を含めた
  • 実装経験にHPXライブラリを追記

などです。

P2295R6 Support for UTF-8 as a portable source file encoding

C++コンパイラが少なくともUTF-8をサポートするようにする提案。

以前の記事を参照

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

この提案は今回(2022/07)の全体会議で承認され、C++23入りしています。

P2361R5 Unevaluated strings

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

以前の記事を参照

このリビジョンでの変更は、asm宣言の文法を実態に合わせたbalanced-token-seqを受け取るように変更したことなどです。

本来のasm宣言はそのオペランドに文字列リテラルのみを取っていたためこの提案の対象となっていましたが、実際の実装では文字列リテラル以上の構文を受理するものがあったため、それを許可するようにする変更がこの提案に含まれることになりました。この変更はこの提案の内容とは直接関係なく、要するにGCCインラインアセンブラの記法を受け入れるようにするものです。

P2374R4 views::cartesian_product

任意個数のシーケンスの直積を取って、その元のシーケンスを生成するcartesian_product_viewの提案。

以前の記事を参照

このリビジョンでの変更は、LWGのレビューを受けての修正です。修正項目は多岐に渡りますが、大きな設計の変更はないはずです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

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

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

以前の記事を参照

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

  • この提案の破壊的影響についてAnnex Cセクションを追加
  • 文言の簡素化のため、3つのコンセプトの意味論要件のために新しいタイプの左辺値(lvalues denoting distinct equal objects)を導入
    • 型を維持してムーブすることができる左辺値
  • CONVERT_TO_LVALUE<C>(E)COMMONにリネーム。
    • 型情報を含み省略を避けることで、よりわかりやすくした
  • 説明専用コンセプトの文言の調整
  • 専用の機能テストマクロの追加

などです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2417R2 A more constexpr bitset

std::bitsetconstexpr対応させる提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを受けての文言修正のみです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2419R2 Clarify handling of encodings in localized formatting of chrono types

<chrono>のフォーマットにおいて、実行時ロケールが指定するエンコーディングリテラルエンコーディングが異なる場合の振る舞いを規定する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを受けての文言修正のみです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2460R2 Relax requirements on wchar_t to match existing practices

wchar_tエンコーディングについての実態になじまない制約を取り除く提案。

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

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2474R2 views::repeat

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

以前の記事を参照

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

  • タイポやフォーマットの修正
  • iterator{...}iterator(...)に置き換え(丸かっこ初期化を使うようにした)
  • iterator入れ子referenceを削除
  • プライベートメンバW*, Boundの初期化をコンストラクタからデフォルトメンバ初期化子に移動し、説明専用とした
  • iterator_valuenullptrに初期化する際、デフォルト初期化するようにした
  • iteratordeference_typeを定義
  • repeat_viewとそのイテレータのコンストラクタ、およびイテレータを変更しうる一部の操作に、boundが空でないとう事前条件を追加
  • repeat_viewの代わりにviews::repeatを使用することで、repeat_viewviews::take, views::drop特殊化の使用を簡素化
  • default_sentinel_tを取るオーバーロードnoexceptにした

などです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2481R1 Forwarding reference to specific type/template

テンプレートパラメータ指定時にconstと値カテゴリを推論可能にする構文の必要性について説明した文書。

このリビジョンでの変更は、Circleのアプローチについて追記した事です。

Circleとは、C++の構文を拡張してメタプログラミングのために便利な構文を追加したある種のプログラミング言語(とそのコンパイラ)です。

Circleでは、この提案の解決したい問題について次のような構文を追加しています。

template<typename T, typename... Args>
void f(T&& y : std::tuple<Args...>);

これは通常のテンプレートと同様に任意の型をconstと参照修飾まで含めて推論しつつその素の型を:の後にある型に制限する構文です。

これはこの提案の案の1つであるQ修飾子による指定と推論とほぼ同じことをしています。

`Q`修飾子 Circle
template <typename... Ts>
struct tuple {
  template <typename... Us, qualifiers Q>
    requires sizeof...(Us) == sizeof...(Ts)
          && (constructible_from<Ts, Q<Us>> && ...)
  tuple(Q<tuple<Us...>> rhs);
};
template <typename... Ts>
struct tuple {
  template <typename... Us, typename Rhs>
    requires sizeof...(Us) == sizeof...(Ts)
          && (constructible_from<Ts, copy_cvref_t<Rhs, Us>> && ...)
  tuple(Rhs&& rhs : tuple<Us...>);
};
`Q`修飾子 Circle
template <typename D>
struct view_interface {
  template <qualifiers Q>
    requires forward_range<Q<D>>
  constexpr bool empty(this Q<D>& self)
  {
    return ranges::begin(self) == ranges::end(self);
  }
};
template <typename D>
struct view_interface {
  template <class Self>
    requires forward_range<Self>
  constexpr bool empty(this Self& self : D)
  {
    return ranges::begin(self) == ranges::end(self);
  }
};

Q修飾子と比較して、Circleのアプローチでは推論された修飾子の伝播に追加の作業が必要となりますが、それはかなり軽微です。

このアプローチの欠点は、Q同様に構文と振る舞いが奇妙であることと、将来的にC++の宣言構文を改善しようとしたときにこれが導入されているとそれを妨げてしまう点です。

この文書はまだ提案に至っておらず、引き続き構文候補を募集しています。

P2494R2 Relaxing range adaptors to allow for move only types

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

以前の記事を参照

このリビジョンでの変更はよくわかりません、多分文言の軽微な修正のみです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2513R4 char8_t Compatibility and Portability Fix

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

以前の記事を参照

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

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています(正確にはC++20へのDR)。

P2547R1 Language support for customisable functions

カスタマイゼーションポイントの言語サポートの提案。

以前の記事を参照

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

  • virtual= 0;の代わりにコンテキスト依存のキーワードcustomisableを使用するようにした
  • デフォルトの実装はcustomisable関数の宣言と同時に定義できなくなった
    • 関数仮引数の処理とテンプレート実引数の処理は互いにかなり異なり、同居させると混乱を招くため
  • オーバーライドできない関数に注釈をつけるためのfinalのサポート
  • カスタマイズ可能な関数のオーバーロード解決を変更して、Customisable Function Prototype(CFP)自体の名前空間及び関連エンティティを常に考慮するようにした
    • これによって、ジェネリックなオーバーライド(特定の型ではなくコンセプトに対するカスタマイズ)を確実に見つかる場所で定義可能となる
    • また、テンプレートなcustomisable関数のオーバーライドを、CFOへの明示的な型のテンプレート実引数の名前空間で発見できる
  • テンプレートなcustomisable関数とジェネリックcustomisable関数の例を追加
  • 用語の一貫性の向上
    • CPOの代わりにカスタマイズ可能な関数(customisable functions)とCFOを使用する

このリビジョンによる変更によって、以前の例は次のように変更されます

namespace std::execution {
  // execution::connect CFPの宣言
  template<sender S, receiver R>
  operation_state auto connect(S s, R r) customisable;
}
namespace std::ranges {
  // std::ranges::contains CFPの宣言
  template<input_range R, typename Value>
    requires equality_comparable_with<range_reference_t<R>, Value>
  bool contains(R range, Value v) customisable;

  // std::ranges::contains CFPのデフォルト実装
  template<input_range R, typename Value>
    requires equality_comparable_with<range_reference_t<R>, Value>
  bool contains(R&& range, const Value& v) default {
    for (const auto& x : range) {
      if (x == v) return true;
    }
    return false;
  }
}
namespace std {
  template<class Key, class Compare, class Allocator>
  class set {
  
  // ...

  private:
  
    // std::ranges::contains CFPのHidden friendsによるカスタマイズ
    template<typename V>
      requires requires(const set& s, const V& v) { s.contains(v); }
    friend bool ranges::contains(const set& s, const V& v) override {
      return s.contains(v);
    }
  };
}
namespace std {
  template<class Key, class Hash, class KeyEq, class Allocator>
  class unordered_set { ... };

  // std::ranges::contains CFPのクラス定義外でのカスタマイズ
  template<class Key, class Hash, class Eq, class Allocator, class Value>
    requires(const unordered_set<Key,Hash,Eq, Allocator>& s, const Value& v) {
      s.contains(v);
    }
  bool ranges::contains(const unordered_set<Key,Hash,Eq,Allocator>& s,
                        const Value& v) override {
    return s.contains(v);
  }
}

finalの使用例

namespace std {
  // 型指定get()
  template<typename T, typename Obj>
  auto get(Obj&& obj) customisable; // (1)

  // インデックス指定get()
  template<size_t N, typename Obj>
  auto get(Obj&& obj) customisable; // (2)

  // インデックスを引数によって指定する非テンプレートのget()
  template<size_t N, typename Obj>
    requires (Obj&& obj) {
      get<N>(std::forward<Obj>(obj));
    }
  auto get(Obj&& obj, std::integral_constant<size_t, N>) final -> decltype(auto) {
    return get<N>(std::forward<Obj>(obj));
  }
}

struct my_tuple {
  int x;
  float y;

  // (1)をカスタマイズ
  friend int&   std::get<int>(my_tuple& self) noexcept override { return self.x; }
  friend float& std::get<float>(my_tuple& self) noexcept override { return self.y; }

  // (2)をカスタマイズ
  friend int&   std::get<0>(my_tuple& self) noexcept override { return self.x; }
  friend float& std::get<1>(my_tuple& self) noexcept override { return self.y; }
};

上記例の使用例

void example() {
  my_tuple t = {42, 0.0f};
  
  int& x1 = std::get<0>(t);
  float& y1 = std::get<1>(t);

  int& x2 = std::get<int>(t);
  float& y2 = std::get<float>(t);
  
  int& x3 = std::get(t, std::integral_constant<std::size_t, 0>{});
  float& y3 = std::get(t, std::integral_constant<std::size_t, 1>{});
}

CFOの明示的な引数の推論例(上記のgetを使用している)

template<typename T, std::size_t N>
struct array {
  T data[N];
  
  // 関数テンプレートの仮引数からCFOの明示的なテンプレート引数(インデックスN)を推定する
  template<std::size_t Idx>
    requires (Idx < N)
  friend T& std::get<Idx>(array& self) noexcept override { 
    return self.data[Idx];
  }
};

template<typename First, typename Second>
struct pair {
  First first;
  Second second;
  
  // クラステンプレートの仮引数からCFOの明示的なテンプレート引数(型T)を推定する
  friend First& std::get<First>(pair& self) noexcept override
        requires (!std::same_as<First, Second>)
  {
    return self.first;
  }

  // ...
};

P2548R0 copyable_function

std::move_only_functionに対して、コピー可能なCallableラッパであるcopyable_functionの提案。

C++23で導入されたstd::move_only_functionは、その関数シグネチャconst/参照修飾とnoexceptを指定することができ、呼び出し時に自信のconst性と値カテゴリを保持するCallableオブジェクトまで伝播させたうえで呼び出しを行うことができます。これによって、std::move_only_functionオブジェクトのconst有無と右辺値であるかの状態と、保持するCallbaleオブジェクトの呼び出し環境を一致させることができます。

一方std::functionにはそのようなサポートはなく、そのためにconst修飾のミスマッチバグ等の設計上の問題がいくつか指摘されていました。

  • const修飾を正しく扱えない
  • ムーブのみ可能な(コピーできない)Callableオブジェクトを保持できない
  • 左辺値から呼び出すCallableオブジェクトしか保持できない(参照修飾を正しく扱えない)

std::move_only_functionstd::fucntionの持つこれらの問題と軽微ないくつかの問題(RTTIへの依存、呼び出し時の空チェック)を解決するために導入されましたが、名前が示すとおりにstd::move_only_functionのオブジェクトはムーブしかできません(ムーブしかできないCallableだけを保持可能なわけではありません)。

コピー可能なstd::move_only_functionが欲しい場合はstd::functionを使用するしかないのですが、std::functionには上記のような問題があります。また、後方互換性の保護のためにstd::functionstd::move_only_functionのような設計に変更することもできません。

この提案は、コピー可能かつ現在のstd::functionの問題を解決した、std::move_only_functionのコピー可能なバージョンであるstd::copyable_functionを標準ライブラリに追加する提案です。

std::move_only_functionが保持するCallablecopyableであっても単にmovableでしかなくても大丈夫ですが、std::copyable_functioncopyableCallableしか保持できません。それ以外のところでは、std::move_only_functionにコピーコンストラクタとコピー代入演算子を追加しただけです。

現在 この提案
auto lambda{[&]() /*const*/ { … }};

function<void(void)> func{lambda};  // ✔
const auto & ref{func};

func(); // ✔
ref();  // ✔
auto lambda{[&]() /*const*/ { … }};

copyable_function<void(void)> func0{lambda};    // ✔
const auto & ref0{func0};

func0();  // ✔
ref0();   // ❌ operator() is NOT const! 

copyable_function<void(void) const> func1{lambda};  // ✔
const auto & ref1{func1};

func1();  // ✔
ref1();   // ✔ operator() is const! 
現在 この提案
auto lambda{[&]() mutable { … }};

function<void(void)> func{lambda};  // ✔
const auto & ref{func};

func(); // ✔
ref();  // ⁉✔ operator() is const! 
        //     this is the infamous constness-bug
auto lambda{[&]() mutable { … }};

copyable_function<void(void)> func{lambda}; // ✔
const auto & ref{func};

func(); // ✔
ref();  // ❌ operator() is NOT const! 

copyable_function<void(void) const> tmp{lambda};  // ❌

P2549R1 std::unexpected should have error() as member accessor

std::unexpectedのエラー値取得関数をerror()という名前にする提案。

以前の記事を参照

このリビジョンでの変更は、ベースとなるstd::expected提案及びワーキングドラフトの更新、LEWGでの投票結果の追記、寄せられたフィードバックの反映、などです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2561R0 operator??

std::expectedなどを返す関数において、エラーの伝播を自動化させる演算子??の提案。

例外を投げうる関数を扱う関数が自身も例外を投げうる場合、例外を伝播させるための構文的なコストはゼロです。

auto foo(int i) noexcept(false) -> int; // might throw an E
auto bar(int i) noexcept(false) -> int; // might throw an E

auto strcat(int i) noexcept(false) -> std::string {
  int f = foo(i);
  int b = bar(i);

  return std::format("{}{}", f, b);
}

// あるいはインライン化してこう書ける
auto strcat(int i) noexcept(false) -> std::string {
  return std::format("{}{}", foo(i), bar(i));
}

例外をハンドルせずに伝播させるために追加で何かを書く必要はありません。

ただし、例外には多くの問題があるためあまり好まれず、その代替手段の一つとしてC++23からはstd::expected<T, E>が使用できます。

auto foo(int i) -> std::expected<int, E>;
auto bar(int i) -> std::expected<int, E>;

auto strcat(int i) -> std::expected<std::string, E> {
  auto f = foo(i);
  if (not f) {
    return std::unexpected(f.error());
  }

  auto b = bar(i);
  if (not b) {
    return std::unexpected(b.error());
  }

  return std::format("{}{}", *f, *b);
}

こちらの場合、エラーをハンドルせず伝播させる場合でも、そのためのかなり冗長なコードを追加しなければならず、値の取り出しにおいても*を使用しなければなりません。また、その際に考慮すべきことがいくつも潜んでいます(適切なムーブなど)。

そのため、std::expectedに似た機能を提供するライブラリでは、このような処理をマクロによってラップする機能を提供しています。

auto strcat(int i) -> std::expected<std::string, E> {
  SOMETHING_TRY(int f, foo(i));
  SOMETHING_TRY(int b, bar(i));
  return std::format("{}{}", f, b);
}

この場合は例外を使用するコードにかなり近くなりますが、マクロを使用していることからf, bの宣言を省いてインライン化することができません。これもまたマクロを工夫することで解決できますが、それは特定のコンパイラ拡張に頼っていたり適切にムーブされないなど効率的とは言えないものです。

コルーチンを用いて近しいシンタックスシュガーを再現することもできます。

auto strcat(int i) -> std::expected<std::string, E> {
  int f = co_await foo(i);
  int b = co_await bar(i);
  co_return std::format("{}{}", f, b);

  // ... or
  co_return std::format("{}{}", co_await foo(i), co_await bar(i));
}

しかし、現在のところコルーチンは動的なメモリ確保を必ずしも回避できないため、これもまた効率的なコードではありません。

結局のところ、現在のC++におけるstd::expectedのエラー伝播手法としては、マクロによるものがベストとなります。

別の言語、例えばRustでは、std::expectedに対応するresult型がエラー伝播によく使用されています。Rustでは、先程のサンプルコードは例えば次のように書けます。

Rust C++
fn strcat(i: i32) -> Result<String, E> {
  let f = match foo(i) {
      Ok(i) => i,
      Err(e) => return Err(e),
  };

  let b = match bar(i) {
      Ok(i) => i,
      Err(e) => return Err(e),
  }

  Ok(format!("{}{}", f, b))
}
auto strcat(int i) -> std::expected<std::string, E> {
  auto f = foo(i);
  if (not f) {
      return std::unexpected(f.error());
  }

  auto b = bar(i);
  if (not b) {
      return std::unexpected(b.error());
  }

  return std::format("{}{}", *f, *b);
}

パターンマッチングの利用によって中間変数が必要ないなど、これだけでもRustの方が良い書き方ができますが、Rustにおいてのベストな書き方はこれではありません。

Rust C++(例外)
fn strcat(i: i32) -> Result<String, E> {
  let f = foo(i)?;
  let b = bar(i)?;
  Ok(format!("{}{}", f, b))

  // ... or simply ...
  Ok(format!("{}{}", foo(i)?, bar(i)?))
}
auto strcat(int i) -> std::string {
  int f = foo(i);
  int b = bar(i);
  return std::format("{}{}", f, b);

  // ... or simply ...
  return std::format("{}{}", foo(i), bar(i));
}

この場合、1文字(?)の構文上のオーバーヘッドによって、C++の例外を用いたコードとほぼ同等の半自動エラー伝播処理を記述できています。1文字とはいえオーバーヘッドではありますが、std::expectedを使用するコードにおけるマクロに比べたらこのオーバーヘッドは無視できるでしょう。

理想的にはこれをC++に導入したいのですが、条件演算子?:と曖昧になる可能性があるためこの?を単項後置演算子として単純に導入できません。

// ?:と?がある場合、次のコードは
auto res = a ? * b ? * c : d;

// 以下の2つのパース先がある
auto res1 = a ? (*(b?) * c) : d;
auto res2 = ((a?) * b) ? (*c) : d;

そのため、この提案では1文字増やした??演算子std::expected等のためのエラー伝播半自動化構文として導入することを提案しています。

この演算子は上で示したRustの?に対応するもので、コンパイル時には範囲forのように展開されます。

展開前 展開後
auto strcat(int i) -> std::expected<std::string, E>{


  int f = foo(i)??;









  int b = bar(i)??;








  return std::format("{}{}", f, b);
}
auto strcat(int i) -> std::expected<std::string, E> {
  using _Return = std::try_traits<
      std::expected<std::string, E>>;

  auto&& __f = foo(i);
  using _TraitsF = std::try_traits<
      std::remove_cvref_t<decltype(__f)>>;
  if (not _TraitsF::is_ok(__f)) {
      return _Return::from_error(
          _TraitsF::extract_error(FWD(__f)));
  }
  int f = _TraitsF::extract_value(FWD(__f));

  auto&& __b = bar(i);
  using _TraitsB = std::try_traits<
      std::remove_cvref_t<decltype(__b)>>;
  if (not _TraitsB::is_ok(__b)) {
      return _Return::from_error(
          _TraitsB::extract_error(FWD(__b)));
  }
  int b = _TraitsB::extract_value(FWD(__b));

  return std::format("{}{}", f, b);
}

展開に当たっては、対象のオブジェクトからエラー状態と中身の値を取り出す必要があり、また、それらの値から戻り値をどう構築するかを指定する必要があります。それを担っているのがstd::try_traitsという型特性で、次の静的メンバ関数を持っています

  • is_ok : オブジェクトのエラー状態を取得する
  • extract_value/extract_error : 正常値/エラー値を取得する
  • from_value/from_error : 正常値/エラー値からその型のオブジェクトを構築する

これは、std::expectedのような型に対して簡単にアダプトできます。

// std::optionalでの例
template <class T>
struct try_traits<optional<T>> {
  using value_type = T;
  using error_type = nullopt_t;

  auto is_ok(optional<T> const& o) -> bool {
    return o.has_value();
  }

  // extractors
  auto extract_value(auto&& o) -> auto&& {
    return *FWD(o);
  }
  auto extract_error(auto&&) -> error_type {
    return nullopt;
  }

  // factories
  auto from_value(auto&& v) -> optional<T> {
    return optional<T>(in_place, FWD(v));
  }
  auto from_error(nullopt_t) -> optional<T> {
    return {};
  }
};

// std::expectedでの例
template <class T, class E>
struct try_traits<expected<T, E>> {
  using value_type = T;
  using error_type = E;

  auto is_ok(expected<T, E> const& e) -> bool {
    return e.has_value();
  }

  // extractors
  auto extract_value(auto&& e) -> auto&& {
    return *FWD(e);
  }
  auto extract_error(auto&& e) -> auto&& {
    return FWD(e).error();
  }

  // factories
  auto from_value(auto&& v) -> expected<T, E> {
    return expected<T, E>(in_place, FWD(v));
  }
  auto from_error(auto&& e) -> expected<T, E> {
    return expected<T, E>(unexpect, FWD(e));
  }
};

また、この提案のtry_traitsC#等のnull条件演算子?.のような演算子のために必要なものをすべて提供します。

auto f(int) -> std::expected<std::string, E>;

// 将来の可能性?
auto x = f(42)?.size();

P2579R0 Mitigation strategies for P2036 “Changing scope for lambda trailing-return-type”

P2036R3による後方非互換性を緩和する提案。

P2036R3(ラムダ式の後置戻り値型がキャプチャする変数のスコープの変更)はC++23のWDに導入されており、以前のバージョンに対するDRとして採択されています。P2036については以前の記事を参照。

この提案の検討段階では、この変更によって影響を受けるコードはほぼ無いだろうと思われていました。しかし、clangで実装されたところclangそのもの(llvm/libstdc++)のコードを壊している事が判明しました。それは次のようなコードです

// なんかイテレータ範囲のendの値
auto local_end = ...;

[local_end](decltype(local_end) it) { return it != local_end; };
//          ^^^^^^^^^^^^^^^^^^^

後置戻り値型指定ではなく引数型でキャプチャした変数を参照しているコードが存在しており、P2036R3ではこれはill-formedとしています。なぜなら、ここではまだmutableが見えていないため、decltype((x))の型を正しく求める事ができないためです。しかし、以前はこのxは外の変数をキャプチャしていたため問題にならず、少なくともコードを書いた人間の意図通りには動いていました。

このようなコードはCWG2569として報告され、これを先行実装することで解決されました。ここでは、mutableが現れる(場所に到達する)前にキャプチャ変数がdecltypeなどで使用される場合、decltype(x)は許可するもののdecltype((x))は許可しないようにすることでmutableの影響を受けないようにしつつ既存のコードが壊れないようにしています。

しかしその後、次のような別のコードが壊れている事が報告されました

template <typename It, typename MapFn>
auto MapJoin(It first, It last, MapFn map_fn) {
  return std::accumulate(first, last, map_fn(*first),
                         // a new diagnostic: error: captured variable 'first' cannot appear here
                         [=](typename std::result_of<MapFn(decltype(*first))>::type result) { });
}

void foo() {
  int x = [x](int y[sizeof x]) { return sizeof x; }(0);
}

これらのコード破壊が報告された結果clangではP2036の実装を一旦停止したためこれ以上の破損例を収集できませんでしたが、実装された場合にはより多くのコードを壊すであろう事が予想されます。結果として、clangの実装者(筆者の方)はP2036R3は実装不可能であると考えているようです。

これらのコードに共通することは、C++11時点でジェネリックラムダが導入されていなかったことによる代替手段であることのようです。従って、現在ジェネリックラムダを使用するコードをC++11で書こうとした場合にP2036に違反するコードになる可能があり、コーナーケースであると切って捨てられるほどおかしなコードであるわけではありません。

この提案は、P2036R3の変更を修正してその悪影響を緩和しようとするものです。ここでは、5つのソリューションが提示されています。

  1. CWG2569の修正
  2. mutableが現れる前は、ラムダの外側の変数をキャプチャする
  3. mutableが現れた場合、ラムダ式の引数宣言内でキャプチャを参照するコードをill-formedにする
  4. パース時にmutableキーワードを先読みする
  5. キャプチャされた変数を参照するものの、常にmutable有無を考慮しない

EWGでは5番目の解決策が選択され、この提案はそのための文言を含んでいます。

それぞれのデメリットの概要は次のようになっています

  1. CWG2569の修正
    • ラムダの変数宣言部で一部の用法(decltype(x)など)だけを許可するようにする
    • 許可されるかが式に左右されるため理解しづらい(decltype(*x)はngなど)
    • 前述の通り、これだけでは破損するコードがまだある
  2. mutableが現れる前は、ラムダの外側の変数をキャプチャする
    • つまりC++20以前(現在)の挙動
    • decltype(expr)の結果がmutableの前後で異なることは、ラムダ式本体開始({)の前後で異なることよりも良いとは言えない
  3. mutableが現れた場合、ラムダ式の引数宣言内でキャプチャを参照するコードをill-formedにする
    • 破損は減少するが完全ではない
    • 後から見つかるmutableによってエラー有無が変わるのは奇妙
  4. パース時にmutableキーワードを先読みする
    • 完璧では無いものの、最善の解決策
    • 実装の負担が大きい
    • 後から見つかるmutableによって構文の意味が変わるのは奇妙
  5. キャプチャされた変数を参照するものの、常にmutable有無を考慮しない
    • decltype((x))の振る舞いが関数引数部分の終端の前後で変化する可能性がある

なお、この場合、キャプチャされた変数xを参照するdecltype((x))xconstでない限り非constとなります。すなわち、デフォルトでmutableがあるかのように扱います。

void f() {
  float x, &r = x;

  [=](decltype((x)) y) {
    decltype((x)) z = x;
  };  // ok、yの型はfloat&, zの型はconst float&

  [=] {
    []<decltype(x) P>;      // ok
    [](decltype((x)) y){};  // ok、yの型はfloat const&(囲むラムダがコピーキャプチャしたxを参照している)
    [x=1](decltype((x)) y){
      decltype((x)) z = x;
    };  // ok, yの型はint&, zの型はconst int&
  };
}

この提案は、CWG Issueと関連する提案でもあることからすでにCWGのレビューをパスしており、今回(2022/07)の全体会議で承認され、C++23入りしています。。

P2585R1 Improving default container formatting

std::formatのコンテナに対するフォーマットを改善する提案。

以前の記事を参照

このリビジョンでの変更は、range_format_kindrange_formatにリネームしたことなどです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2587R1 to_string or not to_string

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

以前の記事を参照

このリビジョンでの変更は、Annex Cセクションを追加したこと、機能テストマクロを追加したこと、to_wstringにも同じ変更を適用するようにしたこと、などです。

P2590R2 Explicit lifetime management

メモリ領域上にあるトリビアルな型のオブジェクトの生存期間を開始させるライブラリ機能の提案。

以前の記事を参照

このリビジョンでの変更は、const/const volatileオーバーロードを追加したこと、noexceptを付加したこと、アライメントに関する事前条件を追加したこと、提案する文言の調整や修正、などです。

この提案は、今回(2022/07)の全体会議で承認され、C++23入りしています。

P2592R1 Hashing support for std::chrono value classes

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

以前の記事を参照

このリビジョンでの変更は、LWGからのフィードバックの反映、カレンダー型の特殊化について構築のされ方によっては未規定の値を取る可能性がある事を追記、Heterogeneous Overloadのサポート(していない)についての追記、などです。

この提案はC++26を目指して現在LEWGでレビュー中です。

P2601R1 Make redundant empty angle brackets optional

クラステンプレート使用時に、不要な<>を省略可能にする提案。

以前の記事を参照

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

  • クラステンプレートと変数テンプレートの両方を対象とすることを明確にした
  • デフォルトテンプレート引数と推定された引数の区別を明確にするために説明と文言を修正
  • 空の<>を省略しても意味がない例とその有無が重要な例を追記

などです。

このリビジョンで追加された例

template <class T = int>
struct C { C(T); };

std::vector<C> v ;  // C<int>
struct S { C c; };  // C<int>
struct D : C {};    // C<int>
void foo(C c);      // C<int>

C x = 1.0;    // C<double>
C<> y = 1.0;  // C<int>, intへの暗黙変換

P2602R1 Poison Pills are Too Toxic

標準ライブラリから、Poison Pillと呼ばれるオーバーロードを削除する提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加したこと、比較系のCPO(<compare>にあるstrong_orderなど)を含めたことです。

この変更はどうやらC++20へのDRとされそうです(まだなってない)。

P2609R1 Relaxing Ranges Just A Smidge

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

以前の記事を参照

このリビジョンでの変更は、indirect_value_tを説明専用にしたこと、P2248R4への影響について追記したこと、__cpp_lib_rangesの値を上げるようにしたこと、などです。

P2610R0 2022-07 Library Evolution Polls

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

次の提案が、LWGに進むための投票にかけられます。

P2613R1 Add the missing empty to mdspan

std::mdspanempty()メンバ関数を追加する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを受けての文言調整のみです。

この提案は今回(2022/07)の全体会議で承認され、C++23入りしています。

P2614R0 Deprecate numeric_limits::has_denorm

std::numeric_limits::has_denorm関連の定数を非推奨化する提案。

std::numeric_limits::has_denormはその環境で、浮動小数点数Tが非正規化数をサポートしているかを調べるものです。これはコンパイル時定数であり、浮動小数点数T非正規化数をサポートしている/いない/わからない、をコンパイル時に取得するものです。

IEE754準拠の浮動小数点数型であっても、ハードウェアによっては非正規化数をサポートしていない場合がありその場合はソフトウェアエミュレーションによってサポートされている場合があります。この場合、同じ系統のハードウェアであっても将来のバージョンでサポートされる可能性があり、この時にABI破壊を回避しようと思うとstd::denorm_indeterminateを常に使用せざるを得なくなります。また、ハードウェアサポートがある場合でも、実行時のフラグ切り替えによって非正規化数をゼロにフラッシュするように設定する事が可能であり、std::numeric_limits::has_denormは必ずしもコンパイル時に確定するプロパティでは無い面があります。

std::numeric_limits::has_denorm_lossは非正規化数が作成され(計算に使用され)る場合に起こる精度の低下をどのように検出できるかを取得するものです。非正規化数が使用されたことによって精度が低下した時にそれを検出する次の2つの方法がIEEE754標準で指定されていました

  1. 非正規化損失(Denormalization loss)
  2. 不正確な結果(Inexact result)

実際には1つ目の実装は存在しなかったため、現在のIEE754からは削除されており、2つ目の実装だけが存在しています。std::numeric_limits::has_denorm_lossはこの2つのどちらがその環境の浮動小数点数型で実装されているかを示すものでしたが、このような理由によりもはや意味がありません。また、この値は実装によってなぜか異なっています(MSVCだけが浮動小数点数型に対してtrueを返す)。

これらの理由から、std::numeric_limits::has_denormstd::numeric_limits::has_denorm_lossは有用なものではなく、最悪勘違いして使用される危険性があるため、非推奨化しようとする提案です。ただし、削除してしまうと互換性の問題を引き起こすため、非推奨に止めようとしています。

P2615R0 Meaningful exports

無意味なexportを行えないようにする提案。

現在のexport宣言にまつわる規定の解釈の一つとして、次のような宣言が許可されているように見えます。

// これは何?
template export void f();
export template void f();

// 本体の関数テンプレートがexportされているならこちらには不要
export template<> void g(int);
template<> export void g(int);

// プライマリテンプレートがexportされていれば不要
export template<class T> struct trait<T*>;

この問題はコア言語のissueとして提起され、この提案はその解決のための文言変更を含んだものです。

ただし、この後でもexport {...}の中でこれらの宣言が現れたとしてもエラーにならないようにされています。exportブロック内では利便性向上のために、本来exportできない宣言が含まれていても単に無視されるようにされる(ようにする傾向にある)ためです。

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

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

std::atomicnotify_one()/wait()操作はC++23で追加され、std::atomicオブジェクトを介したスレッド間の同期プリミティブとして利用できます。

ただ、待機しているスレッドを起床させるnotify_one()操作とstd::atomicオブジェクトの出力(.store())が分かれていることによって、 これを利用した同期プリミティブの移植可能な実装を妨げています。

例えば、std::atomicnotify_one()/wait()のよくある使用法では、std::atomicオブジェクトへ値を出力してからnotify_one()を呼ぶという手順がとられます。この場合に、待機するスレッドがwait()からの復帰時に同期に使用していたstd::atomicオブジェクトをすぐに破棄する場合に問題が起こります。

待機するスレッドでwait()が呼ばれる前に(正確には、その呼び出しで値のチェックが行われる前に)、通知スレッド(notify_one()を呼ぶスレッド)でstd::atomicオブジェクトへの値の出力が行われていた場合、待機スレッドのwait()はすぐにリターンし使用していたstd::atomicオブジェクトの破棄が行われます。すると、通知スレッドではそのように破棄されてしまったstd::atomicオブジェクトに対してnotify_one()を呼ぶ可能性があり、これはいうまでもなく未定義動作です。

#include <atomic>
#include <thread>

int main() {
  {
    // 同期用アトミックオブジェクト
    std::atomic<bool> sync = false;

    std::thread{[&sync]{
      // 値をtrueに更新してから
      sync.store(true);   // #1
      // 待機スレッドを起床させる
      sync.notify_one();  // #2
    }}.detach();

    // 値が更新(trueになる)されるまで待機
    sync.wait(false); // #3
    // 終わったら即リターン、syncは破棄される
  } // #4
}

.wait()では引数に渡された値と現在の値を比較して、等しい場合にブロッキングし、等しく無い場合はすぐリターンします。この例では処理が#1 -> #3 -> #4 -> #2の順番で起こる可能性があり、起こった場合に未定義動作となります。

この例は恣意的に見えますが、例えばstd::atomicを用いてstd::binary_semaphoreが実装されていた場合、このことは表面化しませんが同様の問題を潜在的に引き起こします。

#include <semaphore>
#include <thread>

int main() {
  {
    // binary_semaphoreがstd::atomicを用いて実装されていたとすると・・・
    std::binary_semaphore sync;

    std::thread{[&sync]{
      sync.release();
    }}.detach();

    sync.acquire();
  }
}

このコードだと先ほどよりも問題が見えにくくなっています。std::atomicnotify_one()/wait()を用いて他の同期プリミティブを実装する場合はこの問題を避けるための工夫が必要になり、それらのワークアラウンドはパフォーマンスを損ねたり移植性が無かったりと問題があります。

実は標準ライブラリの主要3実装(GCC/clang/MSVC)におけるstd::binary_semaphorestd::counting_semaphore)はまさにstd::atomicを利用して実装されています。ただし、そこではstd::atomicオブジェクトのアドレスのみを使用して値にアクセスしないため、上記のようなライフタイムにまつわる問題は起こりません。ただしこれは、この3つの実装がプラットフォームの対応する操作に関する追加の知識を仮定できるために可能になっているだけで、その他の標準ライブラリ実装がこの方法を取るかどうかはわからず、ユーザーは同様の仮定のもとでstd::atomicを使用してstd::binary_semaphoreのようなものを安全かつ移植可能に実装することはできません。

この提案はこの問題の解決を図るもので、次の2つの解決策を提示しています。

  1. 名前空間スコープのstd::atomic_notify_one()/std::atomic_notify_all()の規定を変更して、生存期間が終了しているstd::atomicオブジェクトへのポインタを渡されるようにする。
    • 渡されたポインタにはアクセスしないことを保証する
  2. std::atomicオブジェクトの.store()を呼び出す可能性がある関数ごとに、通知操作を融合したオーバーロードを追加する。
    • std::memory_notification列挙体を追加して、それを引数に取るようにする

1つ目の方法では、最初のサンプルコードは次のように書き換えられます

#include <atomic>
#include <thread>

int main() {
  {
    // 同期用アトミックオブジェクト
    std::atomic<bool> sync = false;

    std::thread{[&sync]{
      // 破棄される前にアドレスを取得
      auto* pa = &sync;
      // 値をtrueに更新
      sync.store(true);
      // 通知
      std::atomic_notify_one(pa);
    }}.detach();

    // 値が更新(trueになる)されるまで待機
    sync.wait(false);
  }

この時、ポインタpapaの参照先オブジェクトが破棄された後で使用することが有効であるかには議論があり、Pointer lifetime-end zapという問題として知られています(詳細は以前の記事参照)

したがって、この解決策を適用するためにはコア言語にこれらの提案による解決が導入される必要があります。

2つ目の方法では、次のような列挙体とその定数を標準ライブラリに追加し、.store()などの値を変更する関数にこれを受け取るオーバーロードを追加します。

namespace std {
  enum class memory_notification : unspecified {
    notify_none = unspecified,
    notify_one = unspecified,
    notify_all = unspecified
  };
  inline constexpr auto memory_notify_none = memory_notification::notify_none;
  inline constexpr auto memory_notify_one = memory_notification::notify_one;
  inline constexpr auto memory_notify_all = memory_notification::notify_all;
}

この方法では、最初のコードは次のようになります

#include <atomic>
#include <thread>

int main() {
  {
    // 同期用アトミックオブジェクト
    std::atomic<bool> sync = false;

    std::thread{[&sync]{
      // 値をtrueに更新して通知
      sync.store(true, std::memory_notify_one);
    }}.detach();

    // 値が更新(trueになる)されるまで待機
    sync.wait(false);
  }
}

実際の実装ではstd::atomicオブジェクトのアドレスを取ってからストア操作と通知操作を行う(1のような方法)が取られる可能性がありますが、それは実装定義の振る舞いとして(現在のstd::counting_semaphoreの実装のように)動作が保証されるため、ユーザーコードで同じことをした場合の未定義動作を回避することができます。

P2617R0 Responses to NB comments on DTS 12907 "Extensions to C++ for Transactional Memory Version 2"

Transactional Memory TS2に寄せられたNBコメントを受けての修正を反映する提案。

6つのNB(national body)コメント(WG21の各国毎のサブグループからのレビュー結果みたいなもの)が寄せられ、その指摘に対処するための文言変更が含まれています。どうやら全てカナダの委員会メンバからのものです。

この提案は既に2022年7月の全体会議で承認されたようです。

P2618R0 C++ Standard Library Issues to be moved in Virtual Plenary, Jul. 2022

今回(2022/07)の会議で採択された標準ライブラリについてのIssue報告とその解決。

  1. 3564. transform_view::iterator<true>::value_type and iterator_category should use const F&
  2. 3617. function/packaged_task deduction guides and deducing this
  3. 3656. Inconsistent bit operations returning a count
  4. 3659. Consider ATOMIC_FLAG_INIT undeprecation
  5. 3670. Cpp17InputIterators don't have integer-class difference types
  6. 3671. atomic_fetch_xor missing from stdatomic.h
  7. 3672. common_iterator::operator->() should return by value
  8. 3683. operator== for polymorphic_allocator cannot deduce template argument in common cases
  9. 3687. expected<cv void, E> move constructor should move
  10. 3692. zip_view::iterator's operator<=> is overconstrained
  11. 3701. Make formatter<remove_cvref_t<const charT[N]>, charT> requirement explicit
  12. 3702. Should zip_transform_view::iterator remove operator<?
  13. 3703. Missing requirements for expected<T, E> requires is_void<T>
  14. 3704. LWG 2059 added overloads that might be ill-formed for sets
  15. 3705. Hashability shouldn't depend on basic_string's allocator
  16. 3707. chunk_view::outer-iterator::value_type::size should return unsigned type
  17. 3708. take_while_view::sentinel's conversion constructor should move
  18. 3709. LWG-3703 was underly ambitious
  19. 3710. The end of chunk_view for input ranges can be const
  20. 3711. Missing preconditions for slide_view constructor
  21. 3712. chunk_view and slide_view should not be default_initializable
  22. 3713. Sorted with respect to comparator (only)
  23. 3715. view_interface::empty is overconstrained
  24. 3719. Directory iterators should be usable with default sentinel
  25. 3721. Allow an arg-id with a value of zero for width in std-format-spec
  26. 3724. decay-copy should be constrained

P2620R0 Lifting artificial restriction on universal character names

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

名前付文字エスケープ(Named character escape)はC++23で導入されたもので、U'\N{LATIN CAPITAL LETTER A WITH MACRON}'のようにユニバーサルキャラクタ名を指定するものです。詳細は以前の記事を参照

この提案の指摘している問題とは次のようなものです

int main() {
  auto \N{LATIN CAPITAL LETTER I} = 42; // ng、Iはユニバーサルキャラクタ名で指定できない
  auto \N{LATIN CAPITAL LETTER I WITH DOT ABOVE} = 42 ; // ok
}

LATIN CAPITAL LETTER IとはIU+0049)の文字(アルファベットのI)であり、これは基本文字集合に含まれる文字であるためユニバーサルキャラクタ名によって指定できません。LATIN CAPITAL LETTER I WITH DOT ABOVEはIの上にドットがついている文字İU+0130)で、これは基本文字集合に含まれ無いためユニバーサルキャラクタ名によって指定することができます。

これらのことは、文字/文字列リテラル内では区別されないため問題になりませんが、識別子で使用された時だけこのような違いが生じます。この提案は、この制限を取り払おうとするものです。

P2621R0 UB? In my Lexer?

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

この提案によれば、次のようなコードは規格的には未定義動作となるようです

int \\ // UB : 複数行にわたるユニバーサル文字名
u\
0\
3\
9\
1 = 0;

#define CONCAT(x, y) x ## y
int CONCAT(\, u0393) = 0; // UB: マクロ展開によって形成されるユニバーサル文字名

// UB: 閉じていない文字列リテラル
const char * foo = "

この提案は、これらの未定義動作を実際の実装に合わせる形で振る舞いを定義しようとするものです。

UB GCC clang EDG MSVC
複数行UCN Supported Supported Error Supported
##によるUCNの形成 Supported Supported Supported Supported
閉じていない文字(列)リテラル ill-formed ill-formed ill-formed ill-formed

これらのことを踏まえて、この提案は3つのUBを次のようにしようとしています

UB 提案
複数行UCN Well-formed
##によるUCNの形成 Well-formed
閉じていない文字(列)リテラル ill-formed

従って、MSVCの複数行UCN実装だけがこの提案の影響を受けます。しかし、現在はエラーになっているのでその影響は破壊的なものではありません。

P2622R0 Core Language Working Group "ready" Issues for the July, 2022 meeting

今回(2022/07)の会議で採択されたコア言語についてのIssue報告とその解決。

  1. 2355. Deducing noexcept-specifiers
  2. 2405. Additional type-dependent expressions
  3. 2507. Default arguments for operator[]
  4. 2534. Value category of pseudo-destructor expression
  5. 2535. Type punning in class member access
  6. 2540. Unspecified interpretation of numeric-escape-sequence
  7. 2571. Evaluation order for subscripting
  8. 2582. Differing member lookup from nested classes
  9. 2585. Name lookup for coroutine allocation
  10. 2586. Explicit object parameter for assignment and comparison
  11. 2594. Disallowing a global function template main
  12. 2597. Replaceable allocation and deallocation functions in the global module
  13. 2606. static_cast from "pointer to void" does not handle similar types
  14. 2608. Omitting an empty template argument list

P2623R0 implicit constant initialization

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

一時オブジェクトへの参照によってダングリング参照が発生するのは主に次の2つの場合です

  1. 関数から返された参照
  2. 関数から返された、値のセマンティクスを持たないオブジェクト(std::string_viewなど)

例えば2つ目の場合だと、次のようなコードで簡単にダングリング参照を生成できます

using namespace std::string_literals;

int main () {
  std::string_view sv = "hello world"s; // この行以降svはダングリング参照となる、その使用はUB
}

"hello world"sはユーザー定義リテラルsによってstd::stringの一時オブジェクトを生成します。それをstd::string_viewでバインドすると、すぐにその一時オブジェクトの寿命が尽きてダングリングとなります。

この提案の目的は、このコードがUB(ダングリング参照)にならないようにすることです。この例では、定数式"hello world"sが通常の文字列リテラルと同様に静的記憶域期間(static storage duration)を持つようにする(暗黙的な定数初期化を行う)ことで、ダングリング参照の生成を防止しようとしています。

また、std::string_viewによるダングリングは次のように間接的に発生する場合もあります

std::string operator+(std::string_view s1, std::string_view s2) {
  return std::string{s1} + std::string{s2};
}

auto f() {
  std::string_view sv = "hi";
  sv = sv + sv; // svはダングリング
  ...
}

sv + svの結果はstd::stringの一時オブジェクトであり、その寿命はその式の終わり(;)までです。

この提案では、この場合にこの式の結果生成される一時オブジェクトの生存期間をその式ではなく囲むブロックのスコープとバインドさせることで、このようなダングリングを防止しようとしています。

もちろん、この場合でもこの参照(sv)をこのブロックの外に持ち出してしまえばダングリング参照となりますがそれは現在でも同じことで、この提案の目的はダングリング参照の発生を抑制することにあり、完全に失くすことを目指してはいません。

1つ目の場合(関数の参照戻り値)では、次のような場合にダングリング参照を生成できます

struct X { int a, b; };

const int& f(const X& x) { return x.a; }  // 引数のメンバへの参照を返す

int main() {
  const int& a = f({4, 2}); // 一時オブジェクトを引数で与える
                            // aはダングリング参照、UB
}

この時でも、{4, 2}の一時オブジェクトの生存期間が囲むブロックのスコープに紐づいていれば、ダングリング参照は生成されません。

std::stringでは、この例とよく似たことを簡単に起こすことができます。

char& c = std::string{"hello my pretty long string"}[0];
c = 'x'; // cはダングリング参照、UB
std::cout << "c: " << c << '\n'; // cはダングリング参照、UB

この時でも一時オブジェクトの生存期間がその式ではなく囲むブロックに紐づいていればダングリングを回避できます。ここで、このような一時オブジェクトの寿命にまつわる問題を回避するために一時オブジェクトを変数に受けてみると現在の一時オブジェクトの生存期間のルールがプログラマの期待と一致していないことが垣間見えます。

auto anonymous = std::string{"hello my pretty long string"};
char& c = anonymous[0];
c = 'x'; // ok、cはダングリングではない
std::cout << "c: " << c << '\n'; // ok、cはダングリングではない

これはプログラマから見ればほぼ同じコードですが、このように一時オブジェクトに名前づけをするだけでダングリング参照の生成を回避できます。しかしこれは、一時オブジェクトの生存期間を囲むブロックに紐づけるという操作を手動でやっているだけです。

このようなことが意図せず発生しうるものとして範囲forがよく知られています。

for (auto x : reversed(make_vector())) { ... }

make_vector()std::vectorの右辺値を返し、reversed()std::ranges::owning_viewのような一時オブジェクトの生存期間延長のためのケアをしない場合、この範囲for全体はダングリングした範囲をイテレートします。

例えば、範囲forは次のように展開されています

{// containing block
  auto&& rg = reversed(make_vector());  // この行でmake_vector()の戻り値の寿命が尽きる
  auto pos = rg.begin();
  auto end = rg.end();
  for ( ; pos != end; ++pos ) {
    auto x = *pos;
    ...
  }
}

この時でも、一時オブジェクトの生存期間が囲むブロックに紐づいていれば、このようなUBを回避できます。そのことは、一時オブジェクトに明示的に名前を与えてみるとわかります

{// containing block
  auto anonymous1 = make_vector();
  auto anonymous2 = reversed(anonymous1);
  auto pos = anonymous2.begin();
  auto end = anonymous2.end();
  for ( ; pos != end; ++pos ) {
    auto x = *pos;
    ...
  }
}

この提案の主張することは、プログラマから見れば一時オブジェクトとは名前のない変数であるということです。その観点から、一時オブジェクトの寿命を通常の変数のようにすればダングリング参照の発生を減らすだけでなく、ダングリングする可能性のある関数戻り値を受けるための余計な変数の名前づけを削減することもできます。これによって、一時オブジェクトに注意して関数戻り値を命名するのではなく、一時オブジェクトのまま使用することを奨励することすらできるようになります。

より詳細には、この提案ではこれらの一時オブジェクトのうち、暗黙的な定数初期化が可能な場合(constexprコンストラクタを持つ型のconst参照)にはコンパイル時に定数初期化して静的記憶域期間を与えることで一時オブジェクトではなくし、そのような定数初期化ができない一時オブジェクトについてはその寿命を囲むブロックスコープにまで延長(通常の名前付き変数と同様に)することで、ダングリング参照の発生を防止しようとしています。

// std::mapからキーに対応する値を取得する、なければ指定したデフォルトを返す
const V& findOrDefault(const std::map<K,V>& m, const K& key, const V& defvalue);

void f() {
  std::map<std::string, std::string> myMap;
  const std::string& s = findOrDefault(myMap, key, "none"); // "none"はstd::stringの一時オブジェクト
  // 現在はsはダングリング参照
  // この提案後は定数初期化されたグローバルな"none"(std::stringオブジェクト)を指す
}

std::string make_str(); // 非constexpr関数

void g() {
  std::map<std::string, std::string> myMap;
  const std::string& s = findOrDefault(myMap, key, make_str());  // 実行時文字列で使用した場合
  // 現在はsはダングリング参照
  // この提案後はmake_str()の戻り値の一時オブジェクトの寿命は囲むスコープに拡張されるため、ダングリングではなくなる
}

一時オブジェクトの生存期間を囲むブロックに拡張するのは、現在のC++でも制限的ながら起こっており、このことは全く新しいことではありません。

template<typename T> using id = T;

int i = 1;
int&& a = id<int[3]>{1, 2, 3}[i]; // 配列の一時オブジェクトの寿命はaの寿命と同期する
const int& b = static_cast<const int&>(0); // intの一時オブジェクトの寿命はbの寿命と同期する
int&& c = cond ? id<int[3]>{1, 2, 3}[i] : static_cast<int&&>(0);  // 条件演算子の両方のオペランドの一時オブジェクトの寿命はcの寿命と同期する

この提案の内容は以前のP0936R0を引き継ぐものです。そちらでは追加の注釈によって引数や戻り値の生存期間の延長(この提案の一時オブジェクトの自動変数化)を行なっていましたが、この提案ではそれと同じことを暗黙的に行います。この提案はそこに一時オブジェクトの暗黙的定数初期化を追加することでP0936を補強するとともに、P0936の内容だけでは適切な対策とならないものについてより安全にしようとするものです。

P2624R0 Make operations on bools more portable

bool型に関する標準の矛盾を正す提案。

標準では、bool型について次のように指定しています

  • 実装定義の符号なし整数型と同じオブジェクト表現、値表現、アライメントを持つ
  • bool型の値はtruefalse

Tのオブジェクト表現とはTのオブジェクトをunsigned char[N]で参照した時のバイト列のことで、Tの値表現とはTの値を保持するビット列のことです。

boolの値は2つしか取れないとする場合、少なくとも256の異なる値を取れる符号なし整数型と同じ値表現として実装することはできません。この矛盾によって、bool型のどのような実装も標準に適合することはできておらず、さまざまな方法で標準の規定を近似しています。

  • clang : 1バイトオブジェクト中の1ビットのビットフィールドであるかのように実装
    • 下位1ビットのみが値に関与し、残りはパディングビット
    • 2つの値のみを取れるという規定を満たすものの、ベースとなる符号なし整数型と同じ値表現を持たない
  • GCC/MSVC : enum bool : unsigned char { false, true };のような型として実装
    • 符号なし整数型と同じ値表現を持つものの、異なる256の値を持つことができる

この違いによって、微妙な振る舞いの違いを観測することができます。

// clangは常に 0 or 1のどちらかを返す
// msvc & gccは 0, 1, -1のどれかを返す
int test1(bool b) { 
  switch(b) {
    case false: return 0;
    case true: return 1;
    default: return -1;
  }
}
 
// clang & msvcは常に 1を返す
// gccは 1か2のどちらかを返す
int test2(bool b) {
  int n = 0;

  if (b) n++;
  if (!b) n++;
  
  return n;
}
 
// clangは常に 0 or 2のどちらかを返す
// msvc & gccは 0~510の間の任意の値を返す
int test3(bool b) { return b + b; }
 
// clang & msvcは常に 0 or 1のどちらかを返す
// gccは 0~255の間の任意の値を返す
int test4(bool b) { return b || b; }

この提案の目的は、これらのことを正すことでboolを含む式が数学的論理と一致した予測可能で意外性のない結果を返すようにし、bool型の変数を安全かつポータブルに使用可能とすることです。

この提案の内容は、clangは既に準拠していますがGCC/MSVCはそうではなく、GCC/MSVCの現在の挙動に依存しているコードは動作が変更されることになります。

おわり

この記事のMarkdownソース