[C++]std::exchangeによるmoveしてリセットするイディオムの御紹介

2020年10月公開分の提案文書を眺めていたら良さげなものを見つけたので宣伝です。そのため、この記事の内容は次の提案文書を元にしています。

もくじ

moveしてリセット!

std::exchangeを用いてmoveしてリセットするとは、次のようなものです。

// old_objの値をムーブしてnewobjを構築し、old_objをデフォルト状態にリセット
T new_obj = std::exchange(old_obj, {});

// old_objの値をnewobjにムーブ代入して、old_objをデフォルト状態にリセット
new_obj = std::exchange(old_obj, {});

std::exchangeC++14で追加された関数で、第二引数の値を第一引数に転送し、第二引数の元の値を戻り値として返すものです。

template <class T, class U=T>
constexpr T exchange(T& obj, U&& new_val);

ストリーム的に見ると、第二引数から戻り値まで右から左へその値が玉突き的に流れていくように見えます。

第二引数の型も個別のテンプレートパラメータになっており、デフォルトでは第一引数の型が推論されます。そのため、第二引数に{}を指定したときはT{}と指定したのと同等になるわけです。
そして、そのようにデフォルト構築(正確には値初期化(Value initialization))された値が第一引数にmoveされ、第一引数の元の値が戻り値として返されます。その際、可能であればすべてmoveされます。

このイディオムには次の2つの利点があります。

1. 操作の複合化

このイディオムの利点の1つは複数の操作をひとまとめにする事でミスやエラーを起きにくくすることです。
例えば、std::unique_ptrの様にポインタを所有するようなクラスのムーブコンストラクタとreset()で次のようにコードを改善できます。

struct MyPtr {
  Data *d;

  // BAD, ポインタのコピーとnullptr代入が分割されているため、nullptr代入が忘れられうる
  MyPtr(MyPtr&& other) : d(other.d) { other.d = nullptr; }

  // BETTER, std::exchangeを利用してポインタを移動しリセット
  MyPtr(MyPtr&& other) : d(std::exchange(other.d, nullptr)) {}

  // GOOD, std::exchangeによる一般化されたイディオム(知っていれば意図が明確)
  MyPtr(MyPtr&& other) : d(std::exchange(other.d, {})) {}


  void reset(Data *newData = nullptr)
  {
    // BAD, 読みづらい
    std::swap(d, newData);
    if (newData) {
      dispose(newData);
    }

    // BETTER, 意図は分かりやすい
    Data *old = d;
    d = newData;
    if (old) {
      dispose(old);
    }

    // GOOD, 1行かつストリーム的
    if (Data *old = std::exchange(d, std::exchange(newData, {}))) {
      dispose(old);
    }
  }
};

このように、値が右から左へストリーム的に流れていくように見ることができ、そして移動とリセットの操作が合成されているために、意図が明確でミスも起こしにくいコードを書くことができます。

2. move後状態の確定

もう一つの利点は、move後の抜け殻となっているオブジェクトの状態を確定できる事です。

f(std::move(obj));          // objの状態は良く分からない・・・

f(std::exchange(obj, {}));  // objはデフォルト構築状態にリセットされる

例えば標準ライブラリのものであれば、moveした後の状態は「有効だが未規定な状態」と規定されています。とはいえ結局どういう状態なのか分からず、より一般のライブラリ型などではドキュメント化されていることの方が稀です。
このイディオムを用いることによって、moveとその後のオブジェクトの状態の確定を1行で簡潔に書くことができます。

とはいえ完全にmoveと同等ではなくいくつか違いがあります。

move(old_obj) exchange(old_onj, {})
例外を投げる? 通常ムーブコンストラクタはnoexcept デフォルトコンストラクタ次第
処理後のold_objの状態は? 有効だが未規定 デフォルト構築状態
呼び出しのコストは? ムーブコンストラクタ次第 ムーブ/デフォルトコンストラクタ次第
obj = xxxx(obj);は何をする? 実装依存 例外を投げないと仮定すると、何もしない
old_objをその後使用しない場合に最適な書き方? Yes No

いくつかのサンプル

class C {
  Data *data;
public:
  // ムーブコンストラクタの改善
  C(C&& other) noexcept
    : data(std::exchange(other.data, {}))
  {}
};
template <typename K, typename V, template <class...> class C =  std::vector>
class flat_map {
  C<K> m_keys;
  C<V> m_values;

public:

  // ムーブ後の有効だが未規定な状態を達するにはデフォルトmoveに任せられない
  // Cのムーブ操作によってはflat_mapの不変条件が破られ有効ではなくなってしまう可能性がある
  // 言い換えると、有効だが未規定な状態は合成されない
  // そのため、明示的なリセットが必要
  flat_map(flat_map&& other) noexcept(/**/)
    : m_keys(std::exchange(other.m_keys, {})),
      m_values(std::exchange(other.m_values, {}))
  {}

  flat_map &operator=(flat_map&& other) noexcept(/**/) {
    m_keys = std::exchange(other.m_keys, {});
    m_values = std::exchange(other.m_values, {});
    return *this;
  }
};
void Engine::processAll() {
  // m_dataを消費しつつループする
  for (auto& value : std::exchange(m_data, {})) {
      // この処理はm_dataを変更する可能性がある
      // イテレータ破壊を回避する
      processOne(value);
  }
}
void ConsumerThread::process() {
  // pendingDataをmutexの保護の元で取得し
  // 現在のスレッドがそれを安全に使用できるようにする
  Data pendingData = [&]() {
      std::scoped_lock lock(m_mutex);
      return std::exchange(m_data, {});
  }();

  for (auto& value : pendingData)
      process(value);
}
// 一度だけ実行される関数
void Engine::maybeRunOnce() {
  if (std::exchange(m_shouldRun, false)) {
    run();
  }
}
// Dataのオブジェクトを貯めておくクラス
struct S {
    // C++ Core Guideline F.15に基づいたオーバーロードの提供
    void set_data(const Data& d);
    void set_data(Data&& d);
} s;

Data d = ~~~;

// dをため込むが、明示的にデフォルト状態にする
s.set_data(std::exchange(d, {}));

assert(d == Data());

参考文献

この記事のMarkdownソース

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

文書の一覧 www.open-std.org

提案文書で採択されたものはありません。全部で32本あります。

P0288R7 : any_invocable

ムーブのみが可能で、関数呼び出しのconst性やnoexcept性を指定可能なstd::functionであるstd::any_invocableの提案。

先月の記事を参照

onihusube.hatenablog.com

前回からの変更は、規格書に追加するための文言を調整しただけの様です。

P0443R14 : A Unified Executors Proposal for C++

処理をいつどこで実行するかを制御するための汎用的な基盤となるExecutorライブラリの提案。

以前の記事を参照。

onihusube.hatenablog.com

R13からの変更は、いくつかEditorialなバグを修正した事です。

P0881R7 : A Proposal to add stacktrace library

スタックトレースを取得するためのライブラリを追加する提案。

先月の記事を参照。

onihusube.hatenablog.com

LWGでのレビューが終了し、この提案は本会議での投票に向かうようです。このリビジョンはLWGでのレビューを受けて最終的な文言調整を反映したものです。多分C++23入り確定でしょう。

P0958R2 : Networking TS changes to support proposed Executors TS

Networking TSのExecutorの依存部分をP0443のExecutor提案の内容で置き換える提案。

現在のNetworking TSは以前のBoost.asioをベースにしており、asioの持つExecutorはP0443のものとは異なっているので、Networking TSのExecutor部分もまたP0443のものとは異なった設計となっています。

Networking TSはExecutorに強く依存しており、C++23に導入される予定のExecutorはP0443のものがほぼ内定しているので、Networking TSもそれに依存するように変更するものです。

Boost.asioライブラリは既にP0443ベースのExecutor周りを実装し終えて移行しており、その実装をベースとする形で書き換えています。その経験によれば、一部のものを除いて殆どのアプリケーションでは追加の変更を必要としなかったとのことです。

P1322R2 : Networking TS enhancement to enable custom I/O executors

Networking TSのI/Oオブジェクトをio_contextだけではなく、任意のExecutorによって構築できるようにする提案。

Networking TSのio_contextはBoost.asioではio_serviceと呼ばれていたもので、P0443のExecutorにいくつかの機能が合わさったものです。

io_contextは実行コンテキストを抽象化し表現するという役割がありますが、実行コンテキストを変更する事は単体ではできません。P0443ではexecutorコンセプトによってそれらExecutorと実行コンテキストを表現します。

Networking TSのI/Oオブジェクト(socket, acceptor, resolver, timer)はio_contextを構築時に受け取り、それを利用して実行します。そこに任意のExecutorを渡せるようにすることでユーザーが用意したものやプラットフォームネイティブの実行コンテキストでそれらの処理を実行できるようにしようとする提案です。

また、各I/OオブジェクトのクラスのテンプレートパラメータにExecutorを指定できるようにし、それらの動作をカスタマイズ可能としています。

この提案はおそらくNetworking TS仕様のExecutorをベースとしており、P0443ベースで書かれていないようです。

P1371R3 : Pattern Matching

オブジェクトの実際の状態に応じた分岐処理のシンタックスシュガーであるパターンマッチング構文の提案。

現在のC++にはifswitchという二つの条件分岐用の構文があります。ifは複雑なboolの式を扱うことができますが、switchは整数値しか扱えず、ifは分岐条件を一つづつ書いて行くのに対してswitchは条件を羅列した形で(宣言的に)書くことができるなど、は2つの構文の間にはギャップがあります。
パターンマッチングはそれらの良いとこどりをしたような構文によって、特に代数的データ型の検査と分岐を書きやすく、読みやすくするものです。

この提案ではinspect式によって構造化束縛宣言を拡張する形でswitch的な書き方によってパターンマッチングを導入しています。これを提案中ではstructured inspection(構造化検証?)と呼んでいます。

std::string

`if`文 `inspect`式
if (s == "foo") {
  std::cout << "got foo";
} else if (s == "bar") {
  std::cout << "got bar";
} else {
  std::cout << "don't care";
}
inspect (s) {
  "foo" => { std::cout << "got foo"; }
  "bar" => { std::cout << "got bar"; }
  __ => { std::cout << "don't care"; }
};

std::tuple

`if`文 `inspect`式
auto&& [x, y] = p;
if (x == 0 && y == 0) {
  std::cout << "on origin";
} else if (x == 0) {
  std::cout << "on y-axis";
} else if (y == 0) {
  std::cout << "on x-axis";
} else {
  std::cout << x << ',' << y;
}
inspect (p) {
  [0, 0] => { std::cout << "on origin"; }
  [0, y] => { std::cout << "on y-axis"; }
  [x, 0] => { std::cout << "on x-axis"; }
  [x, y] => { std::cout << x << ',' << y; }
};

std::variant

`if`文 `inspect`式
struct visitor {
  void operator()(int i) const {
    os << "got int: " << i;
  }
  void operator()(float f) const {
    os << "got float: " << f;
  }
  std::ostream& os;
};
std::visit(visitor{strm}, v);
inspect (v) {
  <int> i => {
    strm << "got int: " << i;
  }
  <float> f => {
    strm << "got float: " << f;
  }
};

polymorphicな型

継承ベースの動的ポリモルフィズム `inspect`式
struct Shape { 
  virtual ~Shape() = default;
  virtual int Shape::get_area() const = 0;
};

struct Circle : Shape {
  int radius;

  int Circle::get_area() const override {
    return 3.14 * radius * radius;
  }
};

struct Rectangle : Shape {
  int width, height;

  int Rectangle::get_area() const override {
    return width * height;
  }
};
struct Shape { 
  virtual ~Shape() = default;
};

struct Circle : Shape {
  int radius;
};

struct Rectangle : Shape {
  int width, height;
};

int get_area(const Shape& shape) {
  return inspect (shape) {
    <Circle> [r] => 3.14 * r * r;
    <Rectangle> [w, h] => w * h;
  };
}

この例だけを見てもかなり広い使い方が可能であることが分かると思います。しかし、この例以上にinspect式による構文は柔軟な書き方ができるようになっています(ここで説明するには広すぎるので省略します。そこのあなたm9!解説記事を書いてみませんか??)。

P1701R1 : Inline Namespaces: Fragility Bites

inline名前空間の名前探索に関するバグを修正する提案。

当初(C++11)のインライン名前空間名前空間名の探索には次のようなバグがありました。

namespace A {
  inline namespace b {
    namespace C {
      template<typename T>
      void f();
    }
  }
}

namespace A {
  namespace C {
    template<>
    void f<int>() { }  // error!
  }
}

当初の名前空間名の探索は宣言領域 (declarative region) という概念をベースに行われていました。宣言領域とは簡単にいえば、ある宣言を囲む宣言の事です。
宣言領域の下では、2つ目の名前空間A::Cの宣言は1つ目のA::b::Cとは宣言領域が異なるため、それぞれの名前空間Cは同一のものとはみなされません。

しかし、これはテンプレートの特殊化を行う際に問題となるため、DR 2061によって規格書の定義としては修正されました。その修正方法は、名前空間名を宣言するとき、そのコンテキストからネストしたinline名前空間も考慮したうえで到達可能な名前空間名を探索し、同じ名前が見つかった場合は同一の名前空間として扱い、見つからない場合にのみ新しい名前空間名を導入する。という感じです。

これによって先程のバグは解決されましたが、筆者の方がそれをGCCに実装する時に問題が浮かび上がったようです。

inline namespace A {
  namespace detail { // #1
    void foo() {} // #3
  }
}

namespace detail { // #2
  inline namespace C {
    void bar() {} // #4
  }
}

DR2061以前は2つ目の名前空間detailは新しい名前空間を導入し、#3, #4はそれぞれA::detail::foodetail::C::barという修飾名を持ちます。

しかしDR2061による修正によれば、2つ目の名前空間detailの宣言は一つ目の名前空間A::detailと同一視されます。その結果、#3, #4はそれぞれA::detail::fooA::detail::C::barという修飾名を持つことになります。

このことはヘッダファイルやモジュールのインポートを介すことで、意図しない名前空間名の汚染を引き起こすことになります。
これによって、ヘッダと実装でファイルを分けている場合、実装ファイルで名前空間名の指定が意図通りにならず、最悪別の名前に対する実装を行ってしまうかもしれません。また、inline名前空間usingすることでAPIの一部だけを有効化するような手法をとるライブラリでは、意図しないものがスコープ中にばらまかれることにもなりかねません。

namespace component {
  inline namespace utility {
      namespace detail {
        // component::utility::detail
      }
  }
}

namespace component {
  namespace detail {
    // DR2061以前は component::detail
    // DR2061の後は component::utility::detail
  }
}

一方、DR2061が無いとC++20以降は特に困ったことが起きます。

namespace std {
  namespace ranges {
    template<>
    constexpr bool disable_sized_range<MyType> = true;
  }
}

std::range::disable_sized_rangeは変数テンプレートであり、ユーザー定義の型について特殊化することでstd::range::sized_rangeコンセプトを無効化するものです。C++20のrangeライブラリ周りではこのようなオプトアウトのメカニズムがほかにもいくつかあります。

現在、多くの実装はABI保護のためにstd名前空間内の実装には何かしらの形でインライン名前空間を挿入しています。すると、このコードで行っているような特殊化はDR2061による変更が無ければ特殊化としてみなされなくなります。

この提案ではこれらの問題の解決として次の2つのことをサジェストしています。

  1. DR2061を元に戻す
  2. 標準ライブラリ内でユーザーが特殊化することを定義されているものについて、修飾名で特殊化すること、という規定を追加する。

つまり、先程の特殊化は次のように書くことを規定するということです。

template<>
constexpr bool std::ranges::disable_sized_range<MyType> = true;

こうすれば、名前空間の宣言を伴わないため上記のような問題に悩まされることはありません。

P1885R3 : Naming Text Encodings to Demystify Them

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

C++の標準は文字エンコーディングを参照する時にロケールを介して参照しています。これは歴史的なものですが、ユニコードの登場によってもはや機能しなくなっています。そして、C++はシステムがどのエンコーディングを使用し、また期待しているかを調べる方法を提供していないためそれを推測しなければならず、文字列の取り扱いを満足に行う事ができません。

この提案は、現在取得する方法のないシステムやコンパイラの使用するエンコーディングを取得・識別できるようにし、また、エンコーディングを表現する標準的なプリミティブを提供することを目指すものです。
なお、ここでは文字コード変換のための標準機能の追加を目指してはいません。

#include <text_encoding>  // 新ヘッダ
#include <iostream>

int main() {
  // char文字(列)リテラルのエンコーディングを取得
  std::cout << std::text_encoding::literal().name() << std::endl;  
  // wchar_t文字(列)リテラルのエンコーディングを取得
  std::cout << std::text_encoding::wide_literal().name() << std::endl;
  // システムのマルチバイト文字エンコーディングを取得 
  std::cout << std::text_encoding::system().name() << std::endl;  
  // システムのワイド文字エンコーディングを取得 
  std::cout << std::text_encoding::wide_system().name() << std::endl;  
}

この提案による全てのものはstd::text_encodingクラスの中にあります。上記の4つの関数はその環境で対応する文字エンコーディングを表すstd::text_encodingのオブジェクトを返します。

std::text_encodingは非staticメンバ関数name(), mib(), aliases()によってその名前、IANAのMIBenum、文字エンコーディング名の別名(複数)を取得することができます。

また、システムの文字エンコーディングが特定のエンコーディングであるかを判定する機能も提供されています。

int main() {
  assert(std::text_encoding::system_is<std::text_encoding::id::UTF8>());
  assert(std::text_encoding::system_wide_is<std::text_encoding::id::UCS4>());
}

ただ、残念ながらこれらはconstexprではありません。

std::text_encoding::idは各種文字エンコーディングを表すenum classで、IANAの定義するCharacter Setにある名前が列挙値として登録されています。この列挙値か、対応する文字コード名文字列をによってもstd::text_encodingクラスを構築することができます。

また、std::text_encodingのオブジェクト同士を==で比較することもできます。

P1949R6 : C++ Identifier Syntax using Unicode Standard Annex 31

識別子(identifier)の構文において、不可視のゼロ幅文字や制御文字の使用を禁止する提案。

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンの変更点は、この提案によって変更されるものと変更されないものを明記したのと、いくつかのサンプルを追加した事です。

P2013R3 : Freestanding Language: Optional ::operator new

フリースタンディング処理系においては、オーバーロード可能なグローバル::operator newを必須ではなくオプションにしようという提案。

以前の記事を参照 onihusube.hatenablog.com

このリビジョンの変更点は、8月のEWGの電話会議での投票結果を記載した事と、いくつかの文言の修正、機能テストマクロについてはP2198の将来のリビジョンで検討することになったことです。

P2029R3 : Proposed resolution for core issues 411, 1656, and 2333; escapes in character and string literals

文字(列)リテラル中での数値エスケープ文字('\xc0')やユニバーサル文字名("\u000A")の扱いに関するC++字句規則の規定を明確にする提案。

以前の記事を参照 onihusube.hatenablog.com

このリビジョンの変更点は、電話会議の結果を受けて提案している文言を調整しただけの様です。

P2066R3 : Suggested draft TS for C++ Extensions for Transaction Memory Light

現在のトランザクショナルメモリTS仕様の一部だけを、軽量トランザクショナルメモリとしてC++へ導入する提案。

以前の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、EWGのフィードバックを受けて、完全式(full-expression)の定義にatomicブロック(atomicステートメント)の開始と終了という定義を追加した事と、トランザクションの並行処理に関するメモの追加や、atomicステートメントの導入キーワードのatomic doへの変更と、atomicステートメントで使用可能なライブラリ関数のリストが追加されたことなどです。

int f() {
  static int i = 0;

  // atomicブロック、atomicトランザクションを定義
  // このブロックは全てのスレッドの間で1つづつ実行される
  atomic do {
    ++i;
    return i;
  }
}

int main() {
  std::array<std::thread, 100> threads{};

  // 関数f()の呼び出し毎に一意の値が取得される
  // 同じ値を読んだり、更新中の値を読むことはない
  for (auto& th : threads) {
    th = std::thread([](){
      int n = f();
    });
  }

  for (auto& th : threads) {
    th.join();
  }
}

このatomicブロックで使用可能なものには以下が挙げられています。

  • std::memset, std::memcpy, std::memmove
  • std::move, std::forward
  • 基本型(組み込み型)に対するstd::swap, std::exchange
  • std::array<T>の全てのメンバ関数
    • Tに対する必要な操作がatomicブロックで使用可能である場合のみ
  • std::list<T>, std::vector<T>
    • Tに対する必要な操作がatomicブロックで使用可能である場合のみ
  • TMPとtype traits
  • <ratio>
  • std::numeric_limitsの全てのメンバ
  • placement newと対応するdelete

 

P2077R1 : Heterogeneous erasure overloads for associative containers

連想コンテナに対して透過的な要素の削除と取り出し方法を追加する提案。

C++20では、非順序連想コンテナに対して透過的な検索を行うことのできるオーバーロードが追加されました。「透過的」というのは連想コンテナのキーの型と直接比較可能な型については、一時オブジェクトを作成することなくキーの比較を行う事が出来ることを指します。これによって、全ての連想コンテナで透過的な検索がサポートされました。

一方、削除の操作に関しては順序/非順序どちらの連想コンテナも透過的な削除をサポートしていませんでした。この提案は、全ての連想コンテナのerase()extract()に対しても同じ目的のオーバロードを追加しようとするものです。

std::unordered_map<std::string, int> map = {{"16", 16}, {"1024", 1024}, {"65536", 65536}};

const std::string key{"1"}

// C++20より、どちらも一時オブジェクトを作成しない
map.contains(key);
map.contains("1024");

map.erase(key);     // 一時オブジェクトを作成しない
map.erase("1024");  // stringの一時オブジェクトが作成される

この提案によるオーバロードを有効化する手段は透過的な検索と同様の方法によります。
順序連想コンテナではその比較を行う関数オブジェクトの型がis_transparentというメンバ型を持っていて、渡された型がイテレータ型に変換されない場合に有効になり、非順序連想コンテナではそれに加えてハッシュ関数オブジェクトの型がis_transparentというメンバ型を持っていれば有効化されます。

P2138R3 : Rules of Design <=> Specification engagement

CWGとEWGの間で使用されているwording reviewに関するルールの修正と、それをLWGとLEWGの間でも使用するようにする提案。

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、対象とする読者への説明、提案文書作成時に文言の作成に支援を求める方法、Tentatively Readyステータスをスキップできる条件を明確にしたことと、Tentatively Ready for Plenaryというステータスは次のミーティングの本会議の採決にかける事を意味していることを明確にしたことです。

P2145R1 : Evolving C++ Remotely

コロナウィルスの流行に伴ってC++標準化委員会の会議がキャンセルされている中で、リモートに移行しつつどのように標準化作業を進めていくのかをまとめた文章。

onihusube.hatenablog.com

このリビジョンでの変更は、以前のリビジョン時からの状況変化を反映した様です。

P2164R2 : views::enumerate

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

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、既存のviewと同様にvalue_typeを非参照にしたこと、説明と文言間の矛盾を正したこと、サンプルコードに関連ヘッダ(<ranges>)を追記した事です。

P2166R1 : A Proposal to Prohibit std::basic_string and std::basic_string_view construction from nullptr

std::stringstd::string_viewnullptrから構築できないようにする提案。

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、googleのProtbufで見つかったコード例の追加と類似した問題へのリンクが追加されたことです。

string GetCapitalizedType(const FieldDescriptor* field) {

  switch (field->type()) {
      // handle all possible enum values, but without adding default label
  }

  // Some compilers report reaching end of function even though all cases of
  // the enum are handed in the switch.
  GOOGLE_LOG(FATAL) << "Can't get here.";
  return NULL;
}

この関数はswitchでハンドルされなかった場合にNULLから構築されたstringを返しています。ただ、実際にはこのコードは到達可能ではないらしく、この提案を妨げるものではないようです。

P2169R2 : A Nice Placeholder With No Name

宣言以降使用されず追加情報を提供するための名前をつける必要もない変数を表すために_を言語サポート付きで使用できる様にする提案。

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更はP2011での_の利用(プレイスホルダーとしての利用)との構文の衝突について追記されたことです。

int _ = 0;      // この提案の用法、プレイスホルダーではない
f() |> g(_, _); // P2011の用法、プレイスホルダー

この提案での_の特別扱いは変数宣言にのみ作用するため、上記P2011の用法による場合は常にプレイスホルダーとして扱えば良いと述べられています。

P2192R2 : std::valstat -Transparent Returns Handling

関数の戻り値としてエラー報告を行うための包括的な仕組みであるvalstatの提案。

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、動機やメタステートの説明、付録のサンプルコードなどを改善・書き足しした事と、タイトルが"std::valstat - transparent return type"から"std::valstat -Transparent Returns Handling"に変更されたことです。

P2194R0 : The character set of the internal representation should be Unicode

C++標準の想定するコンパイル中のC++ソースコードの表現について、ユニコード以外を認める必要が無いという根拠を示す文書。

これはSG16(Unicode Study Group)での議論のために書かれた報告書です。

C++20では、翻訳フェーズ1以降のソースコードの規格的な表現(エンコーディング)はユニコードです。SG16での議論の中で、そこにユニコードのスーパーセットたるエンコーディングを許可しようというアイデアが提出されたらしく、この文書はそのアイデアを採用する根拠がない事を説くものです。

P2195R0 : Electronic Straw Polls

委員会での投票が必要となる際に、メールまたは電子投票システムを用いて投票できるようにする提案。

これは標準化のプロセスについてのもので、C++言語そのものに対する提案ではありません。

コロナウィルスの流行によって対面での会議が行えなくなったため、現在のC++標準化委員会の作業は主に電話会議で行われており、投票が行われる場合も電話会議でリアルタイムに行われていました。

より議論と作業を効率化させるために、この投票を電子メールなどによって非同期的、かつ定期的(四半期ごと)に行うことを提案しています。
また、CWGやEWGでは現状の体制でも特に問題が無かったため、この提案の対象はLWG/LEWGに絞られています。

他にも、標準化の作業の投票やコンセンサスがどのように決定されるかなどについて詳細に書かれています。

P2206R0 : Executors Thread Pool review report

Executor提案(P0443R13)のstatic_thread_pool周りのレビューの結果見つかった問題についての報告書。

P2212R1 : Relax Requirements for time_point::clock

std::chrono::time_pointClockテンプレートパラメータに対する要件を弱める提案。

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、提案する既存規格の文言の変更についてフィードバックを受けて修正した事です。

P2215R1 : "Undefined behavior" and the concurrency memory model

out-of-thin-airと呼ばれる問題の文脈における、未定義動作とは何かについての報告書。

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、memory_order_load_storeの順序の問題が逐次一貫性のあるloadにも適用されることを明確にしたことと、"Consequences"セクションが追加されたことです。

P2216R0 : std::format improvements

std::formatの機能改善の提案。

主に、次の二点を改善しようとするものです。

コンパイル時にフォーマット文字列をチェックするようにする

C++20のstd::formatでは、フォーマットエラーは実行時にstd::format_error例外を投げることで報告されます。

std::string s = std::format("{:d}", "I am not a number");

例えばこのコードは、:dが通常の文字列に対するフォーマットとして不適切なため、実行時に例外を投げます。
この提案では、このようなフォーマットのエラーをコンパイル時にコンパイルエラーとして報告することを提案しています。

ただし、言語と実装の特別なサポートなくしてはこのままのコードでコンパイル時のフォーマットチェックを行うことはできません。そのような特別扱いを嫌う場合、ユーザー定義リテラルや静的な文字列型など何かしらの手段が必要となります。

std::vformat_toのバイナリサイズの削減

std::vformat_tostd::format_toの引数型を型消去したもので、フォーマット文字列とフォーマット対象を受けとり、任意の出力イテレータにフォーマット済み文字列を出力します。主にformatライブラリの内部で使用されるものです。

次のように宣言されています。

template<class Out, class charT>
using format_args_t = basic_format_args<basic_format_context<Out, charT>>;

template<class Out>
Out vformat_to(Out out, string_view fmt,
                 format_args_t<type_identity_t<Out>, char> args);

ここで問題となるのは、第二引数の型format_args_t<type_identity_t<Out>, char>が出力イテレータOutに依存していることによって、異なるOut毎に個別のformat_args_tインスタンス化されてしまい、コードサイズが肥大化する原因となることです。これはフォーマットしない引数型に対しても引き起こされうるので、使用しないものに対して余計なコストを支払うことになりかねません。
また、std::format/vformatは内部バッファを介してイテレータ型を型消去しており、同じ方法をとればこの特殊化は必要ありません。

そこで、シグネチャを次のように変更することを提案しています。

template<class Out>
Out vformat_to(Out out, string_view fmt, format_args args);

format_argsはあらかじめ定義されているイテレータ型を型消去し引数を保持しておくものです。これによって、std::format/vformatと同様の方法でイテレータ型の型消去を行うようになり、不要なテンプレートのインスタンス化を防ぐことができます。

なお、これらの事は明らかにC++20の<format>ライブラリに対する破壊的変更となりますが、<format>は現在のところ誰も実装していないので壊れるコードは無い、と筆者の方は述べています(個人的にはDRにしてほしい気持ちです・・・)。

P2217R0 : SG16: Unicode meeting summaries 2020-06-10 through 2020-08-26

SG16(Unicode Study Group)の会議の議事録の要約。

P2218R0 : More flexible optional::value_or()

std::optionalvalue_or()メンバ関数をベースとした、利便性向上のための新しいメンバ関数を追加する提案。

value_or()メンバ関数std::optionalが有効値を保持していればその値を返し、無効値を保持している場合は指定された値を代わりに(std::optionalの要素型に変換して)返すものです。
value_or()関数は有効値からのフォールバックという性質上、多くの場合その型のデフォルト値が使用されます。その場合リテラルを持つ組み込み型ならばあまり困る事も無いのですが、コンストラクタを持つユーザー定義型では意図通りにならない事があります。

例えばstd::optional<std::string>value_orstd::stringのデフォルト値(空文字列)を返そうとしてみると

std::optional<int> oi{};
std::optional<bool> ob{};
std::optional<std::string> os{};

// 無問題、意図通り  
std::cout << oi.value_or(0) << std::endl;
std::cout << ob.value_or(false) << std::endl;

// 実行時エラー
std::cout << os.value_or(nullptr) << std::endl;
// パフォーマンス的に最適ではない
std::cout << os.value_or("") << std::endl;
// コンパイルエラー
std::cout << os.value_or({}) << std::endl;
// 正解、でも型名を省略したい
std::cout << os.value_or(std::string{}) << std::endl;

特に3番目の様な書き方ができないのがとても不便です。要素型がさらに複雑な型になるとより面倒になります。

この原因はvalue_or()のテンプレートパラメータが要素型とは無関係になっていることにあります。そのため、要素型からの推論を行う事が出来ません。

template<typename T>
class optional {

  // おおよそこんな感じの実装になっている
  template<typename U>
    requires (is_copy_constructible_v<T> and is_convertible_v<U&&, T>)
  constexpr T value_or(U&& u) const & {
    return this->has_value() ? this->value() : static_cast<T>(std::forward<U>(u));
  }
};

そこで、このテンプレートパラメータUのデフォルト引数として要素型Tを与えておくことで、UTから推論することができるようになります。ただし、std::optionalの要素型はconst修飾を行っておくことが可能なので、それは除去しておきます。

template<typename T>
class optional {

  // おおよそこのような実装だが、おそらくこのままだとエラーになる
  template<typename U = remove_cv<T>>
    requires (is_move_constructible_v<T> and is_convertible_v<U&&, T>)
  constexpr T value_or(U&& u) const & {
    return this->has_value() ? this->value() : static_cast<T>(std::forward<U>(u));
  }
};

これによって、既存の振る舞いを壊すことなく全ての要素型においてvalue_or({})の様な呼び出しが出来るようになります。

次に、value_or()を発展させて、無効値を保持している場合に指定された引数で要素を構築して返すvalue_or_construct()関数を追加することを提案しています。これは、あくまでその場で構築して返すだけで、そのstd::optionalの無効状態を変更するものではありません。

std::optional<std::string> os{};

// デフォルト構築したstd::stringを返す
os.value_or_construct();

// 指定した文字列で構築したstd::stringを返す
os.value_or_construct("left value");

最後に、value_or_construct()をさらに発展させて、構築のための引数の評価を遅延させるvalue_or_else()関数を提案しています。

using namespace std::string_literals;
std::optional<std::vector<std::string>> ovs{};

// 呼び出し時点でstd::initializer_list<std::string>が構築される
ovs.value_or_construct({"Hello"s, "World"s});

// 呼び出し時点ではstd::vector<std::string>>は構築されない
// 無効値を保持している場合にのみラムダ式の呼び出しを通して構築される
ovs.value_or_else([]{ return std::vector{"Hello"s, "World"s}; });

value_or_construct()value_or_else()は一見似通ったものに見えますが、それぞれ異なる欠点があり使いどころが異なっているため、両方追加することを提案しています。

P2219R0 : P0443 Executors Issues Needing Resolution

Executor提案(P0443R13)のレビューの結果見つかった解決すべき問題点の一覧。

地味に63個も問題があり、これの解決後にさらなるレビューが必要となるので、もう少し時間がかかりそうです・・・

P2220R0 : redefine properties in P0443

Executor提案(P0443R13)における、Executorに対するプロパティ指定の方法を別に提案中のtag_invokeによって置き換える提案。

CPO(Custmization Point Object)は呼び出された引数に対して定義されたいくつかの方法のうちの一つが可能であれば、それによってその目的を達します。中でもほぼ必ず、引数に対してそのCPO自身と同名の関数をADLで探索することが行われます。この時、意図せずに同じ名前の関数を同じ名前空間に持つようなクラス型に対して誤った呼び出しを行ってしまう可能性があります。それを防ぐためにコンセプトによって制約されていますが、それすらもすり抜けるケースが無いとは言えません。

tag_invokeはCPOのその部分(ADLによる探索)をより安全に置き換えるためのものです。CPOの型そのものをタグとして用い、std::tag_invoke(これ自身もCPO)を介して、ユーザー定義型に対してADLでtag_invokeという名前の関数を呼び出します。CPOにアダプトするユーザーはtag_invokeという名前の関数を例えばHidden friendsとして定義し、対象のCPOの型をタグとして受け取れるようにした上で、そこにCPOによって呼び出された時の処理を記述します。要するに少し複雑なタグディスパッチを行うものです。

この提案は、そんなtag_invokeを用いてP1393require/preferによるプロパティ指定の方法をより簡潔に定義し直そうとするものです。

require/preferは主にExecutorライブラリのexecutorに対してプロパティを設定するもので、CPOとして定義されています。呼び出すとメンバ関数またはADLによってrequire/preferという名前の関数を探し処理を委譲します。require/preferによってプロパティ指定可能なクラスを作成する際は、そのように呼ばれるrequire/prefer関数内でプロパティ指定の処理を行い、その後プロパティ指定済みのオブジェクトを返します。

// 何かしらのexecutor
executor auto ex = ...;

// 実行にはブロッキング操作が必要という要求(require
executor auto blocking_ex = std::require(ex, execution::blocking.always);

// 特定の優先度pで実行することが好ましい(prefer
executor auto blocking_ex_with_priority = std::prefer(blocking_ex, execution::priority(p));

// ブロッキングしながら実行、可能ならば指定の優先度で実行
execution::execute(blocking_ex_with_priority, work);

requireは必ずそのプロパティを設定するという強い要求を行い、設定できない場合はコンパイルエラーとなります。対して、preferは弱い要求を行い、そのプロパティ指定は無視される可能性があります。

require/preferは処理の実行方法などのプロパティ(ブロックするかしないか、優先度など)と、処理の実行のインターフェース(std::execution::executeなどのCPO)とを分離し、プロパティ指定毎にその実行用インターフェースが増加することを回避するための仕組みです。

この提案ではrequire/preferの大部分をtag_invokeを使って実装するように変更しています。先程のコードは次のように変化します。

// 何かしらのexecutor
executor auto ex = ...;

// 実行にはブロッキング操作が必要という要求(require
executor auto blocking_ex = execution::make_with_blocking(ex, execution::always_blocking);

// 特定の優先度pで実行することが好ましい(prefer
executor auto blocking_ex_with_priority = std::prefer(execution::make_with_priority, blocking_ex, p);

// ブロッキングしながら実行、可能ならば指定の優先度で実行
execution::execute(blocking_ex_with_priority, work);

require/preferCPOそのものだけではなく、プロパティを表現する型やプロパティサポートを問い合わせるためのquery、プロパティを受け入れるための実装方法などもtag_invokeを使うように変更されています。結果として、requireは消えているようです。

元のrequire/preferはユーザーから見れば効率的で柔軟なプロパティ指定の方法でしたが、それを受け入れるようにする実装は少し大変でした。しかし、tag_invokeを利用することで実装者の負担がかなり軽減されています。

P2221R0 : define P0443 cpos with tag_invoke

Executor提案(P0443R13)で提案されているCPO(Custmization Point Object)を別に提案中のtag_invokeによって定義する提案。

先程も出て来たtag_invokeは、CPOの行う呼び出しのうちCPOと同名の関数をADLによって探索する部分を置き換えるためのものです。

CPO等によってカスタマイゼーションポイントを導入することは実質的にその関数名をグローバルに予約してしまう事になります。CPOはコンセプトによるチェックを行うとは言え、意図せずチェックを通ってしまう型が無いとも限りません。tag_invokeはCPOの型をタグとしてstd::tag_invokeという1つのCPOだけを通してADLによる関数呼び出しを行います。その結果、グローバルに予約する名前はtag_invoke一つだけになります。

Executorでは、executeなど全部で9つのCPOが用意されており、全て同名の関数をADLで探索します。従って、9つの名前を実質的に予約してしまうことになります。tag_invokeを使ってその部分の定義だけを置き換える事で、これらの名前を予約することを避けようとする提案です。

この提案の後でもCPOを使う側は一切何かする必要はありません。CPOにアダプトしようとする場合にtag_invokeを利用することになります。例えば、std::execution::executorCPOにアダプトする例を比較してみます。

P0443 この提案
struct my_executor {

  template<typename F>
  friend void executor(
    const my_executor& ex,
    F&& f)
  {
    // オリジナルのexecutorの処理、略
  }

  auto operator<=>(const my_executor&)
    = default;
};
struct my_executor {

  template<typename F>
  friend void tag_invoke(
    std::tag_t<std::execution::execute>,
    const my_executor& ex,
    F&& f)
  {
    // オリジナルのexecutorの処理、略
  }

  auto operator<=>(const my_executor&)
    = default;
};
my_executor ex{};

// 上記のどちらが採用されるにせよ、こう呼び出せる
std::execution::execute(ex, [](){ /* 何か処理*/ });

これらのfriend関数はHidden friendsというイディオムであり、同じ名前空間のフリー関数として定義していても構いません。しかしどちらにせよ、そこの関数名にCPO名ではなくtag_invokeという名前を使うようになることで、CPOをいくら増やしてもユーザーコードではそれを気にせずに同じ名前を使用することができるようになります。

P2223R0 : Trimming whitespaces before line splicing

バックスラッシュ+改行による行継続構文において、バックスラッシュと改行との間にホワイトスペースの存在を認める提案。

CとC++では行末にバックスラッシュがある場合に、その行と続く行を結合して論理的に1行として扱います。これはプリプロセスより前に処理されるため、マクロを複数行で定義する時などに活用されます。

しかし、バックスラッシュと改行の間にホワイトスペースしかない場合に問題が潜んでいます。

int main() {
  int i = 1  
  // \  
  + 42
  ;
  return i;
}

3行目のバックスラッシュの後にはスペースが挿入されています。この時、このバックスラッシュを行継続のマーカーとして扱うかどうかが実装によって異なっています。EDG(ICC),GCC,Clangはホワイトスペース列を除去して行継続を行い、結果1を返します(+ 42コメントアウトされる)。MSVCはバックスラッシュの直後に改行が無いことから行継続とは見なさず、結果43を返します。

これはどちらも規格的に正しい振る舞いで、実装定義の範疇です。

ただ、この振る舞いは直感的では無いため、バックスラッシュ後のホワイトスペース列は除去した上で行継続を行うように規定しようとする提案です。つまり、MSVCの振る舞いに修正が必要となります。

このような振る舞いに依存しているコードはバックスラッシュの後の見えない空白を維持し続けているはずですが、それを確実に保証することはできずその有用性も無いため、このような振る舞いをサポートし続ける必要はないだろうという主張です。

他にも次のようなコードで影響があります。

auto str = "\ 
";

MSVC以外ではstrは空文字列で初期化されますが、MSVCは\エスケープシーケンスとして有効ではないためコンパイルエラーとなります。

P2224R0 : A Better bulk_schedule

P2181R0にて提案されているbulk_scheduleのインターフェース設計の改善提案。

現在のbulk_scheduleは次のようなインターフェースになっています。

template<executor E, sender P>
sender auto bulk_schedule(E ex, executor_shape_t<E> shape, P&& prologue);

bulk_scheduleExecutorex上で、入力の処理prologueshapeによって表現される範囲内でバルク実行をし、かつその実行は遅延可能であるものです。その戻り値はそれらバルク実行のそれぞれの処理の開始を表すsenderで、呼び出し側はそこから各バルク実行の処理本体(あるいは後続の処理)を表現するsenderを構築する義務を負います(つまり、そのsenderに処理をチェーンする形でバルク実行の本体を与える)。
この場合、そのようなsenderを作成しバルク実行後、そこにどのように後続の処理をチェーンさせるかは不透明であったため、それを解決するためにbulk_join操作が検討されていました。

この提案では、bulk_scheduleのインターフェースと担う役割を変更しそのような問題の解消を目指します。提案されているインターフェースは次のようになります。

template<scheduler E, invocable F, class... Ts>
sender_of<Ts...> auto bulk_schedule(sender_of<Ts...> auto&& prologue, E ex, executor_shape_t<E> shape, F&& factory);

この戻り値はバルク処理全体を表現するsenderで、検討されていたbulk_joinの戻り値に近いものになります。
最後の引数にあるfactoryは、バルク実行の一つ一つの処理を表現するsenderを構築する責任を担うsender factoryで、次のようなインターフェースを持ちます。

auto factory(sender_of<executor_shape_t<E>, Ts&...>) -> sender_of<void>

sender factoryは個別の処理(prologue)の開始を表すsenderを一つ受け取ります。このsenderは接続されたreceiverに各処理の固有番号(インデックス)と処理の結果(あれば)を渡すものです。そして、このsender factoryはそれら1つ1つの処理の全体を表すsender_of<void>を返します。

このsender factoryへの引数は元の(この提案の対象の)bulk_scheduleが返していたsenderに対応しており、この提案によるbulk_scheduleが返すsenderは元のbulk_scheduleの呼び出し元が構築する義務を負っていた(bulk_joinが返していた)senderに対応しています。

結局、これらの変更によってbulk_scheduleを使った処理のチェーンは次のように変化します。

// 元のbulk_schedule
// N個の処理(prologue)に継続して処理Aをバルク実行し、bulk_joinによって統合後処理B(非バルク実行)を実行する
auto S = bulk_schedule(ex, N, prologue) | ..A.. | bulk_join() | ..B..;

// この提案のbulk_schedule
// 上記と同じことを行うもの
auto S = bulk_schedule(prologue, ex, N, [](auto begin) { return begin | ..A..; }) | ..B..;

ユーザーが負っていた責任の多くをbulk_scheduleが担うようになり、バルク処理のそれぞれに後続の処理をチェーンさせることと、バルク処理の全体に後続の処理をチェーンさせることをより明確に書くことができるようになっています。

多分2週間後くらい

この記事のMarkdownソース

[C++]モジュールにおける所有権モデル

モジュールにおける所有権とは、名前付きモジュールに属しているエンティティの名前に関する所有権の事で、次のようなコードがどう振舞うかを決定するものです。

/// moduleA.ixx
export module moduleA;

export int munge(int a, int b) {
    return a + b;
}
/// moduleB.ixx
export module moduleB;

export int munge(int a, int b) {
    return a - b;
}
/// libA.h

int libA_munge(int a, int b);
/// libA.cpp

#include "libA.h"
import moduleA;

int libA_munge(int a, int b) {
    return munge(a, b);
}
/// main.cpp

#include "libA.h"
import moduleB;

// ここでは、moduleAはインポートされていない

int main() {
  // それぞれ、どちらが呼ばれる?
  int a = munge(1, 2);      // moduleBのものが呼ばれる?
  int b = libA_munge(1, 2); // moduleAのものが呼ばれる?
}

こちらのサンプルコードを改変しています)

C++の意味論としてはこれは当然明白なことで、コメントにある通りに呼ばれるはずです。しかし、このプログラムはコンパイルされリンクされた結果として一つの実行ファイルになります。C++20の名前付きモジュールはそれそのものが1つの翻訳単位をなすので、そこには2つのモジュール(moduleAmoduleB)をコンパイルした2つのオブジェクトファイルが含まれます。
その時、翻訳単位main.cpplibA.cppはそれぞれのコードの意味論から期待されるモジュールからの関数を呼び出すでしょうか?

これはつまりODR違反が起きている状態です。C++17まではこれは未定義動作であり、リンカがオブジェクトファイルをリンクする順番に依存して結果が変わります。そして、モジュールファイルでもこれは未定義動作とされています。

モジュールの所有権とはこの未定義動作をどう実装するか?という問題の解です。そこには、2つの選択肢があります。

なお、同じ場所で2つの異なるモジュールからの同じ宣言が見えている場合は確実にコンパイルエラーとなります、今回のケースはリンカからしかそれが見えていません。

import moduleA;
import moduleB;

int main() {
  int a = munge(1, 2);  // コンパイルエラー!
}

弱い所有権(weak ownership)モデル

弱い所有権(weak ownership)はこの問題を解決しないモデルです。すなわち、C++17以前と同じくこれは未定義動作であり、リンカがリンクする順番に依存します。

/// main.cpp

#include "libA.h"
import moduleB;

// ここでは、moduleAはインポートされていない

int main() {
  // 弱い所有権モデルの下では未定義動作!!
  int a = munge(1, 2);
  int b = libA_munge(1, 2);
}

強い所有権(strong ownership)モデル

強い所有権(strong ownership)はこれを解決するモデルです。すなわち、異なるモジュールからエクスポートされているものは、その宣言が完全に同一であったとしても、リンク時にもモジュール名によって別のものとして扱われます。

/// main.cpp

#include "libA.h"
import moduleB;

// ここでは、moduleAはインポートされていない

int main() {
  // 強い所有権モデルの下では期待通りに呼ばれる
  int a = munge(1, 2);      // -1(moduleBのものが呼ばれる)
  int b = libA_munge(1, 2); // 3(moduleAのものが呼ばれる)
}

モジュールリンケージ

C++20モジュールにおいてはリンケージ区分にもう一つモジュールリンケージが追加されました。モジュールリンケージは名前付きモジュール内で外部リンケージを持ち得る名前のうち、エクスポートされていないものが持つリンケージです。モジュールリンケージをもつ名前は同じモジュール内からしか参照できません。つまりは、内部リンケージを同じモジュール内部の範囲まで拡張したものです。

export module moduleA;

// モジュールリンケージ
int func1(int a, int b) {
  return a + b;
}

// 外部リンケージ
export int func2(int a, int b) {
  return a + b;
}

// 内部リンケージ
static int func3(int a, int b) {
  return a + b;
}

リンケージの観点からは、2つの所有権モデルの差異は外部リンケージを持つ(エクスポートされている)名前をモジュールが所有するかどうか?という問題とみることができます。

弱い所有権モデルでは、内部リンケージとモジュールリンケージを持つ名前だけをモジュールが所有し、エクスポートされている名前をモジュールは所有しません。

強い所有権モデルでは、エクスポートされている名前を含めたモジュールに属するすべての名前がモジュールによって所有されます。

所有権と名前マングリング

この2つの所有権モデルのどちらを選択するか?という問題は、実装から見ると名前マングリングの問題となるでしょう(他の実装もあり得ますが)。

1つのモジュールは複数の翻訳単位から構成されうるので、リンカは少なくともモジュールリンケージを識別する必要があります。そのため、モジュールに所有されている名前のマングル方法にはいくつかの選択肢が生まれます。そして、現在の実装では次の2つが存在しています。

  1. モジュールリンケージを持つ名前のマングル方法を変更して、それだけをリンカが特別扱いする
    • エクスポートされている名前のマングル方法は従来通り(現在の外部リンケージを持つ名前と同じ)
  2. エクスポートされている名前のマングル方法を変更して、リンカがモジュールを認識することでリンケージを処理する
    • モジュールリンケージを持つ名前のマングル方法は従来通り

1つ目の方法では、エクスポートされている名前は既存の外部リンケージを持つ名前と同じマングルとすることができ、それ以外はこれまで通りです。また、モジュールリンケージの処理に関してもモジュールを構成する時だけ考慮したうえで、モジュールと他の翻訳単位をリンクするときは内部リンケージと同様に扱うことができ、リンカにかかる負担はかなり軽くなります。また、リンク時の扱いもこれまでと大きく変わらなさそうです。

2つ目の方法では、まずリンカはオブジェクトファイルがモジュールファイルなのかどうかを認識し、そこにある名前を見てエクスポートされている名前かどうかを識別します。この方法ではモジュール毎に名前を識別することができますが、コンパイラだけでなくリンカにもそれなりの変更が必要です。

そして、1つ目の方法はまさに弱い所有権モデルの実装であり、2つ目の方法は強い所有権モデルの実装です(他にも方法はあるでしょうが、現状はこの2つに1つです)。外部リンケージを持つ名前に対してマングル名で区別をつけないということは、リンク時に別々のモジュール(翻訳単位)からの同じ名前を区別できません。

名前マングリングの観点から見ると、所有権の強弱はモジュールリンケージを持つ名前とエクスポートされている名前のどちらのマングリング方法を変更するのか?という問題とみることもできます。そしておそらく、そのマングル方法とは単にマングル名にモジュール名とモジュールであることを示す文字列が入るだけでしょう。

実装の選択

現在のC++処理系の主要3実装は既にある程度モジュールを実装しています。そこでは、GCC\Clangは弱い所有権モデルを採用しており、MSVCは強い所有権モデルを採用しています。

例えばGCC/Clangで次のようなモジュールコードをコンパイルすると、外部リンケージを持つ名前はこれまで通りのマングルであるのに対して、内部リンケージとモジュールリンケージを持つ名前のマングル名にはモジュール名が含まれるようになっていることが分かります。また、モジュールリンケージと内部リンケージはマングル名レベルでは同じ扱いをされていることも分かります。

export module moduleA;

// モジュールリンケージ
int func1(int a, int b) {
  return a + b;
}

// 外部リンケージ
export int func2(int a, int b) {
  return a + b;
}

// 内部リンケージ
static int func3(int a, int b) {
  return a + b;
}

GCCのモジュール実装に関するWikiによれば、GCCとClangは互いの相互運用性のために連携してモジュールの実装を進めており、その結果として双方共に弱い所有権モデルを採用しているようです。

一方、MSVCは単独ですでにC++モジュールの実装をいったん完了しており、そこでは強い所有権モデルを採用していると明言されています。

この実装間の差異は、おそらくリンカの扱いから来ています。MSVCは1つの社内でリンカとコンパイラを開発しており、ターゲットはWindowsのみで、MSVCコンパイラは自社のリンカ(link.exe)以外を考慮する必要はありません。そしてそれはリンカ側から見ても同様です。そのため、リンカとコンパイラをより密に連携することができ、リンカに対して思い切った変更を加えることができます。

一方GCC/ClangはMSVCに比べるとリンカとコンパイラの開発体制には距離があります。また、それぞれ使われうるリンカは特定の一つではありません。例えば、GNU ld、lld、goldなどいくつかの実装があります。また、ClangとGCCはマングル名に関してお互いに強く互換性を意識しており、片方でコンパイルされたバイナリをもう片方でコンパイルされたバイナリとリンクすることができます(標準ライブラリ周りは別として)。そのため、モジュールの大部分を実装するコンパイラとしてはなるべくリンカに影響を与えないアプローチをとらざるを得なかったものと思われます。
また、既存のABIを大きく変更しないという方針を取ったものでもあるようです。FFIで他言語から呼び出すときにも、モジュール前後で変更が必要ないはずです。

(この辺りのことは完全に私の想像なので正しくないかもしれません)

明確にこの所有権が問題となるコードを書くことはないと思いますが、結果としてこの問題にあたってしまうことはあり得ます。その時に、実装のこの差異はモジュールコードのポータビリティに問題を与えるかもしれません。

なお、GCC/Clangはモジュールを実装中なので、このことは最終的には変化する可能性があります(低いと思わますが)。

参考文献

この記事のMarkdownソース

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

文書の一覧 www.open-std.org

提案文書で採択されたものはありません。全部で21本あります。

N4862 : Business Plan and Convener's Report

C++標準化委員会の全体的な作業の進捗状況や今後の予定などについての報告書。

おそらく、C++を利用している企業などに向けて書かれたものです。

P0288R6 : any_invocable

ムーブのみが可能で、関数呼び出しのconst性やnoexcept性を指定可能なstd::functionであるstd::any_invocableの提案。

std::any_invocableは次の点を除いてほとんどstd::functionです。

  1. ムーブのみ可能
    • std::unique_ptrをキャプチャしたラムダのように、コピー不可能なCallableオブジェクトを受け入れられる
  2. 関数型にconst/参照修飾やnoexceptを指定可能
    • const性を正しく伝播できる
  3. target_type()およびtarget()を持たない
  4. 呼び出しには強い事前条件が設定される
    • これによって、呼び出し時のnullチェックが省略される
#include <any_invocable>  // 専用の新ヘッダ
#include <functional>

struct F {
  bool operator()() {
    return false;
  }
  
  bool operator()() const {
    return true;
  }
};

int main() {
  std::cout << std::boolalpha;
  
  const std::function<bool()> func1{F{}};
  const std::any_invocable<bool()> func2{F{}};
  const std::any_invocable<bool() const> func3{F{}};
  
  std::cout << func1() << '\n';  // false
  std::cout << func2() << '\n';  // false
  std::cout << func3() << '\n';  // true
}

このようにconst性を指定して正しく呼び出しが行えることで、並列処理においてスレッドセーフな呼び出しができるようになります。

他にも、noexceptや参照修飾は次のように指定します。なお、volatile修飾は指定することができません。

#include <any_invocable>

struct F {
  int operator()(int n) const & noexcept {
    return n;
  }
  
  int operator()(int n) && {
    return n + 1;
  }
};

int main() {
  std::any_invocable<int(int) const & noexcept(true)> func1{F{}};
  std::any_invocable<int(int) && noexcept(false)> func2{F{}};

  std::cout << func1(1) << '\n';  // 1
  std::cout << func2(1) << '\n';  // 2
}

これらの修飾や指定は全て省略することもできます。

std::any_invocableでも小さいオブジェクトで動的メモリ確保を避けるように規定されているので、std::functionに比べると若干パフォーマンスが良さそうです。

std::any_invocableはすでにLWGでのレビューに入っていて、C++23に入る可能性が高そうです。

P0881R6 : A Proposal to add stacktrace library

スタックトレースを取得するためのライブラリを追加する提案。

このライブラリの目的はひとえにデバッグをより効率化することにあります。例えば次のような実行時アサーションメッセージを

boost/array.hpp:123: T& boost::array<T, N>::operator[](boost::array<T, N>::size_type): Assertion '(i < N)&&("out of range")' failed. Aborted (core dumped)

次のような出力にできるようにします

Expression 'i < N' is false in function 'T& boost::array<T, N>::operator[](boost::array<T, N>::size_type)': out of range.
Backtrace:
0# boost::assertion_failed_msg(char const, char const, char const, char const, long) at ../example/assert_handler.cpp:39
1# boost::array<int, 5ul>::operator at ../../../boost/array.hpp:124
2# bar(int) at ../example/assert_handler.cpp:17
3# foo(int) at ../example/assert_handler.cpp:25
4# bar(int) at ../example/assert_handler.cpp:17
5# foo(int) at ../example/assert_handler.cpp:25
6# main at ../example/assert_handler.cpp:54
7# 0x00007F991FD69F45 in /lib/x86_64-linux-gnu/libc.so.6
8# 0x0000000000401139

このライブラリはBoost.Stacktraceをベースに設計されています。スタックトレースの一行(すなわちスタックフレーム)はstd::stacktrace_entryというクラスで表現され、std::stacktraceというクラスが一つのスタックトレースを表現します。std::stacktraceはほとんどstd::stacktrace_entrystd::vectorです。

#include <stacktrace> // このヘッダに定義される

void f() {
  // 現在のスタックトレースを取得
  auto st = std::stacktrace::current();

  // スタックトレース全体の出力
  std::cout << st << std::endl;

  // スタックトレースのイテレーション
  for (const auto& entry : st) {
    // 例えばソースファイル名だけを取得
    std::cout << entry.source_file() << std::endl;
  }
}

スタックトレースの取得はプラットフォーム毎にそこでのシステムコールAPI呼び出しにマッピングされます。情報を取り漏らさないためにスタックトレースの取得サイズは可変長としているので動的メモリ確保が伴います。また、取得したスタックトレース情報のデコードはギリギリまで遅延されます。上記でいうと、std::stacktrace_entry::source_file()が呼び出された時、あるいは内部でそれを呼び出すstd::stacktraceの標準出力への出力時に取得した情報が逐次デコードされます。

提案によれば、コンパイラオプションによってABIを変化させずにこれらの関数が何もしないように制御する実装をサポート可能としているようで、これもBoost.Stacktraceから受け継いでいる機能です。

この提案は元々C++20に導入することを目指していたようで(間に合いませんでしたが)、すでにLWGでのレビューが一旦完了しており大きな問題もなさそうなので、C++23に入る可能性が高そうです。

P1787R5 : Declarations and where to find them

規格内でのscopename lookupという言葉の意味と使い方を改善する提案。

ほとんど規格用語の新規定義とそれを用いた既存の表現の変更で、主に名前解決に関連したバグが解決されますがユーザーにはほぼ影響はないはずです。

この提案によるコア言語の文言変更は多岐に渡りますが、これによって61(+19)個のCore Issueを解決できるとの事です。

P1875R1 : Transactional Memory Lite Support in C++

現在のトランザクショナルメモリTS仕様の一部だけを、軽量トランザクショナルメモリとしてC++へ導入する提案。

以前に紹介したP2066R2の元となったもので、提案の動機などが述べられています。P2062はこの提案を規格に反映させるための文言変更点だけをまとめたものです。

onihusube.hatenablog.com

P1949R5 : C++ Identifier Syntax using Unicode Standard Annex 31

識別子(identifier)の構文において、不可視のゼロ幅文字や制御文字の使用を禁止する提案。

以前の記事を参照 onihusube.hatenablog.com

onihusube.hatenablog.com

前回(R3)との変更点は絵文字の問題についての説明が追記されたこととGCC10.1で解決されたUTF-8文字を識別子へ使用できなかったバグについての言及が追記されたことです。

絵文字について

現在C++で絵文字の使用が可能なのは、たまたまC++で許可していたユニコードのコードポイント範囲に絵文字が割り当てられたためで、全てが使用可能であるわけではなく、FFFF未満のコードポイントを持つものなど、一部の絵文字の使用は禁止されています。

例えばそこには女性記号(♀)が含まれています。これは結合文字と共に人を表すタイプの絵文字に作用してその絵文字の性別を変化させます。つまり、現在のC++の仕様では男性の絵文字を使用することは合法ですが、女性の絵文字を使用することはできないのです。

これは意図的なものではなく偶然の産物です。意図的にこのこと及びあらゆる絵文字(例えば、肌の色なども含めて)の使用を許可しようとすると、かなり多くの作業が必要となります。

この提案は識別子で使用可能な文字をUnicodeのUAX31規格を参照して決定するように変更するものであり、UAX31でも絵文字の利用については安定していないのでこの提案では全て禁止とする方向性のようです。

GCCUTF-8文字関連のバグ

GCCは長らくUTF-8文字をソースコード中で使用することについてバグを抱えていたようで、GCC10.1でそれが解決されました。ClangとMSVCではすでに問題なく利用可能だったので、これによってUTF-8文字をソースコード中で使用することはほぼポータブルになります。このことは、この提案をより後押しするものです。

P2013R2 : Freestanding Language: Optional ::operator new

フリースタンディング処理系においては、オーバーロード可能なグローバル::operator newを必須ではなくオプションにしようという提案。

以前の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、グローバル::operator newを提供しない場合でもplacment newは利用可能であることを明確にした事と、<new>の宣言やポインタ安全性の文言などに変更はないことを明記した事です。この提案はほぼ規格書に文章を少し追記するだけのものです

EWGでのレビューの結果を次のリビジョンで反映させた上で、CWGに転送される見込みのようです。

P2053R1 : Defensive Checks Versus Input Validation

プログラムの動作の前提条件が満たされないケース2つにカテゴライズし、それらを区別することの重要性を説く報告書。

これは契約プログラミングサポートの議論のために、SG21(Contracts Study Group)のメンバーに向けて書かれたものです。

P2079R1 : Parallel Executor

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

C++23を目指して進行中のExecutor提案(P0443R13)では、スレッドプールを提供するExecutorであるstatic_thread_poolが唯一の標準Executorとして提供されています。

しかし、このstatic_thread_poolatach()メンバ関数によってスレッドプールにスレッドを追加することができます。
static_thread_poolがただ一つの標準Executorであることによって、プログラムの様々な場所(例えば外部dll内など)でとありあえずstatic_thread_poolが使われた結果、static_thread_poolはハードウェアが提供するスレッド数を超えて処理を実行してしまう可能性があります。これが起きると実行環境のシステム全体のパフォーマンス低下につながります。

これを防ぐために、ハードウェアの提供するスレッド数固定のスレッドプールを提供するparallel_executorを、標準Executorのもう一つの選択肢として追加しようという提案です。

parallel_executorはデフォルトではstd::thread::hardware_concurrencyに等しい数のスレッドを保持するように構築されます。その後で実行スレッド数の上限を設定することはできますが、ハードウェアの提供するスレッド数を超えて増やすことはできないようです。

P2096R2 : Generalized wording for partial specializations

変数テンプレートの部分特殊化を明確に規定するように文言を変更する提案。

以前の記事を参照 onihusube.hatenablog.com

このリビジョンでは、CWGのレビューを受けて文言を調整したようです。

この提案が導入されると、変数テンプレートはクラステンプレートと同じことができる!という事が規格上で明確になります。C++14からそうだったのですが、あまりそのように認識されていないようで、おそらくそれは規格上で不明瞭だったことから来ているのでしょう・・・。

P2162R1 : Inheriting from std::variant (resolving LWG3052)

std::variantを公開継承している型に対してもstd::visit()できるようにする提案。

以前の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、主要な3実装ではこの問題はどうなっているのかが詳細に記述されるようになっています。

それによれば、MSVC STLは完全に対応し、libc++はvalueless_by_exceptionというメンバを上書きしているような型への対応に問題があるもののほぼ対応しており、libstdc++はstd::variantが空になっているかのチェックがstd::variantだけでしか動作しないためGCC9.1から無効化されているようです。

P2187R4 : std::swap_if, std::predictable

より効率的な条件付きswapを行うためのstd::swap_ifと、その使用を制御するstd::predictableの提案。

前回の記事を参照 onihusube.hatenablog.com onihusube.hatenablog.com

前回からの変更は、一部の関数にnoexceptが付加されたことです。

P2192R1 : std::valstat - function return type

関数の戻り値としてエラー報告を行うための包括的な仕組みであるvalstatの提案。 onihusube.hatenablog.com

変更点は文書の文面を変更しただけの様です。

P2197R0 : Formatting for std::complex

std::formatstd::comlexのサポートを追加する提案。

// デフォルト
std::string s1 = std::format("{}", 1.0 + 2i);     // s == "(1+2i)"
// iostream互換
std::string s2 = std::format("{:p}", 1.0 + 2i);   // s == "(1,2)"
// 精度指定
std::string s3 = std::format("{:.2f}", 1.0 + 2i); // s == "1.00+2.00i"

虚数単位にはiが使用され、複素数の数値型Tのフォーマットは再帰的にTのフォーマット指定(std::formatter<T>)に委譲されます。虚数部が0.0の時、虚数部を消して出力するのか0.0を出力するのかはさらなる議論を必要としているようです。

P2205R0 : Executors Review - Polymorphic Executor

Executor提案(P0443R13)のPolymorphic Executor周りのレビューの結果見つかった問題についての報告書。

P0443のPolymorphic Executorには次のものが提案されています。

  • bad_executor
  • any_executor
  • prefer_only

bad_executorは空のExecutorで、execute()は常に例外を投げます。

any_executorは簡単に言えばstd::functionExecutor版で、任意のExecutorを型消去しつつ保持し、実行時に切り替える事が出来るラッパーなExecutorです。

prefer_onlyPolymorphic Executorにテンプレートパラメータでサポートするプロパティを指定する際に、そのプロパティのサポートが必須(require)ではないことを示すのに使用するものです。

この文書はP0443のうちこれらのものをレビューし、見つかった問題点の報告と、修正案を提案するものです。

P2207R0 : Executors review: concepts breakout group report

Executor提案(P0443R13)のコンセプト周りのレビューの結果見つかった問題についての報告書。

P2209R0 : Bulk Schedule

P2181R0にて提案されているbulk_scheduleの設計についての補足となる報告書。

bulk_scheduleは遅延(任意のタイミングで)実行可能なbulk_executeで、bulk_executeはバルク実行を行うexecuteで、executeは任意のExecutorを受けてそのExecutorの指す実行コンテキストで処理を実行するものです。また、バルク実行とはSIMDなどのように複数の処理をまとめて同時実行することです。つまり、bulk_scheduleはバルク処理に対応したExecutor上で、バルク処理を任意のタイミングで実行するためのものです。

using namespace std::execution;

// バルク実行可能なExecutorを取得
bulk_executor auto ex = ...;

// 処理のチェーンを構成する
auto s1 = bulk_schedule(ex, [](){...}, 10); // 並列数10で2番目の引数の処理をex上で実行する
auto s2 = bulk_transform(s1, [](){...});    // バルク実行の結果をバルクに変換する
auto s3 = bulk_join(s2);                    // 各バルク実行の結果を1つに結合する

// この時点では一連の処理は実行されていない

// 一連の処理を実行し、完了を待機(ブロックする)
sync_wait(s3);

executeの遅延実行のためのものはscheduleが対応し、bulk_scheduleはバルク実行という点でscheduleと対称的に設計され、また対称的に使用可能となるようになっています。

// Executorを取得
executor auto ex = ...;

// 処理のチェーンを構成する
auto s1 = schedule(ex, [](){...});  // 2番目の引数の処理をex上で実行する
auto s2 = transform(s1, [](){...}); // 実行結果を変換する
auto s3 = join(s2);                 // 処理を統合する(この場合は意味がない)

// この時点では一連の処理は実行されていない

// 一連の処理を実行し、完了を待機(ブロックする)
sync_wait(s3);

これらの対称性と抽象化を支えているのはsenderと呼ばれる抽象で、senderは未実行の一連の処理を表現するものです(サンプル中のs1, s2, s3)。このsenderを用いる事で、処理のチェーンをどう実行するのかをユーザーやライブラリが定義するのではなく、実行環境となるExecutor自身が定義する事ができ、その実行環境において最適な方法によって安全に処理のDAGを実行する事ができます。

bulk_scheduleによって返されるバルク処理を表現するsenderは、そのバルク処理の完了を通知できる必要があります。すなわち、バルク処理のうち一つが終わった時の通知とは別にバルク処理の全体が終わったことを通知できなければなりません。そうでないと、バルク処理を安全にチェーンする事ができません。バルク処理の実行順は通常不定であるので、このことにもsenderによる抽象が必要です。

そのために、バルク処理を表現するsenderにはset_next操作(これはexecuteなどと同じくカスタマイゼーションポイントオブジェクトです)が追加されます。senderに対するset_nextの呼び出しによって1つのバルク処理全体が完了したことを通知し、それを受けた実行環境はそのsenderにチェーンされている次の処理を開始する事ができます。

そして、そのようなsenderを表現し制約するためのコンセプトとしてmany_receiver_ofコンセプトを追加します。many_receiver_ofコンセプトによって制約される事で、バルク処理ではない普通の処理をバルク処理にチェーンする事(あるいはその逆)は禁止され、コンパイルエラーとなります。

P2210R0 : Superior String Splitting

現状のviews::splitの非自明で使いにくい部分を再設計する提案。

views::splitを使った次のようなコードはコンパイルエラーになります。

std::string s = "1.2.3.4";

auto ints =
    s | std::views::split('.')
      | std::views::transform([](auto v){
          int i = 0;
          std::from_chars(v.data(), v.data() + v.size(), &i);
          return i;
        })

なぜなら、views::splitすると得られるこのvforward_rangeforward iteratorで構成されたrangeオブジェクト)なので、data()size()などというメンバ関数はなく、そのイテレータはポインタですらありません。ついでに言うとそのvcommon_rangebegin()end()イテレータ型が同じrange)でもありません。

文字列に対するviews::splitの結果として直接得られるのは、分割後のそれぞれの文字列のシーケンスとなるrangeオブジェクトです(これを構成するイテレータを外部イテレータと呼びます)。そして、それをデリファレンスして得られるのが分割された1つの文字列を示すrangeオブジェクトで、これが上記のコードのvにあたります(こちらを構成するイテレータは内部イテレータと呼びます)。内部イテレータforward iteratorであり、文字列を直接参照するポインタではありません。

例えば次のように文字列を分割したとき

string s = "abc,12,cdef,3";
auto split = s | std::views::split(",");

生成されるrangeの様子は次のようになります。

https://github.com/onihusube/blog/blob/master/2020/20200918_wg21_paper_202008/view_split.png?raw=true

これを踏まえると、正しくは次のように書かなければなりません。

std::string s = "1.2.3.4";

auto ints =
    s | views::split('.')
      | views::transform([](auto v){
          auto cv = v | views::common;  // イテレータ型と番兵型を合わせる
          return std::stoi(std::string(cv.begin(), cv.end()));
        });
}

主に次の理由により、このような事になっています。

  • views::splitはその処理を可能な限り遅延させる
  • views::splitの後に任意のrangeアルゴリズムをチェーンさせられる
  • views::splitは文字列に限らず任意の範囲について動作する

しかし、文字列の分割という基本的な操作は頻繁に必要になるのに対して、文字列以外の範囲の分割というユースケースは滅多に必要にならず、既存の多くの文字列処理のアルゴリズムstd::from/to_charsstd::regexなど)の多くは少なくとも双方向イテレータbidirectional range)を要求します。

これらのことから、views::splitの現在の仕様は一般化しすぎており、文字列の分割という観点からはとても使いづらいので再設計が必要である、という主張です。

主に次のように変更する事を提案しています。

  • forward rangeまたはより強い範囲をsplitすると、同じ強さの部分範囲が得られる
    • さらに、P1391 Range constructor for std::string_viewが採用されれば、そこから文字列への変換も容易になる
      • std::string_viewC++20ですでにRange constructorが追加されていました・・・
    • input rangeでしかない範囲の分割に関しては現状通り
  • split_viewviews::splitの返すrange)はconst-iterableではなくなる
    • begin()の呼び出しで単にイテレータを返す以上のことをする(しなければならい)ようになる

この変更はC++20でviews::splitを用いた既存のコードを壊すことになりますが、これによって得られる文字列分割での利便性向上という効用は互換性を破壊することによるコストを上回る、と筆者の方は述べています。

この部分は以下の方のご指摘によって構成されています。

P2212R0 : Relax Requirements for time_point::clock

std::chrono::time_pointClockテンプレートパラメータに対する要件を弱める提案。

std::chrono::time_pointは時間軸上の一点を指定する型で、その時間基準(始点(epoch)と分解能)を指定するために時計型と時間間隔を表す2つのテンプレートパラメータを受け取ります。うち1つ目の時計型ClockにはCpp17Clock要件という要求がなされています。これは、std::chrono::system_clockstd::chrono::steady_clockなどと同じ扱いができることを要求しています。

C++20ではローカル時間を表す型としてstd::chrono::local_tが導入されました。これは、std::chrono::time_zoneとともに用いることで任意のタイムゾーンにおけるローカルの時刻を表現することができるものです。

local_tに対する特殊化std::chrono::time_point<local_t, Duration>も提供されますが、local_tは空のクラスでありCpp17Clock要件を満たしていません。規格ではlocal_tだけを特別扱いする事でtime_pointを特殊化することを許可しています。

このlocal_tのように、時計型ではないが任意の時間を表す型を定義し、その時間表現としてtime_pointを特殊化することは有用なのでできるようにしよう、という提案です。

例えば次のような場合に便利であると述べられています

  • 状態を持つ時計型を扱いたい場合(非staticnow()を提供したい)
  • 異なるtime_pointで1日の時間を表現したい場合
    • 年月日のない24時間のタイムスタンプで表現される時刻を扱う
  • 手を出せないシステムが使用しているtime_pointを再現したい場合
  • 異なるコンピュータ間でタイムスタンプを比較する

この変更がなされたとしても既存のライブラリ機能とそれを使ったコードに影響はなく、local_tが既にあることから実装にも実質的に影響はないだろうとのことです。

P2213R0 : Executors Naming

Executor提案(P0443R13)で提案されているエンティティの名前に関する報告書。

P0443R13のエンティティ名及びP1897R3で提案されているアルゴリズム名のうち一部について、代わりとなりうる名前とその理由が書かれています。

P2215R0 : "Undefined behavior" and the concurrency memory model

out-of-thin-airと呼ばれる問題の文脈における、未定義動作とは何かについての報告書。

out-of-thin-airとは定式化されたメモリモデルから導かれる1つの起こり得る振る舞いのことです。さも虚空から値を読みだしているかのように見えることからこの名前がついているようです。

int main() {
  int r1, r2;
  std::atomic_int x = 0;
  std::atomic_int y = 0;

  std::thread t1{[&]() {
    r1 = x.load(std::memory_order_relaxed);
    y.store(r1, std::memory_order_relaxed);
  }};
  std::thread t2{[&](){
    r2 = y.load(std::memory_order_relaxed);
    x.store(r2, std::memory_order_relaxed);
  }};

  t1.join();
  t2.join();

  // r1 == 1, r2 == 1, x == 1, y == 1
  // となることがありうる(現実的にはほぼ起こらない)

  return 0;
}

このように、コード中どこにも表れていないはずの値が読み出されることが起こりうることが理論的に導かれます。この事がout-of-thin-air(またはThin-air reads)と呼ばれています。

C++ではメモリモデルにこの事を禁止する条項を加える事でout-of-thin-airの発生を禁止しています。しかし、メモリモデルの定式化を変更する事でこれが起きないようにしようという試みが続いているようです(Javaはそうしている)。

この文書はおそらく、そのような定式化のためにout-of-thin-airの文脈における未定義動作という現象をしっかりと定義しようとするものです。

この部分は以下の方のご指摘によって構成されています。

多分2週間後くらい

この記事のMarkdownソース

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

文書の一覧 www.open-std.org

提案文書で採択されたものはありません。全部で34本あります。

P1068R4 : Vector API for random number generation

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

既存の分布生成器はスカラーAPI(1度に1つの乱数しか取得できないAPI)しか備えておらず、乱数エンジンが状態を持つことと合わさってコンパイラによる最適化を妨げており、1度に多量の乱数を取得する場合に効率的ではなくなることがあります。一方で、既存の擬似乱数生成アルゴリズムには多数の乱数生成の際にSIMDを利用するような形に書き換えて効率化する事ができるものが多数あります。
そのような用途のためのベクターAPI(1度に多数の乱数を取得できるAPI)を別途追加し、実装がその中で各種分布生成器やエンジンの特性によって最適な実装を行えるようにし、またコンパイラに最適化の機会を提供可能にすることがこの提案の目的です。

当初はSIMDタイプのAPIも同時提案していたようですが、途中でそれは別の提案に分離されたため、この提案ではイテレータによるベクターAPIの追加のみを提案しています。

std::array<float, N> array; 

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

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

// この提案によるAPI、イテレータ範囲を乱数列で初期化する
dis(array.begin(), array.end(), gen);

これは必ずしも対応するforループによる範囲の要素毎の初期化コードと同じ効果とはならず、より効率的に実装される可能性があります。

P1184R2 : A Module Mapper

モジュール(特にそのインターフェース)の名前とその中間生成物の間の名前のマッピングやモジュールのビルドについてGCCのTS実装に基づいて書かれた報告書。

C++20のモジュールはそのモジュール名とファイル名の間には何ら関連付けの規則が無く、ビルドの際に必要とされるコンパイル済みモジュールインターフェース(モジュール名とexportされているエンティティ名を記録した中間生成物)の命名に関しても当然何ら規則がありません。この論文はGCCのモジュールTSに基づく実装経験から得られたモジュール名と中間生成物の名前のマッピングのための1つのプロトコルについて説明されているものです。

P1272R3 : Byteswapping for fun&&nuf

バイトスワップ(バイトオーダー変換)のための関数std::byteswap()の提案。

C++20より<bit>ヘッダが導入されpopcountやbit rotation等の基本的なビット操作のための関数が標準で用意されるようになりました。しかし、そこにはバイトスワップ(バイトを逆順にする操作)はありません。現代のほとんどのCPUはバイトスワップのための命令を持っており、それが無い環境でも使用可能なビット演算手法が存在しています。それらを標準で用意しておくことで、よりポータブルかつ効率的に(結果として1命令以下で)バイトスワップを行えるようにしようという提案です。

// 提案されている宣言
namespace std {
  constexpr auto byteswap (integral auto value) noexcept;
}

int main() {
  auto r = std::byteswap(0x1234CDEF);
  // r == 0xEFCD3412
}

LWGでの合意は反対無しで取れていてほとんどC++20入りが決まっていたようでしたが、パディングビット(C++標準としては1byte=8bitではなく、奇数幅整数型の実装がありうる)の扱いについてCWGでの議論が必要となりその文言調整に手間取った結果C++20には間に合わなかったようです。おそらくC++23には入りそうです。

P1478R4 : Byte-wise atomic memcpy

アトミックにメモリのコピーを行うためのstd::atomic_load_per_byte_memcpy()/std::atomic_store_per_byte_memcpy()の提案。

この提案では、主にSeqlockと呼ばれるロック手法におけるユースケースを主眼にそこで有用となるアトミックなmemcpyについて提案しています。

// Seqlockにおける読み手の処理の一例
// ロック対象のshared_dataをdataに読み取る
do {
  // クリティカルセクション開始時点のシーケンス番号取得
  int seq1 = seq_no.load(std::memory_order_acquire);

  // データの読み取り
  // shared_dataがatomic変数ではない場合、この読み取り操作はフェンスを越えてseq2の後に変更されうる
  data = shared_data;

  atomic_thread_fence(std::memory_order_acquire);  // seq2の読み取り順序はここより前に変更される事はない

  // クリティカルセクション終了時点のシーケンス番号取得
  int seq2 = seq_no.load(std::memory_order_relaxed);

  // 開始時点と終了時点のシーケンス番号が異なる、あるいは開始時点のシーケンス番号が奇数であるならクリティカルセクション中に書き込みが起きている
} while (seq1 != seq2 || seq1 & 1);

// dataはshared_dataからアトミックにロード完了している事が期待されるが・・・

shared_dataはロック対象である事もあり普通はatomicでは無いはずで、その場合はこのループが正常に終了した場合でもdata変数の内容はデータ競合を起こした結果を読み取っている可能性があります。つまり、データ競合を回避するためにクリティカルセクション中で読み取られたデータは書き込みが起きている場合に破棄されるようになっているにも関わらず、このクリティカルセクション中の読み取り操作はアトミックに行われなければなりません。

多くの場合Seqlock対象のデータはtrivially copyableなオブジェクトであり、その場合そのコピーにはmemcpyが使用されます。しかし、memcpyはアトミックでは無いので、対象オブジェクトをロックフリーなアトミックサブオブジェクトに分解した上でそれらを1つづつコピーすることになります。

// 上記クリティカルセクション中のコピーは例えばこうなる
for (size_t i = 0; i < sizeof(shared_data); ++i) {
  reinterpret_cast<char*>(&data)[i] =
      std::atomic_ref<char>(reinterpret_cast<char*>(&shared_data)[i]).load(std::memory_order_relaxed);
}
std::atomic_thread_fence(std::memory_order_acquire);

この提案によるatomic_load_per_byte_memcpyはこのようなコピー(およびフェンス)を1文で自明に行うものです。

Foo data;  // trivially copyableだとする

do {
  int seq1 = seq_no.load(std::memory_order_acquire);  // このロードより後のあらゆる読み書き操作はこの前に順序変更されない

  // データの読み取り
  std::atomic_load_per_byte_memcpy(&data, &shared_data, sizeof(Foo), std::memory_order_acquire); // このロードより後のあらゆる読み書き操作はこの前に順序変更されない

  int seq2 =  seq_no.load(std::memory_order_relaxed); // 順序変更について制約はないが、先行する2つのacquireロードを超えて順序変更されることはない
} while (seq1 != seq2 || seq1 & 1);

// dataはshared_dataからアトミックにロード完了している

atomic_load_per_byte_memcpyはメモリオーダー指定を受け取りコピー元に対してアトミックにアクセスする事以外はmemcpyと同様に動作します。これを用いると最初の問題のあるSeqlockの読み取り操作はより単純にそして正しく書く事ができるようになります。誤解の種でもあったatomic_thread_fenceを使用する必要も無くなります。

atomic_store_per_byte_memcpyはこのケースの読み取りに関して双対的なもので、memcpyにおいてコピー先へのアクセスをアトミック化します。Seqlockの書き手側の処理などで使用することを想定しているようです。

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

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

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、機能テストマクロが別の提案に分離されたこと、P1641によるフリースタンディング機能指定の文言への変更、uses-allocator構築に関する2つの機能の追加、などです。

P1659R1 : starts_with and ends_with

任意の範囲に対して動作するstd::ranges::starts_with/std::ranges::ends_withの提案。

C++20にてstd::stringstd::string_viewに対してstarts_with/ends_withメンバ関数が追加され、文字列の先頭(末尾)が指定の文字列で始まっている(終わっている)かをbool値で得られます。それらをより一般化して、任意の範囲について同じ事ができるようにするのがこの提案です。

// 共にtrueとなる
bool s = std::ranges::starts_with(std::views::iota(0, 50), std::views::iota(0, 30));
bool e = std::ranges::ends_with(std::views::iota(0, 50), std::views::iota(20, 50));

また、これらは第3引数に述語オブジェクトを渡す事で行われる比較をカスタマイズできます。例えば、先頭や末尾がある値以下であるか?のようなチェックができるようになります。

// 共にtrueとなる
bool s = std::ranges::starts_with(std::views::iota(0, 50), {-1, 0, 1, 2, 3}, std::ranges::greater);
bool e = std::ranges::ends_with(std::views::iota(0, 50), {46, 47, 48, 49, 50}, std::ranges::less);

また、さらにその後ろに各範囲の要素についての射影(Projection)を渡すこともできます。ただ残念なことに、これは普通の関数なので|で繋ぐことが出来ません。

この提案は既にLWGでのレビューをほとんど終えていて、どうやら次のリビジョンとそのレビューでもってC++23入りが確定する様子です。

P1679R3 : String Contains function

std::string, std::string_viewに、指定された文字列が含まれているかを調べるcontains()メンバ関数を追加する提案。

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は機能テストマクロのの追加と大文字小文字の取り扱いについての説明の追記です。

また、LWGでの全会一致でC++23への導入が承認されたようなので何事もなければC++23に入りそうです。

P1726R4 : Pointer lifetime-end zap

Pointer lifetime-end zapと呼ばれる問題の周知とそれについてのフィードバックを得るための報告書。

Pointer lifetime-end zapとは、現在のC++のポインタセマンティクスが寿命が尽きたオブジェクトを指しているポインタの存在を認めない(その再利用が禁止されている)事で、これによってポインタ再利用を用いると効率的に書けるタイプのアルゴリズム(特に並行アルゴリズム)が未定義動作に陥っている問題の事のようです。

この文書は、Pointer lifetime-end zapは規格としてどのように表現されているかに始まり、なぜそうなっているのか?どこで問題になるのか?そして、この問題をどう解決するのか?を記したものです。
主目的は委員会のメンバに向けてこの問題の周知を図り問題そのものや解決についてのフィードバックを得ることで、何かを提案するものではないですが、この文書を受けてSG1(Concurrency Study Group)はこれを削除することを目指しており、LWGもこの問題の解決に興味を持っているようです。

(内容は私には難しいので識者の方には是非解読をお願いしたい所存)、

P1864R0 : Defining Target Tuplets

ビルドシステムがそのターゲットとなる環境を決めるための名前の列であるTarget Tupletsについて、基礎的な定義を標準に追加する提案。

ビルドシステムの担う最も困難な仕事の一つは、あるプラットフォームに向けてコンパイルする際にどのアーキテクチャ、ツールチェーン、OS、そして実行環境を使用するのかを指定することです。この名前は実装によって好き勝手に選択されており、ツールのベンダーによって異なっています。
この提案は、それらの中でも多少広く使用されているclangの定めるTarget Tripleを基にしてその標準的なものを定義することを目指すものです。

Target Tupletsはビルドのために必要となる次の4つの情報の列として規定されます。

  • Architecture
    • CPUの命令セット(ISA)
    • x86_64, armv8など
  • Vendor
    • C++ツールチェーンを提供する事業体名(組織、個人、PC、なし)
    • apple, nvidia, noneなど
  • Operating system
    • linux, win32など
  • Environment
    • 実行ファイルの形式、ABI、ランタイムのいずれか
    • macho, elfgnu, eabiandroidなど

これらの具体的な名前をこの順番に-で繋いだ文字列がTarget Tupletsとなります。

  • Windows環境の一例 : x86_64-ms-win32-msvc
  • Linux環境の一例 : x86_64-none-linux-elf
  • CUDA環境の一例 : nvptx64-nvidia-cuda-cuda

ただし、標準規格としてTarget Tupletsを定義はせず、具体的なものは別のドキュメントに委ねるようです。規格書に記述してしまうと更新のために時間と手間がかかるためです。

P2000R2 Direction for ISO C++

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

C++標準化の目的や課題、次のバージョンに向けて重視する機能など、より大域的な視点でC++標準化の道筋を示す文書です。今回コロナウィルス流行に伴う社会環境の変化を反映する形で変更されたようです。

P2029R2 : Proposed resolution for core issues 411, 1656, and 2333; escapes in character and string literals

文字(列)リテラル中での数値エスケープ文字('\xc0')やユニバーサル文字名("\u000A")の扱いに関するC++字句規則の規定を明確にする提案。

この提案は以下の3つのIssueを解決するものです。

この提案では、既存の実装をベースとして字句規則をより明確に修正することによってこれらの問題の解決を図ります。

  • Core Issue 411
    • 単一の文字として表現できないユニバーサル文字名のエンコーディングは文字リテラルと文字列リテラルで異なる振る舞いをする、と規定する。
    • 例えば実行文字集合UTF-8である時、'\u0153'は(サポートされる場合)int型の実装定義の値を持つが、"\u0153"\xC5\x93\x00の長さ3の文字配列となる。
  • Core Issue 1656
  • Core Issue 2333
    • UTF-8リテラル中での数値エスケープ文字使用には価値があるので、明確に振る舞いを規定する。

問題範囲としては大きくないのですが、該当する文言をほとんど書き直しているため変更範囲が広くなっています。しかし、よく見るとより明確かつシンプルにこれらのことが表現されるようになっています。

P2075R1 : Philox as an extension of the C++ RNG engines

<random>に新しい疑似乱数生成エンジンを追加する提案。

疑似乱数生成エンジンは次のように様々な側面から特徴付けられます。

  • 乱数の質
  • 生成速度
  • 周期
  • 並列化(ベクトル化)可能性

例えば線形合同法std::linear_congruential_engine)は周期が短く乱数の質が低いのに対して、メルセンヌツイスターstd::mersenne_twister_engine)は周期が長く乱数の質は高いのですが、非常に大きなサイズの状態に依存しており並列化を妨げています。また、準モンテカルロ法をサポートできるような超一様分布を生成するエンジンもありません。

これらの観点から見ると現在のC++に用意されている乱数エンジンだけでは乱数のユースケースを満たすことは出来ず不十分であるので、最新の研究成果に基づいていくつかを検討し追加しようというのが(大本のP1932R0の)狙いです。

この提案ではPhiloxと呼ばれるGPU等のハードウェアによって並列化しやすい特性を持つエンジンに絞って提案されています。

Philoxはカウンタベースと呼ばれるタイプのエンジンで、カウンターベースのエンジンは総じて小さい状態と長い周期が特徴です(Philox4x32はそれぞれ40 [byte]2^130)。Philoxは状態が小さい事と演算が単純であることからSIMDGPUによる並列化が容易であり、intelnVidiaAMDによってそれぞれのハードウェアに最適化されたライブラリ実装が提供されており、それら実装は統計的なテストをパスしています。また、そのような特性を生かして大規模な並列乱数生成が求められる金融や非決定性有限オートマントンのシミュレーションなど、すでに広い使用実績があります。
このように、Philoxエンジンは導入および実装のハードルは低く標準化による利得は大きいため、標準に追加することを提案しています。

そして、Philoxエンジンを導入するに当たっては次の2つのAPIのどちらかを選択することを提案しています。

  • Philoxだけに着目したAPI
    • 既存の乱数エンジンを参考に、1つのPhiloxエンジンのベースとなるクラステンプレートと、そのパラメータ定義済みのいくつかのエイリアスstd::philoxNxM)だけを追加する。
  • カウンタベースエンジンのAPI

現状ではまだどちらを選択するかは議論中のようですが、どちらのAPIが選択されたとしてもユーザーからはstd::philox4x64std::philox4x64_rのような名前で既存の乱数エンジンと同様のインターフェースによって使用可能となります。

P2093R1 : Formatted output

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

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、wchar_tおよびcharN_tオーバーロードの追加に関してを別の提案に分離して、ユニコードテキストのフォーマットについてと共に議論することが明記されたことです。

P2128R2 : Multidimensional subscript operator

多次元コンテナサポートのために添字演算子[])が複数の引数を取れるようにする提案。

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、[]が少なくとも1つの引数を取る必要があるという制約を削除したことと、C配列に対するサポートや既存ライブラリに追加することを目指さないことを明言したことです。 C配列や既存のライブラリ機能(std::valarray)でそれらを使用したい場合は、C++26以降に向けて別の提案で議論することを推奨しています。

P2139R2 : Reviewing Deprecated Facilities of C++20 for C++23

C++20までに非推奨とされた機能をレビューし、標準から完全に削除するかあるいは非推奨を取り消すかを検討する提案文書。

まだ検討中で、削除が決まった物は無いようです。

P2146R2 : Modern std::byte stream IO for C++

std::byteによるバイナリシーケンスのI/Oのための新ライブラリ、std::ioの提案。

以前の記事を参照 onihusube.hatenablog.com onihusube.hatenablog.com

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

  • std::streambufやCのI/O APIの使用例を追加
  • design decisionsの書き直し
  • 参照文書の変更
  • エンディアンを考慮してRIFFファイルを読み取るサンプルコードの追加
  • std::filebufベンチマークを追加

などです。

P2161R2 : Remove Default Candidate Executor

Networking TSのassociated_executorからデフォルトのExecutorを取り除く提案。

以前の記事を参照 onihusube.hatenablog.com onihusube.hatenablog.com

このリビジョンでの変更は、1引数の​defer, dispatch​, ​postがその引数のCompletionTokenから(色々やって)型::executor_typeを取得できることを要求し、それが同様に(色々やって)取得できるget_executor()の戻り値型と一致することを要求するようになった事などです。

P2165R1 : Compatibility between tuple and tuple-like objects

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

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更はstd::tupleの推論ガイドを変更しないことにしたことと、std::tuple_catもより広いtuple-likeな型全般で使用可能となるようにしたことです。

以前のリビジョンではstd::tupleをtuple-likeな型からその要素を分解して構築できるように推論ガイドを変更していたのですが、std::tuple{std::array<int, 2>{}}(この型はstd::tuple<std::array<int, 2>>になるが、変更の下ではstd::tuple<int, int>になってしまう)の様な既存のコードの振る舞いを破壊する事になっていいたためです。ただ、std::pairは以前から特別扱いされていたので変更なしでも2要素tupleへ(現在でも)変換可能です。

P2169R1 : A Nice Placeholder With No Name

宣言以降使用されず追加情報を提供するための名前をつける必要もない変数を表すために_を言語サポート付きで使用できる様にする提案。

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は_がクラスメンバでも同じような意味をもつようにしたことと、同じスコープで複数回_を使う場合の扱いが変更されたことです。

結局、この特別な_は次の場所で使用することができます。

  • 変数名
  • クラスの非静的データメンバ名
  • 関数の引数名
  • ラムダ式のキャプチャ名
  • 構造化束縛宣言の変数名

そして、前回掲載したサンプルコードは次のように動作が微妙に変わります。

/// ここはモジュール実装単位内ではないとする

namespace a {
  auto _ = f(); // Ok, [[maybe_unused]] auto _ = f();と等価
  auto _ = f(); // NG: _の再定義、モジュール実装単位内ならばok
}

void f() {
  auto _ = 42; // Ok、[[maybe_unused]] auto _ = 42;と等価
  auto _ = 0;  // Ok、再定義(参照しない限りOK)

  {
    auto _ = 1; // Ok, 単に隠蔽する
    assert( _ == 1 ); // Ok
  }

  assert( _ == 42 ); // NG、再定義された名前の参照
}

ただし、この様な再定義が許可されるのは_という名前の変数だけです。他の名前の変数は今まで通り定義が一つでなければなりません。

P2178R1 : Misc lexing and string handling improvements

現在のC++の字句規則をクリーンアップし、ユニコードで記述されたソースコードの振舞を明確にする提案。

前回の記事を参照 onihusube.hatenablog.com

このリビジョンでの変更は、FAQを追加した事と、生文字列リテラル末尾の空白やBOM、ill-formedなcode units sequence、文字列リテラルの連結についての追記が行われたことです。

P2183R0 : Executors Review: Properties

C++23への導入を目指してレビューが進んでいるExecutorライブラリ(P0443R13)におけるプロパティ指定(P1393R0)の利用に関する報告書。

P0688R0によってExecutorに対して導入された組み合わせ爆発を抑えるプロパティ指定の方法は、Executorにとどまらず将来のライブラリに対しても有用であると判断され、よりジェネリックなプロパティ指定のためのAPIとしてExecutorから分離されました(P1393R0)。
この提案は分離されたことによって若干変化したAPIをExecutorがどのように利用するのかをLEWGにおけるレビューのためにまとめたもので、boost.asioにおける先行実装を用いたサンプルコードがいくつか掲載されています。また、その際に発見された小さないくつかの問題についても同時に報告されています。

P2186R0 : Removing Garbage Collection Support

ガベージコレクタサポートのために追加された言語とライブラリ機能を削除する提案。

C++11にて、C++処理系としてGC実装を許可するための最小の文言とライブラリサポートが導入されました。2020年現在、BoehmGCというC/C++向けのGCライブラリ(not処理系)があるほか、主要ブラウザのJavascriptエンジンなどGCをサポートする言語がC++で実装され、GCにより管理されているオブジェクトとC++オブジェクトが密接に連携できるなど、C++から利用可能なGCの実装は一定の成功を収めています。ただしC++の処理系としてのGC実装はなく、これらのC++から利用可能なGC実装はC++11で導入されたGCのための仕組みを利用していません。

また、そのような標準で用意されているGC実装のための仕組みにはいくつも問題があり、実際のGC実装のためにはほとんど役に立ちません。実際、既存のC++から利用可能なGC実装が依存しているのは、現在の規定とは異なる要因に対してです。そして、GCのためのライブラリ機能を使用しているコードはGCCLLVM(つまり実装者)以外には見つからなかったそうです。

C++実装としてGCサポートを認めないわけではなく、少なくとも現在の仕様とライブラリ機能は役に立たないので削除しようという提案です。これらの文言と機能は実装される事も使用されることも無く、非推奨にすることには意味が無いためすぐに削除することを提案しています。

P2187R3 : std::swap_if, std::predictable

より効率的な条件付きswapを行うためのstd::swap_ifと、その使用を制御するstd::predictableの提案。

前回の記事を参照 onihusube.hatenablog.com

R1とR2は公開されておらず、前回のR0からいきなりR3に飛んでいます。

変更点はまず、入力となるTのサイズによってswap_ifを使うかどうかを静的に分岐するようにしたことです。そのための、is_trivially_swappable_vcheaply_swappableコンセプトを追加して以下の様な実装をします。

template <typename T>
constexpr bool is_trivially_swappable_v = std::is_trivially_copyable_v<T>;

template <typename T>
concept cheaply_swappable = (std::is_trivially_swappable_v<T> && sizeof(T) <= N); // ここのNは実装定義

template <typename T>
  requires (cheaply_swappable<T> || std::swappable<T>)
constexpr bool swap_if(bool c, T& x, T& y) noexcept(cheaply_swappable<T> || std::is_nothrow_invocable_v<decltype(std::swap<T,T>), T&, T&>) {
  if constexpr (cheaply_swappable<T>) {
    struct alignas(T) { char b[sizeof(T)]; } tmp[2];

    std::memcpy(tmp[0].b, &x, sizeof(x));
    std::memcpy(tmp[1].b, &y, sizeof(y));
    std::memcpy(&y, tmp[1-c].b, sizeof(x));
    std::memcpy(&x, tmp[c].b, sizeof(x));

    return c;
  }

  // Tのコピーコストが大きいとき、普通に条件分岐してswapする
  if (c) std::swap(x, y);
  return c;
} 

この様にすることで、Tのコピーコストが高くつく場合にも自動的に最適な実装に切り替えることができます。
また、is_trivially_swappable_vはカスタマイゼーションポイントになっていて、trivially copyableとみなされないが安全にmemcpyできるような型をswap_ifにアダプトできます。

そして最後に、イテレータに対して直接swap_ifするiter_swap_ifが追加されています。

このswap_ifが使用されるのはstd::sortなどのアルゴリズム中であり、そこではイテレータを介して実際の値にアクセスします。swap_ifの引数はイテレータを間接参照して渡す事になり、わずかではありますがボイラープレートが発生します。iter_swap_ifイテレータを直接受け取り内部で間接参照しつつswap_ifに渡す簡易なラッパーです。

template <typename Flag, typename I>
bool iter_swap_if(Flag c, I p, I q) {
  return swap_if(c, *p, *q);
}

P2188R1 : Zap the Zap: Pointers are sometimes just bags of bits

現在の標準の無効な(指すオブジェクトの生存期間が終了した後の)ポインタについての矛盾した規定を正す提案。

前回の記事を参照 onihusube.hatenablog.com

EWGでのレビューを受けて、不完全だった提案の文言をいったん削除し、代わりに著者の方が重視している側面について追記されています。

1. 等しいポインタは交換可能であるべき

ポインタはValue Semanticsを持っており、コピーすると元の値と同じになる。従って、2つの値が等しければ、それらは交換して扱うことができるはず。

// pとqは初期化されているとする
void f(int* p, int* q){
  *q = 99;
  bool same = false;

  if (p == q) {
    // ポインタの値が等しいならば、pとqは同じ
    *p = 42;
    same = true;
    assert(*q == 42);
  }

  assert(same ? (*q == 42) : (*q == 99));
}

このコードが動作しないとすれば、ポインタの等値性が壊れていることを意味する。

2. ポインタの等値性は一貫しているべき

同じ2つのポインタの等値性は比較される場所によらないはず、これはValue Semanticsの基本的な側面にすぎない。

bool compare(int* const p, int* const q){
  return p == q;
}

// pとqは初期化されているとする
void f(int* const p, int* const q){
  bool const same = (p == q);
  g(p, q);
  assert(same == (p == q));       // ここがtrueならば
  assert(same == compare(p, q));  // ここもtrue
}

このコードはg()がなんであるかや、翻訳単位やインライン化の有無などに関わらず動作するはず。

3. コンパイラは特定の状況ではエイリアスが無いと仮定しなければならない

他の場所にその参照を渡されることがない関数内のローカル変数は、他の場所から変更されることは無いと仮定できるようにしなければならない。

void f(){
  int x = 42;
  g();
  assert(x == 42);  // 常にパスする
}

アサートが起動することはあってはならない。xへのポインタは存在しないので、g()の中で使用されるいかなるポインタと等しくなることはありえない。

4. 無効なポインタのデリファレンスは未定義動作

そのことを変更するつもりはない(ただし、次の場合は除く)。

5. ポインタの有効性は比較によって伝染する

ポインタpを、有効である事が分かっているポインタqと比較した結果等しければ、pも有効でなければならない(上記1に従う)。

void f(){
  int * const p = new int(42);
  delete p;
  int * const q = new int(99);

  if (p == q) {
    assert(*p == 99); // p == qならば指す先は同じ
  }
}

実装がこの場合のp == qが成り立たないようにすることを推奨するが、もしp == qであるならば、pqは同じオブジェクトを指していなければならない。

逆に言うと、現在のC++はこれらの事を必ずしも保証できていないという事です。

P2191R0 : Modules: ADL & GMFs do not play together well (anymore)

グローバルモジュールフラグメントのあらゆる宣言が、モジュール外からのテンプレートのインスタンス化時に参照されないようにする提案。

グローバルモジュールフラグメントとは、モジュール内部に用意される主としてヘッダをインクルードするための領域です。その領域にあるものがそのモジュール内から参照されなかった場合、モジュールのコンパイルの段階で破棄されます。モジュールのビルドはテンプレートを残す必要があるため完全に完了するわけではありませんが、破棄された宣言はその後のテンプレートインスタンス化時には一切参照できなくなります。これはモジュールのインターフェースの肥大化を抑えつつ、モジュール内で#includeを行うための仕組みです。

一方、従来の#includeディレクティブはヘッダユニットのインポートとして書くことができ、コンパイラによって自動的に置換される可能性があります。グローバルモジュールフラグメント内でもヘッダユニットのインポートが発生する可能性があり、その場合はヘッダファイルが一つのモジュールとしてインターフェース依存関係を構築するため、そのimportが破棄されることはありません。すると、同じヘッダを(グローバルモジュールフラグメント内で)#includeしたときと異なり、あらゆる宣言は破棄されません。

そして、モジュール内で定義されエクスポートされているテンプレートがモジュール外でインスタンス化されたとき、そのインスタンス化経路(テンプレートのインスタンス化に伴ってインスタンス化される関連テンプレートを繋いでいったグラフのようなもの)上からグローバルモジュールフラグメント内にあって外部リンケージを持ち破棄されていない宣言はADLによって参照することができます。
この時、コンパイラによってグローバルモジュールフラグメント内の#includeimportに置換されていた場合、ADLによって参照可能な宣言が異なることになります。どのようなヘッダをヘッダユニットとして扱い、いつ#includeimportに置換するのかは実装定義とされているため、このことはコンパイラによって異なる可能性があります。また、ユーザーがそれを制御する手段はありません。

他の問題として、このようなグローバルモジュールフラグメントの仕様によって、実装はモジュールの第一段階のコンパイル後にもグローバルモジュールフラグメントの中身と依存関係グラフを保持し続けなければならず、モジュールパーティションによって複数のグローバルモジュールフラグメントが間接的に外部にエクスポートされる時、それをどのように統合すべきか?という問題が生じます。また、それによってはモジュール内部のパーティションへの分割が外部から観測可能となってしまうことになり得ます。

/// header.hpp

#include <iterator> // このインクルードは実装によってimportに変換されるかもしれない
/// mymodule.cpp
module;
// グローバルモジュールフラグメント

#include "header.hpp" // このインクルードは実装によってimportに変換されるかもしれない

export module mymodule;

template<typename C>
export void f(C&& container) {
  auto it = begin(container);  // 依存名なのでここではstd::beginは参照されているとみなされない
}

// 説明のために実装単位などを省略
/// main.cpp
import mymodule;

#include <vector>

int main() {
  std::vector<int> v{};

  f(v);
  // mymoduleのグローバルモジュールフラグメント内の#includeがそのままならばこれはエラー
  // mymoduleのグローバルモジュールフラグメント内で#includeがimportになっているとコンパイルは通る
}

この提案では、これらの問題を解決するためにグローバルモジュールフラグメント内のあらゆる宣言は、モジュールに属するテンプレートのインスタンス化時のADLにおいて可視とならない、と規定を変更することを提案しています。
これによって、グローバルモジュールフラグメントは完全にモジュール内部で完結するものになり、モジュール外部からは気にしなくてもよくなります。また、実装はモジュールのコンパイル後にグローバルモジュールフラグメント内のものを残すかどうかを自由に選択できるようになります。

ただし、グローバルモジュールフラグメント内の宣言を参照しているようなモジュールに属するテンプレートの利用のためには、モジュール内でusingしておくか、そのモジュールを使う側で必要な宣言が含まれているヘッダをインクルードしなければならなくなります。

/// mymodule.cpp
module;
// グローバルモジュールフラグメント
// この提案の下では、ここの宣言は全て外部から参照されることは無い

#include <vector>
#include "header.hpp"

export module mymodule;

template<typename C>
export void f(C&& container) {
  using std::begin; // beginを後から使用するためにはこうする (1)

  auto it = begin(container);
}

// 説明のために実装単位などを省略
/// main.cpp
import mymodule;

#include <vector>
#include <iterator> // mymodule内部のstd::beginを有効化するために明示的に追加する (2)

int main() {
  std::vector<int> v{};

  f(v); // (1)か(2)のどちらかをしておかないとコンパイルエラー
}

P2192R0 std::valstat - function return type

関数の戻り値としてエラー報告を行うための包括的な仕組みであるvalstatの提案。

現在のC++にはいくつものエラー報告のための方法がありますが、エラーを報告する側と受け取り側にとって単純かつ柔軟な方法はなく、戻り値の形でエラー報告を行う際の標準的な1つの型もありません。

  • 例外機構
    • 例外を報告する最も簡易な方法ではあるが、受け取る側では何が飛んでくるか分からない
    • オーバーヘッドが大きい
  • errnoあるいはstd::errc
    • エラーしか報告できない
  • std::error_code
    • 動的メモリ確保を伴うなどいくつか問題がある(P0824R1
    • エラーしか報告できない
  • outcome<T>/expected<T, E>
    • これらは非常に複雑な型となり、必要以上の複雑さを導入することになる(例えば、Boost.outcomは7223行)
    • エラーかそうでないかの2つの情報しか表現できない

一方、C++標準としてはエラー報告については次のようなコンセンサスがあるようです。

  • 処理(関数)の結果はエラーかそうでないかの2値ではない
    • 例えば非同期処理のキャンセルなど
  • エラーかそうでないかのバイナリロジックを回避すれば複雑さが低減される
    • 早期リターンが失敗ではない場合のエラーがある
  • (エラー報告の結果の)戻り値を消費することは、必然的に複雑となる
    • 戻り値型は多様なので、そのような消費ロジックは複雑にならざるをえない
    • 統一的な戻り値消費処理を決定し、それに従うようにすることが有益

valstatはこれらのコンセンサスに基づいた、戻り値でエラーハンドリングを行うためのコンセプト(概念)とそれに従うものの総称です。次のように使用することができます。

// valstatは何か特定の戻り値型ではない
auto [ value, status ] = valstat_enabled_function();

// valstatは戻り値として4つの状態を表現する
if (   value &&   status )  { /* info */ }
if (   value && ! status )  { /* ok   */ }
if ( ! value &&   status )  { /* error*/ }
if ( ! value && ! status )  { /* empty*/ } 

valstat(に適合する戻り値型)は処理結果と処理のステータスのペアであり、その組み合わせによってMeta Stateを表現します。そして、その状態は!&&によって判別できます。

Meta Stateは処理結果と処理のステータスの有無によって次の4つの状態の1つを表現します

処理結果 \ 処理ステータス 有り 無し(空)
有り Info OK
無し(空) Error Empty
  • OK
    • 関数は期待される値を返し完了した
  • Error
    • 関数は期待通りに完了しなかった(失敗した)
    • ステータスには関連情報が格納される
  • Info
    • 関数は期待される値を返し完了した
    • ステータスには追加の情報が格納されている
  • Empty
    • 戻り値は空
    • (これがどのように使われるのかは提案文書に書いてない、多分重要なのは上3つ)

これはvalstatのコンセプトの一部であり、列挙値などで表現されるものではなく、valstatの任意の実体がこれを明示的に実装するわけではありません。valstatにアダプトした型は必然的にその状態によってこのMeta Stateを表現することになります。

この提案としてはこのようなコンセプトとそれを表現するための標準実装であるstd::valstatを標準ライブラリに導入することを提案しています。それは次のように実装可能です。

#include <optional>

namespace std {
  template<tpyename T, typename S>
  struct [[nodiscard]] valstat {
    using value_type = T;
    using status_type = S;

    optional<T> value;
    optional<S> status;
  };
}

ただし、valstatは特定の型に縛られるものではなく、std::valstatと同じように使用可能なvalstatコンセプトに適合する任意の型によって実装可能です。それは純粋なC言語においてもポインタを利用することで実装でき、そのようなライブラリをC++から利用するときにはvalstatコンセプトに従って統一的にエラー報告を処理できるようになります。

P2193R0 : How to structure a teaching topic

R1が同時に出ているので↓でまとめて。

P2193R1 : How to structure a teaching topic

C++を教えるにあたってのカリキュラムを書く際のガイドラインとなる文書。

ある1つのトピック(C++の言語・ライブラリ機能)についてそこに何をどのように書くか等を説明し、カリキュラム中のトピック説明においての共通の文書構造を提供するものです。

P2196R0 : A lifetime-extending forwarder

std::forwardのように与えられた引数を完全転送し、CV修飾を記憶しつつその生存期間を延長し、必要になった場所で完全転送しつつ取り出すことのできるクラステンプレートであるstd::forwarderの提案。

おおよそ次のように動作するものです(提案文書からの引用。一部正しくないコードが含まれていますが、イメージを掴む分には問題ないのでそのままにしています)。

// オブジェクトの値カテゴリによって呼ばれるメンバ関数が異なる
struct object {
  void test() const & {std::clog << "void test() const &\n";}
  void test() const && {std::clog << "void test() const &&\n";}
};

// Forwarding referenceで入力を受ける
template <class T>
auto function(T&& t) {
  // 引数tの型を記憶し生存期間を延長する
  auto fwd = forwarder<T>(t);
  
  /∗ ... ∗/

  return fwd;
}

int main(int argc, char∗ argv[]) {
  object x;
  auto fwd0 = function(x);
  auto fwd1 = function(object{});
  
  /∗ ... ∗/

  // 構築時の値カテゴリとCV修飾を復元して取り出す
  // (提案文書にはこのように書かれているが実際この呼び出しは出来ない)
  fwd0().test(); // void test() const &
  fwd1().test(); // void test() const &&
  return 0;
}

オブジェクトの値カテゴリとCV修飾を保存したまま持ち運び、必要な時にそれを復元しつつ取り出すことができます。ただし、提案ではメンバ関数は型変換演算子しか定義されておらず、operator.オーバーロード不可能なので実際には上記の.によるメンバ呼び出しみたいなことはできません。

筆者の方は別の提案であるP1772R1(いわゆるoverloadedの提案)の作業中にこのような完全転送と生存期間延長を行うラッパー型の必要性に遭遇し、それがより広範に適用可能なものであったため独立した提案として提出したとのことです。

P2198R0 Freestanding Feature-Test Macros and Implementation-Defined Extensions

フリースタンディング処理系でも使用可能なライブラリ機能について、機能テストマクロを追加する提案。

P1641R3P1642R3の以前のリビジョンで同時に提案されていたものを分離して一つにまとめた提案です。

P2199R0 : Concepts to differentiate types

2つの型が同じではない、2つの型は類似の型(CV修飾を無視して同じ型)である、あるいは2つの型は類似していない、という事を表現する3つの新しいコンセプトを追加する提案。

それぞれ、std::different_from, std::similar_to, std::distinct_fromという名前が当てられています。

2つの型が異なる、CV修飾の違いを除いて異なるとき、のようなテンプレートパラメータに対する制約は標準ライブラリの中でも頻出しています(例えばstd::optionalstd::variantの変換コンストラクタや代入演算子など)。この様なテンプレートパラメータに対する制約はよくある物なのでユーザーコードでも当然頻出し、ユーザーは各々同じコンセプトを最発明することになります。標準でこれらを定義しておくことでそのような手間を省くことができ、また標準ライブラリに対する制約もこれらのコンセプトを直接用いて簡略化することができます。

提案ではそれぞれ次のように定義されます。

namespace std {
  template<typename T, typename U>
  concept different_from = not same_as<T, U>;

  template<typename T, typename U>
  concept similar_to = same_as<remove_cvref_t<T>, remove_cvref_t<U>>;

  template<typename T, typename U>
  concept distinct_from = not similar_to<T, U>;
}

この定義だけを見てもよく使いそうなのは分かるでしょう。

この提案では同時に、different_fromdistinct_fromによって置換可能な標準ライブラリ内の制約条件を書き換えることも行っています。
また、differentdistinctの使い分けが曖昧なので、LWG/LEWGの判断次第では名前が入れ替わる可能性があるとのことです。

P2201R0 : Mixed string literal concatenation

異なるエンコードプレフィックスを持つ文字列リテラルの連結を禁止する提案。

文字列リテラルが2つ並んでいるとき、それらはコンパイラによって連結され1つの文字列リテラルとして扱われます。

int main() {
  auto str = "abc" "def" "ghi"; // "abcdefghi"として扱われる
}

このとき、そのエンコード指定(u8 L u U)が異なっている場合の動作は実装定義で条件付きでサポートされるとされており、実際には主要なC++コンパイラはエラーとします。ただ、UTF-8文字列リテラルのその他のリテラルとの連結は明示的に禁止されています。

int main() {
  auto str1 = L"abc" "def" U"ghi";   // 実装定義
  auto str2 = L"abc" u8"def" U"ghi"; // u8がある場合はill-formed
}

ただ、SDCC(Small Device C Compiler)というCコンパイラはこれをサポートしており、最初のエンコード指定で連結するようです。

このような異なるエンコード指定の文字列リテラル連結のユースケースはないので、明示的にill-formedであると規定しようという提案です。

P2202R0 : Senders/Receivers group Executors review report

Executor提案(P0443R13)のsenderreciever周りのレビューの結果見つかった問題についての報告書。

Executor提案はレビューの効率化のために構成要素に分割したうえで、それぞれ別々の人々によってレビューされているようです。

P2203R0 : LEWG Executors Customization Point Report

Executor提案(P0443R13)のカスタマイゼーションポイント周りのレビューのまとめと、生じた疑問点についての報告書。

この文書は筆者(レビュワー)の方々がP0443のカスタマイゼーションポイント(オブジェクト)について読み込み、どんなカスタマイゼーションポイントがあるかやそれらの枠割についてまとめた部分が主となっています。Executorライブラリのカスタマイゼーションポイントについて比較的簡単にまとめられているので興味のある人は見てみるといいかもしれません。

onihusube.hatenablog.com

この記事のMarkdownソース

Github Actions上のDockerコンテナ上のGCCの最新版でテストを走らせたかった

Github Actionsで用意されているubuntu 20.04環境に用意されているソフトウェアを見るとGCCは9.3が用意されています(2020年7月24日現在)。しかしGCC9.3はC++20対応がほぼなされていないので使い物になりません(個人の感想です)。いつかは追加されるでしょうが、GCC10.2(もう出た)、GCC 11.1とか言いだすと永遠に待ち続けることになりそうです・・・

一方、Docker公式のGCCコンテナは既にGCC10.1が利用可能です(2020年7月24日現在)。

つまり、この2つを組み合わせれば最新のGCCを使いながらGithub Actionsできるはず!

※以下ではGithub ActionsとかDocker関係の用語が必ずしも正しく使われていない可能性があります、ご了承ください。

Dcokerコンテナを起動しその上でビルドとテストを走らせるActionを作成する

この資料にはDockerコンテナを使ってCIを走らせるための初歩初歩の事が書いてあります。ちゃんと読むと、Dockerコンテナを起動し何かするためのオリジナルのActionを作成し、それをテストしたいリポジトリのymlファイルから呼び出す形になる事が分かります。

(なお、私はちゃんと読まなかったのでわかりませんでした)

というわけで、まずはGCCのDockerコンテナを起動してビルドとテストを行うActionを作成します。

Dockerファイルの作成

まずは所望の動作を達成するためのDockerファイルを作ります。

From gcc:latest
RUN apt-get update
RUN apt-get install -y python3-pip
RUN pip3 install meson ninja
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

私はビルドシステムに専らmesonを使っているので、mesonもインストールしておきます。上記Dcokerfileの2-4行目はお好きなビルドシステムのインストールに置き換えてください。

ここのRUNで全部やっても良いのですがあまり汎用性が無くなるので、ここでは最低限の環境構築に留めて、メインの処理は別に書いたシェルスクリプト(ここではentrypoint.sh)に記述します。

5行目で用意したシェルスクリプトをDockerインスタンス上にコピーして、6行目で開始時に実行するように指示します。

シェルスクリプトの作成

このページにあるものを参考にして書きます。ただこのページだけ見ても、テストしたいリポジトリのクローンをどうするのかが分かりません。試してみると別にクローンされてはいないので自分でクローンしてくる必要があります。なので、そのために必要な情報を引数として受け取るようにします。

公式のActionにはcheckoutというのがありますが、今回はDocker上で実行するため利用できなさそうです。

#!/bin/sh -l

git clone $2
cd $1
meson build
meson test -C build -v

第一引数にリポジトリ名(=ディレクトリ名)、第二引数にリポジトリのURLを受けるようにしています。5-6行目はmeson特有のものなので、お使いのビルドシステムのビルドコマンドと成果物の実行コマンドに置き換えてください。

ファイル名は先程Dockerfileに書いたentrypoint.shと合わせます。chmod +x entrypoint.shで実行可能にすることを忘れすに、 Windowsの場合は次のページが参考になります。

Actionファイルの作成

次にActionとして呼ばれたときに何をするかをaction.ymlというファイルに記述します。試してないですがファイル名は多分固定だと思われます。

name: 'build & test'

inputs:
  name:
    description: 'Name of target repository'
    required: true
  uri:
    description: 'URI of target repository'
    required: true

runs:
    using: 'docker'
    image: 'Dockerfile'
    args:
    - ${{ inputs.name }}
    - ${{ inputs.uri }}

inputsで先程のシェルスクリプトに渡す引数を定義しています。上から順番に第一引数(name)、第二引数(uri)で内容は先ほどの通り。descriptionは説明で無くても良いです。requiredは引数が省略可能かを表しており、今回は省略してほしくないのでtrueに設定しておきます。

他にも出力を定義できたりします。出来ることは次のページを参照。

runsにActionとして呼ばれたときにやることを書いておきます。usingは何のアプリケーションを使用するのかを指示するもので、ここではDockerを指定します。imageには使いたいDockerイメージ名を指定します。Docker Hubにあるイメージ名を指定しても良いようですが、今回は先ほど用意したDockerfile(のファイル名)を指定します。
そして、argsで引数をどの順で受け取るかを指定しています。先程inputsで定義した引数名をinputs.nameの様に参照しています。シェルスクリプトにはここで書いた順番に引数が渡されることになります。

Actionリポジトリの作成

ここまで用意したDockerfile、やることを記述したシェルスクリプトentrypoint.sh、Actionを記述したaction.ymlの3つをGithub上の公開リポジトリに保存しておき、リリースタグを打っておくことで、他のリポジトリからActionとして参照できるようになります。
ここまではリポジトリのトップに全てをぶちまける事を前提に書いてありますので、今回はそうします。

私の場合は次のようになりました。

Publish this Action to Marketplaceなるメッセージが表示されていたら、きちんとActionとして認識されています。もし自作のActionをMarketplaceに公開したい場合はreadmeを適切に整えておく必要がありそうです。

テストしたいリポジトリのワークフロー中でActionを呼ぶ

こうして用意した自作Docker ActionをテストしたいリポジトリGithub Actions ワークフローから呼び出します。

name: Test by GCC latest  # ワークフローの名前
on: [push, pull_request]  # 何をトリガーにして実行するか

jobs:
  test:
    runs-on: ubuntu-latest  # Dockerを実行することになるベース環境
    name: run test
    steps:
    - name: run test in docker
      uses: onihusube/gcc-meson-docker-action@v6
      with:
        name: 'harmony'
        uri: 'https://github.com/onihusube/harmony.git'

実物がこちらです。今回対象のリポジトリは私が衝動で書いていたC++のライブラリです。

ベースは普通のGithub Actionsのワークフローを書くのと変わらないはず。今回重要なのは先程作成したDocker Actionを適切に呼び出すことです。

steps中のusesで任意のActionを呼び出すことができます。先程作成したActionのリポジトリ参照する形で作ったユーザー名/リポジトリ名@タグというように指定します(上記タグがv6とかなってるのは試行錯誤の名残です・・・)。
その次のwithで順番に引数を指定します。指定出来るのはActionのymlに書いたものだけで、ここに書いた引数がDockerインスタンス上で実行されるシェルスクリプトに渡されることになります。

でこれをプッシュするとDocker上のGCCでビルドされテストが実行されたので、どうやら目的を果たせたようです(参考の結果)。

参考文献

謝辞

この記事の8割は以下の方によるご指摘によって成り立っています。

この記事のMarkdownソース

[C++]Forwarding referenceとコンセプト定義

コンセプトを定義するとき、あるいはrequires節で制約式を書くとき、はたまたrequires式でローカルパラメータを使用するとき、その型パラメータがForwarding referenceから来ているものだと使用する際に少し迷う事があります。

// operator*による間接参照が可能であること
template<typename T>
concept weakly_indirectly_readable = requires(T& t /*👈ローカルパラメータ*/) {
  *t;
};

template<weakly_indirectly_readable T>
void f(T&& t) {
  auto v = *std::forward<T>(t);
}

int main() {
  std::optional<int> opt{10};

  f(opt); // 左辺値を渡す
  f(std::optional<int>{20});  // 右辺値を渡す
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

さてこの時、weakly_indirectly_readableは右辺値に対してきちんと制約出来ているでしょうか?値カテゴリの情報まできちんと伝わっていますか??
残念ながらこれでは右辺値に対して適切に制約出来ていません。例えば右辺値の時は呼べないような意地の悪い型を用意してあげると分かります。

struct dereferencable_l {
  int n = 0;

  int& operator*() & {
    return n;
  }

  int&& operator*() && = delete;
};


int main() {
  dereferencable_l d{10};

  f(d); // 左辺値を渡す
  f(dereferencable_l{20});  // 右辺値を渡す、エラー
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

エラーにはなりますが、それは右辺値を渡したときにf()の定義内で発生するハードエラーです。コンセプトによる制約はすり抜けています。

ではこの時どのようにすれば左辺値と右辺値両方に対して適切な制約を書けるのでしょう・・・?

Forwarding referenceと参照の折り畳み

Forwarding referenceとは先程のf()の引数にあるT&&のような引数宣言の事です。これはそこに引数として左辺値が来ても右辺値が来ても、テンプレートの実引数推定時にいい感じの型に変化してくれる凄い奴です。

そのルールは単純で、Forwarding referenceに対応する引数の値カテゴリによって次のようになります。

  • 引数として左辺値(lvalue)が渡されたとき
    • テンプレートパラメータTは渡された型をTとしてT&になる
  • 引数として右辺値(xvalue, prvalue)が渡されたとき
    • テンプレートパラメータTは渡された型そのままのTになる
template<typename T>
void f(T&& t);

int n = 0;

f(n);  // intの左辺値を渡す、T = int&
f(1);  // intの右辺値を渡す、T = int

ではこの時、引数宣言のT&&はどうなっていて、引数のtはどうなっているのでしょう?
これは参照の折り畳み(reference collapsing)というルールによって、元の値カテゴリの情報を表現するように変換されます。

  • 引数に左辺値が渡されたとき
    • テンプレートパラメータはT -> T&となり、引数型はT&&& -> T&となる
    • 引数tは左辺値参照
  • 引数に右辺値が渡されたとき
    • テンプレートパラメータはT -> Tとなり、引数型はT&& -> T&&となる
    • 引数tは右辺値参照

これ以降の場所でこのTを使用する場合も同様になります。参照の折り畳みは別にForwarding reference専用のルールではないのでそれ以外の場所でも同様に発動します。
そして、参照の折り畳みによって右辺値参照が生成されるのは、T&&&&を付けた時だけです。それ以外はすべて左辺値参照に折りたたまれます。

using rawt = int;
using lref = int&;
using rref = int&&;

using rawt_r  = rawt&;   // int&
using rawt_rr = rawt&&;  // int&&
using lrefr   = lref&;   // int& & -> int&
using lrefrr  = lref&&;  // int& && -> int&
using rrefr   = rref&;   // int&& & -> int&
using rrefrr  = rref&&;  // int&& && -> int&&

最適解

Forwarding referenceに対して制約を行うコンセプトの定義内、あるいはrequires節の制約式では上記の様に折り畳まれた後の型が渡ってきます。つまりは、それを前提にして書けばいいのです。先程のweakly_indirectly_readableを書き直してみると次のようになります。

// operator*による間接参照が可能であること
template<typename T>
concept weakly_indirectly_readable = requires(T&& t) {
  *std::forward<T>(t);
};

template<weakly_indirectly_readable T>
void f(T&& t) {
  auto v = *std::forward<T>(t);
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

f()に左辺値を渡したとき、weakly_indirectly_readableに渡る型は左辺値参照型T&です。weakly_indirectly_readableのローカルパラメータtは参照の折り畳みによってT&& & -> T&となり、左辺値参照になります。std::forwardに渡しているTT&なので、ここではムーブされません。

f()に右辺値を渡したとき、weakly_indirectly_readableに渡る型は単にTです。weakly_indirectly_readableのローカルパラメータtはそのまま&&が付加されてT&&となり、右辺値参照になります。std::forwardに渡しているTTなので、ムーブされることになります。

これはコンセプト定義内ではなく、requires節で直接書くときも同様です。

template<typename T>
  requires requires(T&& t) {
    *std::forward<T>(t);
  }
void f(T&& t) {
  auto v = *std::forward<T>(t);
}

先程の意地の悪い例を渡してみてもコンセプトによるエラーが発生するのが分かります。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

この様に、Forwarding referenceと参照の折り畳みを考慮することで、値カテゴリを適切に処理しつつ制約を行うことができます。

値カテゴリを指定する制約

先程完璧に仕上げたweakly_indirectly_readableですが、それをあえて右辺値と左辺値パターンで分けて書いてみます。

template<typename T>
concept weakly_indirectly_readable = requires(T& t) {
  *t;             // 左辺値に対する制約
  *std::move(t);  // 右辺値に対する制約
};

これは先ほどのT&&std::forwardを使った制約と同じ意味になります。

あえてこのように書くことで、ローカルパラメータとその型の参照修飾を用いて制約式に値カテゴリの制約を指定している様を見ることができます。すなわち、weakly_indirectly_readableのモデルとなる型はoperator*()が右辺値・左辺値の両方で呼べること!という制約を表現しています。これを略記すると先ほどのT&&std::forwardを使った制約式になるわけです。

例えば制約したい対象の式(ここではoperator*)が左辺値でだけ呼べれば良いのであれば右辺値に対する制約は必要なく、逆に右辺値だけをチェックすればいい場合は左辺値に対する制約は必要ありません(ただし、そのようなコンセプト定義は適切ではないかもしれません)。

さらに、ローカルパラメータをCV修飾することで、値カテゴリに加えてCV修飾を指定した制約を行えます。

template<typename T>
concept weakly_indirectly_readable = requires(const T& t) {
  *t;             // const 左辺値に対する制約
  *std::move(t);  // const 右辺値に対する制約
};

ちなみにこの場合に、非constに対する制約も同時に行いたい場合は以下のようにします。

template<typename T>
concept weakly_indirectly_readable = requires(T& t) {
  *t;             // 左辺値に対する制約
  *std::move(t);  // 右辺値に対する制約
  *const_cast<const T&>(t);             // const 左辺値に対する制約
  *std::move(const_cast<const T&>(t));  // const 右辺値に対する制約
};

constのローカルパラメータを取ってconst_castします。requires式を分けても良い気がしますが、標準ライブラリのコンセプトはこの様に定義されるようです。

これらのように、requires式のローカルパラメータのCV・参照修飾を用いて制約式に対するCV修飾と値カテゴリの制約を表現する事ができます。そして、標準ライブラリのコンセプトは全てそのように書かれています。

結局

脱線しながら長々と語ってきましたが、コンセプトを定義するあるいはrequires節で制約をする際に意識すべきことは一つだけです。

  • コンセプト定義あるいはrequires節において、引数となる型パラメータは常にCV修飾無し、参照修飾無しの完全型が渡ってくると仮定して書く

これに近いことは規格書にも書いてあります。この仮定を置いてそれに従って書けば、結果的に適切なCV修飾と値カテゴリによって制約を行うことができます。これは実際に渡ってくる型が云々と悩むのではなく、そう思って書け!という事です。

標準ライブラリのコンセプトはそのように定義されており、これを意識の端っこに置いておけばそのようなコンセプトを利用しやすくなり、自分で定義する際もすっきり書くことができるようになるかもしれません。

参考文献

この記事のMarkdownソース