[C++]地に足のついた範囲for文

この記事はC++ Advent Calendar 2022の5日目の記事です。

問題です。次のコードには未定義動作が少なくとも1つ含まれています。それは何でしょう?

#include <vector>
#include <string>

// どこかで定義されているとして
auto f() -> std::vector<std::string>;

int main() {
  for (auto&& str : f()) {
    std::cout << str << '\n';
  }

  for (auto&& c : f().at(0)) {
    std::cout << c << ' ';
  }
}

以下、この記事ではここのf()をたびたび再利用しますが、宣言は再掲しません。

答え

#include <vector>
#include <string>

auto f() -> std::vector<std::string>;

int main() {
  for (auto&& str : f()) {
    std::cout << str << '\n';
  }

  for (auto&& c : f().at(0)) { // 👈 この行
    //            ^^^^^^^^^   
    std::cout << c << ' ';
  }
}

f()std::stringを要素に持つstd::vectorprvalueを返す関数です。その戻り値は一時オブジェクトであるので、値として受けるかauto&&で受けるなどして寿命を延長する必要があります。範囲for文でもそれは行われるので、最初のfor文は問題ありません。

ところが、2つ目のfor文はf()の戻り値からその要素を引き出しています。ここで問題なのは、要素数が不明なことではありません。f().at()の戻り値はlvaluestd::string&)であり、範囲forはこの結果のオブジェクトだけを保存してループを廻してくれます。その結果、f()の直接の戻り値はf().at(0)の後で捨てられ、当然ここから取得したstd::string&の参照はダングリング参照となります。そして、ダングリング参照のあらゆる利用は未定義動作です。

なぜ?

範囲for文はシンタックスシュガーであり、その実態は通常のfor文によるコードへ展開される形で実行されます。

例えば、規格においては範囲forの構文はつぎのように規定されています

for ( init-statement(opt) for-range-declaration : for-range-initializer ) statement

init-statementforの初期化式(C++20 初期化式をともなう範囲for文)で(opt)は省略可能であることを表します。

for-range-declarationfor(auto&& v : r)auto&& vの部分で、for-range-initializerrの部分です。

残ったstatementfor文の本体です。

そして、これは次のように展開されて実行されます

{
    init-statement(opt)

    auto &&range = for-range-initializer ;  // イテレート対象オブジェクトの保持
    auto begin = begin-expr ; // std::begin(range)相当
    auto end = end-expr ;     // std::end(range)相当
    for ( ; begin != end; ++begin ) {
        for-range-declaration = * begin ;
        statement
    }
}

つまりはうまい事イテレータを使ったループに書き換えているわけです。そして、問題は展開後ブロック内の3行目にあります。

auto &&range = for-range-initializer ;

この式では、auto&&で範囲forのイテレート対象オブジェクトを受けており、これによって左辺値も右辺値も同じ構文で受けられ、なおかつ右辺値に対しては寿命延長がなされます。ここに先程のfor文から実際の式をあてはめてみてみましょう。

// 1つ目のforから
auto &&range = f() ;  // ✅ ok

// 2つ目のforから
auto &&range = f().at(0) ;  // 💀 UB

2つ目の初期化式の何が問題なのかというと、変数rangeに受けられているのはf().at(0)の戻り値(std::string&)であって、f()の直接の戻り値であり.at(0)で取り出したstd::stringの本体を所有するオブジェクト(std::vector<std::string>)はどこにも受けられていないからです。

このような一時オブジェクトの寿命(lifetime)はその完全式の終わりに尽きる、と規定されていて、それはとても簡単にはその式を閉じる;です。すなわち、この2つ目の初期化式ではf()の戻り値の寿命はこの行で尽き、そこから取り出されたすべての参照はダングリング参照となります。

これを回避するにはf()の戻り値を直接受けてからその要素を参照すればいいので、例えば上記初期化式を次のようにすればいいわけです

auto &&range0 = f();            // ✅ ok
auto &&range = range0.at(0) ;   // ✅ ok

ただし、ユーザーコードからでは展開後のコードをこのようにすることはできないので、範囲forの構文でできる範囲の事をしなければなりません。

int main() {
  {
    // 範囲forの外で受けておく
    auto tmp = f();
    for (auto&& c : tmp.at(0)) {  // ✅ ok
      ...
    }
  }

  {
    // 初期化式を利用する
    for (auto tmp = f(); auto&& c : tmp.at(0)) {  // ✅ ok
      ...
    }
  }
}

C++20で追加された範囲for文における初期化式は、この問題の回避策として導入されたものでもあります。

その他の例

これだけならめでたしめでたしで終わりそうですので、さらに変な例を置いておきます。

struct Person {
  std::vector<int> values;

  const auto& getValues() const {
    return values;
  }
};

// prvalueを返す
auto createPerson() -> Person;

int main() {
  for (auto elem : createPerson().values) {       // ✅ ok
    ...
  }

  for (auto elem : createPerson().getValues()) {  // 💀 UB
    ...
  }
}

なんでこれ1つ目のfor文がokになるんでしょうね。

#include <optional>
#include <string>

auto f() -> std::optional<std::string>;

int main() {
  for (auto c : f().value()) {  // 💀 UB
    ...
  }
}
#include <optional>
#include <string>

struct S {
  std::string str;

  auto& value() && {
    return str;
  }

  auto&& rvalue() && {
    return std::move(str);
  }
};

auto f() -> S;

auto g() -> std::optional<std::string>;

int main() {
  for (auto c : f().value()) {  // ✅ ok
    ...
  }

  for (auto c : f().rvalue()) { // 💀 UB
    ...
  }
  
  for (auto c : g().value()) {  // 💀 UB
    ...
  }
}

この差が何で生まれるんでしょうか・・・

#include <vector>
#include <span>

auto f() -> std::vector<int>;

int main() {
  for (auto n : std::span{f().data(), 2}) {  // 💀 UB
    ...
  }
}
#include <variant>
#include <string>

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

int main() {
  for (auto c : std::get<std::string>(f())) {  // 💀 UB
    ...
  }
}
#include <tuple>
#include <string>

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

int main() {
  for (auto c : std::get<0>(f())) {  // 💀 UB
    ...
  }
}
#include <map>
#include <string>

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

int main() {
  for (auto c : f()[0]) {  // 💀 UB
    ...
  }
}
#include <coroutine>
#include <string>

// std::lazyはC++26予定
auto f() -> std::lazy<std::string&>;

std::lazy<> g() {
  for (auto c : co_await f()) {  // 💀 UB(コルーチンローカルのstd::stringへの参照を返す場合)
    ...
  }
}

さて、これらの例を見て、これらの問題のあるコードを絶対書かないと断言できるでしょうか?私はやってしまいそうです・・・

初学者やC++言語そのものにさほど興味のないプログラマなど、範囲forの仕様を知らない場合はこの問題に気付くことはできないでしょう。この問題を把握するほど詳しい人でも、この問題の起こる場所が範囲forに隠蔽されていることによって、ぱっと見て気づくことが難しい場合があるでしょう。

この問題は範囲forに初期化子を指定できるようにした程度で解決できるようなものではなく、より確実な解決策が必要な問題です。

C++23における解決

この問題はP2644R0の採択によって、C++23にてようやく解決されます。

解決は単純で、範囲forの初期化式(構文定義上のfor-range-initializer)内で作成されたすべての一時オブジェクトの寿命は範囲for文の完了(ループ終了)まで延長される、と規定されるようになります。

展開後のコードに何かアドホックなものを加えるわけではなく、この規定によってこれを実装したコンパイラでは範囲for文は完全に安全になり、ここまでに紹介したようなUBの例の問題はすべて解消(UBではなくなる)されます。

実際にどのようにこれがなされるのかは実装定義です。Cの複合リテラルのようにするかもしれないし、展開後コードが初期化式を分解しているかもしれません。いずれにせよ、この変更によって既存のプログラムの動作が壊れることはないはずです。

なお、これはC++23に対する修正であり、C++20以前のバージョンに対する欠陥報告ではありません。少なくとも今のところは

紆余曲折

ここからは余談です。

この問題が把握されたのは近年かというとそんなわけはなく、少なくとも13年前(2009年)には把握されていました(CWG Issue 900)。そう、C++11策定よりも前です。また、その後もたびたび同様のIssueが提出されていたようです。

なぜかは知りませんがなかなか解決がされないまま、ようやくこの解決のための提案(P2012R0)が提出されたのが2020年の11月、もはやC++20に間に合わせるのもつらい時期でした。

P2012はEWGの議論においてその解決の必要性が確認されたものの、なぜかその後C++23に向けてP2012を進めるところでコンセンサスが得られず、提案の追求は停止されました。

その後1年ほど動きが無く、もはや忘れられたのかと誰もが思っていた頃、2022年10月後半にドイツのWG21 NB(national body)からのC++23 CD(committee draft)に対するNBコメントと共に、P2644R0が提出されました。

P2644はP2012を踏襲したもので、そこで提案されていた解決策の一つ(範囲forの初期化式内で生成された一時オブジェクトの寿命を延長するように規定する)を再提案するものでした。これがそのまま2022年11月にKona(ハワイ)で行われたWG21全体会議においてスピード採択され、C++23に適用されることになりました。

P2644によれば、P2012が合意を得られなかったのは本質的な一時オブジェクトの寿命問題について、範囲forだけにとどまらないより広範な解決策がのぞまれたため、だったようです。つまり、範囲forの展開後のコードに対するアドホックな対応は忌避され、かといって標準文言による規定も将来の広範な解決策を妨げてしまうかも・・・と考えられたようです。

おそらくそのような解決策とはP2623のようなものをいうのでしょうが、これはC++23に間に合うものでもなく、範囲forのこの問題を解決するための施策は結局何も取られていませんでした。ドイツからのNBコメント及びP2644はそのような状況にしびれを切らして提出されたようです。P2644の提案の内容は、どうやって寿命を延長するだとかいう部分は何も言っていないため、将来的なソリューションを妨げないようにされています。

ところで、P2012もP2644も同じNicolai Josuttisさんという人がメインの著者です。そして記載されているメールアドレスから察するにこの人はドイツの方のようです。

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で80本あります。

一部の記事は次の方々に手伝っていただきました、ご協力ありがとうございました!

もくじ

N4923 Working Draft, Extensions to C++ for Transactional Memory Version 2

P2066ベースの最小トランザクショナルメモリのTechnical Specifications。

差分は下の方のP2682R0の項を参照。

P0592R5 To boldly suggest an overall plan for C++26

C++26に向けて、具体的に取り組むべき項目についての文書。

P2000はC++の大まかな方向性を指し示すような文書ですが、これはより具体的に次の3年間の作業項目(の内最高優先度のもの)をリストアップするものです。

C++26に向けて取り組むべきものとしては次のものが挙げられています

  • C++26入りを目指すもの
  • C++26作業期間中に進捗させるもの
    • 契約
    • パターンマッチ

コルーチンのライブラリサポートはExecutionに含まれており、ネットワークはExecutionとの親和性などの問題から一旦ストップしています。また、特別に優先されてはいませんが線形代数ライブラリの提案も順調に進行しています。

P0876R11 fiber_context - fibers without scheduler

スタックフルコルーチンのためのfiber_contextの提案。

C++20で導入されたコルーチンはスタックレスコルーチンのための言語基盤とライブラリ機能のみでした。この提案はそれに加えてスタックフルコルーチンを導入しようとするものです。

コルーチンではコルーチンの中断・再開のためにコルーチンフレームという保存領域を必要とします。ここにはコルーチン引数やコルーチン内部の自動変数が保存されており、コルーチンが終了しコルーチンハンドルが解放されるまで複数回の中断・再開を跨いで1つの状態を維持しています。

スタックレスコルーチンではコルーチンフレームにコールスタックが保存されていないことから、コルーチン内部でネストした関数呼び出し内部からコルーチンを中断することができません(スタックレスコルーチンは、コルーチンフレームに状態を維持してそれを利用していること以外は通常の関数呼び出しと同じように処理されています)。対してスタックフルコルーチンは、コルーチンフレームに加えてコールスタックも独自に保持していることによって、コルーチン内部でネストした関数呼び出し内からでも中断することができます。

これによって、スタックフルコルーチンは任意のタイミングでコルーチンを中断/再開することができるようになり利便性が増しますが、デメリットとして呼び出しに伴うスタックをほぼ全て自前で用意し利用するため、生成や維持が高コストになります。また、スタックフルコルーチンはその特性から言語機能を必要とせずにライブラリ機能として実装可能で、この提案もコア言語に対する追加・変更を含んでいません。

ここで導入しようとしてるのはfiber_contextというクラス型のみで、これはBoost.Contextに相当するスタックフルコルーチン(提案の用語では専らファイバー(fiber))アプリケーションのための基盤となる機能です。ちょうど、C++20でスタックレスコルーチンの基盤となる言語とライブラリ機能のみが整備されたのと同じ感じです。

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

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

    ~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) &&;

    // stop token handling
    [[nodiscard]] stop_source get_stop_source() noexcept;
    [[nodiscard]] stop_token get_stop_token() const noexcept;
    bool request_stop() noexcept;

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

    void swap(fiber_context& other) noexcept;

  private:
    stop_source ssource; // exposition only
  };
}

fiber_contextコンテキストスイッチ(すなわち、コルーチンの中断と再開)のための最小限の機能を持つクラスです。どのようにコンテキストスイッチを行うかという部分はこれを用いてユーザーが指定するものであるため、スケジューラの機能も含まれてはいません。また、fiber_contextは内部でスタックフルコルーチンのためのスタック等を保持しているためコピーできず、ムーブオンリーなクラスとされています。

基本的には.resume()によって操作します。あるコルーチン中でfc.resume()を呼ぶと現在のコルーチンは中断しfcのコルーチンが再開します。

さらに、std::jthreadと同様のstd::stop_source/std::stop_tokenによる協調的キャンセル機構を備えていることで、そのコンテキスト上の処理のキャンセルを行うことができます(ただし、実際にそれを行うのは処理を定義するユーザーの責任です)。

提案文書より、STLアルゴリズムを使用する例

int maine() {
  // 生成された値をやり取りする中間変数
  int a;

  autocancel consumer, generator;

  // フィボナッチ数列を生成する
  generator = autocancel{[&a, &consumer, &generator](std::fiber_context&& m){
    a = 0;
    int b = 1;

    while (! generator.stop_requested()){
      // generatorを中断し、consumerを再開する
      generator.resume(consumer);
      
      // フィボナッチ数列の計算
      int next = a+b;
      a = b;
      b = next;
    }

    return std::move(m);
  }};

  // generatorが生成した値を出力する
  consumer = autocancel{[&a, &consumer, &generator](std::fiber_context&& m){
    std::vector<int> v(10);

    // vectorに生成した値を保存する、数はこの範囲の長さ=10
    std::generate(v.begin(), v.end(), [&a, &consumer, &generator]() mutable {
      // consumerを中断し、generatorを再開する
      consumer.resume(generator);
      return a;
    });

    std::cout << "v: ";

    for (auto i: v) {
      std::cout << i << " ";
    }

    std::cout << "\n";

    return std::move(m);
  }};

  // consumerの開始
  consumer.resume();
}

出力

v: 0 1 1 2 3 5 8 13 21 34

ここで使用されているautocancelは、fiber_contextによって次のように実装されています

// fiber_context上の処理を自動停止し、安全に終了させるためのRAIIラッパー
class autocancel{
private:
  std::fiber_context f_;

public:
  autocancel() = default;

  template <typename Fn>
  autocancel(Fn&& entry_function)
    : f_{std::forward<Fn>(entry_function)}
  {}

  autocancel& operator=(autocancel&& other) = default;
  
  ~autocancel() {
    // キャンセル要求を送ってから
    f_.request_stop();

    // 実行終了(fiber_contextが空になるの)を待機
    // fiber_contextが空になる=そのコンテキスト上の処理が実行中
    // request_stop()を通知しているので、その処理はそこで終了する(はず
    // fiber_contextのoperator bool()は、!f_.empty() と等価
    while (f_) {
      resume(*this);
    }
  }

  // 処理をキャンセルする
  bool stop_requested() const noexcept {
    return f_.get_stop_source().stop_requested();
  }

  // 初回に処理を開始するためのresume()
  std::fiber_context resume() {
    // これによって、一旦this->f_は空になる
    return std::move(f_).resume();
  }

  // 別のautocancelを受け取り、自身を中断しそれを再開する
  void resume(autocancel& ac) {
    // 別のautocancel(ac)を再開する
    std::move(ac.f_).resume_with([this](std::fiber_context&& f) -> std::fiber_context {
      // このラムダは、ac.f_のコンテキスの最初(再開直前)に呼ばれる
      // ここでfに渡ってくるのはthis->f_だったもの、コンテキストスイッチ時に適切に保存する
      f_ = std::move(f);

      // 空のfiber_contextを返す(これは上記例では捨てられる)
      return {};
    });
  }
};

あるスタックフルコルーチンが実行中である場合そのスタックフルコルーチンのfiber_contextは空の状態(f_.empty() == true)になっています。autocancel::resume()の処理は、制御フローが予測可能でない場合に適切にfiber_contextを伝播するためのパターンです。

fiber_contextを用いることで、Boost.CoroutineやBoost.fiberなどのより高機能なスタックフルコルーチンアプリケーションを記述することができるようになります。提案内にそのリストがあり、これらのアプリケーションの存在こそがスタックフルコルーチンを標準でサポートするべき理由であり、また、このfiber_contextのようなクラスは標準C++の範囲で移植可能なように実装することは不可能であるためこの機能を標準に用意しておくことには価値がある、と述べられています。

P0987R2 polymorphic_allocator instead of type-erasure

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

以前の記事を参照

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

P1061R3 Structured Bindings can introduce a Pack

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

以前の記事を参照

このリビジョンでの変更は、構造化束縛によってパックを導入可能な個所として名前空間スコープを除外していたのを削除した事です。

これによって、構造化束縛宣言が使用可能なところではどこでもタプル等からパックを導入できるようにます。

この提案はEWGのレビューを通過して、C++26をターゲットとしてCWGに転送されています。

P1068R6 Vector API for random number generation

<random>にある既存の分布生成器にイテレータ範囲を乱数で初期化するAPIを追加する提案。

以前の記事を参照

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

  • 提案するベクターAPIメンバ関数からCPOベースのものに変更
  • イテレータベースのAPIを削除(LEWGのレビューにおいてコンセンサスが弱かったため)
  • CPO化に伴って必要なくなったため、以前のリビジョンで導入したコンセプトを削除
  • 設計に関するセクションの拡充

などです。

このリビジョンでは分布生成器クラス毎のメンバ関数.generate()によるAPIから、std::ranges::generate_randomというCPOを用いるものに変更されています。

std::array<float, N> array; 

std::mt19937 eng(777);  // 乱数エンジン
std::uniform_real_distribution dis(1.f, 2.f); // 分布生成器

// 範囲を乱数列で初期化する、現在はこう書くしかない
for (auto& el : array) {
  el = dis(eng);
}

// R5までのAPI、イテレータ範囲を乱数列で初期化する
dis.generate(array.begin(), array.end(), eng);

// このリビジョンのAPI、範囲を乱数列で初期化
std::ranges::generate_random(eng, dis, array);

std::ranges::generate_randomはRangeアルゴリズムと異なりCPOであり、std::ranges::generate_random(E、D, R)のように呼び出して、非メンバ関数generate_random(E、D, R)を呼び出すか、std::ranges::generate(R, [&E,&D]() {return D(E);})にフォールバックします。ここで、Eは乱数エンジン、Dは分布生成器、Rは出力rangeです。

2引数で、std::ranges::generate_random(E、R)のように呼び出すこともでき、分布生成器を用いない以外は先程と同じように呼び出しを行おうとします。

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

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

以前の記事を参照

このリビジョンでの変更は、標準の要件に従うようにAllocatorパラメータの要件を修正したこと、.get_adapted_allocator()noexceptを追加したことなどです。

P1385R7 A proposal to add linear algebra support to the C++ standard library

標準ライブラリに線形代数関連の型と関数を追加する提案。

線形代数は様々な問題領域で直接使用され、現代のコンピューティングの非常に多くのところで線形代数計算が必要とされます。しかし、C++標準ライブラリには線形代数計算を扱うための機能がほとんど含まれていません。

この提案は、その欠点を解消し標準C++の範囲で線形代数計算を扱えるようにしようとするものです。

この提案は線形代数ライブラリとして次のようなことを目指しています

  1. 線形代数に関連する数学的対象や基本操作を表現するための、行列のvocabulary typeを提供する
  2. 直感的かつ教えやすく従来の数学表記法の表現力を最大限模倣した、線形代数計算のための公開APIを提供する
  3. LAPACK、Blaze、Eigen等の既存のライブラリの関数呼び出しシーケンスに近い、すぐに使うことができる(直観的で使いやすい)
  4. 線形代数型を表現するために必要なメモリのソース、所有権、寿命、レイアウト、アクセスを管理するビルディングブロックを提供する
  5. カスタマイズのための分かりやすい機能と手法を提供し、特定のハードウェア上で特定の問題領域に対して性能を最適化できるようにする
  6. カスタマイズの粒度を適切にし、開発者が最小の型と関数を実装するだけで性能向上と他の(カスタイマイズしない)機能との統合ができるようにする

このライブラリのインターフェースの上位の部分は、大きく次の4つに分割されます

  1. エンジン
    • メモリ所有権や寿命、要素アクセスなど、MathObjインスタンスに関連するリソースを管理する実装型
  2. MathObj
    • 対応する数学的抽象概念(例えば行列)をモデル化するための統一インターフェースを提供する
  3. 演算子
    • 必要な数学的構文を提供し、それに期待される算術演算を実行する
  4. operation traits
    • element promotion, engine promotion, arithmetic traitsなどの容器であり
    • エンジン・MathObj演算子を接続する接着剤の役割を提供する
    • MathObj型のテンプレートパラメータであり、算術演算の実行方法を決定する際に使用可能なarithmetic traits演算子に通知する方法を提供する

より下位の部分には、演算子の戻り値型を決定し対応する算術演算をを実行するために、operation traitsによって使用される型特性があります

  • element promotion
    • 2つの要素に対する算術演算の結果の要素型を決定する
  • engine promotion
    • 行列オブジェクトに対する算術演算の結果のエンジン型を決定する
    • その過程で、element promotionを使用して結果の要素型を決定する
  • arithmetic traits
    • 算術演算の結果であるMathObjの型と値を決定する
    • この型特性は、engine promotionを使用して結果のMathObjのエンジン型を決定する
    • 結果の型を決定した後実際の計算を行うメンバ関数も持っている
  • operation selector
    • 算術演算子が演算を行うoperation traitsを選択する手段を提供する
    • 演算子の引数型が異なるoperation traitsを持つ可能性があるため、演算子はこの特性を用いてどのoperation traitsを使用して計算するかを決定する

例えば、MathObj型の一つである行列型、std::matrixstd::matrix<ET, OT>のように、エンジン型EToperation traitsOTを受け取ります。

#include <linear_algebra>

// スタックに配置され固定サイズの3x3行列型
using mat33 = std::matrix<std::fs_matrix_engine<double, 3, 3>, std::matrix_operation_traits>;

std::fs_matrix_engine<T, R, C>T型の値を要素とするRC列の固定サイズ行列のためのエンジン型であり、std::matrix_operation_traitsは行列演算のためのoperation traitsです。

std::matrix_operation_traitsはメンバ型として各種数値演算における結果の型を求める型特性(上記下位レベルの型特性)がデフォルトで指定されています。std::matrix_operation_traitsをカスタマイズした別の型に差し替えることで、演算結果や方法などをユーザーが制御できます。

std::matrix_operation_traitsを使用するstd::matrix型にはデフォルトの演算子オーバーロード)が提供されます。

#include <linear_algebra>

using mat33 = std::matrix<std::fs_matrix_engine<double, 3, 3>, std::matrix_operation_traits>;

int main() {
  // 3x3行列の初期化
  mat33 A = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0};
  mat33 B = {};

  // 要素アクセス
  B(0, 0) = 1.0;
  B(1, 1) = 1.0;
  B(2, 2) = 1.0;

  // 行列計算
  auto add = A + B;
  auto sub = A - B;
  auto mul = A * B;
}

これは見た目通りの3x3行列の計算に対応した結果が得られます。std::matrixの2つのテンプレートパラメータを変更することで配置されるメモリの場所を動的領域にして実行時にサイズを可変にしたり、特定の環境向けに計算の仕方を調整したりできます。

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

P1673R10 A free function linear algebra interface based on the BLAS

標準ライブラリに、BLASをベースとした密行列のための線形代数ライブラリを追加する提案。

以前の記事を参照

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

  • scaledconjugatedの文言を改善
  • すべての行列ビュー関数をconstexprにした
  • mdspanの部分にP2642R1を適用
  • P2642によって不要になったため、layout_blas_generalを削除
  • mdspanの他のレイアウトクラスに倣って、layout_blas_packedを修正
  • mdspanの他のアクセサクラスに倣って、アクセサを修正

などです。

P1709R3 Graph Library

グラフアルゴリズムとデータ構造のためのライブラリ機能の提案。

グラフデータ構造は機械学習などの科学分野やビジネスやゲームをはじめとする一般的なプログラミングにおいて非常によく使用されますが、現在のC++の標準ライブラリにはグラフデータ構造とそれを利用するアルゴリズムのサポートがほぼ皆無です。機械学習の一分野であるディープラーニングが近年大きく注目を集めるなど、グラフデータ構造を扱える標準ライブラリの必要性は増しています。

この提案はグラフデータ構造とそのアルゴリズムのためのライブラリの統一的なインターフェースを提示するもので、概念実証のような段階です。

ここで提案されているAPIはおおよそ次のような感じで使用することができるものです

using G = … <graph definition>
G g(...); // グラフの構築

for (auto& u : g) { // グラフGの頂点をイテレーション
  for (auto& uv : edges(g, u)) { // 頂点uのエッジをイテレーション
    ...; // uとuvを用いた処理
  }
}

for (auto& uv : edges(g)) { // グラフの全てのエッジをイテレーション
  auto& u = in_vertex(g, uv);  // エッジuvの頂点の一方
  auto& v = out_vertex(g, uv); // エッジuvの頂点のもう一方
  ...; // uv, u, vを用いた処理
}

グラフとは頂点の範囲であり、頂点はエッジの範囲を持ちます(つまり、グラフは範囲の範囲です)。エッジに焦点を当てる場合、グラフとは2つの頂点を持つエッジの範囲でもあります。

グラフには有向グラフと無向グラフがあり、このAPIではそれらを統一的に扱うことにも重点を置いています。APIは無向グラフを基本として整備されており、有向グラフに対してはout_付のものを使用するようになっています。ただし、それはエイリアスによってほぼ自動で処理されており、例えば上記例のedges(g, u)は有向グラフに対してはout_edges(g, u)エイリアスとなります。

この提案のAPIは、グラフのプロパティを問い合わせる各種エイリアステンプレートとフリー関数テンプレートを用いて構成されています。

Wikipediaの幅優先探索のページにある「ドイツの都市間の接続を示した例」のグラフを構築するサンプル。

// グラフのデータ型
struct route {
  string from;
  string to;
  int km = 0;

  route(string const& from_city, string const& to_city, int kilometers)
    : from(from_city), to(to_city), km(kilometers) {}
};

std::vector<route> routes{
  {"Frankfürt", "Mannheim", 85}, {"Frankfürt", "Würzburg", 217},
  {"Frankfürt", "Kassel", 173}, {"Mannheim", "Karlsruhe", 80},
  {"Karlsruhe", "Augsburg", 250}, {"Augsburg", "München", 84},
  {"Würzburg", "Erfurt", 186}, {"Würzburg", "Nürnberg", 103},
  {"Nürnberg", "Stuttgart", 183}, {"Nürnberg", "München", 167},
  {"Kassel", "München", 502}
};

// グラフコンテナ型
using G1 = adjacency_list<name_value, weight_value, empty_value,
                          edge_type_undirected, edge_link_double,
                          map_vertex_set_proxy>;

// グラフコンテナの構築
auto g1 = create_adjacency_list<G1>();

// グラフの構築
for (auto& r : routes) {
  create_edge(g, r.from, r.to, r.km);
}

これを用いて、深さ優先探索するサンプル

dfs_vertex_range rng(g, g.find_vertex("Frankfürt"));

for (vertex_iterator_t<G> u = rng.begin(); u != rng.end(); ++u) {
  std::cout << std::string(u.depth() * 2, ' ') << u->name << std::endl;
}
Frankfürt
  Mannheim
  Karlsruhe
    Augsburg
      München
  Würzburg
    Erfurt
    Nürnberg
      Stuttgart
  Kassel

幅優先探索するサンプル

bfs_vertex_range bfs_vtx_rng(g, begin(g) + 2); // Frankfürt

for (auto u = bfs_vtx_rng.begin(); u != bfs_vtx_rng.end(); ++u) {
  std::cout << std::string(u.depth() * 2, ' ') << u->name << std::endl;
}
Frankfürt
Mannheim
Würzburg
Kassel
  Karlsruhe
  Erfurt
  Nürnberg
  München
    Augsburg
    Stuttgart

エッジに注目して深さ優先探索するサンプル

dfs_edge_range rng(g, begin(g) + 2); // Frankfürt

for (vertex_edge_iterator_t<G> uv = rng.begin(); uv != rng.end(); ++uv) {
  vertex_iterator_t<G> u = out_vertex(g, *uv);
  vertex_key_t<G> u_key = vertex_key(g, *u);

  if (uv.is_back_edge()) {
    std::cout << std::string(uv.depth() * 2, ' ')
              << "view " << uv.back_vertex()->name << std::endl;
  } else {
    vtx_iter_t v = out_vertex(g, *uv); // or vertex(g, *uv)
    std::cout << std::string(uv.depth() * 2, ' ') << "travel " << u->name
              << " --> " << v->name << " " << uv->weight << "km" << std::endl;
  }
}
travel Frankfürt --> Mannheim 85km
  travel Mannheim --> Karlsruhe 80km
  travel Karlsruhe --> Augsburg 250km
  travel Augsburg --> München 84km
view München
  travel Frankfürt --> Würzburg 217km
  travel Würzburg --> Erfurt 186km
    view Erfurt
  travel Würzburg --> Nürnberg 103km
  travel Nürnberg --> Stuttgart 183km
    view Stuttgart
  travel Nürnberg --> München 167km
travel Frankfürt --> Kassel 173km
travel Kassel --> München 502km

提案されているグラフアルゴリズムには、ダイクストラ/ベルマンフォード法、各種連結成分の抽出、関節点や推移平方の抽出のほか、std::erase/std::erase_ifなどがあります。

P1759R4 Native handles and file streams

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

以前の記事を参照

このリビジョンでの変更は、ベースとなる文書を最新のワーキングドラフトに更新したことと、.native_handle()の呼び出しはファイルが開かれている時にのみ有効である、としたことなどです。

P1928R1 Merge data-parallel types from the Parallelism TS 2

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

std::simd<T>を含むParallelism TS v2は2018年に発行されました。その後GCC9リリースの後、std::simd<T>の完全な実装(ただしGCC専用)が行われ、GCC11以降はlibstdc++の一部となっています。そして、Parallelism TS v2発行後std::simd<T>に対する十分なフィードバックが寄せられています。

この提案は、それらのフィードバックをstd::simd<T>に適用しつつ、std::simd<T>C++26を目指してワーキングドラフトにマージしようとするものです。

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

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

以前の記事を参照

このリビジョンはC23において同様の#embedが採択されたことを通知するもののようです。

この提案は、C++26に向けてCWGのレビューに進んでいます。どうやら、C23との互換性を取る方向のようです。

P2000R4 Direction for ISO C++

C++26に向けて、C++の標準化の方向性を示す文書。

この文書の内容を念頭に、各種提案の優先順位づけや新しい機能についての議論が行われることになります。

このリビジョンでの大きな変更は、中期目標(3~10年)としてより安全なC++に向けてのセクションが追加されたことです。

P2019R2 Usability improvements for std::thread

std::thread/std::jthreadにおいて、そのスレッドのスタックサイズとスレッド名を実行開始前に設定できるようにする提案。

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

P2047R4 An allocator-aware optional type

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

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

  • alloc_optional()std::allocator_arg_tをとるstd::make_optional()オーバーロードに置き換えた
  • ↑の戻り値の引数順序の修正
  • LWG Issue 2833に関する調整
  • 推論補助に関する議論の追加
  • エイリアスのアプローチには問題があったため、型エイリアスの使用に関する言及を削除
  • std::pmr::optionalAllocator Awareな型もそうでない型もサポートするように規定
  • CTADに関する規定に追加
  • a
  • std::pmr::optional<optional>へ配置
  • std::pmr::optionalをよりアロケータジェネリックなクラスへ変更
    • std::basic_optionalクラスの特殊化として定義
  • フリー関数のswap()を追加

などです。

P2164R7 views::enumerate

元のシーケンスの各要素にインデックスを紐付けた要素からなる新しいシーケンスを作成するRangeアダプタviews::enumerateの提案。

以前の記事を参照

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

  • reference/value_typeとしてenumerate_resultstd::tupleのどちらを選択するかを決定する必要があることを示した
  • iter_move/iter_swapを追加
  • フリースタンディング機能であることを明記
  • 機能テストマクロの追加

などです。

現在の最大の懸案は、enumerate_viewイテレータreference/value_typeをどうするかという点です。R6ではreferenceenumerate_result(集成体)、value_typestd::tupleでした。

enumerate_resultのような明確なメンバ名を持つ集成体が好まれたのは、enumerate_viewの入力要素型がインデックスの型に容易に変換可能である場合に、構造化束縛を使用する事によって取り違える危険性があることが理由でした。

// valueとindexが相互変換可能だったりすると気づけない可能性がある
for(auto && [value, index] : view | std::views::enumerate) {}
for(auto && [index, value] : view | std::views::enumerate) {}

集成体とメンバ名によって、この問題は回避することができます。

for(const auto & e : view | std::views::enumerate) {
  // インデックスと値を間違えづらくなる
  print("{} {} \n", e.index, e.value);
}

一方で、ranges::toでコンテナに入れていく場合などには、要素型はtupleであってほしい要望があります

auto vec = enumerate(view) | ranges::to<std::vector>();
// decltype(vec) : vector<enumerate_result<std::size_t, range_value_t<decltype(view)>>

この要求をいいとこ取りするために、R6ではreferenceenumerate_result(集成体)、value_typestd::tupleという風になっていました。ただし、このアプローチは機能しません。

なぜなら、標準のイテレータコンセプト(特に、indirectly_~系)では、イテレータを間接参照して呼び出す処理の制約においてreference/value_type及びcommon_referenceで呼び出し可能であることが求められています。例えば、std::indirectly_unary_invocableコンセプトでは次のようになっています

template<class F, class I>
concept indirectly_unary_invocable =
  indirectly_readable<I> &&
  copy_constructible<F> &&
  invocable<F&, iter_value_t<I>&> &&    // ここ
  invocable<F&, iter_reference_t<I>> && // ここ
  invocable<F&, iter_common_reference_t<I>> &&  // ここも
  common_reference_with<
    invoke_result_t<F&, iter_value_t<I>&>,
    invoke_result_t<F&, iter_reference_t<I>>>;

このような制約はイテレータ経由の呼び出しやアルゴリズムコンセプトにおいても頻出しています。

この制約は、referencevalue_typeが全く異なる(その使用感さえも異なる)型だと満たすのが困難になります。それはfind()を使用するときに述語をカスタマイズするのを考えるとわかります

std::ranges:::find(std::views::enumerate(/*...*/), [](const auto& p) {
  p.value;  // invocable<F&, iter_value_t<I>&>の時にエラー
  return /*...*/;
})

*itの結果はenumerate_resultなので当然こういう風に書かれるはずですが、iter_value_t<I>&(すなわち、std::tuple<...>&)でもinvocableでなければならず、std::tuplevalueメンバを持たないためエラーになり、invocable<F&, iter_value_t<I>&>falseになります。他にもviews::as_constを通すと要素型が変わるなどの問題もありました。

結局、中途半端なアプローチは取れないということで、このリビジョンではreference/value_typeとしてenumerate_resultstd::tupleのどちらかの型を選択することを提案しています。投票はすでに行われており、std::tupleを選択することで合意されました。

この提案は一度C++26へ回されていましたが、NBコメントを受けてC++23ターゲットに戻りました。

P2248R6 Enabling list-initialization for algorithms

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

以前の記事を参照

このリビジョンでの変更は数値アルゴリズム<numeric>にあるもの)を提案から覗いたことです。数値アルゴリズムでは初期値の型が結果に強く影響するため、単に{}と指定したときのデフォルトをどうするのかについて議論が必要となったためです。

P2263R1 A call for a WG21 managed chat service

WG21が管理するチャットサービスを確立するための提案。

以前の記事を参照

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

  • ISOの行動規範に関する方針変更によって不要になった要件の削除
  • 提案するチャットサービスがMattermostになった
    • すべてのWG21メンバーに参加を推奨
  • zoomチャットの利用についての解説を追加

などです。

現在のところ参加できるのはWG21のメンバのみです。

P2392R2 Pattern matching using is and as

現在のパターンマッチングをクリーンアップし、使用可能な所を広げる提案。

以前の記事を参照

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

  • 実装経験からのフィードバックを反映した事
  • 空の状態をテストするための、is voidのサポート追加

などです。

P2505R5 Monadic Functions for std::expected

std::optionalのMonadic interfaceをstd::expectedにも導入する提案。

以前の記事を参照

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

この提案は11月の全体会議で承認され、C++23ワーキングドラフト入りしています。

P2527R1 std::variant_alternative_index and std::tuple_element_index

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

以前の記事を参照

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

  • std::tuple_element_index_vを追加
  • std::variant_alternative_indexstd::integral_constantを継承するようにした
  • std::tuplestd::variantを継承したクラスで何が起こるかを明確化
  • const修飾されている型を入力した場合を明確化
  • 実装例の追記

などです。

P2530R1 Why Hazard Pointers should be in C++26

標準ライブラリにハザードポインタサポートを追加する提案。

以前の記事を参照

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

  • 機能テストマクロの追加
  • Concurrency TS v2ではなく最新のドラフトをベースとして文言を書き直した
  • ユーザー定義イテレータでこの提案のハザードポインタを使用する例を追加
  • ABI安定性に関するガイダンスを追加

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

P2539R2 Should the output of std::print to a terminal be synchronized with the underlying stream?

P2539R3 Should the output of std::print to a terminal be synchronized with the underlying stream?

提案中のstd::printP2093)が出力するストリームについて、同じストリームに対する他の出力との同期を取るようにする提案。

R2での変更は、LEWGでの投票結果を追記したこと、フラッシュが必要な場合の指定をより具体的にしたことです。

R3(このリビジョン)での変更は、フラッシュが必要な場合の指定をさらに具体的にしたことです。

この提案は11月の全体会議で承認され、C++23ワーキングドラフト入りしています。

P2545R1 Why RCU Should be in C++26

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

以前の記事を参照

このリビジョンでの変更はConcurrency TS v2ではなくP1122R4をベースとして文言を書き直したことです。

最終的な文言は

If the native Unicode API is used, the function flushes the stream's buffer before writing out.

あるいは

If the native Unicode API is used, the function flushes the os's buffer before writing out.

となり、システムネイティブのユニコードAPIを使用している場合にのみ、std::print()による出力は出力後にフラッシュを行います。

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

P2546R2 Debugging Support

標準ライブラリにデバッグサポートの為のユーティリティを追加する提案。

以前の記事を参照

このリビジョンでの変更は、フリースタンディング機能であることを指定した事です。

P2548R1 copyable_function

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

以前の記事を参照

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

  • P2511R2で提案された内容をcopyable_functionに適用
    • std::nontypeによる構築のサポート
  • std::move_only_functionからstd::copyable_functionへの変換を追加
    • 左辺値からの変換はexplicit
    • 右辺値からの変換は暗黙変換可能

などです。

P2561R1 An error propagation operator

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

以前の記事を参照

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

  • タイトルの変更
    • ??演算子を提案しているわけではないため
  • try_traitserror_propagation_traitsへ変更
  • is_okhas_value()へ変更
  • 生存期間について追記
  • delctyperequiresで使用したときの振る舞いについて追記

などです。

EWGの投票では、この提案のために時間を割くことに合意が取れているようです。

P2581R2 Specifying the Interoperability of Built Module Interface Files

ビルド済みモジュールを扱う際に、ビルドシステムがそのビルド済みモジュールファイルを直接扱うことができるかどうかを調べられるようにする提案。

以前の記事を参照

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

  • 理解のギャップを埋めるために“Problem Statement”のセクションを追加
  • 要件のセクションの例を拡充し移動
  • “Producing a new BMI in the context of another translation unit”と“Built Module Interface Compatibility Identifiers”セクションを書き直し

などです。

P2592R2 Hashing support for std::chrono value classes

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

以前の記事を参照

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

  • LEWGからのフィードバックの反映
  • durationtime_pointの特殊化について、hash<K>(k)が何を評価するかの指定を削除
  • 未規定状態のハッシュオブジェクトに関する文言の簡略化

などです。

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

P2614R1 Deprecate numeric_limits::has_denorm

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

以前の記事を参照

このリビジョンでの変更は、提案する文言を追加したことです。

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

P2630R1 Submdspan

std::mdspanの部分スライスを取得する関数submdspan()の提案。

以前の記事を参照

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

  • strided_index_range導入の根拠の更新
  • submdspan_mappingsubmdspan_offsetsubmdspan_mapping()にマージ
  • ADLに関する文言の更新

この提案はまだ、C++23を目指して作業されています。

P2632R0 A plan for better template meta programming facilities in C++26

C++26及びそれ以降に向けて、テンプレートメタプログラミングサポート改善のための機能を入れていくための計画表。

ここでは主に、コア言語の拡張によって既存のテンプレートメタプログラミングテクニックの問題(専門知識が必要、コンパイル時間増大、使いづらい、など)を解決するべく、そのような機能の列挙と関連する提案についての優先順位付を行うものです。

  • Tier 1
  • Tier 2
    • Reflection : P1240R2
    • Packs in structured bindings : P1061R2
    • Generalized structural types : P2484R0
    • Integer sequences in the core language
    • Deducing forwarding reference : P2481R1
    • Pack Slicing : P1858R2
    • Universal template parameters : P1985R1
    • Variable template template params
    • Concept template template params
    • Deleting variable templates : P2041R1
    • Packs outside of templates : P1858R2P2277R0
    • Non-trailing parameter packs deduction : P2347R2
    • std::forward in the language : P0644R1
    • Simplified structured bindings protocol : P2120R0
    • std::is_structured_type
    • static_assert with expression as message
    • static_assert packs
  • Tier 3
    • Packs as return value
    • Destructuring aggregates/C-arrays : P2141R0P2580R0
    • Pack literals
    • Meta algorithms on packs (unique, etc)
  • Tier 4
    • Multiple levels of pack expansions
    • Language variants/tuples
    • Pack of variables declaration
    • Step for pack slicing
    • Member traits(Dot syntax on types/enums)

Tier1の機能は理解が進んでおりコストメリットが高く、相互に独立して標準化することができるものです。Tier2の機能はTier1と比較すると複雑さが増したり有用性が低いものです。この2つのグループはC++26をターゲットにしています。

Tier3の機能は便利である可能性があるもののリスクがありさらに研究が必要なものです。Tier4の機能は他の機能と重複していたりコストメリットが低いものです。

これらの機能の一部、特にTier1/2の機能のほとんどはCircleによる実装経験があります。

提案ではそれぞれの機能についての簡単な解説が行われています。

P2636R1 References to ranges should always be viewable

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

以前の記事を参照

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

などです。

追記されたTL;DRによれば、この提案は次のようにviews::allおよびviewable_rangeの定義を変更します

  • 右辺値はムーブされる
  • コピー可能でコピーが安価な場合、左辺値はコピーされる
  • そうではない左辺値は、ref_viewにラップされる

これによって、すべての左辺値rangeviewable_rangeになります。viewable_rangeではないrangesは、ムーブ不可能な右辺値のみです。

ただ、この提案はSG9/LEWGのレビューにおいてほとんどコンセンサスを得られていません。

P2637R1 Member visit

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

以前の記事を参照

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

  • メンバ関数apply()を削除
  • .visit<R>()basic_format_argに追加
  • .visit().visit<R>()を、std::variantから非公開で継承した型に対応

などです。

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

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

以前の記事を参照

このリビジョンでの変更はよくわかりません(というかファイルがほぼ空です・・・)。

P2641R1 Checking if a union alternative is active

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

以前の記事を参照

このリビジョンでの変更は、実装経験(GCC/clangが同じことをする__builtin_constant_p()を持っていた)について追記した事です。

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

P2642R1 Padded mdspan layouts

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

以前の記事を参照

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

  • padding strideとして指定された値が、その次元(n)の要素数extent(n))より小さい場合、オーバーアライメント係数として機能するように変更
  • extents_typeextents<index_type, padding_stride>を取るマッピングコンストラクタを削除
    • 後者はpadding strideではない可能性がある
  • layout_{left,right}_padded::mappingからlayout_{left,right}::mappingへの変換で、コンパイル時にストライドの互換性をチェックするために制約をMandatesにした
  • layout_{left,right}_padded::mappingの実際のpadding strideは、コンパイル時に分かっているならindex_type型の値として表現可能であることを規定
  • アライメントの小さくなる方向の変換を許可しない理由を追記

R0では、layout_{left,right}_paddedに指定する値はpadding strideそのものでなくてはならず、それはパディングを行う対象配列の次元数以上でなければなりませんでした。これはオーバーアライメントアクセスを行いたい場合に使いづらく、std::assume_aligned<N>の指定(Nはバイト数)と一貫していませんでした。

例えば、1次元が13要素ある配列(extent(0) == 13)を4の倍数でオーバーアライメントしたい場合(パディングは3要素、無効領域ではないとして)、R0ではlayout_left_padded<16>と指定する必要がありました。これは、13要素以上で最小の4の倍数を自分で計算する必要があります。`

対して、このリビジョンではlayout_left_padded<4>と指定すると、extent(0)の値(13)よりも小さいことからオーバーアライメント係数だとみなして、13要素の配列を4要素ごとの2次元配列にするために必要な計算を自動で行い、パディングを含めた要素数stride(1) == 16)をユーザーが計算する必要が無くなります。

P2644R0 Get Fix of Broken Range-based for Loop Finally Done

範囲for文の13年間放置されているバグを修正する提案。

これは以前にP2012で提案されていたのを再提案するものです。

範囲for文はシンタックスシュガーであり、イテレータを用いたループに書き換えられて実行されていますが、そのせいで見えづらいダングリング参照/イテレータが発生することがあります。

この問題は少なくとも13年前から知られていましたが解決されることなく今に至っています。C++23に向けてP2012がこの問題の解決を目指していましたが、問題の解決の必要性では合意されたものの局所的な解決よりもより広範な解決策が望まれたこともあり、P2012はC++23に間に合いませんでした。

結局その後それを補うような提案が出てくることはなかったため、この提案は改めてP2012で提案していたことを提案し直すものです。

提案する内容はシンプルで、範囲for文で暗黙的に導入されるオブジェクトの生存期間について特別扱いするもので、その生存期間は範囲for文終了までというように規定しようとしています。

このアプローチは範囲forの動作を変更するものではないため、一時オブジェクトの寿命についてのより広範な解決策が後から提案されたとしてもそれを妨げることはありません。

問題のあるコード 現在 この提案後
for (auto e : getTmp().getRef()) UB OK
for (auto e : getVector()[0]) UB OK
for (auto valueElem : getMap()["key"]) UB OK
for (auto e : get<0>(getTuple())) UB OK
for (auto e : getOptionalColl().value()) UB OK
for (char c : get<string>(getVariant())) UB OK
for (auto s : std::span{arrOfConst()}.last(2)) UB OK
for (auto e : std::span(getVector().data(), 2)) UB OK
for (auto e: co_await coroReturningRef()) UB OK
getValue()が参照を返すとして
for (char c : getData().value)
for (char c : getData().getValue())

OK
UB

OK
OK

P2646R0 Explicit Assumption Syntax Can Reduce Run Time

[[assume(expr)]]の利点についての理論的な面と実験結果を記述した文書。

[[assume(expr)]]C++23で追加される、関数内での仮定をコンパイラに通知することで最適化を促進しようとするものです。

実際それがどれほど効果があるのかや細心のプラットフォームで再現可能な研究はあまりありませんでした。この文書は、まず[[assume(expr)]]がどのようなものでどういう時に効果的なのかを説明し、その効果の実証のためのベンチマークフレームワークを紹介します。最後に、それをもちいて一部の場合における[[assume(expr)]]の効果を測定しその結果を記載しています。

例えば、[[assume(expr)]]は次のような場合に効果を発揮します

  • 条件分岐の削除
  • ループ展開
  • ベクトル化
  • エイリアスの仮定
    • 異なるポインタが同じアドレスを指さないことを仮定、など
  • 浮動小数点演算
    • NaNにならないことを仮定、など
  • 符号付整数の演算

文書ではこの条件分岐の削除の場合についての測定結果が示されており、複数のコンパイラgcc/clang/msvc)で効果が得られる(少なくとも悪くはならない)ことが示されいます。また、実行時間だけでなく、コンパイル時間やバイナリサイズに対しても有利な影響がある事も示されています。

P2647R0 Permitting static constexpr variables in constexpr functions

関数スコープのstatic constexpr変数をconstexpr関数内で使用可能にする提案。

例えば次のような関数があるとします

// 数値を文字に変換する
char xdigit(int n) {
  static constexpr char digits[] = "0123456789abcdef";
  return digits[n];
}

この関数はコンパイル可能で実行時には正しい結果を得ることができます。ただ、ここにconstexprを付加するととたんにコンパイルエラーになります。

constexpr char xdigit(int n) {
  // エラーになる
  static constexpr char digits[] = "0123456789abcdef";  // constexpr関数内でstatic変数宣言できない
  return digits[n];
}

この提案は、constexpr関数内でstatic constexpr変数の宣言を許可することでこの問題を解決しようとするものです。

元々C++11から、constexpr関数内でstatic変数を宣言することは禁止されていました。C++23でP2242R3によって、コンパイル時に初期化に到達しなければ許可されるようにはなりましたが、関数内static変数をコンパイル時に使用することはできませんでした。

これはstatic変数の初期化が任意のコードを実行できるための規制ですが、static constexpr変数の初期化はコンパイル時に行われるためいつどのような初期化が行われるかは問題とならないはずです。

提案ではシンプルに、定数式に現れてはならないものリストからstatic constexpr変数の初期化を除外しています。

この提案は11月の全体会議で承認され、C++23ワーキングドラフト入りしています。

P2648R0 2022-10 Library Evolution Polls

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

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

P2652R0 Disallow user specialization of allocator_traits

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

std::allocator_traitsは主に次の2つの目的をもってC++11で導入されました

  1. アロケータのデフォルト実装を提供することで、アロケータの要件を最小限にする
  2. 将来的に、アロケータ要件を変更せずに(既存アロケータを変更せずに)アロケータインターフェースを拡張できる仕組みを提供する

ただし、2つ目の目標はユーザーがstd::allocator_traitsの特殊化を定義することが許可されているために損なわれています。ユーザーがstd::allocator_traitsを特殊化する場合は標準のものと同じインターフェースを備えることを要求していますが、std::allocator_traitsが拡張されるとそのユーザー定義特殊化は損なわれてしまいます(ある時点では準拠しているが、標準のバージョンアップとともに自動的に非準拠になってしまう)。

このことは既に悪影響を及ぼしており、C++23で追加されたstd::allocate_at_least()は本来std::allocator_traitsの拡張となるはずでしたが、この問題を考慮した結果無関係なフリー関数として追加されています。

この提案は、このような問題を解決するためにstd::allocator_traitsのユーザーによる特殊化を禁止しようとするものです。

この提案では次のどちらかを選択することによってこの問題を解決することを提案しています

  1. std::allocator_traitsのユーザーによる特殊化を禁止する
  2. std::allocator_traitsを廃止し、アロケータのインターフェース関数を文書で指定する
    • アロケータインターフェースの全てをstd::allocate_at_least()と同様に指定する

この提案は主として1の解決策を推していますが、これは稀であると考えられるものの破壊的変更を伴うため、合意が取れなかった場合の次善の策として2の解決策を提案しています。どちらにせよ、アロケータインターフェースの更なる拡張が行われる前にこの方向性を決定する必要がある、と述べています。

P2653R0 Update Annex E based on Unicode 15.0 UAX 31

ユニコードのUAX#31への準拠の文面を更新する提案。

ユニコード15で更新されたガイダンスに従った更新とのことです。

P2655R0 common_reference_t of reference_wrapper Should Be a Reference Type

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

std::common_referenceC++20で追加されたもので、二つの型の間の共通の参照型を求めるものです。

std::reference_wrapper<T>Tの参照となるクラス型で、ほぼT&と同様の働きをし、相互に変換することができます。

int i = 1;
std::reference_wrapper<int> jr = j; // ok、暗黙変換
int & ir = std::ref(i);             // ok、暗黙変換

int j = 2;
int & r = false ? i : std::ref(j); // error!

ただ、2つの型が相互変換可能であるために条件演算子では型を1つに絞ることができずにエラーとなります。

std::common_referencestd::basic_common_referenceが特殊化されていない場合、この例の最後の条件演算子の結果型としてそれを求めようとし、それもだめならstd::common_typeに頼ります。

そのため、std::common_reference_t<T, std::reference_wrapper<T>>の結果は、std::common_type_t<T, std::reference_wrapper<T>>が選択され、この型はTとなります。

std::reference_wrapper<T>T/T&の間のcommon_referenceの決定過程は単なる偶然であり、このような振る舞いはstd::reference_wrapper<T>の意義とそぐわないとして、これをT&になるようにしようとする提案です。

提案では、std::basic_common_referenceの特殊化を通じてこれを実現しようとしています。

namespace std {
  template<class T, template<class> class TQual, template<class> class UQual>
  struct basic_common_reference<T, reference_wrapper<T>, TQual, UQual> {
      using type = common_reference_t<TQual<T>, T&>;
  };

  template<class T, template<class> class TQual, template<class> class UQual>
  struct basic_common_reference<reference_wrapper<T>, T, TQual, UQual> {
      using type = common_reference_t<UQual<T>, T&>;
  };
}

この恩恵はstd::common_referenceを内部で使用しているところ、特に一部のRangeアダプタで得られるはずです。

// views::join_withの例
class MyClass {
  vector<vector<Foo>> foos_;
  Foo delimiter_;

 public:
  void f() {
    auto r = views::join_with(foos_, 
               views::single(std::ref(delimiter_)));
    for (auto&& foo : r) {
      // 現在 : fooは要素をコピーした一時オブジェクト(Foo&&)
      // 提案 : fooは要素への参照(Foo&)
    }
  }
};


// views::concatの例
class MyClass {
  vector<Foo> foo1s_;
  Foo foo2_;

 public:
  void f() {
    auto r = views::concat(foo1s_, 
               views::single(std::ref(foo2_)));
    for (auto&& foo : r) {
      // 現在 : fooは要素をコピーした一時オブジェクト(Foo&&)
      // 提案 : fooは要素への参照(Foo&)
    }
  }
};

この例の2つのRangeアダプタ(join_withconcat)は複数の範囲を受けて1つの範囲に変換するタイプのviewであるため、結果の要素型の参照型(std::iter_reference_t)を決定するためにstd::common_referenceを使用しています。上述した問題のため、この結果の範囲の要素への参照型はprvalue(ここではFoo)となるため、範囲forで受けている要素は要素をコピーした一時オブジェクトになってしまっています。

この挙動は明らかに直感的ではなくstd::reference_wrapper<T>を用いている意味がありません。この提案による変更後は、結果範囲の参照型は意図通りにT&(ここではFoo&)となるようになります。

P2656R0 C++ Ecosystem International Standard

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

ここで提案しているのは特定のツールを標準化することや特定のツールを指定することではなくツールの周辺環境を整備することで、主に次の3点を含めることを目標にしています

  1. Definitions
    • 言葉の定義。コンパイラ・リンカ・パッケージマネージャなどの共通言語の規定
  2. Format Specifications
    • ツールが生成・消費するデータのフォーマットの規定
  3. Operation Specifications
    • あるデータがどのように生成・消費されるかの規定

ツールとはコンパイラや静的アナライザを含みますが、どうやら主眼に置いているのはビルドシステムとパッケージマネージャの相互運用性に主眼を置いているようです。

これらのことはC++標準ではなく、別の標準規格として発効しようとしています。

P2657R0 C++ is the next C++

C++言語機能として静的解析を組み込む提案。

C++プログラマやその利用者はC++をより安全にしたいと考えており、一部のC++プログラマは新しい言語やプリプロセッサを開発しています。ただ、それは結局根本的な問題の解決になっていないため、この提案はあくまでC++言語自体をより安全かつシンプルにするために、静的解析を標準化しようとするものです。

この提案では、モジュール宣言の属性指定によって静的解析を有効化します。

// プライマリモジュールインターフェース単位で有効化、自動的にこのモジュール内全域に適用
export module some_module_name [[static_analysis("...")]];

// もしくは、実装単位で有効化
module some_module_name [[static_analysis("...")]];

static_analysis属性を用いて静的解析をモジュール毎に有効化することを指定し、その引数によって有効化する静的解析の種類を指定します。静的解析の種類としては大きく次の2つのものを提案しています。

静的解析の種類 効果
[[static_analysis("safer")]] メモリ関連の安全性についての静的解析
[[static_analysis("modern")]] saferを含めて、モダンC++的観点でのコード品質担保のための静的解析

mdoernsaferを包含しています。

これら静的解析に引っかかったものはエラーとなります。これらは安全性が重要なC++プログラマや企業などのためのものです。

また、mdoernsaferを包含しているように、この二つの静的解析器はどちらも多数のサブ静的解析器で構成されており、C++標準の進化とともにサブの静的解析器を追加・廃止していくことができます。その場合、言語バージョンが上がるとそれまで問題なかった項目がエラーになる可能性があり、それはコードを修正するまで解消されなくなります。

提案では、理想的にはエラーの詳細についてのリンクがエラーメッセージに含まれ、そこにはバージョンごとの解析器の仕様などが記載されており、それらはC++教育グループやコアガイドラインの著者、コンパイラ開発者などによって管理されるようになっていることが望ましいと書かれています。

提案では、サブの静的解析器と構造として次のように例があります

  • modern
    • safer
      • use_lvalue_references
      • no_unsafe_casts
      • no_union
      • no_mutable
      • no_new_delete
      • no_volatile
      • no_c_style_variadic_functions
    • no_deprecated
    • use_std_array

提案では理想として

  • この静的解析器指定文字列をなんからの形でコンパイラに渡すことができること
  • その方法(コマンドライン引数名など)の標準化
  • 静的解析結果の機械可読なレポートの生成
  • その内容の標準化
  • 静的解析器の名前

などを標準化できれば良いと書かれています。

P2658R0 temporary storage class specifiers

一時オブジェクトのための記憶域指定を追加する提案。

これは、P2623のオブジェクト生存期間の延長を明示的に行おうとするものです。基本的なモチベーションなどはそちらと共通しているので、以前の記事を参照してください。

この提案では、一時オブジェクトが生成されているところにストレージ指定を行うことで、P2623で提案されている2つの寿命延長を手動で有効化します。

int main() {
  // 現在、UB
  std::string_view sv1 = "hello world"s;

  // この提案、OK
  std::string_view sv2 = constinit "hello world"s;
}

この例の場合、sv1std::stringの一時オブジェクトを参照してダングリングになっていますが、sv2は定数初期化され静的ストレージにあるstd::stringオブジェクトを参照しておりダングリングになっていません。

これはP2623の提案している一時オブジェクト寿命延長方法のうちの1つです。もう一つ、一時オブジェクトの寿命を囲むスコープに紐づけることはblock_scopeという指定によって行うことができます。

// 文字列結合関数
// 戻り値はstd::stringの一時オブジェクト
std::string operator+ (std::string_view s1, std::string_view s2) {
  return std::string{s1} + std::string{s2};
}

int main() {
  std::string_view sv = "hi";

  // 現在、UB
  sv = sv + sv;

  // この提案、OK
  sv = block_scope sv + sv;
}

ここでの1つ目のsv+の戻り値のstd::string一時オブジェクトを参照しておりダングリングとなっていますが、2つ目のsvではそれは囲むブロックスコープにまで寿命延長されており、ダングリングになりません。

この2つを含めた4つの指定子を追加することを提案しています

  • constinit : 定数初期化によって、一時オブジェクトを静的記憶域期間に昇格させる
  • variable_scope : 一時オブジェクトが代入されているオブジェクト、もしくは囲むブロックスコープ(block_scope相当)のどちらか長い方に合わせて寿命を延長する
  • block_scope : Cの複合リテラル相当の寿命延長(囲むブロックスコープまで)を行う。C互換のためのもの
  • statement_scope : それを含む完全式の終わりまでの寿命延長。現在のC++の一時オブジェクトの寿命と同等
    • デフォルトであり、下位互換のためのもの。一時オブジェクトのマーカーとしての利用など

これらの指定は一時オブジェクトが生成されているところにつけて回る必要があり、冗長な多くの作業が伴います。そのため、モジュール単位でこのデフォルトを変更できるようにする属性も同時に提案されています

  • [[default_temporary_scope(variable)]] : variable_scopeをデフォルトにする
  • [[default_temporary_scope(block)]] : block_scopeをデフォルトにする
  • [[default_temporary_scope(statement)]] : statement_scopeをデフォルトにする
    • 何も指定しない場合のデフォルト

モジュール宣言に対してこれらの属性が指定されている場合、そのモジュール内で一時オブジェクトが生成されるところでは、default_temporary_scope属性の引数に指定された指定子が指定されているかのように扱われます。なお、これらのデフォルトは、個別の指定によって上書き可能です。

P2659R0 A Proposal to Publish a Technical Specification for Contracts

契約プログラミング機能に関するTechnical Specificationを発効する提案。

ここでのContracts TSは以前にC++20から削除された契約機能とその提案(P0542R5, P1323R2,)、およびその後C++20で契約が削除される前に議論されていた追加の機能(P1607R1, P1344R1)をベースとしたもので、現在議論の主流であるMVPによるものではないようです。

これらの部分にはある程度の実装経験があり、TSとして発効することで実装がユーザーに開かれ、これまでなかなか集めづらかったユーザーからのフィードバックが得られるようになることで、本当にC++ユーザーにとって有用となるような契約プログラミング機能についてのコンセンサスを得られるようにすることが目的です。それによって、C++26で有用な契約プログラミング機能を導入するための議論を前進させることが期待されています。

P2660R0 Proposed Contracts TS

↑で提案されているContracts TSの暫定文書。

ここで提案されているContractは以前にC++20で導入されたものと非常に近く、属性構文によって契約を指定します。契約属性には、pre, post, assertの3つがあります。

// 事前条件
int f(int n) [[pre : n != 0]];

// 事後条件
int f(char * c) [[post res: res > 0 && c != nullptr]];

int f(double d) {
  // アサーション
  [[assert : not std::isnan(d)]]
}

契約条件においての副作用は許可されており、契約条件の評価が例外を投げる場合はstd::terminate()が呼ばれます。

各契約属性には契約の振る舞いを追加で指定でき、次の4つが用意されています

  • ignore : 契約条件は評価されない
  • assume : 契約条件は評価されない
    • 契約違反時(契約条件がfalseとなる場合)は未定義動作
  • enforce : 契約違反時にstd::terminate()で終了する
  • observe : 契約条件違反時にも継続する
// 事前条件
int f(int n) [[pre assume : n != 0]];

// 事後条件
int f(char * c) [[post enforce res: res > 0 && c != nullptr]];

int f(double d) {
  // アサーション
  [[assert observe : not std::isnan(d)]]
}

事後条件(post)においては関数引数を参照することができますが、この引数が参照型ではなく関数本体内で変更される場合、未定義動作となります。

int f(int x) [[post enforce r: r == x]]
{
  return ++x; // undefined behavior
}

void g(int * p) [[post enforce: p != nullptr]]
{
  *p = 42; // OK, pは変更されていない
}

契約違反が起きた際のハンドラ(violation handler)は用意されていますが、それはユーザーが変更可能なものではなく実装が用意し呼び出すものです。ハンドラの振る舞いとしては、std::contract_violationクラスのメンバに保存された情報(違反が起きた場所など)を出力することが推奨されています。

P2661R0 Miscellaneous amendments to the Contracts TS

Contracts TSに対する修正と機能追加の提案。

  • assume属性と同等の仮定のための契約
    • [[pre assume : expr]][[assume(expr)]]と同等とする
  • 契約条件が何回評価されるかを不定とする
    • 契約条件の副作用に依存したプログラムを書きづらくする
  • std::contract_violationstd::source_locationアクセス関数を追加
    • TS(P2660R0)の定義を全面変更
  • std::contract_violation::contract_behavior()を追加
    • 指定された契約の振る舞い(ignore, enforceなど)の情報を取得
  • 契約条件はSFINAEのコンテキストではないことを規定
    • 契約違反によってSFINAEできなくする
  • 事後条件における非const非参照の関数引数へのアクセスをコンパイルエラーにする
  • default/delete関数に対する契約の影響を明確化
    • deleteされた関数は契約を持てない
    • default定義された関数は契約を持たない

などです。

P2662R0 Pack Indexing

パラメータパックにインデックスアクセスできるようにする提案。

これは元々P1858R2で提案されていたもので、この提案はそれを標準に導入するための文言を含むものです。P1858R2及びその前身の提案では、std::tuplestd::variantをより効率的に定義するために欠けているパックの操作としてパックのインデックスアクセスの必要性を説いていました。

提案するパックのインデックスアクセスはpack...[index]のような構文を用います

template <typename... T>
constexpr auto first_plus_last(T... values) -> T...[0] {
  return T...[0](values...[0] + values...[sizeof...(values)-1]);
}

int main() {
  //first_plus_last(); // ill-formed
  static_assert(first_plus_last(1, 2, 10) == 11);
}

このサンプルのT...[0]はパラメータパックTの1つ目の要素を引き当てており、その結果は型です。values...[0]はパラメータパックvaluesの1つ目の要素を引き当てており、その結果は値です。見づらいですが、first_plus_last()の内部ではパックの展開は行われていません(インデックスアクセスのみが行われています)。また、空のパックはインデックスアクセスすることができず、コンパイルエラーになります。

この提案のインデックスアクセスでは、パラメータパックの先頭からのインデックス付けのみがサポートされており、パック末尾からのインデックスアクセスは行えません。

この構文は将来的に次のような拡張を想定しています

  • 構造化束縛等パックに依存せずに導入されたパックのインデックスアクセス
  • テンプレートテンプレートパラメータパックのインデックスアクセス
  • パックの末尾からのインデックスアクセス
  • パックのスライス
  • ユニバーサルテンプレートパラメータのパック

この提案の構文はCircleで実装されたものをベースにしており、clangによるテスト実装が既になされているようです。

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

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

複素数は科学計算の様々なところで使用されており、それを受けて近年のCPUは複素数値を直接扱う命令を持っていることがあります(AVX512 FP16やARM NEON v8.3)。std::simd<T>Tの配列に対する演算を環境のSIMD命令にマッピングすることで処理を高速化し、かつそれをポータブルにするためのものであり、std::simd<T>Tstd::complexを受けられるようにするとより有用である可能性があります。

この提案は、std::simdが要素型として複素数型をサポートするようにしようとするものです。

std::complexをはじめとして、ほとんどの場合複素数型は浮動小数点数Tのペアとして実装されます。したがって、複素数型の配列はメモリ上で実部の値と虚部の値が交互に並ぶレイアウトを取ります。これはC++だけに限ったことではなく、多くのプログラミング言語やソフトウェアコンポーネントにおいて共通しており、この形式は複素数値データのデフォルトレイアウトとして認識されています。


複素数型配列のメモリレイアウト

intel及びARMのSIMD命令における複素数値サポートでは、このデータレイアウトをそのまま扱って(再配置の必要なく)効率的な複素数計算を可能としています。

そのため、std::complex<T>はそのままstd::simdの要素型として使用できるため、そうすることを提案しています。

std::simd<std::complex<float>, ABI> myIntrlvValue;

環境のCPUがこのような複素数レイアウトの取り扱いをネイティブにサポートしていない場合でも、複数の通常SIMD命令の組み合わせによって複素数計算を合成するのはそれほど難しくはありません。

P2665R0 Allow calling overload sets containing T, constT&

値渡しの関数とconst参照渡しの関数がオーバーロード候補としてある時、コンパイラがその環境で最適な方を選択するようにする提案。

現在、値渡しの関数とconst参照渡しの関数が共にオーバーロード候補にある時、多くの場合はオーバロード解決が曖昧になるためコンパイルエラーになります。

f(int);
f(const int&);

f(1); // ng

この提案は、この場合にコンパイラがその環境で(ABIや呼び出し規約等の意味で)最適な方を選択することを許可しようとするもので、この目的は偏に関数呼び出しの最適化のためです。

関数テンプレートでは入力の型に何がわたってくるのかわからないため、引数型をとりあえずconst T&T&&にすることがよく行われます。しかし、コピーコストが軽い一部の型、特に組み込み型を渡す場合は参照渡しよりも値渡しの方が効率的となります。その事情は環境によって変化しうるため、コンパイラに環境固有の事情を考慮してもらった上で最適な方を選択して貰えれば、最適な関数の選択を自動化できます。

これは特に、関数にconst左辺値か右辺値を渡す場合に起こります。

提案では追加で、右辺値参照を受けるオーバーロードも対象にしています。これら3つ全て、あるいはどれか2つの間でオーバーロードが曖昧になる時に、コンパイラに最適なオーバーロードを選択してもらいます。

f(int);
f(int&&);
f(const int&);

f(1); // ok、最適なオーバロードをコンパイラが選択する

提案では例として、std::vector<T>::push_back()を上ています

class std::vector<T> {
  void push_back(const T&); // #1, 左辺値からのみ呼び出される
  void push_back(T&&);      // #2, 右辺値からのみ呼び出される
  void push_back(T);        // #3 (New!), 左辺値/右辺値どちらからでも呼び出される
};

この場合、引数の値カテゴリによって3つのオーバロードのうちどれか2つの間で曖昧となり、その際にパフォーマンス的に最適となりうるものをコンパイラに選択してもらいます。

P2666R0 Last use optimization

関数に渡す引数がそれ以降使われない場合にその受け渡しの最適化を許可する提案。

関数のreturnにおいてのRVO/NRVOやコピー省略などの最適化はC++11以降継続的に整備され、一般的なものになっています。この提案はそれらとは逆に入り口部分、すなわち関数の引数渡しに関して同様の最適化を許可しようとするものです。この提案では、そのような最適化のことをLast use optimization(LUO)と呼んでいます。

また、このような最適化を義務付けることで、std::move()/std::forward()忘れによるバグを回避できるようになります。

void add_member(const std::string& first, const std::string& last) {
  auto row = std::format("First name: {}, Last name: {}", first, last);
  members.push_back(row); // move()忘れ
}

この例では、push_back(row)においてローカル変数のrowstd::move()していないため、本来不要なコピーが発生しています。

関数引数の評価順序が不定なためにstd::move()できない例

template<typename T1, typename T2> auto concat(T1&& lhs, T2&& rhs) {
  return std::forward<T1>(lhs) + std::forward<T2>(rhs);
}

std::string twice(std::string word) {
  return concat(word, word);  // move()したくてもできない・・・
}

std::move()/std::forward()を正しい場所に全て追加するのは面倒で冗長になり、コードの可読性を低下させます。また、必要なところに追加するのを忘れたり、コードが複雑になると関数渡しにおいてムーブすべきかどうかを判断することが難しくなります。さらに、テンプレートなコードでstd::forward()を使用する必要性を理解するのは簡単ではありません。LUOが許可あるいは義務化されればこれらについて考えなくても良くなり、上記の例における不要なコピーが解消されます。

この提案のLUOでは、暗黙のオブジェクトパラメータ(thisパラメータ)に対しても適用されます

struct A {
  void f() const & { std::cout << "lvalue f"; }
  void f() const && { std::cout << "rvalue f"; }
};

void g(A&& a) {
  a.f(); // LUOが適用され、const &&の方がよばれる
}

この提案のいうLast useとは、単に変数が最後に使用される場所だけではなく、その値(オブジェクト)が最後に使用される場所も含んでいます。そのため、name = toUpper(name)のように変数の値を更新する場合もLUOの適用対象です。

LUOは主に次のようなものに対して適用されます

  • ローカルの値変数
  • 値渡しされた引数
  • 右辺値参照引数
  • 最後に使用された値を持つローカル変数へのローカル参照
  • 最後に使用された値を持つローカル変数を指していることが確実なローカルポインタのデリファレンス
  • 右辺値修飾メンバ関数内でのメンバ変数

これら対象のものが別の関数に渡されていて、渡しているところがその値(オブジェクト)の最後の使用であることが分かる時、LUOによって暗黙ムーブ/コピー省略が適用されます。

LUOの問題点として、コンパイラの実装が(N)RVOほど簡単ではないこと、(N)RVO同様にコンストラクタの呼び出しが変わることによって暗黙的に副作用が変化すること、などが挙げられています。

P2667R0 Support for static and SBO vectors by allocators

std::vectorで静的ストレージとSBOをサポートするためのアロケータの提案

static_vectorP0843R5)やsbo_vector(Small Buffer Optimaizationが適用されたもの)は長年標準ライブラリに求められてきた機能です。static_vectorに関してはstd::vectorとは別の一つのクラスとして議論が進行しています。

しかし、C++23でstd::alocate_at_least()が導入されたことによって、アロケータのカスタマイズのみでそれらのカスタムstd::vectorを構成できるようになっています。ただし、現在のstd::vectorの規定ではstd::alocate_at_least()を使うようになっておらず、std::vectorがそれを用いる場合のメモリ割り当て戦略に関しての保証がありません。

それを有効化したとしても、そのようなカスタムstd::vectorではムーブの振る舞いが変化します。さらに、バッファの最大値が固定されたこれらのバッファではバッファのアドレスや使用サイズ、キャパシティなどのメンバ変数を使用サイズのみに減らすことができ、その値も8ビットや16ビットに収まる程度の整数値になります。これを実現するには、新しいstd::allocator_traitsが必要になります。

そのようにアロケータをカスタムすることによって構成されたstatic_vectorsbo_vectorの利点は、std::vectorの特殊化を期待する既存コードをそのまま使用できるところです。さらに、この提案の内容が適用された場合、std::vectorによるアロケータカスタマイズはさらに柔軟になり、カスタムアロケータによるstd::vectorの構成可能性を向上させることができます。

この提案は、std::vectorのアロケータカスタマイズによってstatic_vector等のカスタムstd::vectorを作成可能にするために、std::vectorの規定を変更し、必要なアロケータを導入しようとするものです。

まず前述のように、std::vectorがメモリの確保にstd::alocate_at_least()を使用し、キャパシティをその戻り値を使用して更新するように変更します。std::alocate_at_least(a, n)はアロケータaを用いて最低n要素分のメモリを確保し、戻り値には実際に確保したサイズが返されます。

これを用いることで、最初のアロケーション要求時にアロケータ(ここではstatic/sbo目的のために単一固定バッファを内蔵するもの)のもつバッファの全サイズを取得し、それによって最大キャパシティを知ることができます。現在のstd::allocator_traits::allocate(a, n)では、実際に確保したサイズを知ることができず、確保した領域は要求値n個分としか扱うことができません。通常のアロケータ使用時にも同様に処理すれば問題なく、メモリ再確保を行うのはそのように取得した最大キャパシティを超える領域が必要になった時に行うようにすることで、現在の振る舞いとの一貫性を保つことができます。

static_vector等の固定バッファによるstd::vectorではムーブの仕方が異なり、メンバ関数のうち不要なものを削減することも可能となります。そのハンドリングのために、アロケータの性質の問い合わせのための2つの変数テンプレートを追加します。

namespace std::allocator_info {
  // アロケータの内部バッファ最大キャパシティを取得
  template<typename Alloc>
  constexpr size_t buffer_capacity = 0;

  // アロケータが実際に動的確保を行うかどうかを取得
  template<typename Alloc>
  constexpr bool can_allocate = true;
}

std::vectorのムーブ構築/代入時(src -> dst)、src.size()srcのアロケータ型Allocによるstd::allocator_info::buffer_capacity<Alloc>の値よりも小さい場合、ポインタの交換ではなく要素のムーブによってコンテナのムーブを行う必要があります。

// このbool値によってムーブ時の振る舞いを決定
// 通常のアロケータでは恒偽式となる
bool move_elements = src.capacity() <= std::allocator_info::buffer_capacity<SrcAlloc>;

通常のアロケータではstd::allocator_info::buffer_capacityの値をカスタムしない(0にしておく)ことで、現在の振る舞いを維持します。

そして、std::allocator_info::buffer_capacity<Alloc>を用いてこれらのことを判定し行うようにstd::vectorのムーブの規定を変更します。

std::allocator_info::can_allocatetrueとなる場合、固定長バッファを前提としたあらゆる最適化は不可能であり、現在のstd::vectorの振る舞いを維持しなければなりません。逆に、std::allocator_info::can_allocatefalseとなる場合はアロケータは次の保証を提供します

  1. std::allocator_info::buffer_capacityよりも要求メモリ量が小さい間、std::allocate_at_least()は内部バッファポインタとstd::allocator_info::buffer_capacityを返す
  2. std::allocator_info::buffer_capacityよりも要求メモリ量が大きい場合、std::allocate_at_least()はオーバーフロー時の適切な処理を行う
  3. allocate()は内部バッファへのポインタを返す
  4. deallocate()は何もしない

これらの保証によって、そのようなアロケータによって特殊化されているstd::vectorでは次のようにすれば、アロケータオブジェクト以外のストレージを要素数(サイズメンバ1つ)まで削減することができます。

  • .data()allocate(0)を返す
  • .capacity()std::allocator_info::buffer_capacityを返す

サイズメンバは、std::allocator_info::buffer_capacityの値(コンパイル時定数)からその適切な幅を判定することができ、さらにstd::vectorのサイズを削減できます。

そして、static/sbo vectorを実装するためのアロケータとそのアダプタクラスを追加します。

namespace std {
  // 固定バッファによる動的確保を行わないアロケータ

  // 最大キャパシティを超えるとstd::terminate()する
  template<typename T> 
  struct terminating_allocator;

  // 最大キャパシティを超えるとstd::bad_alloc例外を投げる
  template<typename T> 
  struct throwing_allocator;

  // 最大キャパシティを超えるとnullptrが返る
  template<typename T> 
  struct unchecked_allocator;

  
  // 上記アロケータを`std::allocator`と同等にするためのアダプタ
  template<typename T, size_t SZ, typename Backing = allocator<T>>
  class buffered_allocator;
}

そして、これらを用いた設定済みエイリアスを提供します。

namespace std {
  template<typename T, size_t SZ, typename Backing = std::allocator<T>>
  using sbo_vector = vector<T, buffered_allocator<T, SZ, Backing>>;
  
  template<typename T, size_t SZ>
  using static_vector_throw = sbo_vector<T, SZ, throwing_allocator<T>>;
  
  template<typename T, size_t SZ>
  using static_vector_terminate = sbo_vector<T, SZ, throwing_allocator<T>>;
  
  template<typename T, size_t SZ>
  using static_vector_unchecked = sbo_vector<T, SZ, unchecked_allocator<T>>
}

P2668R0 Role based parameter passing

限定された型の集合を表すtype_set構文の提案。

ある特定の型のグループ(例えば整数型や浮動小数点数型など)だけを対象とした関数テンプレートを書くことはよくあります。しかし、それを想定する型以外で呼び出せないようにするのは大変で、コンセプトを用いても必ずしも簡易に書くことはできません。

この提案は、特定かつ少数の型のグループを表現する構文を導入することで、そのような関数の記述を簡易にしようとするものです。

提案文書より、サンプル

// 1. type_setによるsin関数宣言
auto sin(<float, double, const long double&> x) { ... }

// 2. type_setに名前を付ける
type_set floats = <float, double, const long double&>;

// 3. 名前を付けたtype_setを用いた関数宣言
auto cos(floats x) { ... }
auto tan(floats x) { ... }

// 4. 9種類(3x3パターン)の関数宣言をまとめる
auto floating_multiply(floats a, floats b) { return a * b; }

// 5. type_set関数のオーバーロード(特殊化)
double floating_multiply(float a, float b) { return double(a) * b; }

// 6. type_setテンプレート
template<typename T>
type_set fwd = <const T&, T&&>;

// 7. type_setテンプレートの使用例
//    f(type_set T name)のように指定してtyps_setテンプレートをTで特殊化
void set_name(fwd std::string name) {
  // decltype(name)はconst std::string& or std::string&&
  m_name = std::forward<decltype(name)>(name);
}

// 8. 関数テンプレートのペアを宣言
//    ここでのT&&はT&を意味しない
template<typename T>
void use_value(fwd T value);

// 上記と同等の宣言、短縮構文
void use_value2(fwd auto value);

// 9. コンセプトによる制約
template<std::regular T>
void use_value3(fwd T value);

// 上記と同等の宣言、短縮構文
void use_value4(fwd std::regular auto value);

// 10. 1要素からなるtype_set
//     フォワーディングリファレンスを無効化する
template<typename T>
void only_rvalue(<T&&> rvalue);

template<typename T>
void only_reference(<T&> lvalue);


class Test {

  // 11. type_setによる修飾、暗黙のthisパラメータ(ポインタ型)に影響する
  void give_contents(Taker& taker) fwd {
    // この関数内でメンバは、右辺値修飾関数の左辺値となり、常にコピーする
    // fwdのどちらに対しても、m_contentsを正しくstd::forwardできない
    taker.take(std::forward<decltype(m_contents)>(m_contents));
  }

  // 12. Deducing thisでのtype_setの使用
  void give_contents2(this fwd Test self, Taker& taker) {
    // 上記問題を回避できる
    taker.take(std::forward<decltype(self.m_contents)>(self.m_contents));
  }
};

type_setを使用して関数を宣言すると、type_setに含まれる型によるオーバーロードが暗黙に生成されます。オーバーロード解決は、この暗黙に生成されている関数群に対して行われるため、そのルールはほぼこれまでと同一です。

関数の定義もそれに従い、暗黙に生成された関数宣言ごとに1つの定義が存在します。すなわち、暗黙に生成した以降は通常の非テンプレート関数と同じ性質を持っており、同じ扱いを受けます。

この提案ではさらに、上記fwdのようによく使用すると思われるtype_setを事前定義して提供しています。

namespace std::inline namespace type_set_templates {
  // P2665によって、値渡しと参照渡しを自動選択する
  template<typename T>
  type_set in = <const T, const T&>;

  // 非const参照渡し用
  template<typename T>
  type_set ref = <T&>;

  // 関数戻り値の型が入力に依存する場合にdeducng thisと組み合わせて使用する
  template<typename T>
  type_set dual = <const T&, T&>;

  // T&&のように振舞うが、Tを渡すと値渡しになる
  template<typename T>
  type_set fwd = <T, const T&, T&&>;

  // 内部でムーブすることを意図する場合のT&&として使用できる
  template<typename T>
  type_set mv = <T, T&&>;
}

P2669R0 Deprecate changing kind of names in class template specializations

クラステンプレートの明示的特殊化において、名前の種類の変更を禁止する提案。

クラステンプレートのメンバ型を使用する場合、一部の箇所ではtypenameキーワードを付加する必要があります。これは、構文からは(静的)メンバ変数との区別がつかないため、それをプログラマが指定するためのものです。

#include <vector>

template<typename T>
void f() {
  typename std::vector<T>::value_type* x; // value_typeはメンバ型である(はず
}

この例においては、std::vectorの定義はこの場所で見えているため、value_typeがメンバ型であってメンバ変数ではないことはコンパイラにはわかっているはずです。にもかかわらずtypenameが必要となるのは、一部のTについて別の定義、すなわち明示的特殊化が追加されている可能性があるためです。

#include <vector>

template<typename T>
void f() {
  typename std::vector<T>::value_type* x; // value_typeはメンバ型である(はず
}

struct my_class {};

template<>
class std::vector<my_class> {
  static int value_type = ...;
};

f<my_class>();  // std::vector<my_class>::value_typeは変数

この可能性を常に考慮する必要があるため、このような場合におけるtypenameは省略不可能となっています。

この提案は、このようにクラステンプレートのプライマリテンプレートの定義が見えていて、依存名からそのメンバ型を使用する時、typename省略を妨げる上記のような問題が起こらないようにするために、クラステンプレートの明示的特殊化においてプライマリテンプレートのメンバ名の種類の変更を禁止しようとするものです。

#include <vector>

template<typename T>
void f() {
  /*typename*/ std::vector<T>::value_type* x; // value_typeはメンバ型
}

struct my_class {};

template<>
class std::vector<my_class> {
  static int value_type = ...;  // ng、名前の種類が変わっている(型 -> 変数)
};

筆者の方の調査によれば、この提案の変更によってこのような場合のtypename使用の75%が削減できる、としています。

ただし、この提案の変更は明らかに既存のコードを壊していると思われます。

この提案では、3つのレベルでの名前(以下、クラステンプレートにおけるメンバのものとする)の明示的特殊化における種類変更の禁止を挙げています

  1. 全ての名前の種類変更の禁止
  2. ある名前の種類変更がされている特殊化の定義が見えている時、その名前の使用を禁止
  3. 名前の種類変更が行われるテンプレートの特殊化の禁止(名前の種類変更は行われないと仮定する)
template<typename T>
struct template_name {
  using name = T*;
};

// nameの種類を変更する特殊化
template<>
struct template_name<int> {
  static const char* name = "Nisse"; // 1ではエラー
};

template<typename T> int f() {
  template_name<T>::name x; // 2ではエラー
  return 0;
}

int a = f<int>(); // 3ではエラー、`template_name<T>::name`は想定する種類(型)と異なる
std::cout << template_name<int>::name; // 2と3ではOK、1の場合は宣言でエラー

実装難易度やコンパイル時間等のコストを考慮して、どの段階での禁止とするのかを選択することを意図しています。

P2670R0 Non-transient constexpr allocation

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

C++20で定数式における動的メモリ確保が許可されましたが、確保した領域を実行時に持ち越すことは許可されていません。

constexpr auto f() -> int {
  std::vector<int> v = {1, 2, 3}; // ok
  return v.size();
}
static_assert(f() == 3);  // ok

constexpr std::vector<int> v = {1, 2, 3}; // ng

現状では、コンパイル時に確保したメモリ領域は全てコンパイル時に解放されなければなりません。これを一時的な割り当て(transient constexpr allocation)と呼び、持ち越す場合を非一時的な割り当て(Non-transient constexpr allocation)と呼びわけます。

非一時的な割り当てが許可されていないのは、クラス型のconst性伝播が不完全であることから定数式で確保したメモリ領域を実行時に解放できてしまう場合があるためです。

constexpr unique_ptr<unique_ptr<int>> ppi(new unique_ptr<int>(new int(1)));

int main() {
  // ppiはconstであり、この呼び出しはコンパイルエラー
  ppi.reset(new unique_ptr<int>(new int(2))); // ng

  // しかし、これはできる・・・
  ppi->reset(new int(3)); // ok!?
}

std::unique_ptrオブジェクトに対するconst指定はそのオブジェクトそのものには当然適用されていますが(すなわち、ポインタそのものはconstですが)、ポインタの指す先にあるオブジェクトには伝播しません。それによって、定数式で確保したメモリ領域を実行時に解放することができてしまいます。非一時的な割り当てによる領域は実行時には静的ストレージに昇格されており、これをdeleteしようとするとそれは失敗し、おそらくプログラムはそこで落ちます。

C++20に対する議論の時間もなかったことから、C++20の仕様としては非一時的な割り当ては禁止されました。

この提案は、改めてそれを許可しようとするものです。ただし、このリビジョンでは、既に提案されている2つのソリューションを紹介し、そのうちの1つを採用するべきと推奨しているに過ぎません。

この2つのソリューションはどちらも、入れ後になったstd::unique_ptrのようなケースを拒否することを目的としていました。

constexpr unique_ptr<int> pi(new int(1));                                   // ok(としたい
constexpr unique_ptr<unique_ptr<int>> ppi(new unique_ptr<int>(new int(2))); // error(としたい

constexpr std::vector<int> vi = {1, 2, 3};                              // ok(としたい
constexpr std::vector<std::vector<int>> vvi = {{1, 2}, {3, 4}, {5, 6}}; // ok(としたい
constexpr std::vector<std::string> vs = {"this", "should", "work"};     // ok(としたい

std::mark_immutable_if_constexpr

1つ目は、C++20で定数式におけるメモリ確保を導入した提案に当初含まれていたものです。上記の理由により、これは最終的に削除されました。

当初の非一時的な割り当ては

  • constexpr変数の初期化で行われていて
  • その変数のデストラクタを評価すれば初期化時に確保された領域を全て解放することができる(ことがわかる)
    • 実際にデストラクタが呼ばれるわけではない

場合にのみ認められていました。すなわち、非一時的な割り当てを含むconstexpr変数を初期化後に、別の定数式中からその変数を操作して確保し直すようなことはできません、

この場合問題となるのは、コンストラクタの実行からデストラクタの実行時点(コンパイル終了時点など)までの間に非一時的な割り当て領域を指しているポインタが変更可能な場合です。それが起こると、コンパイル時に確保されたまま解放されない領域が存在してしまいます。

それを回避するために、非一時的な割り当てを行なっている初期化式中でstd::mark_immutable_if_constexpr(p)とすることで動的確保した領域を指すpの不変性を宣言します。そして、定数式中のデストラクタにおいて読み取られる動的確保領域は、constであるかこの関数によってマークされている必要がある、と規定していました。

例えば、std::vectorではコンストラクタの最後などでこれを行うようにすることで、そのオブジェクトのconst性に関わらずデストラクタ実行時点で必ずどちらかの条件を満たします。一方、std::unique_ptrはそれを行わないため、ネストしていないconstなもの以外はこの条件を満たせません。これによって、ネストしたstd::unique_ptrのようなケースを拒否できます。

// ok: 領域を指すポインタはconst
constexpr std::unique_ptr<int> a(new int(1));

// error: ネストしたstd::unique_ptr<int>のポインタ(int*)は非constでマークもされていない
constexpr std::unique_ptr<std::unique_ptr<int>> b(new std::unique_ptr<int>(new int(2)));

// ok: ネストしたstd::unique_ptr<int>のポインタ(const int*)はマークされていないが、const
constexpr std::unique_ptr<std::unique_ptr<int> const> c(new std::unique_ptr<int>(new int(3)));

// ok: 領域を指すポインタはconst
constexpr std::vector<int> v = {3, 4, 5};

// ok: ネストしたstd::vector<int>のポインタはconstではないが、マークされている
constexpr std::vector<std::vector<int>> vv = {{6}, {7}, {8}};

すなわち、std::mark_immutable_if_constexpr()はdeep constな型において、ユーザーが手動でそれを宣言するためのものです。ただし、このアプローチは型が実際にdeep constであるかを保証しないため、unique_ptrと同等の自作クラスのコンストラクタでstd::mark_immutable_if_constexpr()を領域ポインタに対して行うことができます。std::mark_immutable_if_constexpr()の使用が適切かどうかを判断するのはユーザーの責任です。

propconst

2つ目は、C++20策定後に改めて考案しなおされた方法です。

propconstは新しい修飾子で、ポインタに対してもconst性の伝播を行うものです。deep constなクラスではこれを用いて確保領域を指すポインタを保存しておくことで、deep constであることを保証できるようになります。したがって、上記のconst伝播の問題が起こらないことをコンパイラが認識できます。

例えば、std::vectorではその領域を指すポインタにpropconstを付加しておくだけです。

// ok: 領域を指すポインタはconst
constexpr std::unique_ptr<int> a(new int(1));

// error: std::unique_ptr<int>へのポインタはpropconstではない(const伝搬が途切れている)
constexpr std::unique_ptr<std::unique_ptr<int>> b(new std::unique_ptr<int>(new int(2)));

// ok: ネストしたstd::unique_ptr<int>のポインタ(const int*)はconst
constexpr std::unique_ptr<std::unique_ptr<int> const> c(new std::unique_ptr<int>(new int(3)));

// ok: 領域を指すポインタはconst
constexpr std::vector<int> v = {3, 4, 5};

// ok: メンバのポインタはstd::vector<int>*ではなく、propconst std::vector<int>*であり
//     const std::vector<int>*と同等に動作する
constexpr std::vector<std::vector<int>> vv = {{6}, {7}, {8}};

2つの方法によるstd::vector実装の比較

mark_immutable_if_constexpr propconst
template <typename T>
class vector {
  T* ptr_;
  size_t size_;
  size_t capacity_;

public:
  constexpr vector(std::initializer_list<T> elems) {
    ptr_ = new T[xs.size()];
    size_ = xs.size();
    capacity_ = xs.size();
    std::copy(xs.begin(), xs.end(), ptr_);

    std::mark_immutable_if_constexpr(ptr_); // <==
  }

  constexpr ~vector() {
    delete [] ptr_;
  }
}
template <typename T>
class vector {
  T propconst* ptr_; // <==
  size_t size_;
  size_t capacity_;

public:
  constexpr vector(std::initializer_list<T> elems) {
    ptr_ = new T[xs.size()];
    size_ = xs.size();
    capacity_ = xs.size();
    std::copy(xs.begin(), xs.end(), ptr_);
  }



  constexpr ~vector() {
    delete [] ptr_;
  }
}

propconstは型システムを変更するため、その影響は言語全体に広く伝播します。良し悪しはともかく、全く無関係なコードにおいてその存在を考慮する必要が出てきます。mark_immutable_if_constexprは非常に限定的なもので、ライブラリ内の特定の型のうちの一部だけで使用し、それ以外のところには影響がありません。一方で、mark_immutable_if_constexprはその目的や意味、使うべきところと使ってはいけないところを理解しなければならない、あるいは理解されないで使用されてしまう、という危険性があります。shallow constな型で誤って使用されてしまってもすぐには問題にならないかもしれません。

潜在的に危険であったとしても、mark_immutable_if_constexprが使用されるのは限られた数箇所だけであるためその妥当性チェックにかかるコストは小さく、非一時的な割り当ての必要性は明らかであるため、すぐにでも導入可能なmark_immutable_if_constexprによるアプローチをまず導入すべき、と提案しています。

とはいえ、これはまだ具体的な文言を含んではいません。

P2671R0 Syntax choices for generalized pack declaration and usage

より一般的に使用可能なパラメータパックの構文についての提案。

この提案は、P1858R2で提案されていたパラメータパック周りの次の機能について、その構文を検討するものです。

  1. より多くの場所でパックを宣言可能にする
  2. パックの追加機能(インデックスアクセスとスライス)
    • 先頭からのアクセス
    • 後ろからのアクセス
  3. タプルの追加機能(インデックスアクセスとパック展開)

このうち、2と3の構文について検討し、次の2つのオプションを提案しています

機能 Option1 Option2
パックのインデックスアクセス(最初の要素) pack...[0] pack![0]
パックのインデックスアクセス(最後の要素) pack...[std::from_end{1}] pack![std::from_end{1}]
パックのスライス取得 pack...[1..]... pack![1..]~...
タプルのインデックスアクセス tuple.[0] tuple.[0]
タプルのパック展開 tuple.[1..]... tuple.[1..]~...
タプルのパック展開(全要素) tuple.[..]... tuple~...

packは型パックとオブジェクトパックの両方で共通ですが、tuplestd::tupleのオブジェクトのみです。

これらの構文の意味論については、P1858R2のものを踏襲しています。

P2672R0 Exploring the Design Space for a Pipeline Operator

パイプライン演算子|>)の考えられる設計について比較検討する文書。

この提案のパイプライン演算子|>)は、以前にP2011で提案されていたものです。

P2011の演算子は、x |> f(y)f(x, y)と書き換えるものでした。しかし、そのような書き換えには他にも選択肢があります。この文書はそれら潜在的な設計の紹介と、長所短所の比較を行うものです。

潜在的には4つの設計が考えられます

  1. Left-Threading
    • P2011のモデル
  2. Inverted Invocation
  3. Placeholder
  4. Language Bind

1と2は、現在のP2011のように、左辺の式で右辺の式を書き換えるものです。

Left-Threading Inverted Invocation
x |> f(y) f(x, y) f(y)(x)
x |> f() f(x) f()(x)
x |> f ill-formed f(x)
x |> f + g ill-formed (f + g)(x)

3と4は、プレースホルダー(ここでは%)を置くことで書き換えの方法を制御できるようにするものです。

Placeholder Language Bind
x |> f ill-formed f(x)
x |> f(y) ill-formed f(y)(x)
x |> f(%, y) f(x, y) f(x, y)
x |> f(y, %) f(y, x) f(y, x)
x |> y + % y + x y + x
x |> f(y) + g(%) f(y) + g(x) f(y) + g(x)
x |> f + g ill-formed (f + g)(x)
x |> co_await % co_await x co_await x
x |> f(1, %, %) f(1, x, x) f(1, x, x)

Language Bindモデルは、|>を言語サポートのあるstd::bindのように見るもので、プレースホルダを伴うf(y, %)は部分適用された単項関数だと思うものです。そのため、上2つの例やf + gの場合に、それを単項関数として左辺の値を渡しています。これはプレースホルダを伴うInverted Invocationモデルでもあります。

このうち、Inverted InvocationモデルはF#などで採用されていて理解しやすく実装も簡単ではありますが、C++にはそぐわない部分があります。

この演算子の目的は<ranges>におけるパイプライン演算子の諸問題を解決するためでした。

// |>は2のモデルによるとすると
r |> views::transform(f) |> views::filter(g);

// こう書き換えられる
views::filter(g)(views::transform(f)(r));

この書き換え後のコードは現在のRangeアダプタにおいても有効ですが、これを可能とするための追加の複雑なコードが必要となり(Rangeアダプタにrange遅延入力可能にしてるところ)、まさにそれは|>によって削減できるはずのものです。これを回避しようとすると、また小さくはないラッピングコードを書かなければならなくなります。結局、Inverted InvocationモデルはC++においてはあまり旨味がありません。

Placeholderモデルは、この演算子の柔軟性を向上させます。渡したいパラメータが関数の第1引数とは限らない場合があり、<ranges>にもそのようなRangeアダプタが存在しています。

r1 |> zip_transform(f, %, r2);

some_tuple |> apply(f, %);

some_variant |> visit(f, %);

ただし、必然的にLeft-Threadingと比べると冗長な記述になってしまいます

// left-threading
r |> views::transform(f)
  |> views::filter(g)
  |> views::reverse();

// placeholder
r |> views::transform(%, f)
  |> views::filter(%, g)
  |> views::reverse(%);

<ranges>及びC++26<excution>において圧倒的に一般的なのは、左辺の値を最初の引数に渡すことです。Left-Threadingモデルでは構文上のオーバーヘッドはほぼありませんが、Placeholderモデルでは現状よりも1つのチェーンごとに4文字長くなります。

ただ、Left-Threadingモデルだと冗長になるケースがあり、その場合はPlaceholderモデルの方が簡潔になります。

// HTMLタグの中身を抽出する
auto filter_out_html_tags(std::string_view sv) -> std::string {
  auto angle_bracket_mask =
    sv |> std::views::transform([](char c){ return c == '<' or c == '>'; });

  return std::views::zip_transform(
      std::logical_or(),
      angle_bracket_mask,
      angle_bracket_mask |> rv::scan_left(std::not_equal_to{})
    |> std::views::zip(sv)
    |> std::views::filter([](auto t){ return not std::get<0>(t); })
    |> std::views::values()
    |> std::ranges::to<std::string>();
}

この例では、zip_transformがLeft-Threadingモデルの2つの制限(最初のパラメータ以外に渡せない、複数の箇所に渡せない)の両方に該当しています。zip_transformに同じ入力rangeangle_bracket_mask)を2回渡したいために、angle_bracket_maskをまず別に作成してから、それを手動で指定する必要があります。

これをPlaceholderモデルによって書くと次のようになります

auto filter_out_html_tags(std::string_view sv) -> std::string {
  return sv
    |> std::views::transform(%, [](char c){ return c == '<' or c == '>'; })
    |> std::views::zip_transform(std::logical_or{}, %, % |> rv::scan_left(%, std::not_equal_to{}))
    |> std::views::zip(%, sv)
    |> std::views::filter(%, [](auto t){ return not std::get<0>(t); })
    |> std::views::values(%)
    |> std::ranges::to<std::string>(%);
}

Placeholderモデルでは1つの入力を複数回使用可能であるため、angle_bracket_maskが不要になります。

Language Bindモデルでは、ここからさらに2つ%を削減できます。

auto filter_out_html_tags(std::string_view sv) -> std::string {
  return sv
    |> std::views::transform(%, [](char c){ return c == '<' or c == '>'; })
    |> std::views::zip_transform(std::logical_or{}, %, % |> rv::scan_left(%, std::not_equal_to{}))
    |> std::views::zip(%, sv)
    |> std::views::filter(%, [](auto t){ return not std::get<0>(t); })
    |> std::views::values             // ここと
    |> std::ranges::to<std::string>;  // ここ
}

Left-Threadingモデルの構文を分離して共存させると(例えば\>)、ここからさらに18文字削減できます

auto filter_out_html_tags(std::string_view sv) -> std::string {
  return sv
    \> std::views::transform([](char c){ return c == '<' or c == '>'; })
    |> std::views::zip_with(std::logical_or{}, %, % \> rv::scan_left(std::not_equal_to{}))
    \> std::views::zip(sv)
    \> std::views::filter([](auto t){ return not std::get<0>(t); })
    |> std::views::values
    |> std::ranges::to<std::string>;
}

PlaceholderとLanguage Bindの構文上の違いは、右辺の式が単項呼び出しの場合にプレースホルダ%)を省略できるかどうかです。

2つのモデルが同じ記法となる別の例を考えます。

auto squares() -> std::generator<int> {
  std::views::iota(0)
    |> std::views::transform(%, [](int i){ return i * i; })
    |> co_yield std::ranges::elements_of(%);
}

どちらのモデルでもこれ(特に3行目)は単に式の書き換えを行うだけです。Language Bindモデルが単純に言語サポートのあるstd::bindだと思うと、3行目の式は別の関数にラップされてそこで実行されそうに思えます(スタックレスコルーチンではそれはできない)。

このように、Language BindモデルはPlaceholderモデルと比較して3文字削減できる代わりに、少し複雑なモデルとなっています。

この文書では、プレースホルダとして%を用いていますが、これはプレースホルダプレースホルダです。なるべく簡潔であり既存の操作と混同しづらい記号として、次の候補が挙げられています

プレースホルダ 備考
% printfプレースホルダとの共通性、剰余演算と衝突
%% 剰余演算との衝突が緩和
^ xorと衝突
^^ xorとの衝突が緩和
# C++演算子ではないが、プリプロセッサと衝突しうる
__ 予約されたトークン、_と近いが見づらい
$ BashAWKなどにおけるプレースホルダ、新しいトークンであり衝突しない
$$ 新しいトークンであり衝突しない
@ 新しいトークンであり衝突しない
@@ 新しいトークンであり衝突しない

筆者の方は、2文字よりは1文字の方がよく、$を押しているようです。

余談であり文書でもそう前置されていますが、Language Bindモデルは簡潔なラムダ式としてプレースホルダラムダへの道を開く可能性があります。

プログラミング言語において、ラムダ式の構文には3つのアプローチがあります

  1. 部分適用の言語サポート
  2. プレースホルダ
  3. 短縮関数宣言

パラメータが負であるかをチェックする単項述語を各言語のラムダ式による記述で比較してみます。

1の言語では次のようになります

言語
(<0) Haskell
(<) 0 F#

3を採用する言語では次のようになり、C++もここに含まれます

言語
|e| e < 0 Rust
e -> e < 0 Java
e => e < 0 C# 3, JavaScript, Scala
\e -> e < 0 Haskell
{ |e| e < 0 } Ruby
{ e in e < 0 } Swift
{ e -> e < 0 } Kotlin
fun e -> e < 0 F#, OCaml
lambda e: e < 0 Python
fn e -> e < 0 end Elixir
[](int e){ return e < 0; } C++
func(e int) bool { return e < 0 } Go
delegate (int e) { return e < 0; } C# 2

ここで注目すべきは、2のプレースホルダ式によるラムダ式です

言語
_ < 0 Scala (and Boost.HOF)
_1 < 0 Boost.Lambda (and other Boost libraries)
#(< % 0) Clojure
&(&1 < 0) Elixir
{ $0 < 0 } Swift
{ it < 0 } Kotlin

|>におけるLanguage Bindモデルによる意味論とプレースホルダは、このプレースホルダによるラムダ式と非常に近しいものがあります。つまり、x |> % < 0という一連の式はLanguage Bindモデルによれば途中で[](auto x) -> bool { return x < 0; }のような部分適用を既に行なっていると思うことができ、この部分だけを抽出すればプレースホルダによるラムダ式が実現できそうです。

ただ、x |> % < 0そのものからラムダを生成するのはこの文書のそもそもの目的であるパイプライン演算子と衝突し、右辺だけを特別扱いするのは難しそうです。結局、上記他言語プレースホルダ式によるラムダの表の下4つのように、ラムダ式の境界を示すトークンが必要になります。しかし、プレースホルダーラムダも外部の変数をキャプチャする必要がありその方法を指定できなければなりません。すると、C++においてのプレースホルダラムダ式の構文は、ラムダ導入子[]の後にプレースホルダラムダであることを示す何らかの記号(: or =>など)が来て、その後にプレースホルダ式を記述するものになりそうです。

先ほどまでの例を実装すると

[]: % < 0;
[]: %1 < 0;

%%1の差は、何番目の引数を参照するかを指定するかどうかだけで、この例では等価です。

あるいは、Elixirに近い構文を採用すると

[] %(%1 < 0);

のようにもできます。

これらによって、パラメータが負であるかをチェックする例は、現在の26文字必要なところから9~11文字で記述することができるようになります。

|>の話に戻ると、結局筆者の方の結論は

  • Placeholderモデルを採用
  • |>の優先度はとても低く、代入と同等
  • プレースホルダの複数回の使用時には、左辺を一回評価した後その結果の右辺値を渡す
  • プレースホルダとしては$が望ましい

そして、より簡潔なラムダ式の構文としてプレースホルダラムダを検討すべき、としています。

P2673R0 Common Description Format for C++ Libraries and Packages

モジュールベースC++ライブラリを扱うための情報を記述する、共通のフォーマット標準化のための作業グループの立ち上げを要請する提案。

C++20でモジュールが導入されたことで、C++のライブラリ(パッケージ)提供者と消費者はモジュールベースライブラリを扱うための情報を記述する方法を策定する必要性に迫れています。C++コミュニティは長年これらの仕組みの構築を怠ってきたため、コンパイラ・ビルドシステム・パッケージマネージャ・リポジトリメンテナ間の相互運用性に欠ける複数のソリューションを生み出してしまいました。

モジュールの登場によりそのような仕組みの重要性は高まっており、今行動しないと同じことがまた繰り返され、C++コミュニティに新たな分断を招く可能性があります。

この提案は、それらの懸念から、C++のライブラリ(パッケージ)を取り扱うための情報を記述するフォーマット仕様を策定すべく、それにフォーカスした作業グループを設立することをC++標準化委員会に要請するものです。このグループの成果はC++パッケージ記述に関する技術報告(Technical Report)のための入力として使用されるものであり、他の作業部会と並行して行われるべき、と述べています。

P2674R0 A trait for implicit lifetime types

implicit lifetime typeを識別するための型特性、is_implicit_lifetime<T>の提案。

C++20にて、implicit lifetime typeと呼ばれる型のカテゴリが追加されました。これは、その生存期間が暗黙的に開始される(構築を明示する必要がない)型で、malloc()などによる動的メモリ確保における未定義動作を回避するための仕組みです。

しかしそれは、スカラ型や配列型、クラス型では集成体型やトリビアルなコンストラクタ/デストラクタを持つ型、とされていて特定の指定された型ではありません。implicit lifetime typeではない型に対してはC++23で導入されるstd::start_lifetime_as<T>()を使用する必要があるなど、プログラマimplicit lifetime typeを識別できる必要がありますがC++23時点でもその手段は提供されていません。

他にも、自作のクラス型が当初はimplicit lifetime typeだったものの変更の過程でその性質が失われた場合、静かに未定義動作を引き起こすことになります。これを静的に検査できればそのようなバグをコンパイルエラーとして報告することができます。

これらの問題の解決のために、std::is_implicit_lifetime<T>型特性(メタ関数)を導入することでimplicit lifetime typeを識別する方法を提供しようとする提案です。

namespace std {
  template<class T>
  struct is_implicit_lifetime;

  template<class T>
  inline constexpr bool is_implicit_lifetime_v = is_implicit_lifetime<T>::value;
}

C++20時点の規定であれば、この型特性をユーザーが定義することが可能です。しかし、C++23でimplicit lifetime typeなクラス型の定義が変更(集成体型 -> ユーザー定義デストラクタを持たない集成体型)される予定であり、この場合はデストラクタがユーザー定義されているかを検出する方法がないため、コンパイラマジックに頼らざるを得なくなります。このことはまた、標準ライブラリの機能としてこれが必要である理由の一つでもあります。

P2675R0 LWG3780: The Paper (format's width estimation is too approximate and not forward compatible)

std::formatの文字幅の規定を改善する提案。

出力先がユニコード文字列である場合のstd::formatにおいて文字幅を考慮する必要があるオプションが指定された時、ある文字の文字幅は特定の文字が2、それ以外が1として推定されます。

具体的には次のユニコードのコードポイント範囲の文字が文字幅2として推定されます

  • U+1100U+115F
  • U+2329U+232A
  • U+2E80U+303E
  • U+3040U+A4CF
  • U+AC00U+D7A3
  • U+F900U+FAFF
  • U+FE10U+FE19
  • U+FE30U+FE6F
  • U+FF00U+FF60
  • U+FFE0U+FFE6
  • U+1F300U+1F64F
  • U+1F900U+1F9FF
  • U+20000U+2FFFD
  • U+30000U+3FFFD

これ以外の文字は文字幅1となります。例えばほとんどの漢字やひらがなは幅2になり、Ascii範囲の文字は全て幅1になります。

規格書においてもこのように指定されていますが、この指定には次のような問題があります

  • このコードポイント範囲の選択基準が不透明
  • 将来のUnicode仕様との前方互換性が不十分
    • このリストは時間の経過とともに意味のないものになるか、Unicodeのバージョンアップの度にメンテナンスが必要になる
  • これらコードポイント範囲の一部は未割り当てか予約されている

この提案は、これらの問題の解決のためにこの規定を修正しようとするものです。

次のような変更を提案しています

  • これらコードポイント範囲はUAX-11を参照するようにする
  • これを導入した提案(P1868R1)の意図と既存の慣行を踏襲したコードポイント範囲を選択し、それを明確化するためにその名称を追加する

多くの絵文字はEast_Asian_Width="W"というカテゴリに含まれるため、まずこれを文字幅2として指定します。そして、C++20との互換性のために、一部の範囲を維持し、それを名称によっても指定するようにします。

これによって、文字幅2として推定されるのは次のコードポイント範囲の文字になります

  • East_Asian_Width="W"であるコードポイント
  • East_Asian_Width="F" Derived Extracted Property(UAX-44)に該当するコードポイント
  • U+4DC0U+4DFF (Yijing Hexagram Symbols)
  • U+1F300U+1F5FF (Miscellaneous Symbols and Pictographs)
  • U+1F900U+1F9FF (Supplemental Symbols and Pictographs)

この変更によって、8477個のコードポイント(主にTangut Ideographs西夏文字)が幅2として推定されるようになり、未割り当ての85個のコードポイントが幅1と推定されるようになります。

また、ユニコードではワイドとみなされていない(East_Asian_Width="W"ではない)8個の文字(U+3248 - U+324F)の幅を2から1に変更しています。ただし、これはどうすべきか不明なのでユニコードの指定に合わせたもので、Open Questionとされています

U+3248 ~ U+324Fの文字
㉈㉉㉊㉋㉌㉍㉎㉏

P2676R0 The Val Object Model

プログラミング言語Valで実装された、値セマンティクスに基づくオブジェクトモデルを紹介する文書。

ソフトウェアの安全性に関する懸念は産業界などで高まっており、もはやC++は安全な言語であるとはみなされておらず、より安全な言語への移行が推奨されるような状況になっています。C++にとっても安全性の向上は急務であり、標準化委員会でもその対応が検討されています。

C++のようなシステムプログラミング言語においてメモリ安全性を達成するには、非現実的なプログラム全体の解析やライフタイム注釈の追加などを意味していました。この文書は、プログラミング言語Valで実装された、C++に既に深く浸透している値セマンティクスに基づくオブジェクトモデルによる別の方法ついて紹介するものです。

値セマンティクスに全力投球(going all in)することで、次のメリットが得られます

  • 構成的なメモリ安全性
  • 構成的なスレッドセーフ性(Rustのfearless concurrencyのような)
  • 有用な診断によるシンプルで強力なプログラミングモデル

Valモデルは、独立性(independence)を値のセマンティクスが持つ重要なプロパティとして認識する事から出発します。独立性とは、ある変数への変更が別の変数に影響を与えない、という事です。

C++は3つの方法でこの独立性を提供します

  • 値渡し。呼び出し先に独立した値を与える
  • 値のreturn。戻り値は呼び出し側で独立している
  • 変更可能な状態を共有しないために、コピーや代入操作を記述できる

一方で、C++には独立性を損なっている部分もあります

  • 値の変更は、他の可変参照(ポインタ)のエイリアスを作成可能なthisポインタを介して発生する(const伝播の断絶?)
  • 値渡しはコピーが発生するため、参照渡しが好まれる

Valモデルの背後にある重要なアイデアは、独立性を完全に維持しながら(嫌われがちな)コピーを排除することです。

const &による引数渡しは、"効率的な値渡し"として使用されがちで、C++に深く根付いています。それは不変ではあるものの参照であるため、変数の独立性を損ねます。

非効率 効率的・間違い
// xに2*deltaを足す
void offset2(BigNum& x, BigNum delta) {
  x += delta
  x += delta
}

void main() {
  BigNum x = 3;
  offset2(x, x);
  std::cout << x << std::endl; // Prints 9
}
// xに2*deltaを足す
void offset2(BigNum& x, BigNum const& delta) {
  x += delta
  x += delta
}

void main() {
  BigNum x = 3;
  offset2(x, x);
  std::cout << x << std::endl; // Prints 12
}

右側のコードを正しくすることを保証する唯一の方法は、追加の独立性要件を要求し、ユーザーがそれを満足することを期待することです。

// xに2*deltaを足す
// Requires: x と delta は異なるオブジェクトであること
void offset2(BigNum& x, BigNum const& delta) {
  x += delta
  x += delta
}

void main() {
  BigNum x = 3;
  offset2(x, BigNum(x)); // 明示的なコピー
  std::cout << x << std::endl; // Prints 9
}

このような要件が明示されることはほぼありません。実際には、可変参照で参照されるオブジェクトが他の全ての変数の値から独立していることを要求する排他性の法則(Law of Exclusivityが暗黙的に存在しています。そしてこれは、すべてのC++プログラマのメンタルモデルに組み込まれているはずです。なぜなら、上記例のような意図しない変更は一般的に使う方(呼び出し側)の問題とみなされており、このようなことが起こらないようにする合理的な方法が他にないためです。

また、このことはC++標準の各所でも要件や例として見ることができます。

未定義動作 未規定の動作
#include <algorithm>
#include <vector>

int main() {
  std::vector v = { 0, 1 };
  std::ranges::sort(
    v, [&v](int x, int y) {
      v.push_back(2); return y < x;
    });
}
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
  std::vector v = { 0, 1, 2, 1, 2 };
  std::ranges::replace(v, v[1], v[2]);
  for (auto x: v) { std::cout << x; }
}

どちらの例もコンパイルは通りますが、左の例は未定義動作(述語オブジェクトが副作用と状態を持つ)であり、右の例は注意深く標準を読むと未規定の動作であることがわかります。どちらも、暗黙の排他性の法則を満たしていないからです。

Val言語では、そのオブジェクトモデルの基礎となる4つの引数渡し規則をサポートしています。

構文 意味 C++ 保証
x: let T const値渡し const T& 参照は真に不変
x: inout T 可変な借用 T& xの値はxを通してのみアクセス可能
x: sink T 所有権の移動 T&&(右辺値参照) 呼び出し先はxのオブジェクトの破棄責任を持つ
x: set T 初期化 placement new 事後条件 : xは初期化済み

デフォルト(x: T)はletとみなされます。letはコピーされない値渡しであり、呼び出し先内ではxの独立性が保証されています。すなわち、xは不変であり、xのオブジェクトをほかの手段(非表示の参照)によって変更することはできません。

Val言語では、関数内からの引数の変更はinout引数を通してのみ行えます。inoutは、呼び出し元が完全に所有する独立した値であり、その引数を介してのみアクセス可能です。これは、呼び出し側で&をマークすることで排他性の法則を維持し、独立性を保証します。

swap(&x.a, &x.b)  // OK : ただし、x.aとa.bが異なる場合のみ
&v.remove(v[2])   // ERROR : vはv[2]とオーバーラップする。代わりにv[2].copy()を渡す
swap(&x[i], &x[j])  // ERROR: x[i] と x[j] はオーバーラップしうる。代わりにi != jを保証したうえでindex_swap()を使用
&x.index_swap(i, j) // OK : i == j ではないことは実行時に検査される

sinkは引数の消費を意味しています。C++における参照渡しとの違いは、無意味な抜け殻を残すのではなく、左辺値引数が呼び出し元でアクセスできなくなり、破棄の責任が値の予所有権とともに呼び出し先に渡されることです。

// 関数宣言
fun consume(_ x: sink Int) {}

var a = 1, b = 1
consume(a) // OK : aの最後の使用
consume(b) // ERROR : bはこの後で使用されている。代わりに、b.copy()を使用
print(b) // bの最後の使用

Val言語においてバインディングvarで宣言されている変数)の寿命は最後に使用されるまでです。そのため、左辺値はstd::moveのような明示的なマーキングなしでsink引数に渡すことができます。

なお、これらの例で見られるように、Val言語ではコピーが明示的です。ただし、これはこのオブジェクトモデルの基本ではありません。

Valにおけるバインディング(参照への束縛)はC++における変数宣言と同等の構文で、3種類提供されています。

構文 規則 保証
var x = expr sink xは変数。exprの左辺値は以降他の方法でアクセスできない
let x = expr let xおよびexprの左辺値は、xの生存期間中真に不変。コピーされていない
inout x = expr inout expr及びそれに含まれるすべてのものは、xの生存期間中アクセスできない

それぞれの形式は、バインディングがその最後の使用時点までのコードによって形成された継続(例えばラムダ式)への引数であるかのように理解することができ、その意味は引数渡し規則に対応しています。左辺値(の一部)に対するlet/inoutバインディングはその生存期間中、独立性を維持するためにその左辺値の使用を制限します。

fun use<T>(_ x: T) {}

fun test(_ inout x: (first: Int, second: Int)) { // xは2つのフィールドをもつタプル
  let y = x.first
  print(x)      // OK
  x.first += 1  // ERROR: `x.first`は`y`によって`let`バインディングされている
  use(y)

  inout z = x.first
  z += 1    // OK
  print(x)  // ERROR: `x`は`z`によって`inout`バインディングされている
  use(z)    // zの生存期間の最後
  print(x)  // OK; `x`はどこからもバインディングされていない
}

(文書にはもう一つProjection(オブジェクトの一部の参照)が説明されていますが省略・・・)

文書では、型システムの拡張なしで、局所的な安全性向上と動的な安全性検査を受け入れることは、すべてのプログラムの安全性を静的に証明する言語機能を追加するよりも良いかもしれない、と述べています。

また、この文書はこれらのことをC++に導入する提案ではなく、C++が安全性の向上策を検討する際の参考にしてほしい、というものです。

P2677R0 Reconsidering concepts in-place syntax

autoによる簡易関数宣言構文において、型名を取得可能にする提案。

C++20では、コンセプトの導入に伴ってジェネリックラムダの宣言構文が通常の関数宣言でも使用可能となりました。これによって、関数テンプレートを単純かつ明快に書くことができるようになっています。

auto square(auto x) { return x*x; }

ただしこれにはジェネリックラムダから継承している問題があり、関数本体で引数の型が必要となる際にそれを効率的に参照できません。典型的には関数内で引数を完全転送(std::forward())する場合です。

[](auto&& x) { return f(std::forward<decltype(x)>(x)); }

これによって実質的に、多くのテンプレートコードではautoによる短縮表現を利用することができません。

この提案は、auto短縮構文をより利用可能とするために、この場合に推論された引数の型を取得する簡易な構文を追加しようとするものです。

提案する構文はauto{T}のように宣言するものです。

[](auto{T}&& x) { return f(std::forward<T>(x)); }

これによって、最小の変更で型名を取得できるようになり、autoによる簡易構文のメリットを損なうことなく利用できるシーンを広げることができます。

未定義動作 未規定の動作
template<typename T>
void f(T t);

template<integral T>
T gcd(T l, T r);

template<typename X, typename ...Ts>
void wrap(X x, Ts&& ...ts);

template<Animal Predator, Animal Prey>
void simulate(Predator &predator, Prey &prey);
// 単純な使用例
void f(auto{T} t);

// 意味論に変更はない
auto gcd(integral auto{T} l, T r) -> T;

// std::forwardには型名のみが必要
void wrap(auto x, auto{Ts}&& ...ts);

void simulate(Animal auto{Predator} &predator,
              Animal auto{Prey} &prey);

P2678R0 SG16: Unicode meeting summaries 2022-06-22 through 2022-09-28

SG16(Unicode Study Group)の2022年6月から9月にかけての議事録。

提案やIssueなどについての議論の際に、誰がどんな発言をしたのかが記述されています。

P2679R0 Fixing std::start_lifetime_as for arrays

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

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

この関数は単一オブジェクトへのポインタを受け取るもの(std::start_lifetime_as)と配列へのポインタを受け取るもの(std::start_lifetime_as_array)の2つが用意されています。

まず、この点が既存の似た機能(std::make_shared()std::make_unique())と一貫していません。既存のものは単一オブジェクトのものと配列のもの(とさらに要素数不定の配列のもの)について同じ名前でオーバーロードとして提供しています。

次に、この関数の名前づけはそれ自身と矛盾しています。std::start_lifetime_as_arrayは要素数不明の配列のためのもので、実は要素数が既知の配列のためのものではありません。要素数が既知の配列に対してはstd::start_lifetime_as<T[N]>(p)とし、要素数不定の配列に対しては要素数nも用意してstd::start_lifetime_as_array<T>(p, n)とします。このように、要素数が既知かどうかで配列に対する使用方法がかなり変化します。

最後は戻り値型についてです、配列に対して使用したとき、std::start_lifetime_as_arrayは配列の最初の要素へのポインタを返しますが、std::start_lifetime_asは配列そのもののポインタを返します。つまり、std::start_lifetime_as_array<int>(p, 16)の戻り値型はint*ですが、std::start_lifetime_as<int[16]>(p)の戻り値型はint(*)[16]です。

このように、この関数は配列に対して利用する時に非直感的な操作感と振る舞いを持ち、使いづらくなっています。この提案はこれらを正そうとするものです。

次のように変更することを提案しています

  • 既存のstd::start_lifetime_asの適格要件(Mandates)及び制約にTが配列型ではないことを追加
  • 素数が既知の配列型(T[N])用のstd::start_lifetime_asオーバーロードの追加
    • 戻り値が配列型のポインタから配列の先頭要素のポインタになる以外は現在と同じ
  • 既存のstd::start_lifetime_as_arraystd::start_lifetime_asオーバーロードT[]のためのもの)に置き換える

これによって、std::start_lifetime_as_arrayはなくなり、1つの名前で3つのケースを処理することができます。また、戻り値型も(意味はともかく)すべての場合で同じになります。

現在 この提案
unsigned char* buf = /∗ ... ∗/;
int* p1 = std::start_lifetime_as<int>(buf);
int(* p2)[10] = std::start_lifetime_as<int[10]>(buf);
int* p3 = std::start_lifetime_as_array<int>(buf, 10);
unsigned char* buf = /∗ ... ∗/;
int* p1 = std::start_lifetime_as<int>(buf);
int* p2 = std::start_lifetime_as<int[10]>(buf);
int* p3 = std::start_lifetime_as<int[]>(buf, 10);

この提案はバグフィックスでありC++23に間に合わせることを提案しており、C++23 CDに対するNBコメントでも同様の指摘があります。

P2680R0 Contracts for C++: Prioritizing Safety

C++契約プログラミング機能は安全第一で設計されるべき、という提言。

安全性は次のバージョンのC++が解決すべき問題の最優先の事項であり、契約プログラミングはそれを提供できる機会の一つです。ただし、そこではデフォルトで安全でなければならず、安全性を実現するのに事前知識や慎重な検討が必要になることは避け、広く一般のC++プログラマが実践可能である必要があります。

C++における契約プログラミングをそのようなものにするために、次の事を提案しています

  • コントラクト述語における副作用の禁止
  • constexprの実行モデルと似たモデルを当てる
    • constexprでは、1つの定数式実行の外部に出なければ内部での副作用は許可される

すなわち、契約条件(述語)内の副作用はその評価の外部に影響を及ぼさない場合に限り許可するという事を提案しています。これは、既に10年以上の実績を持つconstexprの実行モデルから借りてくるものであり、constexpr関数のみを契約条件で使えるようにするわけではありません。

P2682R0 Transactional Memory TS2 Editor's Report

N4923のEditor's Report。

N4906からN4923への変更点が記載されています。

設計上の変更はなく、typoなどの軽微な修正のみです。

P2683R0 SG14: Low Latency/Games/Embedded/Finance/Simulation virtual meeting minutes 2022/02/09-2022/10/12

SG14の2022年2月から10月にかけてのオンラインミーティングの議事録。

P2684R0 SG19: Machine Learning Virtual Meeting Minutes 2022/02/10-2022/10/13

SG14の2022年2月から10月にかけてのオンラインミーティングの議事録。

P2685R0 Language Support For Scoped Allocators

スコープ付アロケータモデル(scoped allocator model)に基づくアロケータのカスタマイズのための言語機能の提案。

C++標準ライブラリはアロケータのカスタマイズサポートのために長年作業してきました。しかし、アロケータのカスタマイズは簡単ではなく、一部の言語機能には相性が悪いものもあります。

スコープ付きアロケータモデルにおいては、あるアロケータを使用するオブジェクト(例えばコンテナ)が内包するオブジェクトにも同じアロケータが伝播する必要があります。これはもちろん自然に行える事ではないので、その対応のためには追加のコードが必要になります。

今日のスコープ付アロケータモデルによるカスタムアロケータ(例えば、std::pmr::polymorphic_allocator)の問題点は大きく次の2つです

  1. 過剰なボイラープレートコード
    • アロケータを受け取るコンストラクタと代入演算子
    • uses allocator construction関連
    • アロケータの伝播についての指定(select_on_container_copy_constructionなど)
  2. 言語のいくつかの機能との相互運用性の欠如
    • コンストラクタを書かないクラス(集成体)や書けないクラス(ラムダクロージャ型)をアロケータ対応できない

たとえばpolymorphic_allocatorを使用していて、アロケータを使用するところでは同じアロケータ(すなわちmemory_resource)を使用したいとします。その際、集成体のメンバにアロケータを使用するものがあると、コンストラクタを書かないことから伝播させられなくなります。そこで、アロケータの一貫性確保のために手動で注意深くアロケータを伝播しようとするとします。

// 集成体でpolymorphic_allocatorを使う例
struct Aggregate {
  std::pmr::string data1;
  std::pmr::string data2;
  std::pmr::string data3;
};

std::pmr::memory_resource* mr = ...;
std::pmr::polymorphic_allocator<> a1{mr};  // 用意したmemory_resource実装を渡す

// 集成体初期化時にアロケータを渡す
Aggregate ag1  = {{"Hello", a1}, {"World", a1}, {"!", a1}};
Aggregate ag2  = {{"Hello", a1}, {"World", a1}, {"!", a1}};

// vector初期化時にアロケータを渡す
std::pmr::vector<Aggregate> va(a1);

va.emplace_back(std::move(ag1));  // Correct ムーブでアロケータは伝播する
va.emplace_back(ag2);             // Error コピーではアロケータは伝播しない
va.resize(5);                     // Error アロケータが一致しないオブジェクトがある
va.resize(1);                     // OK アロケータが一致しないオブジェクトが削除される

集成体のコピー/ムーブはメンバごとのコピー/ムーブになるため、初期化の段階でアロケータの一貫性が保たれていればそれ以降も保たれます。ただし、その初期化の段階でアロケータ一貫性を保とうとするのはかなり難しく、意図せずコピーやデフォルト構築が発生するとpolymorphic_allocatorはシステムデフォルトのアロケータ(std::pmr::get_default_resource())を使用してしまいます。

スコープ付きアロケータを使用するコンテナでは、少なくとも各要素のアロケータが一致していることが暗黙の不変条件となっています。しかし上記のように、集成体型やラムダ式クロージャ型(と一部のコンテナ)ではそれは容易に破られてしまい、手動で管理しようとするのは著しく困難です。

スコープ付アロケータを使用する型においては、アロケータはその型のオブジェクトの構成要素の一部ではありません。swapやコピー/ムーブによって必ずしも伝播せず、通常比較に参加しません。このことは、C++におけるオブジェクトが通常全てのメンバを構成要素とみなすのとは異なっています。このような問題の解決はライブラリレベルではもはや不可能なため、言語機能の拡張によって解決しようとするのがこの提案です。

コンストラクタの冗長化を回避し、コンストラクタを書けない(書かない)型にまでアロケータを到達させるには、オブジェクトの初期化時にオブジェクトの一部でありながら一部ではないスコープ付きの値(ここではアロケータ)を伝達する別の経路が必要です。この提案ではそのために、usingによってオブジェクト初期化時にスコープ付きの値を渡す構文を提案しています。

int main() {
  // スコープ付きのリソース
  std2::test_resource tr;

  // スコープ付きのリソースとしてtrを使用し、vectorを初期化
  std2::vector vi using tr = {1, 2, 3};
  //              ^^^^^^^^
}

std2名前空間std::pmrのようなものだと思ってください。

using trによって、スコープ付リソースをオブジェクトの初期化とは別の経路で提供し、std2::vectorではアロケータとしてtrを利用します。

前述のように、スコープ付アロケータモデルにおいては同じデータ構造に参加する全てのオブジェクトは同じアロケータを使用しなければなりません。この性質の確実な保持と受け渡しを担うボイラープレートコードの削減のために、アロケータを使用する型に対してその基底クラス及び非静的メンバに暗黙的にアロケータを供給する機能も提案します。そのような型はallocator-enabled type(アロケータ対応型)と呼ばれ、アロケータ対応型を基底クラスかメンバに持つ型として再帰的に定義されます。そのような再帰の底、一番プリミティブなアロケータ対応型には新しい基本型が用意されます。

アロケータ暗黙伝播の例

int main() {
  std2::test_resource tr;

  // 配列に対するスコープ付きリソースの指定
  // 配列の各要素にtrが暗黙供給される
  std2::string s2[] using tr = { "Hello", "world" };
}

この例では、現在アロケータ伝播機能を持たない配列型(集成体型)においてスコープ付アロケータの適切かつ自動伝播が行われています。

同様に、全てのコンストラクタがスコープ付アロケータを暗黙に取得することができるため、テンプレートパラメータに指定された型を直接受け取る標準ライブラリの型でも同様のサポートがなされるはずです。

int main() {
  using namespace std2::string_literals;
  std2::test_resource tr;

  // uses allocator constructionと同等のことが暗黙かつ自動で行われる
  std::pair  p2 using tr = { "Hello"s, "world"s };
  std::tuple t4 using tr = { "Bonjour"s, "tout"s, "le"s, "mond"s };
}

アロケータ対応型のようなスコープ付リソースを持つオブジェクトでは、スコープされたメンバは伝播しないため変更することができません。スコープ付のメンバはオブジェクトの一部ではないため、代入演算子や比較演算子などのdefault実装の理想的な定義はスコープ付の値を無視し、変更しないことです。

std::allocator_traitsは引数としてアロケータが渡されることなどに依存していることから、このような変更が取り入れられたとすると現在のstd::allocator_traitsは使用できなくなります。この提案では、std::allocator_traitsを取り除くことも提案しています。これによって、アロケータは決して伝播しない、allocate()は生のポインタを返すなどの仮定を焼き付けます。

アロケータ対応型ではムーブによってもそのスコープ付リソースは伝播しません。しかし、現在のpolymorphic_allocatorのモデルでは、ムーブが行われる際にムーブ先と元で使用するmemory_resourceが異なる場合に例外を投げる可能性があるという追加の制約があります。その根本原因は、polymorphic_allocatorのデフォルト構築がシステムデフォルトのアロケータ(std::pmr::get_default_resource())を使用することにあり、これはコピーコンストラクタのデフォルト動作ではありますが、ムーブコンストラクタの動作として意図されたものではないようです。ムーブを適切に行うには、ムーブ先と元で同じアロケータを持つ必要があります。

したがって、アロケータ対応型が右辺値から構築される際に特別のルールを設けます。ムーブコンストラクタは、using引数が提供されない場合常に右辺値(ムーブ元)のアロケータを受け取ります。using引数が提供された場合、コンパイラはその呼び出し時点で2つのアロケータを比較するコードを挿入します。アロケータが同じであればそのままムーブし、異なる場合はusing引数のアロケータを使用してコピーコンストラクタを呼び出します。

現在のところ、この提案は問題の周知や問題点に関するフィードバックを得るための早期のものです。

P2686R0 Updated wording and implementation experience for P1481 (constexpr structured bindings)

構造化束縛にconstexpr指定できるようにする提案。

この提案はP1481R0で提案されていた内容をベースに、実装経験に基づいた調整を加えるものです。

この提案でのモチベーションとしては、将来的に導入が予定されているExpansion Statementsと呼ばれるコンパイルfor文において有用であり使用可能とすることが挙げられています。

auto tup = std::make_tuple(0, ‘’a, 3.14);

// Expansion Statementが入ったとしても、constexpr + 構造化束縛が使えない
template for (constexpr auto [idx, member] : std::views::enumerate(meta::data_members_of(^T)) ) {
  fmt::print("{} {}", idx, foo.[:member:]);
}

Expansion Statementstemplate forから始まるfor文で、主にリフレクションの結果として得られたコレクションを処理することを目的としています。このループはコンパイル時に行われるもので、振る舞いとしては一種のマクロのように内側のコードをループごとにその場に展開していくようなことを行います。したがって、Expansion Statements内のコードは定数式である必要がなくtemplate forの初期化式も定数式ではないため、初期化変数をconstexprにしたい場合は指定する必要があり、今のままだと構造化束縛がその場合に使用できなくなります。

P1481R0はC++20を目指していましたが実装経験が不足していたこともあって議論は止まっていました。この提案では、clang(とcircle)による実装経験について報告しています。定数式によって初期化された参照の扱いに少し調整が必要かもしれないものの、大きな問題や文言の変更は必要なさそうです。

以前にP1481R0で基礎的なレビューを終えていたこともあって、この提案は既にEWGをパスしてCWGに転送されています。C++26を目指しているようです。

P2687R0 Design Alternatives for Type-and-Resource Safe C++

安全性向上のための、静的解析を補助するアノテーションを導入する提案。

共用体の誤用、ダングリングポインタ、キャストの誤用などなど、C++の型システムを壊すことは簡単に行えます。これは通常意図的なものではなく、ある程度は回避することができます。しかし、このような型システム破損の可能性とそれに対する対処には開発者の追加の作業を招き、深刻なエラーやセキュリティの問題につながることがあります。C++はこの問題に対処する必要があります。

ただ単に気をつけるだけではこれらのエラーを回避することは困難で、注意深くは定義が曖昧であり様々な意見に左右されます。違反に対する保証が必要であり、可能なら静的な保証が必要です。保証のために実行時検査が必要ならば実行時検査が行われることを保証しなければなりません。

これは無視できるような小さな問題ではなく、これらの問題を放置すればC++が最適な言語であるはずの領域でC++が使用されなくなる可能性があります。

この提案はこれらの問題に対処しC++の安全性を向上させるために、静的解析の促進とそれを補助するアノテーションを追加しようとするものです。既存のC++の部分はそのままに、静的解析によって強制されるルールによって安全に記述できるものを制限し、アノテーションは制限を課したり静的解析を支援するためだけに機能します。

安全性を破壊するものにはいくつかの種類があります

  • ロジックエラー
  • リソースリーク
  • 並行性エラー
  • メモリ破壊
  • 型エラー
  • オーバーフローや予想外の変換
  • タイミングエラー
  • 終了エラー

この提案が解決しようとしている安全性の問題は次の2つです

  • 型安全性
    • 全てのオブジェクトが定義された型に従ってのみ使用される
    • 適切な初期化、type punningの禁止、不適切な型のポインタからのアクセスの禁止
    • 生存期間が終了したオブジェクトへのアクセス禁止
  • リソース安全性
    • リソースを表す(所有する)全てのオブジェクトがその生存期間の終わりに所有するリソースを解放し、生存期間後にオブジェクトへのアクセスできない
    • そのためには、リソースを表すオブジェクトの使用は型安全であり、その型はコピー・ムーブ・デストラクタなどの適切な操作が可能である必要がある
    • 動的メモリ領域に配置され一度も削除されないオブジェクトは永久に生存しているとみなされる

この二つは相互に影響しており、リソース安全性は型安全性が存在している場合にのみ達成可能で、型安全性はリソースが枯渇してプログラムが失敗しないために、リソースリークが起きていない場合にのみ達成されます。この2つを達成するには特に、ポインタが破棄されたオブジェクトを決して指さないことを保証する必要があり、これはメモリ安全性の一般化です。

これらのことが保たれこれらにまつわるエラーが発生しないことを保証する理想は静的な保証です。これはコンパイラや静的解析及びその組み合わせによって保証可能です。ただし、一部のエラーはどうしてもコンパイル時に検出できないため、それらについては実行時チェックが行われることを保証する必要があります。

この提案と同様のことあるいはそれ以上のことを達成するための手段はいくつか考えられます

  • 安全な言語サブセット
  • 安全な方言
  • ある程度互換性のある新しい言語
  • 互換性を捨てたC++
  • パッチ

これらの手段には利点もありますが、主にC++後方互換性という部分で欠点が大きいため、この提案ではいずれも採用しません。この提案は、これらの解決策に対しての代替設計(Design Alternatives)です。この提案のアプローチはこれらの方法よりも大幅に、段階的な採用や部分的な採用、および安全性を構成するものについての別の見方をサポートします。

この提案の内容およびアイデアのベースはC++コアガイドラインにあり、その5年間の経験と安全と言われている言語やライブラリの観察に基づいています。静的解析によって捕捉されうる問題のあるコード例の大部分は既にVSのコアガイドラインベースの静的解析によって捕捉可能になっているなど、この論文のアイデアは単なる憶測ではありません。完全な型とリソースの安全性は早い段階(1980年代初頭)からC++の理想的な到達点の一つでした。

提案の背後にあるハイレベルな考え方は次のようなものです

  • 全ての構成要素の意味はC++標準で定義されている
  • 最も基礎的な保証は完全な型とリソースの安全性
  • 古い(現在の)コードから保証を提供する最新(将来)のコードへの漸進的な変換をサポートする
  • 所有権はDAGを構成する
  • ポインタはnullptrであるか有効である
  • ポインタは1つのオブジェクトを指す
  • nullptrチェックや範囲チェックを強制する仕組みがある
  • 添字はstd::spanstd::vectorなど抽象化されたものに対して行う
  • 保証の集合は開集合
  • 基礎的な保証の集合が標準である
  • 異なる保証をサポートするコード辺を合成するルールがある
  • コード単位で、想定される保証と提供される保証の集合はコードに表示される

保証は、強制力のあるコーディングルール、基礎となるライブラリ、ローカルの静的解析の組み合わせによって確立されます。

提案による、静的解析とアノテーションの例

// 削除責任を負う(所有権を持つ)ポインタにアノテーション
// 静的解析はこのポインタがdeleteされ無ければならないことを認識する
int* f([[owner]] int* p) {
  X* q = new X{ /* initializers */ };

  // use *p
  
  // error: no delete p;
  // error: no delete q;
  return p; // error: p returned without owner indicator
  return q; // error: q returned without owner indicator
} 
// ポインタのエスケープ
void f() {
  int* p = nullptr;
  {
    int x = 7;
    p = &x;
    // …
  }
  *p = 9; // #1 likely disaster (but we won’t get here)
}

静的解析は#1のようなケースをキャッチし、エスケープが条件付きである場合でも拒否することができます。

// ポインタ(イテレータ)を無効化する
void f(vector<int>& vi) {
  vi.push_back(9); // may relocate vi’s elements
}

void g() {
  vector<int> vi { 1,2 };
  auto p = vi.begin(); // point to first element of vi

  f(vi);
  *p = 7; // likely disaster (but we won’t get here)
}

この場合にf()が引数オブジェクトを変更しないならば、[[not_invalidating]]アノテーションを付加することで静的解析機にその関数が内部でポインタを無効化するようなことをしないことを表明します。このアノテーションがなされた関数は静的解析によってそれが本当かどうかをチェックすることができます(そうすべきです)。

実行時検査の例

// Guideline Support Libraryによる実行時のnullチェック
void f (gsl::not_null<int*> p) {
  // no need to check for nullptr here
}

void g(int* q) {
  f(q); // 実行時にnullチェックが入る
}

あるいは、[[not_null]]のようなアノテーションによってこれを個別に有効化する設計も考えられます。

// ポインタの実行時nullチェックを指定するアノテーション
void f ([[not_null]] int* p) {
  // no need to check for nullptr here
}

void g(int* q) {
  f(q); // 実行時にnullチェックが入る
}

翻訳単位レベルで保証を制御する例

// この翻訳単位で、型安全性を保証(するための静的解析とチェックを有効化)
[[check(type_safety)]]; // activate type_safety profile

int64_t make_pun(double x) {
  union {
    double d;
    int64_t i;
  } y { x };

  return y.i; // error: type punning not allowed: use a bit_cast
}

int* glob(int i) {
  return new int{i}; // error: resource leak
}

// ...

あるいは局所的に無効化する例

[[check(type_safety)]]; // activate type_safety profile


// 全体保証を局所的に無効化する
template<regular Elem>
[[suppress(type_safety)]]
class Slist {
  // Slist is implemented using unsafe pointer manipulation
  void insert(const Elem&);

  void remove(Elem*);
  
  // ...
};

同様の構文で、局所的に保証を有効化する例

// 安全な数値演算の保証を局所的に有効化
[[check(arithmetic)]]
double compute(double x, double y) {
  // code here is checked for overflow
}

モジュールインポート時に保証を有効化する例

// stdモジュールのものの使用をmemory_safetyルールに準拠するように強制する
import std [[enable(memory_safety)]];

void f(vector<int>& v) {
  int& a = v[23]; // the bound is checked
  v.resize(42);   // potential error: dangling reference a
}

この提案はまだ完全なものではなく文言も含んでいませんが、同様の機能の議論の方向性や、機能詳細についてより良いアイデアを募ることを目的としています。そのため、上記のアノテーションの例はまだ構文等が固まったものではありません。

P2688R0 Pattern Matching Discussion for Kona 2022

※この部分は@bleaguedbさんに執筆していただきました。

C++へのパターンマッチング導入に向けて、別提案との基本構造の違いに焦点を当てた議論。

執筆者のマターンマッチングの提案(P1371R3)をR4に進めるために、別提案(P2392R1)との差異の議論と新構文の提案を行っています。 なおP2392はR2が今月提出されています(P2392R2)。

以前の記事はこちら。

P1371R2では、inspect式とcase/letキーワードによるパターンマッチングが提案されていました。

caseキーワードは既存の識別子の参照に、letキーワードは主に構造化束縛を使用する場面などで新しい識別子の導入に際して使用されていました。

R3で構文の複雑化を理由にletキーワードを廃止していましたが、本提案ではこれを復活し更にinspect式の代わりにmatch構文を提案しています。

整数

本提案 P2392
x match {
  0 => { cout << "got zero"; }
  1 => { cout << "got one"; }
  _ => { cout << "don't care"; }
};
inspect (x) {
  is 0 => { cout << "got zero"; }
  is 1 => { cout << "got one"; }
  is _ => { cout << "don't care"; }
}

文字列

本提案 P2392
s match {
  "foo" => { cout << "got foo"; }
  "bar" => { cout << "got bar"; }
  _ => { cout << "don't care"; }
};
inspect (s) {
  is "foo" => { cout << "got foo"; }
  is "bar" => { cout << "got bar"; }
  is _ => { cout << "don't care"; }
}

Tuple

本提案 P2392
p match {
  [0, 0] => {
    cout << "on origin";
  }
  [0, let y] => {
    cout << "on y-axis at " << y;
  }
  [let x, 0] => {
    cout << "on x-axis at " << x;
  }
  let [x, y] => {
    cout << x << ',' << y;
  }
};
inspect (p) {
  is [0, 0] => {
    cout << "on origin";
  }
  [_, y] is [0, _] => {
    cout << "on y-axis at " << y;
  }
  [x, _] is [_, 0] => {
    cout << "on x-axis at " << x;
  }
  [x, y] is _ => {
    cout << x << ',' << y;
  }
}

構造化束縛に際して、letキーワードにより新しい変数が導入されています。

Variant

本提案 P2392
v match {
  <int32_t> let i32 => {
    cout << "got int32: " << i32;
  }
  <int64_t> let i64 => {
    cout << "got int64: " << i64;
  }
  <float> let f => {
    cout << "got float: " << f;
  }
  <double> let d => {
    cout << "got double: " << d;
  }
};
inspect (v) {
  i32 as int32_t => {
    cout << "got int32: " << i32;
  }
  i64 as int64_t => {
    cout << "got int32: " << i64;
  }
  f as float => {
    cout << "got float: " << f;
  }
  d as double => {
    cout << "got double: " << d;
  }
}
v match {
  <std::integral> let i => {
    cout << "got integral: " << i;
  }
  <std::floating_point> let f => {
    cout << "got floating point: " << f;
  }
};
// Unsupported.
v match {
  <int32_t> let i32 => {
    cout << "got i32: " << i32;
  }
  <auto> let x => {
    cout << "got something else: " << x;
  }
};
// Unsupported.

多態型(Polymorphic Types)

本提案 P2392
int get_area(const Shape& shape) {
  return shape match {
    <Circle> let [r] => 3.14 * r * r;
    <Rectangle> let [w, h] => w * h;
  };
}
int get_area(const Shape& shape) {
  return inspect (shape) {
    [r] as Circle => 3.14 * r * r;
    [w, h] as Rectangle => w * h;
  };
}

Optional

本提案 P2392
void f(const optional<int>& opt) {
  opt match {
    let ?x => {
      cout << "optional is storing: " << x;
    }
    _ => {
      cout << "optional is empty";
    }
  };
}
void f(const optional<int>& opt) {
  inspect (opt) {
    *x is _ => {
      cout << "optional is storing: " << x;
    }
    _ => {
      cout << "optional is empty";
    }
  }
}
void f(const std::optional<int>& opt) {
  if (opt match let ?x) {
    cout << "optional is storing: " << x;
  } else {
    cout << "optional is empty";
  }
}
void f(const std::optional<int>& opt) {
  if (auto *x is _ = opt) {
    cout << "optional is storing: " << x;
  } else {
    cout << "optional is empty";
  }
}

本提案では参照外し(dereference)に演算子ではなく、新しく?演算子の導入を提案しています。 の使用は混乱を招く場合があり、EWG Kona 2019でもP1271R0に同様のフィードバックがあったそうです。

入れ子の構造体/Variant

struct Rgb { int r, g, b; };
struct Hsv { int h, s, v; };

using Color = variant<Rgb, Hsv>;

struct Quit {};
struct Move { int x, y; };
struct Write { string s; };
struct ChangeColor { Color c; };

using Command = variant<Quit, Move, Write, ChangeColor>;

Command cmd = ChangeColor { Hsv { 0, 160, 255 } };
本提案 P2392
cmd match {
  <Quit> => ...
  <Move> let [x, y] => ...
  <Write> let [text] => ...
  <ChangeColor> [<Rgb> let [r, g, b]] => ...
  <ChangeColor> [<Hsv> let [h, s, v]] => ...
  _ => ...
};
inspect (cmd) {
  is Quit => ...
  [x, y] as Move => ...
  [text] as Write => ...
  [ [r, g, b]] as ChangeColor as [Rgb] => ...
  [ [h, s, v]] as ChangeColor as [Hsv] => ...
  _ => ...
}

本提案では、ChangeColor構造体->Color構造体->RGBという様に逐語的に(verbatim)パータンの記述ができるのに対してP2392ではそれができないというのが議論の主張です。

また、マッチングしながら一部を新しい変数に束縛する際に、P2392では宣言が重複するのも問題だと述べています。

本提案 P2392
c match {
  <ChangeColor> [<Rgb> [0, 160, let b]] => {
    // use `b` here
  }
};
inspect (c) {
  [ [_, _, b]] as ChangeColor
              as [Rgb]
              is [ [0, 160, _]] => {
    // use `b` here
  }
}

シングルパータンマッチング

パターンマッチングは分岐の中から選択するだけでなく、シングルパターンマッチングとしてif文の中でも使えます。これはP2392へのフィードバック内容を取り込んだもののようです。

if (expr match [0, let foo]) {
  // `foo` is available
}

guard構文を使ってより複雑なマッチングを出来るようにする提案もあります。

pair<int, int> fetch(int id);
bool is_acceptable(int id, int abs_limit) {
  return fetch(id) match let [min, max] if min >= -abs_limit && max <= abs_limit;
}

P2689R0 atomic_accessor

※この部分は@acd1034さんに執筆していただきました

アトミック操作を適用した参照を返すmdspanのアクセッサである、atomic_accessorの提案。

シーケンスに対するアルゴリズムを並列実行し、その結果をmdspanの指すシーケンスに出力したい場合があります。この場合にmdspanのアクセッサとしてatomic_accessorを渡すだけで、参照にアトミック操作を容易に適用できるようにすることを、本提案は目指しています。この提案によって、実行ポリシーに依らずに、mdspanを活用したアルゴリズムが記述できるというメリットがあります。

atomic_accessorを用いてヒストグラムを計算する例:

template <class T, class Extents, class LayoutPolicy>
auto add_atomic_accessor_if_needed(
  std::execution::sequenced_policy,
  std::mdspan<T, Extents, LayoutPolicy> m) {
    return m;
  }

template <class ExecT, class T, class Extents, class LayoutPolicy>
auto add_atomic_accessor_if_needed(
  ExecT,
  std::mdspan<T, Extents, LayoutPolicy> m) {
    return std::mdspan(m.data_handle(), m.mapping(), atomic_accessor<T>());
  }

template <class ExecT>
void compute_histogram(ExecT exec, float bin_size,
                       std::mdspan<int, std::dextents<int, 1>> output,
                       std::mdspan<float, std::dextents<int, 1>> data) {
    static_assert(std::is_execution_policy_v<ExecT>);
    auto accumulator = add_atomic_accessor_if_needed(exec, output);

    std::for_each(exec, data.data_handle(), data.data_handle() + data.extent(0),
                  [=](float val) {
                    int bin = std::abs(val) / bin_size;
                    if (bin > output.extent(0))
                      bin = output.extent(0) - 1;
                    accumulator[bin]++;
                  });
  }

int main() {
  // データを指すmdspanを用意
  std::vector<float> data(100);
  srand(234);
  std::for_each(std::begin(data), std::end(data),
                [=](float& val) { val = rand() % 100; });
  std::mdspan m{data.data(), data.size()};

  // 出力先のmdspanを用意
  std::vector<int> histo_data(10);
  std::mdspan histogram{histo_data.data(), histo_data.size()};

  // 逐次実行の実行ポリシーを指定してヒストグラムを計算
  compute_histogram(std::execution::seq, 10.0, histogram, m);

  // 並列実行の実行ポリシーを指定してヒストグラムを計算
  compute_histogram(std::execution::par, 10.0, histogram, m);
}

atomic_accessorの実装は、default_accessorの実装における型エイリアスreferenceを、atomic_ref<element_type>に単に置き換えることになります。また、default_accessorからの変換コンストラクタを追加することも提案されています。

メモリアクセス順序に関する問題

シーケンスに対するアトミック操作が必要とされる計算は、多くの場合、1変数を同時に更新するsummationの計算となります。この場面で必要とされるメモリアクセス順序保証は、ほとんどの場合、順序保証なし(memory_order_relaxed)で十分です。

一方、atomic_refオーバーロードされた演算子(operator++, --, +=, -=, &=, |=, ^=)のアトミック操作は、すべてメモリアクセスの順序一貫性を保証した操作(memory_order_seq_cstの指定された操作)となっています。常にmemory_order_seq_cstが指定されているのでは非効率なため、本論文ではmemory_order_relaxedを容易に指定できる手法を模索しています。

そのような手法として以下の3点が考えられます。

  1. atomic_refmemory_order_seq_cst をデフォルトとするテンプレートパラメータを追加する
  2. 新しいクラス relaxed_atomic_ref を追加する。これは atomic_ref とほとんど同じ実装だが、更新関数 (operator++など) のデフォルトの memory_ordermemory_order_relaxed とする
  3. relaxed_atomic_refatomic_accessor の説明専用クラスとして持つ

1.が望ましい手法であるものの、これは破壊的変更となり、受け入れられない可能性が高いです。そのため本論文では2.を採用することを提案しています。

P2690R0 C++17 parallel algorithms and P2300

※この部分は@acd1034さんに執筆していただきました

P2300で提案されている実行コンテキストの指定を、C++17の並列アルゴリズムにも適用できるようにすることを目指す提案。

P2300では任意の実行コンテキストで任意の非同期処理を構成・実行するためのフレームワークが提案されています。そこでは実行コンテキストを表現するために、スケジューラ(Scheduler)という概念が導入されています。スケジューラとはスレッドプールなどから取得できる、実行コンテキストに作業を割り当てる戦略を表し、いわば実行リソースの「どこで」処理を行うかを表したものです。

(非同期処理に関する提案については、[C++] ExecutorとNetworking TSで起きていたこと も合わせてご確認ください。)

一方C++17で導入された並列アルゴリズムでは、並列実行を許可するための実行ポリシー(Execution Policy)を指定することができます。これは、いわばアルゴリズムを「どのように」行うかを指定するためのものです。

本論文では、これら「スケジューラ」と「実行ポリシー」の両方を指定できるアルゴリズムのインターフェースの導入を目指しています。そのために、以下のカスタマイゼーションポイントを提案しています。

  • execute_on
  • policy_aware_scheduler

execute_on

execute_onとは、スケジューラと実行ポリシーを組み合わせるためのカスタマイゼーションポイントです。

struct __execute_on {
    policy_aware_scheduler auto operator()(scheduler auto sched,
                                           execution_policy auto policy) const
    {
        return std::tag_invoke(*this, sched, policy);
    }
};

inline constexpr __execute_on execute_on;

ここでschedulerとは、P2300で提案されているスケジューラを表すコンセプトです。

template<class S>
  concept scheduler =
    copy_constructible<remove_cvref_t<S>> &&
    equality_comparable<remove_cvref_t<S>> &&
    requires(S&& s, const get_completion_scheduler_t<set_value_t> tag) {
      { execution::schedule(std::forward<S>(s)) } -> sender;
      { tag_invoke(tag, execution::schedule(std::forward<S>(s))) } -> same_as<remove_cvref_t<S>>;
    };

またexecution_policyは、本論文で提案されている以下のコンセプトです。

template <typename ExecutionPolicy>
concept execution_policy = std::is_execution_policy_v<std::remove_cvref_t<ExecutionPolicy>>;

注意: 本論文で用いられるカスタマイゼーションポイントの実装方法は、P2300と同じ方法(現在は tag_invoke)が採用されています。これは将来の提案(例えば P2547)を受けて、変更される可能性があります。

policy_aware_scheduler

execute_onの戻り値の満たすコンセプトであるpolicy_aware_schedulerとは、実行ポリシーを伴うスケジューラのことであり、以下のように表されます。

template <typename S>
concept policy_aware_scheduler = scheduler<S> && requires (S s) {
    typename S::base_scheduler_type;
    typename S::policy_type;
    { s.get_policy() } -> execution_policy;
};

拡張された並列アルゴリズムの実装例

以上の2つのカスタマイゼーションポイント(execute_onおよびpolicy_aware_scheduler)を用いて、本論文は並列アルゴリズムを以下のように拡張することを目指しています。ここではstd::for_eachを例に示しています。

struct __for_each
{
    template <std::policy_aware_scheduler Scheduler, typename It, typename Callable>
    void operator()(Scheduler s, It b, It e, Callable c) const
    {
        if constexpr (std::tag_invocable<__for_each, Scheduler, It, It, Callable>)
        {
            std::tag_invoke(*this, s, b, e, c);
        }
        else
        {
            // default implementation
        }
    }
};

inline constexpr __for_each for_each;

このfor_eachは以下のように使用することができます。

for_each(execute_on(scheduler, std::execution::par), begin, end, callable);

本論文は現時点(R0)では叩き台であり、open questionも残されています。設計に合意が形成され次第、すべての並列アルゴリズムに適用される予定です。

P2691R0 Allow referencing inline functions with internal linkage from outside their defining header unit

ヘッダユニット内の内部リンケージをもつinline関数を参照できるようにする提案。

C++20で導入された既存ヘッダをモジュールとして扱う仕組みであるヘッダユニットでは、ヘッダユニットを1つの翻訳単位としてコンパイルしてしまう形になるため、そこに定義されている内部リンケージを持つ関数を外部から参照できません。

一方、C言語ではinlineの意味がC++と若干異なることからCとC++の両方で使用することを意図するヘッダではstatic inlineで関数を定義することがよく行われています(そのほかにも、マクロの代わりやABI分離などの目的で使用されているようです)。

/// myheader.h

// 内部リンケージ
static inline int f(int n) {
  ...
}

// 外部リンケージ
int g(int n) {
  ...
}
/// main.cpp
// ヘッダユニットとしてインポート
import "myheader.h"

int main() {
  f(0); // ng
  g(0); // ok
}

ヘッダユニットは既存のコードやモジュール化できないコード(主にCのコード)をモジュール時代にもサポートするための仕組みであり、static inline関数が使用できないのはモジュールへの移行を妨げる要因になる可能性があります。

このことは、C++20が固まる以前にP2003でも指摘されていて、そこではMacOSのシステムヘッダが影響を受ける例として挙げられていましたが、P2003の内容が規格に適用された際にその部分は除外されていました。

モジュールの実装が少しづつ進み、ユーザーのフィードバックが入ってくるようになると、このことが当初の予想よりも影響の大きい問題になっていることが分かってきました。この提案は、それを受けてこの問題を解決するために、static inline関数に限ってヘッダユニット外からの呼び出しを許可しようとするものです。

そこでは、ヘッダユニットに加えて名前付きモジュール(モジュール宣言によるモジュール)に対しても同じことを提案していて、セマンティクスは現在のヘッダファイルにおけるそれを踏襲します。

加えて、名前付きモジュールにおいて内部リンケージをexportできるようにすることも提案しています(現在は禁止)。これはstdモジュールの実装において問題になったことのようです。

// <ctime>の実装においてこのようにしたい
#include <time.h>
namespace std {
  export using ::time;  // ::time()がstatic inlineで定義されている場合エラー
}

このような例は他にもいくつかありそうで、システムヘッダやCライブラリをラップする場合など、ユーザーがモジュールを定義する際にも有用です。

おわり

この記事のMarkdownソース

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

文書の一覧

全部で29本あります。

N4917 Working Draft, Standard for Programming Language C++

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

N4918 Editors’ Report - Programming Languages – C++

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

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

N4919 Programming Languages - C++

C++23のCommittee Draft

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

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

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

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

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

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

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

P0543R2 Saturation arithmetic

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

以前の記事を参照

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

P0792R11 function_ref: a non-owning reference to a Callable

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

以前の記事を参照

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

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

P0957R9 Proxy: A Polymorphic Programming Library

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

以前の記事を参照

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

P0987R1 polymorphic_allocator instead of type-erasure

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

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

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

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

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

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

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

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

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

polymorphic_allocator<> get_allocator() const noexcept;

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

P1030R5 std::filesystem::path_view

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

以前の記事を参照

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

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

などです。

P1985R3 Universal template parameters

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

以前の記事を参照

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

P2348R3 Whitespaces Wording Revamp

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

以前の記事を参照

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

P2495R1 Interfacing stringstreams with string_view

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

以前の記事を参照

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

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

などです。

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

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

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

P2586R0 Standard Secure Networking

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2587R3 to_string or not to_string

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

以前の記事を参照

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

P2588R1 Relax std::barrier phase completion step guarantees

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

以前の記事を参照

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

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

P2603R1 member function pointer to function pointer

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

以前の記事を参照

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

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

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

P2620R2 Improve the wording for Universal Character Names in identifiers

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

以前の記事を参照

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

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

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

P2623R2 implicit constant initialization

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

以前の記事を参照

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

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

などです。

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

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

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

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

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

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

P2631R0 Publish TS Library Fundamentals v3 Now!

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

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

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

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

P2636R0 References to ranges should always be viewable

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

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

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

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

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

より詳しくみてみると

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2637R0 Member visit and apply

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2639R0 Static Allocations

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Friend.cpp
export module Friend;

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

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

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

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

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

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

// 前方宣言
export class X;

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

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

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

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

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

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

export module Struct;

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

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

関数引数

export module FnParm;

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

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

関数のデフォルト引数

export module DfltFnArg;

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

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

export module DfltMemArg;

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

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

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

export module NTTP;

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

スコープなしenum

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

export module E;

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

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

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

P2641R0 Checking if a union alternative is active

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

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

1つはunionを用いたもの

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

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

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

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

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

struct OptBool {
  char c;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2642R0 Padded mdspan layouts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

P2643R0 Improving C++ concurrency features

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

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

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

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

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

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

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

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

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

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

おわり

この記事のMarkdownソース

[C++]type punningとオブジェクト生存期間

この記事は規格書や提案文書から読み取れる暗黙の気持ちを前提にしている部分があり、必ずしも出典や根拠が明確でない場合があります。

std::bit_cast

C++20で追加されたstd::bit_castはあるオブジェクトのバイト表現(ビット列)を維持したまま別のオブジェクトとして扱うことを可能とするライブラリ機能です。このようなことは、type punningと呼ばれます。

#include <bit>

std::uint64_t punning(double v) {
  // double -> uint64_tのバイト表現を保った変換
  return std::bit_cast<std::uint64_t>(v);
}

よく知られているようにtype punningをしているつもりの多くのコードは未定義動作に陥っており、std::bit_castはそれを合法かつ安全に、しかも定数式でも行うことができる唯一の方法です。

しかし、std::bit_castが未定義動作を起こさない理由はどこにあって、他の方法はなぜ未定義動作となるのでしょうか?

C++20以前の方法

以下の3つの方法のうち、最初の2つは未定義動作となる危険な方法です。

reinterpret_cast

reinterpret_castはこういう場合にまず思いつく方法だと思います。

#include <bit>

std::uint64_t punning(double v) {
  auto* p reinterpret_cast<const std::uint64_t*>(&v);
  return *p;
}

ポインタは単にメモリアドレスであってメモリのどこかを指していてそこから値を読み出すだけなので、これは動作しそうに思えます。不思議ですね。

union

もうひとつよく知られているのは、unionを使用した方法です。

#include <bit>

std::uint64_t punning(double v) {
  union U {
    double d;
    std::uint64_t n;
  };

  // doubleで初期化
  U u{v};

  // uint64_tとして読み出し
  return u.n;
}

これはC言語においては合法ですが、C++においては未定義動作とされます。共用体のメモリレイアウトを考えればu.du.nも同じメモリ領域を参照しているのだから、これも動作しそうに思えます。不思議ですね。

memcpy

これは有名なわりに知名度がない気がするのですが、memcpyを用いた方法があります。

#include <bit>

std::uint64_t punning(double v) {
  std::uint64_t n;

  // nの領域にvの値をコピー
  std::memcpy(&n, &v, sizeof(std::uint64_t));

  return n;
}

なんと、これはC++においても合法です。なんででしょうか、不思議ですね。

std::bit_castmemcpyの方法

memcpyによる方法はC++17までの世界でtype punningを合法的に行う唯一の方法であり、std::bit_castはそれを1つの関数にしたものです。従って、この2つの方法はおおよそ同じことをしていると思うことができます。

memcpyの方法を汎用的に書くと次のようになります(細かい制約は省略します)。

template<typename To, typename From>
To bit_cast_by_memcpy(const From& from) {
  To to;  // デフォルト構築

  std::memcpy(&to, &from, sizeof(To));

  return to;  // 暗黙ムーブ?
}

こうしてみると、変換先の型Toにデフォルト構築可能であることを要求していることが分かりやすくなります。あと多分Toは少なくともムーブ構築可能である必要がありそうです。

この方法が好かれない?のは、memcpyを用いている点でしょう。他の方法はゼロコピーで行えるのに(unionによる方法はよく見るとコピーしてますが)、この方法はmemcpyという明確なコピー操作を伴っています(実際には最適化によって取り除かれうるようです)。また、memcpyconstexprではないことからC++では定数式でこの方法を実行できなくなっています。

std::bit_castmemcpyの方法にあるそれらの問題を解消したものでもあります。その利点とは

  • type punningを関数呼び出しとして1行で書ける
  • 定数式で使用可能
  • Toの型にデフォルト構築可能性を要求しない
  • のぞき穴的最適化が効きやすくなる
    • コピーを削減しやすくなる

などが考えられます。

より正確には、std::bit_castはその実装については何も指定していません。C++23 ワーキングドラフトの[bit.cast]から翻訳すると、戻り値に関して次のように指定されているだけです

(戻り値は)To型のオブジェクト。 結果の型(To)にネストされたオブジェクト(基底クラスやメンバ)を暗黙的に作成する。 結果オブジェクトの値表現の各ビットは、fromオブジェクトの対応するビットと等しくなる。 ...(以下パディングビットの扱いや、ビット表現に対応する値が無い場合、fromのオブジェクト(サブオブジェクト)が生存期間の外にある場合、などについて述べられている)

ほとんどの場合、これはコンパイラの組み込み命令(通称コンパイラマジック)によって実装されるでしょう。

合法的type punning手法の境界

よく、reinterpret_castunionによる方法が未定義動作となるのはStrict Aliasing Ruleに抵触しているからだと言われます。たしかにmemcpyの方法は抵触していませんが、std::bit_castはどうなのでしょうか?std::bit_castが合法なのはそういう関数だからでしょうか?それともコンパイラマジックで実装されるから?もちろん、そうではありません。

その境界線を別つのは、オブジェクトの生存期間(lifetime)という概念です。合法で行える方法は常に生存期間内にあるオブジェクトを参照してしかいません。

unionによる方法を見てみましょう

#include <bit>

std::uint64_t punning(double v) {
  union U {
    double d;
    std::uint64_t n;
  };

  // doubleで初期化
  U u{v}; // (1)

  // uint64_tとして読み出し
  return u.n; // (2)
}

C++の共用体オブジェクトが生存期間内にある場合、そのメンバ変数はいずれか1つだけが生存期間内にあります。このメンバのことを特にアクティブメンバと呼びます。(1)の部分で初期化されているのはu.dのみであり、以降この関数内で生存期間内にあるUのメンバ(アクティブメンバ)はu.dのみです。

C++において、生存期間外にあるオブジェクトの操作(値の読み出しや非静的メンバ関数/変数の使用)は未定義動作です([basic.life]/4)。したがって、unionの方法が未定義となるのは(2)で非アクティブなメンバの値を読み出そうとしているところです。

#include <bit>

std::uint64_t punning(double v) {
  union U {
    double d;
    std::uint64_t n;
  };

  U u{v}; // ok、u.dをアクティブメンバとして初期化、u.dの生存期間が開始

  return u.n; // ub、u.nの生存期間は始まっていない(アクティブメンバはu.d)
}

共用体のアクティブメンバを切り替えるには明示的に非アクティブなメンバを初期化する必要がありますが、この関数ではそれは行われていません。

U u{1.0}; // u.dがアクティブメンバ

{
  double d = u.d; // ok
  int n = u.n;    // ub、u.nは生存期間外
}

u.n = 10; // ok、u.dの生存期間は終了しu.nの生存期間が開始される

{
  double d = u.d; // ub、u.dは生存期間外
  int n = u.n;    // ok
}

これを踏まえると、reinterpret_castの方法もポインタの参照先で生存期間内にあるオブジェクトとは別のオブジェクトに対してアクセスしようとしているため、未定義動作を踏んでいることがわかります。

#include <bit>

std::uint64_t punning(double v) {
  auto* p reinterpret_cast<const std::uint64_t*>(&v); // vには当然、double型のオブジェクトが生存期間内にある
  return *p;  // ub、pの参照先ではuint64_tのオブジェクトが生存期間内にない
}

std::bit_castが未定義とならない理由は、その効果が明確にToのオブジェクトを返すと指定されていることによります。これは生存期間内にあるオブジェクトを返すという意味であり、その実装の如何によらず戻り値のオブジェクトは生存期間内にあることが保証され、コンパイラはそう認識します。合法性を保つ重要な点は返されるオブジェクトが入力のバイト表現を保ちながらも生存期間が開始していることが保証されているところにあります。

memcpyの方法が未定義とならない理由は生存期間が既に開始しているオブジェクトに対して、別のオブジェクト由来のバイト表現をコピーしているからです。memcpyによるバイト表現のコピーによる書き換えはその場所にあるオブジェクトの生存期間を終了させないため、コピー前後でオブジェクトは生存期間内にあり続けます。これはmemmoveを使用しても同じ保証が得られますが、それ以外に同様の保証がある関数は無いようです。

余談

ところで、memcpyの方法が真に合法であることを確認するには、バイト表現をmemcpyによってコピーした時でもオブジェクトの有効性は保たれ続けるのかということを確認しなければなりません。巷にある説明(つまりググって出てきたもの)にはその部分に言及しているものが見つからず、memcpyの方法は合法だから合法なのだという感じで終了している気がします。

cppreferenceのstd::memcpyのページには、以前(2015)から次のような記述があります

Where strict aliasing prohibits examining the same memory as values of two different types, std::memcpy may be used to convert the values.

これはCのリファレンスから持ってきたもののようで、おそらくCからずっとmemcpyにはこういう効果があったのでしょう。

じゃあこれを裏付ける規定は規格書のどこにあるのか?というと見つかりませんでした・・・

近しい規定は[basic.types.general]/2

Trivially Copyableな型Tの任意のオブジェクト(potentially-overlappingサブオブジェクトを除く)は、そのオブジェクトが型Tの有効な値を保持しているかどうかに関わらず、基礎となるバイトをchar, unsigned char, std::byteの配列にコピーすることができる。
その配列の内容が元のオブジェクトにコピーし直された場合、オブジェクトはその後元の値を保持する。

constexpr std::size_t N = sizeof(T);
char buf[N];
T obj;                          // objは元の値で初期化されている
std::memcpy(buf, &obj, N);      // この2つのmemcpy呼び出しの間にobjが変更されたとしても
std::memcpy(&obj, buf, N);      // この時点で、objのスカラ型のサブオブジェクトは元の値を保持する

及び[basic.types.general]/3に見つけることができます。

Trivially Copyableな型Tの異なる2つのオブジェクトobj1, obj2obj1obj2potentially-overlappingサブオブジェクトではない)は、obj1を構成する基礎となるバイトがobj2にコピーされると、その後obj2obj1と同じ値を保持する。

T* t1p;
T* t2p;
    // t2pは初期化済のオブジェクトを指しているとする
std::memcpy(t1p, t2p, sizeof(T));
    // この時点で、*t1pのTrivially Copyableな型の全てのサブオブジェクトには、*t1pの対応するサブオブジェクトと同じ値が含まれる

ここに書かれているのは、まず、Trivially Copyableな任意の型Tのオブジェクトについてバイト表現をバイト列(char, std::byte等の配列)としてコピーすることができて、別の場所にコピーしたものを後で書き戻した後で、そのオブジェクトはコピーした時点と同じ値を持つということです。そして、同じTrivially Copyableな型Tのオブジェクトの間でバイト表現を直接コピーすることもできて、その場合はコピー元とコピー先オブジェクトは同じ値を持つということです。

これら2つの規定からまず読み取れることは、memcpyによるバイト表現の上書きコピーはオブジェクトの生存期間を終了させないということです。

そして、これら2つのケースにおいてコピー元バイト列の出所を気にしないようにすると(すなわち、バイト列のコピーとはいつもそのバイト列に対応する値からのコピーであると思うようにすると)、memcpyによるtype punningの合法性が導けます。

ただし、std::bit_castの規定にあるように、不定値やそれを含むオブジェクトからのコピーや、コピーしたバイト表現に対応する値をコピー先オブジェクトが持たない場合、結果は未規定ないし未定義動作になるでしょう。

とはいえ、これらの推測は明確に書かれていることではないので間違っているかもしれません(ご指摘歓迎です)。しかし、memcpyによるtype punningが合法であるのは間違いないはずです。

制約について

ここまで特に触れていませんが、ここまで書かれていることは型がTrivially Copyableであることを前提としています。std::bit_castmemcpyによるtype punningを合法的に行うにはToFromの型が共にTrivially Copyableでありサイズが同じでなければなりません。例えば、std::vectorのような型(確実にTrivially Copyableではない)を合法的にtype punningする方法は存在しないし、int32_tdouble(通常8バイト)としてpunningすることもできないわけです。

前項のmemcpyの規定に関しても、型がTrivially Copyableである場合にのみ保証されています。

おわり

ここまで読むと、type punningの合法性についてStrict Aliasing Ruleに抵触しているからという理由は間違っているのか?と思うかもしれませんが、そうではありません。この記事の最終的な主張はここで述べていることとStrict Aliasing Ruleによることは同じ事について別の側面から見たものにすぎず、同じことを言っているという事です。

実のところ、これはそのような主張のポエムの序文です。次回は、ここでのreinterpret_castによるtype punningのコードから出発して、オブジェクト生存期間とポインタについての独自研究が展開される予定です。

参考文献

この記事のMarkdownソース

[C++]集成体のコピー/ムーブコンストラクタを制御する

C++17にて集成体はあらゆるコンストラクタを宣言できなくなり、その結果コピーコンストラクタとムーブコンストラクタは常に暗黙定義されるようになりました。しかし時には、それをコントロールしたくなることがあります。

暗黙のコンストラク

まず前提として、集成体はコピー/ムーブコンストラクタ宣言できなくなったからといって常にそれがdeleteされているわけではありません。通常のクラス型同様に暗黙的に宣言されており、含むメンバが全てムーブ構築可能であればそのムーブコンストラクタは有効となり、含むメンバがすべてコピー可能であればコピーコンストラクタが有効となります。

#include <vector>
#include <memory>
#include <concepts>

struct A1 {
  int n;
  std::vector<int> v;
};

struct A2 {
  int n;
  std::unique_ptr<int> p;
};

// A2はコピー構築不可
static_assert(std::copyable<A1>);
static_assert(not std::copyable<A2>);

// どちらもムーブ構築可能
static_assert(std::movable<A1>);
static_assert(std::movable<A2>);

単純に集成体のコピーを制御したければ、コピー不可能な型をメンバに持つことによって行うことができるわけです。

集成体の利便性とコンストラク

集成体にはいくつかメリットがあり、コピーを制御したいからといってそのメリットが失われてしまうことは好ましくありません。今回は次の3つを重視します

  • 集成体初期化によるコンストラクタ定義の省略
  • 指示付初期化
  • 構造化束縛
    • これはコンストラクタを定義するだけでは失われない

コンストラクタを定義すると上2つは失われ、制御のためにメンバを追加すると3つ目の性質が損なわれます(受けるために余計な変数が増える)。

この3つを変わらず利用しつつコピー/ムーブコンストラクタを制御するには、コンストラクタを定義せずに余計なメンバを追加することも避けなければなりません。

解決策

まず次のような何でもなさそうな集成体を用意して、これについてコンストラクタの制御を試みます。

struct A {
  std::size_t l;
  std::vector<int> v;
};


auto f() -> A {
  // 指示付初期化(+集成体初期化)
  return { .l = 4, .v = {1, 2, 3, 4}};
}

int main() {
  // 構造化束縛(2メンバ分)
  auto [l, v] = f();
}

前述のように、このAにメンバを追加すると指示付初期化と集成体初期化には影響ありませんが、構造化束縛は3変数で受けないといけなくなります。

struct A {
  std::size_t l;
  std::vector<int> v;
  int a = 0;
};


auto f() -> A {
  // 指示付初期化(+集成体初期化)
  return { .l = 4, .v = {1, 2, 3, 4}};  // ok
}

int main() {
  // 構造化束縛(2メンバ分)
  auto [l, v] = f();  // error、1つ足りない
}

メンバを追加せずに集成体のコピー/ムーブを制御するには、制御用のクラスを別に作って継承させてやります。

例えばムーブオンリーにしたければ次のような型を作って

struct enable_move_only {
  enable_move_only() = default;

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

  enable_move_only(enable_move_only&&) = default;
  enable_move_only& operator=(enable_move_only&&) = default;
};

これを継承させてやります

struct A : enable_move_only {
  std::size_t l;
  std::vector<int> v;
};

// コピー構築不可
static_assert(not std::copyable<A>);

// ムーブ構築可能
static_assert(std::movable<A>);

集成体はpublicに限って継承をすることができて、基底クラスは別に集成体型でなくても構いません。

このAは継承以前と同じように使用することができます。

auto f() -> A {
  // 指示付初期化(+集成体初期化)
  return { .l = 4, .v = {1, 2, 3, 4}};  // ok
}

int main() {
  // 構造化束縛(2メンバ分)
  auto [l, v] = f();  // ok
}

tuple-likeな構造体に対する構造化束縛は、そのクラスの直接の非静的メンバのみを対象にとって分解しようとするため、基底クラスは無視されます。なおこのとき、基底クラスがメンバを持っているとエラーになりますが、今回の場合はメンバは必要ないので問題にはなりません。

この場合の指示付初期化は基底クラスに対する初期化子が省略されたものとして扱われエラーにならず、デフォルト初期化(デフォルトコンストラクタを呼ぶ初期化)によって初期化されます。

なお、enable_move_onlyにデフォルトコンストラクタを宣言しているのは、この場合に基底クラスが未初期化であるという警告を抑制するためです。集成体初期化において初期化子が省略されデフォルトメンバ初期化子も持たないメンバ(サブオブジェクト)はデフォルト初期化と呼ばれる初期化が行われるのですが、クラス型にデフォルトコンストラクタが存在しない場合は初期化しない初期化が行われます。それはint型の変数をint n;の様に宣言した場合と同様の初期化であり、値が不定となります。enable_move_onlyのような型はメンバを持たないので悪影響は皆無なのですが、警告はうざいのでデフォルトコンストラクタを追加しておきます(こうすると、デフォルトコンストラクタによって初期化される)。

ちなみにGCCでは、基底クラスに対する初期化子がないぞ!って警告が出ますが、現在のC++指示付初期化の仕様では基底クラスに対する指示付初期化の方法が無いのでどうしようもありません。無視するか-Wno-missing-field-initializersによって抑制しましょう・・・

さらに説明しておくと、enable_move_onlyのムーブコンストラクタをわざわざ書いているのは、コピーコンストラクタを宣言するとムーブコンストラクタの宣言は暗黙deleteされるからです。deleteかどうかに関係なく、コピーコンストラクタの宣言はムーブコンストラクタを暗黙的に削除するため明示的な宣言が必要です。ちなみにこれは逆(ムーブコンストラクタをdelete宣言した場合)も同様です。

このテクニックは、noncopyableとかコピー禁止Mix-in等と呼ばれてC++11以前の世界でクラス型のコピーを禁止するためによく使用された古のテクニックの応用です。

その他の場合

コピー/ムーブコンストラクタを制御したくなる時とは次の3パターンのいずれかになると思います。

  1. ムーブのみ許可したい
  2. ムーブもコピーも禁止したい
  3. 常にコピーになってほしい

1の場合は先程のenable_move_onlyによって行えますが、他の場合の例も一応書いておきます。

2の場合

// コピーとムーブを禁止する
struct disable_copy_move {
  disable_copy_move() = default;

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

  disable_copy_move(disable_copy_move&&) = delete;
  disable_copy_move& operator=(disable_copy_move&&) = delete;
};

// コピー構築不可
static_assert(not std::copyable<A>);

// ムーブ構築不可
static_assert(not std::movable<A>);

この時、ムーブもコピーも不可能なのにf()からAの値を返せているのは、C++17から保証されるようになったコピー省略の効果によります。

3の場合(いつ?

// ムーブを禁止(コピーとして実行)する
struct disable_move {
  disable_move() = default;

  disable_move(const disable_move &) = default;
  disable_move& operator=(const disable_move&) = default;

  disable_move(disable_move&&) = delete;
  disable_move& operator=(disable_move&&) = delete;
};

// コピー構築可能
static_assert(std::copyable<A>);

// ムーブ構築可能
static_assert(std::movable<A>);

この場合、ムーブ構築は常にコピー構築として実行されます。

ADLの考慮

あるクラス型のオブジェクトに対するADLにおいては、そのクラス型の基底クラスの名前空間も考慮されるため、このenable_move_onlyなどが別の名前空間にあるとADLによって予期しない関数呼び出しが行われる可能性があります。それを防ぐためにADL Firewallというテクニックがあります。詳細は以下を参照

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で27本あります。

N4914 WG21 2022-07 Admin telecon minutes

WG21の各作業部会の管理者ミーティングの議事録。

前回から今回の会議の間のアクティビティの報告がされています。

N4915 Business Plan and Convener's Report: ISO/IEC JTC1/SC22/WG21 (C++)

ビジネスユーザ向けのC++およびWG21の現状報告書。

N4916 WG21 2022-07 Virtual Meeting Minutes of Meeting

2022年2月7日(北米時間)に行われた、WG21全体会議の議事録。

CWG/LWG/LEWGの投票の様子などが記載されています。

P0843R5 static_vector

静的な最大キャパシティを持ちヒープ領域を使用しないstd::vectorであるstatic_vectorの提案。

static_vector<T, N>std::vectorstd::arrayのキメラのようなコンテナで、Nに指定した値を最大キャパシティとして、スタック領域(グローバル変数として使用する場合は静的ストレージ)を用いてstd::vectorのような可変長配列を実現するものです。

主たる性質は

  • 動的メモリ確保を必要としない
  • スタックor静的ストレージを使用する
  • キャパシティ最大値がコンパイル時に指定される
  • 要素は後から挿入/削除可能
  • 要素のストレージはstatic_vectorオブジェクト内に配置される
  • contiguous_rangeコンセプトを満たす
  • 要素型Tトリビアルであれば、全ての操作が定数式で可能

などで、static_vectorは次のような場合に有用です

  • 動的メモリ確保を行えない
    • 例えば、組み込み環境など
  • 動的メモリ確保のコストが高くつく
    • 例えば、メモリ確保に伴うレイテンシに敏感なプログラム
  • 静的ストレージ上に、変則的な生存期間をもつオブジェクトを構築したい
  • デフォルト構築できない型の配列など、std::arrayが選択肢にならない
  • constexpr関数内で可変長配列を使用したい
    • これはC++20以降std::vectorでも可能
  • static_vectorの要素のストレージはstatic_vectorオブジェクト自体が内包している必要がある

この提案のstatic_vectorは既存実装であるboost::container::static_vectorをベースとして設計されており、インターフェースや性質はstd::vectorとの共通化が図られています。

#include <static_vector> // <vector>になるかもしれない

int main() {
  std::static_vector<int, 8> sv = {1, 2, 3, 4};

  std::println("{}", sv); // [1, 2, 3, 4]

  sv.push_back(5);
  sv.emplace_back(6);

  std::print("{}", sv); // [1, 2, 3, 4, 5, 6]
}

なお、最大キャパシティを超えて要素を挿入しようとした場合、全ての操作において事前条件違反として未定義動作となります。例外を投げたリabortしたりするのかは実装の選択とされます。

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

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

以前の記事を参照

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

  • maybe_viewからnullable_viewを分離
  • maybe_view/nullable_viewT&(左辺値参照型)の特殊化を追加
  • maybe_viewにモナディックインターフェース追加
    • and_then/or_else/transform
  • フリースタンディングライブラリ機能として指定

などです。

このリビジョンで新たに追加されたnullable_viewviews::nullable)は、nullable_objectviewに変換するものです。nullable_objectとは文脈的なbool変換が可能であり間接参照が可能な型のことで、voidではないポインタ型やstd::optionalstd::expectedなどが該当します。イテレータ型は通常単体でbool変換できないためnullable_objectではありません。

#include <ranges>

using std::ranges::nullable_view;

int main() {
  std::optional o{4};

  nullable_view m{o};
  for (int k : m) {
    cout << k;  // "4"が出力
  }

  o = std::nullopt;
  nullable_view m2{o};
  for (int k : m2) {
    cout << k;  // 実行されない(ループが回らない)
  }
}

nullable_viewは、nullable_object専用のmaybe_viewです。

これによって、maybe_viewviews::maybe)はより一般の型のオブジェクトをviewに変換するものとなり、maybe_viewが空になるのはデフォルト構築された時です。

#include <ranges>

using std::ranges::maybe_view;

int main() {
  int i{4};

  maybe_view m{i};
  for (int k : m) {
    cout << k;  // "4"が出力
  }

  maybe_view<int> m2{};
  for (int k : m2) {
    cout << k;  // 実行されない(ループが回らない)
  }
}

maybe_viewnullable_viewも共に長さ0もしくは1のシーケンスになりますが、maybe_viewは値を渡して構築された時にのみ長さ1になるのに対して、nullable_viewは渡されたnullable_objectの状態によって長さが決まります。

nullable_viewはこの提案の元々のmaybe_viewであり、nullable_objectでは無い型についてmaybe_viewを拡張しようとした結果、1つのクラスで実装するのは色々問題があったため、2つのクラス(とさらに2つの部分特殊化)に分離されたようです。

P2019R1 Usability improvements for std::thread

std::thread/std::jthreadにおいて、そのスレッドのスタックサイズとスレッド名を実行開始前に設定できるようにする提案。

現在のstd::thread/std::jthreadには、スタックサイズを設定する方法もスレッド名を設定する方法も用意されていません。一方で、この2つのクラスが使用している実行環境のスレッドAPIでは、広くこの2つのスレッドプロパティの設定が可能となっています。

環境 スタックサイズの設定 スレッド名の設定
Linux, QNX pthread_attr_setstacksize() pthread_setname_np()
Windows CreateThread() SetThreadDescription()
Darwin pthread_attr_setstacksize() pthread_setname_np()
Fuchsia zx_thread_create()
Android pthread_attr_setstacksize() JavaVMAttachArgs()
FreeBSD, OpenBSD, NetBSD pthread_attr_setstacksize() pthread_setname_np()
RTEMS pthread_attr_setstacksize() pthread_setname_np()
FreeRTOS xTaskCreate() xTaskCreate()
VxWorks taskSpawn() taskSpawn()
eCos cyg_thread_create() cyg_thread_create()
Plan 9 threadcreate() threadsetname()
Haiku spawn_thread()
Keil RTX osThreadNew()
WebAssembly

※ 空白はなし、スレッド名の設定は一部事後的にしか行えないものがある

また、他のプログラミング言語C++ライブラリのスレッドAPIにおいても、これらに対応した機能を提供している場合が多くみられます。

スタックサイズの設定は、次のような場合に必要となります

  • 特定のスタックサイズで実行できるように設計されているアプリケーションの移植性と信頼性のために、プラットフォーム間で一貫したスタックサイズを指定する
  • プラットフォームのデフォルトよりも小さいスタックサイズを使用する
    • Windows : 1MB, Unix 2MB
    • 多数のスレッドを起動したときにメモリ消費を抑えられる(仮想メモリのないシステムにおいて)
  • 一部のアプリケーションでは、メインスレッドに大きなスタックトレースを設定し、そこから起動されたスレッドにも継承させるものがある。これが望ましくない場合がある
  • 有名なゲームや大規模アプリケーションなどでは、デフォルトよりも大きいスタックサイズを使用することがある

スレッド名はデバッガを始めとする外部ツールにおいて有用で、主にデバッグに活用できます

  • デバッガーにおける利用
  • 各種クラッシュダンプや実行トレースツール
  • タスク/プロセスモニタ
  • プロファイル/トレース/診断ツール
  • Windows Performance Analyzer, ETW tracing

これらのことが欠けている事によって、std::threadstd::jthreadを使用することができず、ほぼ同等のクラスを再実装したり、基底のAPIを直接使用したりしなければならないケースがあります。また、筆者の方は、「スタックサイズのサポートがないためにstd::threadを使うことができず、std::threadは語彙型として失敗している」という話をゲーム開発者の人々から聞いているようです。

この提案は、それらの既存のプラクティスを標準化することで、現在std::thread/std::jthreadを使用できていない所で使用可能にしようとするものです。

ここで提案されているAPIは、プロパティ指定クラスをstd::thread_attributeから継承させて定義した上で、そのオブジェクトをstd::threadのコンストラクタで受け取るようにするものです。

namespace std {
  // スレッドプロパティ指定識別用基底クラス
  class thread_attribute {};

  // スレッド名を指定するクラス
  class thread_name : thread_attribute {
  public:
    constexpr thread_name(std::string_view name) noexcept;
    constexpr thread_name(std::u8string_view name) noexcept;
  private:
    implementation-defined __name[implementation-defined]; // 説明専用
  };

  // スタックサイズを指定するクラス
  class thread_stack_size : thread_attribute {
  public:
    constexpr thread_stack_size(std::size_t size) noexcept;
  private:
    constexpr std::size_t __size; // 説明専用
  };


  class thread {
    ...

    // デフォルトコンストラクタ(元からある
    thread() noexcept;

    // 処理とその引数を受け取るコンストラクタ(元からある
    template<class F, class... Args>
    explicit thread(F&& f, Args&&... args);

    // 処理とプロパティ指定を受け取るコンストラクタ(この提案で追加
    template<class F, class... Attrs>
      requires (sizeof...(Attrs) != 0) &&
               ((is_base_of_v<thread_attribute, Attrs>) && ...) &&
               ...
    explicit thread(F&& f, Attrs&&... attrs);

    ...
  };

  // jthreadも同様(略

}

std::thread_name/std::thread_stack_sizeが渡された設定値をどのように保持して取り出せるようにするかは実装定義とされています。

これらのものを次のように使用してスレッドに設定します。

void f();

int main() {
  // スレッド名とスタックサイズの指定
  std::jthread thread(f, std::thread_name("Worker"),
                         std::thread_stack_size(512*1024));
  return 0;
}

指定されたプロパティが設定可能でない場合(例えばWASM環境など)、実装はこの指定を無視することができます。

このプロパティ指定の方法はこの2つのプロパティ以外にも拡張可能で、例えばスレッドのアフィニティや優先度を指定可能とすることも将来的には可能なようにされています。とはいえ、この提案ではスタックサイズとスレッド名の2つのプロパティのみを提案しています。

P2164R6 views::enumerate

元のシーケンスの各要素にインデックスを紐付けた要素からなる新しいシーケンスを作成するRangeアダプタviews::enumrateの提案。

以前の記事を参照

このリビジョンでの変更は、enumerate_resultを説明専用コンセプトtuple-likeを満たす型のリストに追加したこと、enumerate_view::iterator::operator*は値(prvalue)を返すためCpp17ForwardIteratorコンセプトを満たす事ができず、それに応じてイテレータカテゴリを調整したことなどです。

P2264R4 Make assert() macro user friendly for C and C++

assertマクロをC++の構文に馴染むように置き換える提案。

このリビジョンでの変更は、Cでの採用に伴う文書の調整などです。

P2477R3 Allow programmers to control coroutine elision

コルーチンの動的メモリ確保を避ける最適化を制御するAPIを追加する提案。

以前の記事を参照

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

  • promise_type::must_elide(P0, ..., Pn)が最初に評価されるようにすることを提案
  • コンパイラが処理(最適化)できない非同期ケースの例を追加
    • この提案のAPIが必要であるケース
  • 最適化が起きたことを検出するAPIを提案から外した
  • 最適化とメモリ使用量についての議論を追加
  • 例を追加

などです。

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

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

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加したこと、実装経験を追加したことです。

P2517R1 Add a conditional noexcept specification to std::apply

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

以前の記事を参照

このリビジョンでの変更は、提案する文言の修正とベースとなるワーキングドラフトの更新です。

この提案は既に、2022年7月の全体会議で承認され、C++23ワーキングドラフト入りしています。

P2537R1 Relax va_start Requirements to Match C

可変長引数関数を0個の引数で宣言できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、C23の提案(N2975、C23に導入済み)に仕様を整合させたことです。

P2581R1 Specifying the Interoperability of Built Module Interface Files

ビルド済みモジュールを扱う際に、ビルドシステムがそのビルド済みモジュールファイルを直接扱うことができるかどうかを調べられるようにする提案。

以前の記事を参照

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

  • Binary Module Interfaceという用語をBuilt Module Interfaceに置き換えた
  • モジュールをインポートする翻訳単位と、モジュールのインターフェースの翻訳単位の間で、独立したパースコンテキストを持つことについての調査に関するセクションを追加
  • 読みやすさ向上のための調整

  • P2581 進行状況

P2587R2 to_string or not to_string

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

以前の記事を参照

このリビジョンでの変更は、LEWG投票の結果を追記した事、std::formatによって将来的に他の型にstd::to_stringを拡張できることを追記、コードベースの調査結果に関して追記したことなどです。

この提案の変更を受けるのは浮動小数点数用のstf::to_string()だけですが、筆者の方の調査によればそれはあまり使われておらず、ロケールに関するバグがあるものもあったとのことです。

P2611R0 2022-07 Library Evolution Poll Outcomes

2022年の7月に行われた、LEWGでの全体投票の結果。

次の提案が、LWGに転送されることが承認されました

C++23向けの提案の一部は、7月の全体会議でC++23ワーキングドラフト入りが承認されたものが含まれています(全体会議ではこれについてLWGの座長が苦言を呈していたようですが・・・)。

また、投票にあたって寄せられたコメントが記載されています。

P2620R1 Lifting artificial restriction on universal character names

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

以前の記事を参照

このリビジョンでの変更は、タイポ修正と提案する文言の改善のみです。

P2621R1 UB? In my Lexer?

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

以前の記事を参照

このリビジョンでの変更は、タイポ修正のみです。

P2623R1 implicit constant initialization

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

以前の記事を参照

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

  • 既存資料からの参照の明確化
  • 説明の明確化
  • Why not beforeセクションを削減
  • constを使用していないstd::initializer_listの例(この提案によって安全になる)を追加

などです。

P2625R0 Slides: Life without operator() (P2511R1 presentation)

P2511(std::nontype)の解説スライド。

P2511はstd::function, std::move_only_function(将来的にはstd::function_refにも)にオーバーヘッドなく簡易なメンバポインタサポートを入れようとする提案です。詳細は以前の記事参照

このスライドはLEWGのメンバに向けてP2511のモチベーションや利点、使用感について説明するものです。

P2626R0 charN_t incremental adoption: Casting pointers of UTF character types

互換性のあるユニコード文字表現を持つ異なる型の間での合法的なポインタキャストを行うライブラリ機能の提案。

char8_t, char16_t, char32_t(以降、ユニコード文字型)などの登場前、C++では任意のユニコード文字(列)を表すのにcharを使用してバイト単位でそれらを扱うことがよく行われていました。しかし、charはそれ自身が文字型であり、しばしばそのエンコーディングUTF-8ではなく、整数型やバイト型と区別されずに使用されています。charは文字、特にユニコード文字を表す型としては適切ではありません。

そのような背景もあってC++11でchar16_t, char32_tが、C++20でchar8_tが導入され、これらを用いて型によって文字エンコーディングを表現することができるようになりました。

しかし、C++20あるいはC++11以前からあるコードでは前述のcharを用いたプラクティスが残っており、またwchar_t等別の方法でユニコード文字(列)を表現するコードが多数存在しています。それらの方法で表現されているユニコード文字列とユニコード文字型のシーケンスによるユニコード文字列との間でコピーや未定義動作を伴うことなく変換する方法がないことから、それらのレガシーなインターフェースにユニコード文字型をアダプトできず、それによってユニコード文字型の使用が妨げられています。

この提案は、ユニコード文字型と既存のchar文字列やwchar_t文字列などとの間で、エンコーディングを仮定しそれを保持したまま変換するための関数を標準ライブラリに追加することで、そのような問題を解決しようとするものです。

ここで提案されているのは、次の2つの関数です

  • std::cast_as_utf_unchecked() : std::byte, char, wchar_tあるいはサイズNの符号なし整数型のポインタから、charN_tのポインタへ変換する
  • std::cast_utf_to<T>() : charN_tのポインタからTのポインタへ変換する
    • Tstd::byte, char, wchar_tあるいはサイズNの符号なし整数型
    • std::cast_as_utf_unchecked()の逆変換

この2つの関数はstd::start_lifetime_as_array()を適切にその文字列に対して適用するのと似たセマンティクスを持ち、ソース領域のオブジェクトのライフタイムを終了させ、指定された型のオブジェクトのライフタイムをその値を保ったまま開始させます。ただし、単なるconstexpr std::start_lifetime_as_array()ではなく、適切な文字エンコーディングサポートのために、追加のコンパイラサポートを必要とします。

有効な変換の例

char8_t char16_t char32_t
char
unsigned char
uit_least_16_t
uit_least_32_t
wchar_t ❗️ ❗️ ❗️
std::byte

行要素->列要素 : std::cast_as_utf_unchecked() 列要素->行要素 : std::cast_utf_to<T>()

表中の❗️は実装定義であることを表します。

これらの関数はその変換にあたって実際にその変換が有効なのかとか、文字がきちんと変換先のエンコーディングになっているのかなどのチェックを一切行いませんが、これらの関数を使用した変換はその意図をコードに反映させる意味も持ちます。例えば、std::cast_as_utf_unchecked()によってchar8_tに変換する場合、変換後の文字列はUTF-8の正しいシーケンスでありそうなっていなければなりません。このように、文字エンコーディングについてのプログラマの暗黙的な仮定をコード上で明確にする役割を担ってもいます。

// 現在よく試みられる方法
void before() {
  // 入力文字列(wchar_tのエンコーディングをUTF-16とする)
  const wchar_t* str = L"Hello 🌎";

  // 多くのプログラマ: ill-formed
  const char16_t* u = static_cast<const char16_t*>(str);

  // 経験10年のプログラマ: UB
  const char16_t* u = reinterpret_cast<const char16_t*>(str);

  // Cのアプローチ: UB in C++
  const char16_t* u = (const char16_t*)(str);

  // rangesの利用: O(n)かかる、contiguous_rangeでなくなる
  auto v = std::wstring_view(str) | std::views::transform(std::bit_cast<char16_t, wchar_t>);

  // 別のメモリにコピー: O(n)、動的確保
  auto v = std::wstring_view(str) | std::views::transform(std::bit_cast<char16_t, char16_t>) 
                                  | std::to<std::vector>;

  // launderの利用: まだUB
  const char16_t* u = std::launder(reinterpret_cast<const char16_t*>(str));

  // エキスパート: constexprではない
  const char16_t* u = std::start_lifetime_as_array(reinterpret_cast<const char16_t*>(str), std::char_traits<wchar_t>::length(str));
}

// この提案
void after() {
  // コピーなし、UBなし、constexpr、明確なセマンティクスを持ち実際には何もしない
  constexpr std::u16_string_view v = std::cast_as_utf_unchecked(L"Hello"sv);
}

利便性向上のため、std::cast_as_utf_unchecked()std::cast_utf_to<T>()にはそれぞれ、string_viewspanを受け取るオーバーロードが用意されます。

P2627R0 WG21 2022-07 Virtual Meeting Record of Discussion

2022/07の全体会議の議事録。

N4916のものよりも、誰がどのような発言をしたかが詳しく記録されています。

P2628R0 Extend barrier APIs with memory_order

std::barrierの各操作に、メモリオーダーを指定できるようにする提案。

std::barrierC++20で追加された、複数スレッドの進行管理に使用することのできる同期プリミティブです。その実体はカウンタであり、.arrive()によってカウンタ減算を、.wait()によってカウンタリセットまでの待機を行います。ほかにも、それらの複合操作である.arrive_and_wait()、同期するスレッドのグループから途中離脱するための.arrive_and_drop()も用意されています。

これらのstd::barrierAPIは全てメモリオーダーを指定するものではなく、.arrive()よりも前に行われた全てのメモリ操作は、.wait()による待機解除後に可視になることが(すべてのスレッドに渡って)保証されます。

この保証はかなり強いもので、場合によってはこの保証が望ましくない場合があります

  1. C++プログラムの外部とやり取りをする場合
    • 例えば、バリアに参加するすべてのスレッドがそれぞれファイルを開いて読み書きする時、.arrive_and_wait(1, memory_order_relaxed)によってすべてのスレッドがファイルを閉じたことを同期する。
      • この場合、メモリの可視性(ファイルクローズの可視性)はファイルシステムによって確保される
    • 例えば、すべてのスレッドはvolatile操作によって実行環境(マシン)の設定を行ってから、その中の1つのスレッドが環境をスタートさせるような場合。これは、memory_order_relaxed.arrive()/.wait()操作と、std::barrierの完了関数によって実現できる。
      • この場合、volatileな書き込みが環境開始時(待機解除後)に可視になっていればよく、それはvolatile操作によって保証される。
  2. オブジェクトフェンス(P2535)を利用する場合
    • 一部のメモリ操作についてのみ可視になればよく、すべてのメモリ操作にその保証は必要ない

この提案は、これらの場合などに、std::barrierを用いたより効率的な同期を可能とするために、std::barrierAPIstd::memory_orderを追加で受け取れるように拡張しようとするものです。

上記2のケースのサンプル

現在 この提案
// Thread 0:
x = 1;
atomic_object_fence(memory_order_release, x);
bar.arrive(); // release fence

// Thread 1
bar.arrive_and_wait(); // acquire fence
atomic_object_fence(memory_order_acquire, x);
assert(x == 1);
// Thread 0:
x = 1;
atomic_object_fence(memory_order_release, x);
bar.arrive(1, memory_order_relaxed); // no fence

// Thread 1
bar.arrive_and_wait(memory_order_relaxed); // no fence
atomic_object_fence(memory_order_acquire, x);
assert(x == 1);

現在の例では、この場合のatomic_object_fenceは意味がありません(std::barrierがより強い同期を保証しているため)。この提案後、memory_order_relaxedと共にstd::barrierを使用することでstd::barrierの保証がほぼなくなり、atomic_object_fenceによって特定の変数のメモリ可視性のみが保証されるようになります(それによって、同期のコスト低減が可能となりうる)。

この提案の内容はstd::latchをはじめとした他の同期プリミティブにも適用可能ですが、この提案では現在のところ、std::barrierにのみ焦点を絞っています。

P2629R0 barrier token-less split arrive/wait

std::barrierをデータパイプラインの実装においてより効率的に使用できるように拡張する提案。

データパイプラインはデータ処理のモデルの一つで、データ処理の1連の流れを並列化可能なブロックに区切り、それら処理ブロックをパイプライン化することで高速化を図るものです。データパイプラインによる処理は例えば、ディープラーニングやHPCアプリケーションにおいて一般的に行われいます。

そのようなところでは典型的に、各パイプラインステージにおいてproducer-consumerパターンによるリソース共有が行われます。producerとconsumerの2つのスレッドグループは、バリアのペアを使用して共有リソースへのアクセスを同期します。

consumerスレッドはconsumerバリアによってリソース読み取りを待機(wait)し、バリアが解除されるとリソースを使用し、使用終了するとproducerバリアに到着(arrive)して共有リソースの再利用(更新)が安全であることをproducerに通知します。

producerスレッドはproducerバリアによってリソース更新を待機(wait)し、バリアが解除されるとリソースを更新し、更新終了するとconsumerバリアに到着(arrive)して共有リソースの使用(読み取り)が安全であることをconsumerに通知します。

この同期パターンでは、consumerスレッドはconsumerバリアに到着することなく待機し、producerバリアには待機することなく到着します(producerスレッドの場合はこの逆)。

このパターンのバリアにstd::barrierを使用しようとすると、std::barrierの同期がバリア上で待機する前に到着することを要求するため、同期が非効率になってしまいます。

#include <barrier>

// 共有リソース
// 1つのオブジェクトを使いまわす
int shared_resource = 0;

// producerバリア
// consumer -> producer への読み込み終了通知
std::barrier producer_barrier{2};
// consumerバリア
// producer -> consumer への書き込み終了通知
std::barrier consumer_barrier{2};

// リソースを更新する関数
void produce(int&);
// リソースを消費する関数
void consume(const int&);

// producer本体
void producer() {
  // consumerバリアのカウントを1にしておく(デッドロック回避のため)
  std::ignore = consumer_barrier.arrive();

  while(true) {
    // consumerスレッドの読み込み終了を待機
    producer_barrier.arrive_and_wait();       // A
    // リソース更新
    produce(shared_resource);
    // consumerスレッドへ書き込み終了を通知
    std::ignore = consumer_barrier.arrive();  // B
  }
}

// consumer本体
void consumer() {
  while(true) {
    // producerスレッドの書き込み終了を待機
    consumer_barrier.arrive_and_wait();       // A
    // リソース利用
    consume(shared_resource);
    // producerスレッドへ読み込み終了を通知
    std::ignore = producer_barrier.arrive();  // B
  }
}

int main() {
  std::jthread th{producer};
  consumer();
}

問題があるのはサンプル中のAB(コメント)のところです。Aのところでは到着(.arrive())は必要なく待機(.wait())だけが必要であり、Bのところではarrive()によって発行されるバリアトークンが不用で、[[nodiscard]]による警告を消すために余計なコードが必要になります。

このAで起こる不要なarrive()Bで起こる不要なバリアトークンの発行がこのようなパターンの実装時のオーバーヘッドとなり、しかも現在のstd::barrierAPIでは回避できません。

この提案は、この問題の解決(このようなパターンの効率実装)のために、std::barrierに「到着しない待機」と「待機しない到着」を行うためのAPIを追加する提案です。提案では、「到着しない待機」のために.wait_parity(bool)、「待機しない到着」のために.arrive_and_discard()を追加することを提案しています。これらを用いると上記のサンプルは次のように書くことができます

#include <barrier>

// 共有リソース
// 1つのオブジェクトを使いまわす
int shared_resource = 0;

// producerバリア
// consumer -> producer への読み込み終了通知
// producerスレッドにおける到着を考慮する必要が無くなり、同期スレッド数は-1される
std::barrier producer_barrier{1};
// consumerバリア
// producer -> consumer への書き込み終了通知
// consumerスレッドにおける到着を考慮する必要が無くなり、同期スレッド数は-1される
std::barrier consumer_barrier{1};

// リソースを更新する関数
void produce(int&);
// リソースを消費する関数
void consume(const int&);

// producer本体
void producer() {
  // consumerバリアのカウントを0にしておく(デッドロック回避のため)
  // ここで最初のバリアフェーズが完了する(0 -> 1)
  consumer_barrier.arrive_and_discard();

  bool phase = false;
  while(true) {
    // consumerスレッドの読み込み終了を待機
    producer_barrier.wait_parity(phase);    // A
    // バリアフェーズの更新
    phase = !phase;
    // リソース更新
    produce(shared_resource);
    // consumerスレッドへ書き込み終了を通知
    consumer_barrier.arrive_and_discard();  // B
  }
}

// consumer本体
void consumer() {
  bool phase = false;
  while(true) {
    // producerスレッドの書き込み終了を待機
    // 1番最初は、フェーズ1に対してフェーズ0で待機するため、すぐブロック解除
    consumer_barrier.wait_parity(phase);    // A
    // バリアフェーズの更新
    phase = !phase;
    // リソース利用
    consume(shared_resource);
    // producerスレッドへ読み込み終了を通知
    producer_barrier.arrive_and_discard();  // B
  }
}

int main() {
  std::jthread th{producer};
  consumer();
}

Aの点では.wait_parity()によってバリアのカウントに触ることなく待機し、Bの点では.arrive_and_discard()によって待機せず(バリアトークンを発行せず)に到着しています。これによって、現在のAPIでは回避できなかった不要な到着と待機(バリアトークン発行)を回避することができており、なおかつそれは実装に通知されるためより効率的な同期となることが期待できます。

また、これによってそれぞれのスレッドにおいての不要な到着を管理するために本来不要な同期数(std::barrierのコンストラクタに渡している値)の+1が必要なくなってもいます(もっとも、これは実際のスレッド数と一致する以外の恩恵はなさそうですが)。

.wait_parity()に渡しているboolphaseは、ある連続した2回のバリアフェーズ(std::barrierによる1回の同期)を識別するためのもので、true/falseによって異なるバリアフェーズに対する.wait()を行うものです。これは、.wait_parity()による待機がどのバリアフェーズに対するものなのかを識別するためのものです。

std::barrierによる同期では、ある同期ポイント(.arrive_and_wait())に指定した数のスレッドが到達した後、完了関数を実行してから内部カウンタをリセットし待機しているスレッドグループを再開、という流れを繰り返すものです。この1つの流れ(同期ポイントから同期ポイントの間の処理)のことをバリアフェーズと呼び、std::barrierの保証によって各バリアフェーズは時間方向に順番に実行されるため最初を0としてインデックスを振ることができます。

そのようなバリアフェーズのインデックスについて、trueの場合に奇数インデックスfalseの場合に偶数インデックスとしてbool値を対応づけて、このbool値のことをパリティと呼びます。

バリアフェーズの進行(インデックス)とパリティの関係

phase  :   0   ->   1  ->   2   ->   3  ->   4   ->   5  -> ...
parity : false -> true -> false -> true -> false -> true -> ...

.wait_parity()に渡すbool値は、待機対象のパリティ(すなわちバリアフェーズ)を指定するもので、指定したパリティtrue/false)から次のパリティへ変化するフェーズを待機します。例えば、wait_parity(false)とした場合は偶数インデックスのバリアフェーズを待機し、待機が解除されたら奇数インデックスのバリアフェースが開始しています。もし現在のフェーズのパリティと指定されたパリティが異なる場合、それは1つ前のフェーズに対する待機だとして処理(すなわちすぐブロック解除)されます。

P2630R0 Submdspan

std::mdspanの部分スライスを取得する関数submdspan()の提案。

この提案は以前はstd::mdspanの提案(P0009)に含まれていましたが、std::mdspanC++23に間に合わせるための時間の制約から切り離され、個別の提案として議論していくことになりました。

mdspanとそこでのsubmdspanについては以前の記事を参照

submdspanによってstd::mdspanから部分領域をスライスとしてstd::mdspanで取り出すことができると、std::mdspanを受け取るように実装されたより小さい問題を効率的に解決する関数を再利用することができるようになります(行列積における、各要素ごとの内積計算など)。

提案文書より、長方体内の3次元格子を表すstd::mdspanの表面領域を0クリアするサンプル

// 2次元領域をゼロクリアする
template<class T, class E, class L, class A>
void zero_2d(mdspan<T, E, L, A> grid2d) {
  // ランク2(2次元)のmdspan
  static_assert(grid2d.rank() == 2);

  for(int i = 0; i < grid2d.extent(0); ++i) {
    for(int j = 0; j < grid2d.extent(1); ++j) {
      grid2d[i,j] = 0;
    }
  }
}

// 長方体表面をゼロクリアする
template<class T, class E, class L, class A>
void zero_surface(mdspan<T, E, L, A> grid3d) {
  // ランク3(3次元)のmdspan
  static_assert(grid3d.rank() == 3);

  // 6つの表面毎に、2次元平面をゼロクリアするzero_2d()を再利用
  zero_2d(submdspan(grid3d, 0, full_extent, full_extent));
  zero_2d(submdspan(grid3d, full_extent, 0, full_extent));
  zero_2d(submdspan(grid3d, full_extent, full_extent, 0));
  zero_2d(submdspan(grid3d, grid3d.extent(0) - 1, full_extent, full_extent));
  zero_2d(submdspan(grid3d, full_extent, grid3d.extent(1) - 1, full_extent));
  zero_2d(submdspan(grid3d, full_extent, full_extent, grid3d.extent(2) - 1));
}

submdspan()std名前空間に定義されるフリー関数です。1つ目の引数にstd::mdspanを取り、2つ目以降の引数でスライス指定を次元毎に受け取ります。スライス指定は、1つ目の引数をx、対応する次元の番号をdとして[0, x.extent(d))の範囲の値のどの要素が返されるstd::mdspanに含まれているかを指定します(std::pair{4, 10}のようにして任意の範囲を指定できます)。

この提案ではP0009から切り離したsubmdspan()を個別提案として再構成するとともに、次の変更を加えています

  • スライス指定として指定可能なものにstrided_index_range型を追加
    • 以前は、整数値(先頭からの要素数)、2要素tuple-like(範囲指定)、full_extent_t(全体の指定)
    • strided_index_rangeは、開始インデックスと長さを指定することで範囲指定ができるほか、ストライド(要素間隔)の指定も可能
  • ユーザー定義のレイアウトポリシーを使用可能なようにカスタマイゼーションポイントを追加
    • 入力mdspan<T, E, L, A>L(レイアウトマッピングクラス)をカスタムするためにsubmdspan_mapping()submdspan_offset()を追加
    • submdspan()内部ではこの2つの関数をLのオブジェクトを用いたADLによって呼ぶことで、この2つの関数をL毎にカスタム可能にする
    • これらの結果を用いて、返すmdspanのレイアウトポリシーとアクセサ、データハンドル(領域を参照する方法、通常はポインタ)を取得する
  • スライスをコンパイル時の値として指定する機能を追加
    • スライス指定が定数値で指定されたときに、それを返すmdspanのエクステントテンプレートパラメータに埋め込む

submdspan()の実装イメージ

template<class T, class E, class L, class A,
         class ... SliceArgs>
constexpr auto submdspan(const mdspan<T, E, L, A>& src, SliceArgs... args) {
  // 部分mdspanのオフセット取得
  size_t sub_offset = submdspan_offset(src.mapping(), args...); // ADLによるカスタマイズポイント
  // レイアウトマッピングの取得
  auto sub_map = submdspan_mapping(src.mapping(), args...);     // ADLによるカスタマイズポイント
  // アクセサの取得
  typename A::offset_policy sub_acc(src.accessor());  // A::offset_policy入れ子型によるカスタマイズポイント
  // データハンドルの取得
  typename A::offset_policy::data_handle_type         // A::offset_policy::data_handle_type入れ子型によるカスタマイズポイント
    sub_handle = src.accessor().offset(src.data_handle(), sub_offset);

  return mdspan(sub_handle, sub_map, sub_acc);
}

この提案はわざわざP0009から分離したこともあり、おそらくC++23には間に合わないと思われます。

P2633R0 thread_local_inherit: Enhancing thread-local storage

呼び出し元でのスレッドローカル変数の値を引き継いで初期化されるスレッドローカル変数を作成するための、thread_local_inherit指定の提案。

スレッドローカル変数はスレッド生成時に呼び出し元のスレッドの対応する変数が持っている値とは無関係に初期化されます。しかし、呼び出し元スレッドの対応するスレッドローカル変数の値を引き継ぎつつ、後からその値を更新したい場合があるようです。そのような変数はスレッドで実行する関数の引数として渡すこともできますが、スレッド起動時点ではその変数を用意できない場合があるほか、スレッドを起動しているプログラマ(コード)が呼び出すスレッドに関しての情報を持っていない場合もあります。スレッドにデータを渡す他の方法は面倒であり、スレッド起動側/スレッド内処理側のプログラマがそれぞれどのようなデータを受け渡す必要があるかを知っている必要があります。

呼び出されたスレッドに必要なデータを必要な時点で必要な場所に自動的に供給可能な言語機能にはメリットがあり、それを実現可能なthread_local_inheritという新しいスレッドローカルストレージ指定を導入しようとする提案です。

thread_local_inherit変数は、初期化周りのこととtrivially copyableな型にしか指定できないこと以外はthread_local変数と同じ性質を持ちます。

メインスレッドにおけるthread_local_inherit変数はthread_local変数と同様に初期化され、子スレッドのthread_local_inherit変数は呼び出し元スレッドの変数の値を単純にコピーすることによって静的初期化されます。thread_local_inherit変数に対する動的初期化はその変数がまだ初期化されていない場合にのみ起こり、これはおそらくメインスレッドでの最初の初期化時にのみ起こります。

#include <future>
#include <iostream>

thread_local int th1 = 0;
thread_local_inherit int th2 = 0;

void f() {
  std::cout << "thread_local : " << th1 << "\n";
  std::cout << "thread_local_inherit : " << th2 << "\n";
}

int main() {
  std::ignore = std::async(f);
  th1 = 1;
  th2 = 1;
  std::ignore = std::async(f);
  th1 = 2;
  th2 = 2;
  std::ignore = std::async(f);
}
thread_local : 0
thread_local_inherit : 0
thread_local : 0
thread_local_inherit : 1
thread_local : 0
thread_local_inherit : 2

P2634R0 Allow qualifiers in constructor declarations

コンストラクタに、const/volatileおよび参照修飾を行えるようにする提案。

オブジェクトがconstで構築される時とそうでない時、あるいは左辺値で構築される時と右辺値で構築される時、これらによって呼び出すコンストラクタを分けると便利な場合があります。メンバ関数では、CV/参照修飾によって呼び出される関数をオブジェクトの状態毎に定義することができますが、コンストラクタにはそれらの修飾が行えないため、現在はこのようなことはできません。

この提案は、コンストラクタにCV/参照修飾できるようにすることでコンストラクタを構築のされ方によって呼び分けることができるようにしようとするものです。

struct S {
  S() &;        // #1、左辺値として構築する際のコンストラク
  S() &&;       // #2、右辺地として構築する際のコンストラク
  S() const &;  // #3、const左辺値として構築する際のコンストラク
};

S x;          // #1が呼ばれる
S();          // #2が呼ばれる
const S y;    // #3が呼ばれる
new const S;  // #3が呼ばれる

これは例えば、deleteと共に使用して右辺値としての構築を禁止したり、constとして構築された時にだけコンストラクタの処理を単純化する、などの用法がありそうです。

P2635R0 Enhancing the break statement

break文を拡張して、ネストしたループにおける利便性を改善する提案。

ネストしたループにおいて内側のループをbreakで終了させる時、終了直後に何かしたい(特に、外側のループの継続に関して)場合があります。このような場合、典型的にはフラグを使用するかgotoを使用する必要があります。

void before1(std::ranges::range auto&& range_i) {
  for (auto&& range_j : range_i) {
    bool broke = false;

    for (auto j : range_j) {
      if (cond(j)) {
        // ループを継続する処理
        ...
      } else {
        // ループを中断する
        broke = true;
        break;
      }
    }
    
    if (broke) {
      continue;
    } else {
      // 内側ループが完了した後の処理
      ...
    }
  }
}

void before2(std::ranges::range auto&& range_i) {
  for (auto&& range_j : range_i) {
    for (auto j : range_j) {
      if (cond(j)) {
        // ループを継続する処理
        ...
      } else {
        // ループを中断する
        goto broke;
      }
    }
    
    // 内側ループが完了した後の処理
    ...

    // 内側ループの中断先
    broke: ;
  }
}

他にも関数にするとかラムダ式を使うとかありますが、いずれにせよこのような場合にbreakの効果は不十分でした。

この提案は、breakを拡張してこのような場合に余分なコードを最小にしつつ同じことを達成できるようにするものです。

提案では、break statement;という構文を許可するようにし、break直後にstatementを実行させるようにします。

void after(std::ranges::range auto&& range_i) {
  for (auto&& range_j : range_i) {
    for (auto j : range_j) {
      if (cond(j)) {
        // ループを継続する処理
        ...
      } else {
        // ループを中断し、外側ループではcontinueする
        break continue;
      }
    }

    // 内側ループが完了した後の処理
    ...
  }
}

break statement;statementにはreturnとかbreakそのものもおく事ができで、多重ループからの脱出にはbreak break ...;と書く事ができます

void after(std::ranges::range auto&& range_i) {
  for (auto&& range_j : range_i) {
    for (auto j : range_j) {
      if (cond(j)) {
        // 条件を満たしたら2重ループから脱出
        break break;
      }
    }
  }
}

おわり

この記事のMarkdownソース

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