[C++]TU-local Entityをexposureするのこと(禁止)

モジュールと内部リンケージ名

内部リンケージをもつエンティティ(関数、クラスや変数)は、外部リンケージをもつ関数など翻訳単位のインターフェースの実装詳細をある粒度で分解して表現するために使用されています。それによって、コードの保守性や再利用性の向上が見込めます。

従来のヘッダファイルとソースファイルからなる一つの翻訳単位の構成においては、内部リンケージ名はソースファイル内に隠蔽されているか、ヘッダファイルに書かれていてもヘッダをインクルードした翻訳単位それぞれで内部リンケージを持つ別のものとして扱われるため、内部リンケージを持つエンティティが翻訳単位外部から参照されることはありませんでした。

/// header.h

#include <iostream>

int external_f(int);

// 翻訳単位毎に定義される
// それぞれの定義が翻訳単位を超えて参照されることはない
static void internal_f(int n) {
  std::cout << n << std::endl;
} 
/// source.cpp

#include "header.h"

namespace {
  // この翻訳単位の外から呼ぶことはできない
  int internal_g() {
    return 10;
  }
}

int external_f(int n) {
  internal_f(n);
  return n + internal_g();
}

しかしC++20以降のモジュールにおいては、そのインターフェース単位で定義されている内部リンケージ名がそのモジュール(インターフェース単位)をインポートした先の翻訳単位からも参照できてしまいます。

ヘッダファイルとは異なり、モジュールのインターフェース単位は一つの翻訳単位であり、そのインポートはインターフェース単位にあるすべての宣言を(インポートした側から)到達可能にし、エクスポートされている宣言を可視(名前探索で見えるように)にします。この到達可能性は、内部リンケージを持つ名前であってもインポートした側の翻訳単位の宣言に影響を及ぼす可能性があります。

また、モジュールで定義されたテンプレートがインスタンス化されたとき、インスタンス化が発生した地点から定義に至る一連のインスタンス化経路上で可視になっている宣言を参照することができます。このインスタンス化経路は翻訳単位を超えて定義され、そこでは内部リンケージを持つ名前を参照することができます。

/// mymodule.cpp
module;
#include <iostream>
export module mymodule;

// 内部リンケージ名を翻訳単位外から参照できないのは変わらないが
// インポートした側から宣言は到達可能となる
static void internal_f(int n) {
  std::cout << n << std::endl;
}

namespace {
  int internal_g() {
    return 10;
  }
}

// エクスポートされている、外部リンケージ
export inline int external_f(int n) {
  // 外部リンケージを持つ定義内での内部リンケージ名の使用
  // 例えばこの関数がインライン展開されたとき、何が起こる・・・?
  internal_f(n);
  return n + internal_g();
}

直接的に別の翻訳単位にある内部リンケージを持つ名前を参照できるわけではありませんが、間接的に内部リンケージを持つ名前が翻訳単位から露出してしまいます。例えばエクスポートされた関数が使われた先でインライン展開されるとき、内部リンケージの名前が参照されていたらどうするべきでしょうか・・・?

そのため、最終的なC++20モジュール仕様では、内部リンケージを含めた翻訳単位ローカルのエンティティ(TU-local Entities)が翻訳単位の外から間接的にも直接的にも参照されることが禁止されました。内部リンケージ名は実装の整理や分割のために用いられるものであり、そのような実装詳細はモジュールの外部に公開されるべきではない、という判断です。

export module M;

// 内部リンケージの関数
static constexpr int f() { return 0; }

static int f_internal() { return f(); } // 内部リンケージ、OK
       int f_module()   { return f(); } // モジュールリンケージ、OK
export int f_exported() { return f(); } // 外部リンケージ、OK

// 外部orモジュールリンケージを持つinline関数はTU-localエンティティを参照できない
static inline int f_internal_inline() { return f(); } // OK
       inline int f_module_inline()   { return f(); } // ERROR
export inline int f_exported_inline() { return f(); } // ERROR

// constexpr/consteval関数は暗黙inline
static constexpr int f_internal_constexpr() { return f(); } // OK
       constexpr int f_module_constexpr()   { return f(); } // ERROR
export constexpr int f_exported_constexpr() { return f(); } // ERROR

static consteval int f_internal_consteval() { return f(); } // OK
       consteval int f_module_consteval()   { return f(); } // ERROR
export consteval int f_exported_consteval() { return f(); } // ERROR

// 戻り値型に露出しているのも禁止
static decltype(f()) f_internal_decltype() { return 0; } // OK
       decltype(f()) f_module_decltype()   { return 0; } // ERROR
export decltype(f()) f_exported_decltype() { return 0; } // ERROR


namespace {
  struct c_internal {
    int mf();
    int mf_internal_inline() { return f(); } // OK
  };
  int c_internal::mf() { return f(); } // OK
} // namespace

// モジュールリンケージのクラス定義
struct c_module {
  int mf_module();
  int mf_module_inline() { return f(); }  // OK、暗黙inlineではない
};
int c_module::mf_module() { return f(); } // OK

// 外部リンケージのクラス定義
export struct c_exported {
  int mf_exported();
  int mf_exported_inline() { return f(); } // OK、暗黙inlineではない
};
int c_exported::mf_exported() { return f(); } // OK


static int v_internal = f(); // OK
       int v_module   = f(); // OK
export int v_exported = f(); // OK

static inline int v_internal_inline = f(); // OK
       inline int v_module_inline   = f(); // ERROR
export inline int v_exported_inline = f(); // ERROR

struct c_sdm_module {
  static int sdm_module;
  static constexpr int sdm_module_constexpr = f(); // ERROR
};
int c_sdm_module::sdm_module = f(); // OK

より深遠なサンプルコードはP1498R1をご覧ください。ただし、メンバ関数とテンプレートの例は最終的な仕様とは異なります。

このようなことを規格書では、TU-local Entitiesとその曝露(exposure)として表現しています。

TU-local Entities?

TU-localとは、翻訳単位内にローカルな、みたいな意味です。

TU-localとなるエンティティは基本的には内部リンケージなものを指しています。より正確には次のものです

  1. 内部リンケージ名をもつ関数、型、変数、テンプレート
  2. TU-localエンティティの定義内で、ラムダ式によって導入または宣言された、リンケージ名を持たない関数、型、変数、テンプレート
  3. クラスの宣言・定義、関数本体、初期化子、の外側で定義されている名前のない型
  4. TU-localエンティティを宣言するためだけに使用される、名前のない型
  5. TU-localテンプレートの特殊化
  6. TU-localテンプレートを実引数として与えられたテンプレートの特殊化
  7. その宣言が曝露されているテンプレートの特殊化
    • 特殊化は、暗黙的あるいは明示的なインスタンスによって生成される

基本的には1と3が1次TU-localエンティティであり、他はそれによって副次的にTU-localとなっています。つまり、ほとんどの場合に気にすべき対象は内部リンケージ名を持つものです。

7だけは意味が分かりませんが、テンプレートの事を考えるとおぼろげながら浮かんでくるものがあります。後程振り返ることにします。

また、値やオブジェクトは次の場合にTU-localとなります

  1. TU-local関数またはTU-local変数に関連付けられているオブジェクトであるか、そのポインタ型の場合
  2. クラスか配列のオブジェクトであり、そのサブオブジェクト(メンバ、基底クラス、要素)のいずれかがTU-localである
  3. クラスか配列のオブジェクトであり、その参照型の非静的データメンバが参照するオブジェクトまたは関数のいずれかがTU-localである

ややこしいですが、TU-localなものの中にある変数や値はTU-localで、TU-localなものを参照しているものも、それを含むものもTU-localという事です。

説明のため、以降TU-localと言ったらTU-localエンティティとTU-localな値(オブジェクト)両方を指すことにします。ですが、分かり辛かったらTU-local=内部リンケージ名と思っても差し支えありません。

TU-local Entityの曝露(exposure

ある宣言は次の場合にTU-localなものを曝露(exposure)しています

  1. TU-localな値によって初期化されたconstexpr変数を定義する場合
  2. 次の場合を除いて、TU-localエンティティを参照する場合
    1. . 非inline関数、または関数テンプレートの本体
      • TU-localな型が、プレースホルダによる戻り値型で宣言された関数の推定された戻り値型となる場合を除く
    2. . 変数または変数テンプレートの初期化子
    3. . クラス定義内フレンド宣言
    4. . 非volatileconstオブジェクトへの参照、またはodr-useされておらず定数式で初期化された内部リンケージかリンケージ名の無い参照

TU-localなものが曝露されているとはすなわち、TU-localなものが翻訳単位外部から参照できうる場所に現れている事です。

たとえば1のケース、constexpr変数は定数伝播の結果、その初期化子が参照先にインライン展開される可能性があります。それがTU-localな値を使って初期化されている場合、そのTU-localな値が翻訳単位外に曝される可能性があります。

注意としては、TU-localなものを一切含まない宣言は、外部リンケージを持つものであっても何かを曝露しているわけではありません。曝露されているというのはTU-localなものに対してのことで、TU-localなものを含んでいてかつそれが翻訳単位外に曝される可能性がある場合です。

また、TU-localエンティティを曝露(exposure)するのは常にTU-localではないものです。そして、inline関数がTU-localエンティティを参照する場合、常に曝露する事になります。

export module tu_locale.sample;

// TU-localなもの
namespace {
  constexpr int tul_n = 10;

  void tul_f();

  struct tul_C {};
}


// 曝露していない例

export void ext_f() {
  tul_f();  // 暴露していない(条件2-1)
}

export int ext_n = tul_n; // 暴露していない(条件2-2)

export struct S {
  friend tul_C; // 暴露していない(条件2-3)

  friend void mem_f(tul_C); // 暴露していない(条件2-3)
};

export const int& ext_ref = tul_n; // 暴露していない(条件2-4)


// 曝露している例

constexpr int mod_n = tul_n;  // 曝露している

export inline void ext_f() {
  tul_f();  // 曝露している
}

export decltype(tul_n) ext_g();  // 曝露している

この例で示されていることは、exportの有無で変わらないはずです。つまり、外部リンケージとモジュールリンケージの違いでは曝露するかしないかは変化しません。

TU-local Entityを曝露してたら・・・

モジュールインターフェース単位(プライベートモジュールフラグメントの外側)、あるいはモジュールパーティションにおいて、TU-localではない宣言(あるいはそのための推論補助)がTU-localエンティティを曝露している場合、コンパイルエラーとなります。

TU-localな宣言が単にモジュールのインターフェース単位に書いてあるだけではコンパイルエラーとはなりません。それらの宣言が別の宣言を介して翻訳単位の外から参照される 可能性がある 場合にコンパイルエラーとなります。実際に参照されたときではなく、参照することができる状態になっていたらエラーです。

モジュール単位も一つの翻訳単位をなすため、あるモジュール単位のTU-localなものは同じモジュール内の他の翻訳単位に対しても曝露されてはなりません。

ただし、非モジュールなところ(グローバルモジュール)、あるいはモジュール実装単位(notパーティション)においては、この事は単に非推奨とされコンパイルエラーとはなりません。

そしてもう一つ、ある宣言が、ヘッダユニットではない別の翻訳単位のTU-localエンティティを参照する場合もコンパイルエラーとなります。

こちらの条件はモジュールであるかどうかにかかわらずすべての所に適用されます。ヘッダユニットが例外として除かれているのは、#includeから置換されたときでも後方互換を損ねないようにするためだと思われます。つまりほとんど、モジュールをインポートした時にインポート先にあるTU-localエンティティを参照することを禁ずるものです。

/// mymoudle.cpp
export module mymodule;

static int f() { /* ... */ }
/// main.cpp
import mymodule;
// f()が到達可能となる

int f();  // f()が可視になる

int main() {
  int n = f();  // NG!
}

まとめると、次のどちらかの場合にTU-localエンティティを参照することはコンパイルエラーとなります

  1. ヘッダユニットを除いたインポート可能なモジュール単位において、TU-localではない宣言(あるいはそのための推論補助)がTU-localエンティティを曝露している
  2. ある宣言が、ヘッダユニットではない別の翻訳単位のTU-localエンティティを参照している

テンプレート

TU-localエンティティを曝露してはならないのはテンプレートも同様です。しかし、テンプレートがTU-localエンティティを曝露するのかどうかはインスタンス化されるまでわかりません。そのため、テンプレートがTU-localなものを曝露しているかの判定はテンプレートがインスタンス化される時まで延期されます。

そして、インスタンス化される時、以下のどちらかに該当する場合にコンパイルエラーとなります。

  1. 現れる名前が内部リンケージ名である
  2. 関数名のオーバーロード候補集合に内部リンケージ名が含まれている
/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1);  // (1)

export template<typename T>
void f(T t);  // (2)

// 宣言はOK
export template<typename T>
void external_f(T t) {
  f(t);
}
/// main,cpp
import mymodule;

struct S2{};

void f(S2);  // (3)

int main() {
  S1 s1{};
  S2 s2{};

  external_f(10);  // OK、(2)を呼ぶ
  external_f(s2);  // OK、(3)を呼ぶ
  external_f(s1);  // NG、(1)を呼ぶ
}

勘のいい人は引っかかるかもしれません。さっきと言ってたこと違わない?と

  • 次の場合を除いて、TU-localエンティティを参照する場合
    • inline関数、または 関数テンプレートの本体

これはインスタンス化が発生する前は非依存名であっても、とりあえず内部リンケージを参照する式を書いてもいいよ、という事を言っているにすぎません。インポートした先でインスタンス化が発生したとき、そこでの名前解決の結果、あるいはオーバーロード候補集合にインポート元の内部リンケージなものが含まれているとエラーになります。これはどうやら、TU-localなものの曝露とは別ルートの規定の様です。

明示的インスタンス

テンプレートがその翻訳単位で明示的インスタンス化されていれば、本体で内部リンケージ名を参照していてもコンパイルエラーにはなりません。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
void external_f(T t) {
  f(t);
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1);
/// main,cpp
import mymodule;

int main() {
  S1 s1{};

  external_f(s1);  // OK
}

この様な場合、普通の関数をその宣言によって参照しているのと同じとみなすことができます。どうやら、インポート元に明示的インスタンス化の定義がある場合、インポートした側ではそれに対応する特殊化の暗黙的インスタンス化は発生しない様です(明確に発生しないとされているわけではないですが)。

これを踏まえると、先ほどのTU-localなものの中の条件に羅列されていた謎が一つ解決されます

  • その宣言が曝露されているテンプレートの特殊化
    • 特殊化は、暗黙的あるいは明示的なインスタンスによって生成される

つまりは、TU-localなものを曝露しているテンプレートがその翻訳単位で明示的インスタンス化されているとき、それに対応する特殊化もまたTU-localとなり、それを曝露することも禁止です。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
void external_f(T) {
  f();
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1);

export void g() {
  S1 s1{};
  f(S1);  // NG、TU-localな特殊化external_f<S1>()の曝露
}

なお、この明示的インスタンス化による例外は、inlineとマークされている関数テンプレートでは無効です。inlineの示すとおりにインライン展開された場合、結局その定義がインポートした側に展開されることになるためです。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
inline void external_f(T t) {
  f(t);
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1);
/// main,cpp
import mymodule;

int main() {
  S1 s1{};

  external_f(s1);  // NG!
}

メンバ関数の暗黙inline

ここまで見てきたようにおおよそinline関数(テンプレート)においては、TU-localなものの曝露がごく簡単に起きます。それで困ってしまうのが、クラスのメンバ関数が暗黙的にinlineになることです。

export module mymodule;

static void internal_f();

// モジュールリンケージのクラス定義
struct c_module {

  // inlineではない
  int mf_module();

  // 暗黙inline
  int mf_module_inline() { 
    return internal_f();  // NG、内部リンケージ名を曝露している
  }
};

int c_module::mf_module() { 
  return internal_f();  // OK、曝露していない
}

モジュール内でいつものようにクラスを書いたとき、ヘッダファイルからモジュールへ移行するとき、この様なエラーに遭遇する可能性は非常に高いでしょう。これを回避しようとすると、内部リンケージ名を使用しているメンバ関数は全てクラス外で定義しなければなりません。明らかに冗長な記述が増え、とても面倒くさいです・・・

C++20の最終仕様では、モジュール内でのみメンバ関数の暗黙inline化は行われなくなります。これによって、クラスの定義は今まで通りに行うことができるようになります。

export module mymodule;

static void internal_f();

// モジュールリンケージのクラス定義
struct c_module {

  // inlineではない
  int mf_module();

  // inlineではない
  int mf_module_inline() { 
    return internal_f();  // OK、曝露していない
  }

  // inliine
  inline int inline_f() {
    // ...
  }
};

int c_module::mf_module() { 
  return internal_f();  // OK、曝露していない
}

暗黙のinlineが行われない事によってインライン展開がされなくなり、パフォーマンスが低下する可能性は無くもありません。inlineが必要な場合は明示的に指定するようにしましょう。

なお、ここでのモジュールにはグローバルモジュールを含んでいません。モジュールの外ではこれまで通りにメンバ関数は暗黙inlineです。

さんぷるこーど

規格書より、複雑な例。

/// A_interface.cpp(プライマリインターフェース単位)
export module A;

static void f() {}

inline void it() { f(); }           // error: fを曝露している
static inline void its() { f(); }   // OK

template<int>
void g() { its(); }   // とりあえずはOK、これはモジュールリンケージ
template void g<0>();

decltype(f) *fp;                    // error: fはTU-local(fの型ではない)
auto &fr = f;                       // OK
constexpr auto &fr2 = fr;           // error: fを曝露している(fのアドレスはTU-localな値)
constexpr static auto fp2 = fr;     // OK

struct S { void (&ref)(); } s{f};               // OK, 値(fのアドレス)はTU-local
constexpr extern struct W { S &s; } wrap{s};    // OK, 値(sのアドレス)はTU-localではない

static auto x = []{f();};           // OK
auto x2 = x;                        // error: decltype(x)を曝露している(クロージャ型はTU-local)
int y = ([]{f();}(),0);             // error: fを曝露している(クロージャ型はTU-localではない)
int y2 = (x,0);                     // OK

namespace N {
  struct A {};
  void adl(A);
  static void adl(int);
}
void adl(double);

inline void h(auto x) { adl(x); }   // OK, ただしその特殊化はN::adl(int)を曝露しうる
/// A_impl.cpp(実装単位)
module A;
// Aのインターフェースを暗黙的にインポートしている

void other() {
  g<0>();                   // OK, 特殊化g<0>()は明示的にインスタンス化されている
  g<1>();                   // error: 特殊化の実体は、TU-localなits()を使用している
  h(N::A{});                // error: オーバーロード候補集合はTU-localなN::adl(int)を含んでいる
  h(0);                     // OK, adl(double)を呼ぶ
  adl(N::A{});              // OK; N::adl(N::A)を呼び、N::adl(int)は見つからない
  fr();                     // OK, f()を呼ぶ
  constexpr auto ptr = fr;  // error: frは定数式で使用可能ではない
}

テンプレートの例。

/// moduleM.cpp
export module M;

namespace R {
  export struct X {};
  export void f(X);
}
namespace S {
  export void f(R::X, R::X);  // (1)
}
/// moduleN.cpp
export module N;
import M;

export R::X make();

namespace R {
  static int g(X);  // (2)
}

// 宣言まではOK
export template<typename T, typename U>
void apply(T t, U u) {
  f(t, u);  // (1)を参照、OK
  g(t);     // (2)を参照、内部リンケージ名の曝露、NG
}
/// main.cpp
module Q;
import N;

namespace S {
  struct Z { 
    template<typename T>
    operator T();
  };
}

int main() {
  auto x = make();  // OK、decltype(x)はR::Xでmodule Mにあり、可視ではないが名前を参照していない

  apply(x, S::Z()); // NG、S::fはインスタンス化コンテキストで可視
                    // しかし、R::gは内部リンケージであり、翻訳単位の外からは呼べない
}

モジュールにおけるinlineの意味

これらの変更によってモジュールにおけるinlineはある意味で本来の意味と役割を取り戻します。すなわち、inlineとマークされた関数のみをインライン展開するという意味になり、その他の効果はほぼ意味をなさなくなります。

意味をなさなくなるというのはinlineの持つ定義の唯一性などの効果がなくなるわけではなく、モジュールにおいてはその意味がなくなるということです。例えば、モジュールでエクスポートされているinline関数・変数はインポートされた側から可視かつ到達可能となり参照できるようになりますが、そこでは#includeの時のように定義が翻訳単位ごとにコピペされる事はないので、inlineの定義を一つに畳み込む効果は必要ありません。

モジュール内部ではODRが厳しくなっており、モジュール内で定義されたinline関数の定義はただ一つでなくてはならず、参照する場合はその定義に到達可能となっている必要があります。このことにリンケージは関係なく、1つのモジュール内においてもinlineはインライン展開のためのコンパイラへの口添え以外の意味を持っていません。

ただし、ここでモジュールと言っているものにグローバルモジュールは含んでいません。すなわち、モジュールの外側では従来と変わりありません。

また、モジュールにおいてinlineと宣言されていない関数をインライン展開してはいけないという規定はありません。どうやらそのような実装を可能にするために意図的に空白を設けているようです。

モジュールリンケージ

これらの変更によって、内部リンケージ名はモジュールのABIの一部とはなることは無くなり、完全に翻訳単位内に隠蔽されるようになります。

一方、モジュールリンケージ名はそうではなく、エクスポートされたinline関数の内部など、使用される場所によってはモジュールのABIの一部となる事があります。

export module M;

// 内部リンケージ
static void internal_f() { /* ... */ }

// モジュールリンケージ
void module_f() { /* ... */ }

export inline void ng() {
  internal_f();  // NG
}

export inline void ok() {
  module_f();    // OK
}

なおどちらもAPIの一部となることはありません。

わからん、3行で

モジュールでは内部リンケージ名を
inline関数や関数テンプレートから
参照するのはやめましょう

参考文献

この記事のMarkdownソース

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

文書の一覧

全部で36本あり、SG22(C/C++相互互換性に関する研究グループ)のCの提案を除くと32本になります。

採択された文書

P2313R0 Core Language Working Group "tentatively ready" issues for the February, 2021 meeting

2月の会議で採択されたコア言語のIssue解決の一覧。

解決されたIssueは一件だけです。

  1. 2470. Multiple array objects providing storage for one object
    • unsigned char/std::byteの配列上に別のオブジェクトを構築する時、新しく作成されるオブジェクトにどの配列がストレージを提供したのかが曖昧にならないようにした。

その他文書

N4884 WG21 2021-02 Virtual Meeting Minutes of Meeting

2021年2月22日(米国時間)に行われた、WG21全体会議の議事録。

CWG/LWG/LEWGの投票の様子などが記載されています。

N4885 Working Draft, Standard for Programming Language C++

C++23ワーキングドラフト第4弾。

N4886 Editors’ Report - Programming Languages - C++

↑の変更点をまとめた文書。

2月の会議で採択された提案とコア言語/ライブラリのIssue解決が適用されています。

P0448R4 A strstream replacement using span as buffer

長い間非推奨のまま代替手段のなかったstd::strstreamの代替となるstd::spanによるspanstreamを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのレビューでのフィードバックに基づいて、提案する文言を調整したことです。

この提案は既にこのリビジョンのLWGでのレビューと投票を終えており、全体会議での投票待ちをしています。そこで反対が無ければ、C++23に導入されます。

P0958R3 Networking TS changes to support proposed Executors TS

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

以前の記事を参照

このリビジョンでの変更は、executorコンセプトについてsatisfiesを使用していた所をmodelへ変更したことや、timersocketなどにbasicプリフィックスを加えたことなど、文面の調整です。

P1018R9 C++ Language Evolution status 🦠 pandemic edition 🦠 2021/01–2021/03 - 2021/01–2021/03

EWG(コア言語への新機能追加についての作業部会)が2021/01–2021/03の間に議論した提案やIssueのリストや将来の計画、テレカンファレンスの状況などをまとめた文書。

これらの提案はコンセンサスが得られ、CWGに転送されています。また、その議論や投票の際の賛成・反対のコメントが記載されています。

また、次の提案はライブラリ機能についてのものですが、その内容を言語サポートとすべきかが投票にかけられました。

Executorのプロパティ指定の方法は独立したライブラリ機能として提案されていますが、これを言語サポートする方向でコンセンサスが得られたようです。

P1315R7 secure_clear

特定のメモリ領域の値を確実に消去するための関数secure_clear()の提案。

以前の記事を参照

このリビジョンでの変更は、C言語に向けた文言の表現の選択肢の改善や、選択されなかったものの削除、C/C++委員会での投票結果の記載などです。

P1425R4 Iterators pair constructors for stack and queue

std::stackstd::queueに、イテレータペアを受け取るコンストラクタを追加する提案。

以前の記事を参照

このリビジョンでの変更は、2つに分かれていた機能テストマクロを__cpp_lib_adaptor_iterator_pair_constructor一つに統一した事と、提案する文言の調整です。

このリビジョンは既にLWGのレビューと投票が済んでおり、次の会議の全体投票を待っています。

P1518R1 Stop overconstraining allocators in container deduction guides

P1518R2 Stop overconstraining allocators in container deduction guides

コンテナとコンテナアダプタのクラステンプレート引数推論時の振る舞いを修正し、pmrコンテナの初期化を行いやすくする提案。

std::pmr::monotonic_buffer_resource mr;
std::pmr::polymorphic_allocator<int> a = &mr;
std::pmr::vector<int> pv(a);

// CTADを使用しない構築、全てok
auto s1 = std::stack<int, std::pmr::vector<int>>(pv);
auto s2 = std::stack<int, std::pmr::vector<int>>(pv, a);
auto s3 = std::stack<int, std::pmr::vector<int>>(pv, &mr);

// CTADを使用する構築
auto ds1 = std::stack(pv);
auto ds2 = std::stack(pv, a);
auto ds3 = std::stack(pv, &mr); // NG!

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

※CTAD = Class Template Argument Deduction (クラステンプレートの実引数推定)

stackをはじめとするコンテナアダプタのアロケータ引数は、クラステンプレートのテンプレートパラメータの推論に寄与しません。従って、対応するCTADを使用しない構築の時と同様にコンテナ型からの推論を行うのが望ましいはずです。

namespace std {

  template<typename Container, typename Allocator>
  class stack;

  // stackの2引数推論補助
  template<class Container, class Allocator>
  stack(Container, Allocator)
    -> stack<typename Container::value_type, Container>;
}

このことは何か意図があってのものではなく、単に見落とされただけだと思われるので修正しようとする提案です。

なぜこのようなことが起こるのかというと、コンテナアダプタの推論補助について次のような規定が存在しているためです。

A deduction guide for a container adaptor shall not participate in overload resolution if any of the following are true: - ... - It has an Allocator template parameter and a type that does not qualify as an allocator is deduced for that parameter. - ...

[N4861 container.adaptors.general]/4.4

アロケータとしての資格のない型がアロケータ型として渡されている場合、そのアロケータ型がテンプレートパラメータの推論に寄与しないとしても、アロケータ型を受け取る推論補助を無効化してしまいます。

アロケータとしての資格がある型というのは実装定義ですが、最小の要件の一つとして、アロケータ型Aについてメンバ型A::value_typeが利用可能であることがあります([container.requirements.general]/17)。

auto ds3 = std::stack(pv, &mr); // NG!

先程の例のここでは、第二引数の&mrの型はstd::pmr::monotonic_buffer_resource*というポインタ型であって、当然メンバ型を持っておらず、アロケータとしての資格がある型ではないため対応する推論補助は考慮されなくなります。ただ一方で、std::pmr::vector<int>std::pmr::monotonic_buffer_resource*をアロケータとして利用して構築することができます。

さらに、似た問題がstd::vectorそのものにも存在しています。

std::pmr::monotonic_buffer_resource mr;
std::pmr::polymorphic_allocator<int> a = &mr;
std::pmr::vector<int> pv(a);

// CTADによらない構築、全てok
auto v1 = std::vector<int, std::pmr::polymorphic_allocator<int>>(pv);
auto v2 = std::vector<int, std::pmr::polymorphic_allocator<int>>(pv, a);
auto v3 = std::vector<int, std::pmr::polymorphic_allocator<int>>(pv, &mr);

// CTADを使用する構築
auto dv1 = std::vector(pv);
auto dv2 = std::vector(pv, a);
auto dv3 = std::vector(pv, &mr);  // NG!

ここでの問題は先ほどとは少し違っていて、暗黙に生成される推論補助を利用する経路で問題が起きています。

推論補助が無い場合、対応するコンストラクタから推論補助を生成してテンプレートパラメータを推論しようとします。ここで対応しているコンストラクタはアロケータを受け取るコピーコンストラクタです。

namespace std {

  template<typename T, typename Allocator>
  class vector {

    // アロケータを受け取るコピーコンストラクタ
    vector(const vector<T, Allocator>&, const Allocator&);
  };
}

そして、第1引数からはT = int, Allocator = std::polymorphic_allocator<int>が導出され、第2引数からはAllocator = std::pmr::monotonic_buffer_resource*が導出されます。同一のパラメータに対して衝突する候補が発生しているので、推論は失敗しコンパイルエラーとなります。

この提案ではこれらの解決のために、すべてのコンテナのアロケータを受け取るコンストラクタ引数をstd::type_identity_tで包むことでアロケータ引数をCTAD推論の対象から外し、またコンテナアダプタのアロケータ型がある場合の推論補助の要件を「コンテナ型が無く、アロケータ型がある場合」のように少し緩和します。

namespace std {

  template<typename T, typename Allocator>
  class vector {

    // 現在
    vector(const vector<T, Allocator>&, const Allocator&);

    // この提案
    vector(const vector<T, Allocator>&, const type_identity_t<Allocator>&);
  };
}

void stack() {
  std::pmr::monotonic_buffer_resource mr;
  std::pmr::polymorphic_allocator<int> a = &mr;
  std::pmr::vector<int> pv(a);

  // この提案の後では、共にOK
  auto ds3 = std::stack(pv, &mr);
  auto dv3 = std::vector(pv, &mr);
}

std::vectorの場合は、std::type_identity_tによって第2引数の&mrからAllocatorパラメータを推論しなくなるのでAllocatorが一つに定まるようになり、std::stackの場合は文言の変更によって&mrから推論されるアロケータ型の適格性がチェックされなくなるので既存の推論補助によって正しく推論が行われるようになります。

この提案の内容は標準ライブラリの主要な3実装がそれぞれ、誤って 先行して実装しているようです。特に連想コンテナはMSVCとClang(と一部GCCも)すでにこうなっているようです。

P1875R2 Transactional Memory Lite Support in C++

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

以前の記事を参照

変更履歴が無いため変更点はよくわかりませんが、おそらくP2066の議論の進行に伴って必要となった変更などを反映したのだと思われます。

P2025R2 Guaranteed copy elision for return variables

NRVO(Named Return Value Optimization)によるコピー省略を必須にする提案。

以前の記事を参照

この提案は主に明示的に注釈することで(N)RVOをオプトインする構文を追加するために、EWGに差し戻されました。

このリビジョンでは、その検討のセクションおよびABIレベルのコピー省略の問題についての説明のセクションを追加しています。

そこでは、[[nrvo]]属性や変数宣言時のreturn注釈、return explicit文、関数宣言(定義)のreturn指定など、様々な構文が検討されています。

P2041R1 template = delete

関数テンプレートがdeleteオーバーロードを提供可能なように、クラス/変数テンプレートでもdelete指定オーバーロード(特殊化)を提供できるようにする提案。

複数の事を意味する同じ名前があり、それらのうちのいくつかは定義されるべきではないとき、現在はその定義を禁止するシンプルで簡易な方法がありません。変数・クラステンプレートについてdelete指定できるようにすることで、一部の特殊化を禁止したり、逆に一部の特殊化だけを許可したりすることができるようになります。

// プライマリ変数テンプレートはdelete、特殊化は許可
template<typename>
int x = delete;
template<>
int x<int> = 5;

// 変数テンプレートの特定の特殊化を禁止
template<typename T>
auto y = T();
template<>
auto y<int> = delete;

// プライマリクラステンプレートはdelete、特殊化は許可
template<typename>
struct s = delete;
template<>
struct s<int> {
};

// クラステンプレートの特殊化を禁止
template<typename>
struct t {
};

template<>
struct t<int> = delete;

非テンプレートのクラスなどでは、deleteするクラスを定義するのではなく最初から定義しないようにすれば同じ効果が得られます。そのため、ここではテンプレートではないものまでdelete指定できるようにすることは提案されていません。

P2066R6 Suggested draft TS for C++ Extensions for Minimal Transactional Memory

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

以前の記事を参照

このリビジョンの変更点は、R5で標準ライブラリのもののほとんどがatomicブロックでの使用を許可されましたが、そのうち同期の問題が発生しうるものを除外した事です。例えば、shared_ptrsynchronus_memory_resourceタイムゾーンのデータベースなどが該当します。

P2093R5 Formatted output

p2093r4.html)

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

前回の記事を参照

このリビジョンでの変更は、ostreamを取るオーバーロードをヘッダ分けしたことと、ostreamを取らないものの配置するヘッダの候補をリスト化した事です。現在は<io>に配置することを提案しています。

この提案はSG16での議論とレビューを終えて、LEWGに送られました。非常に素早く作業が進行しているため、C++23に入る可能性は高そうです。

P2210R2 Superior String Splitting

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

前回の記事を参照

このリビジョンでの変更は、現在のviews_splitlazy_split)にある問題の解決を含んだうえで文言を調整し、実装例を追記した事です。

この提案はLEWGでの議論が完了しLWGに転送する最終投票を待っていますが、C++20にDRとして適用するためにLWGで先行してレビューが完了しています。LEWGでの投票が問題なく終われば、C++23(C++20)導入に向けて全体会議での投票に進みます。

P2242R2 Non-literal variables (and labels and gotos) in constexpr functions

constexpr関数において、コンパイル時に評価されなければgotoやラベル、非リテラル型の変数宣言を許可する提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロに関する議論を追記したことと、サンプルと文言の微修正です。

この提案の修正は実際にはconstexpr関数で実行可能なものを増やしているわけではありませんが、__cpp_constexprの値を微増させています。検出が必要になるとは思えないけれどconstexprの許容範囲を広げるときに値を更新する方向性を支持している、ということが説明されています。

P2266R1 Simpler implicit move

return文における暗黙のムーブを改善する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言のリファクタリングと、ラムダの内部でローカル変数がスローされるときにも、スローされる変数を暗黙ムーブするかのような文言になっている部分を修正する文言を追加した事です。

P2299R0 mdspan and CTAD

提案中のstd::mdspanのCTAD対応についての問題を報告する文書。

P0009R10で提案されているstd::mdspanは、std::basic_mdspanを基礎として、そのエイリアスとしてstd::mdspanを定義しています。

template <class ElementType, ptrdiff_t... Extents>
using mdspan = basic_mdspan<ElementType, extents<Extents...>>;

これは例えば次のように利用できます

int main() {
  // 何かメモリ領域
  double* data = ...;

  // 静的サイズ
  mdspan<double, 64, 64> a1(data);
  // 動的サイズ
  mdspan<double, dynamic_extent, dynamic_extent> a2(data, 64, 64);
  // 静的+動的サイズ
  mdspan<double, 64, dynamic_extent> a3(data, 64);
}

C++20からはエイリアステンプレートに対するCTADが利用可能となっているので、上記の2つめの例は次のように書けるはずです。

int main() {
  // 何かメモリ領域
  double* data = ...;

  // 動的サイズ
  mdspan a2(data, 64, 64);
}

冗長なdynamic_extent指定を排除し非常に読みやすくなります。しかし、これはどうやら意図通りに動かないようです。

ここでは、std::mdspan<double>を推論した後、std::basic_mdspan<double, extents<>>を推論しインスタンス化します。これはstd::basic_mdspanの動的エクステントのコンストラクタでstatic_assertに引っかかりコンパイルエラーを起こします。

単純には、std::mdspanに推論補助を追加すればこの問題の解決は図れるはずですが、現在の仕様ではエイリアステンプレートに推論補助を追加できません。かといって、std::mdspanを単独のクラスにしてしまうとstd::basic_mdspanとの相互運用性がなくなるなど様々な問題が発生します。

この提案はこの問題の周知を図り、よりよい解決策を募るものです。

P2314R1 Character sets and encodings

規格文書中の ~ character setという言葉を明確に定義し直す提案。

以前の記事を参照

このリビジョンでの変更は、文言の修正とP2297R0との目的の差異を説明するセクションの追加、ロケール依存の実行文字集合execution (wide) character set)の文言の場所をライブラリに移動した事です。

この提案は、既存の振る舞いを維持しながら、言葉の定義を明確にすることで字句解析におけるコア言語のルールの再構築を目指すものです。従って、EWGの負担は軽くなる筈、という事が説明されています。

P2322R1 ranges::fold

rangeアルゴリズムであるranges::foldの提案。

以前の記事を参照

このリビジョンでの変更は、以前にregular_invocableコンセプトを用いていたところをinvocableコンセプトに変更した事(regularであることは必要なかったため)と、以前にfold_firstなどとしていた初項を範囲から補う関数ファミリを初項の提供有無でのオーバーロードに変更したことです。

以前に書いたサンプルは次のようになります。

std::vector<int> vec = {1, 2, 3, 4, 5};

int sum1 = std::ranges::fold(vec, 0, std::ranges::plus{});
int sum2 = std::ranges::fold(vec, std::ranges::plus{});
// sum1 == sum2 == 15

std::vector<std::string> vec2 = {"aaa", "bbb", "ccc"};
std::string concat1 = std::ranges::fold(vec2, std::ranges::plus{});
// concat1 == "aaabbbccc"

std::string concat2 = std::ranges::fold_right(vec2, std::string{}, std::ranges::plus{});
std::string concat3 = std::ranges::fold_right(vec2, std::ranges::plus{});
// concat2 == concat3 == "cccbbbaaa"

P2325R1 Views should not be required to be default constructible

Viewとみなされる型にデフォルト構築可能性を要求しない様にする提案。

以前の記事を参照

このリビジョンでの変更は、パイプラインでの左辺値/右辺値の固定エクステントstd::spanの扱いに関する例を追加した事です。

std::spanviewであるはずなのですが、固定エクステントの場合はデフォルト構築できないようになっています。これによって、右辺値と左辺値でそれぞれ異なる、非効率な結果が得られます

std::span<int, 5> s = /* ... */;

// spanはviewではないため、transform_viewはコピーする代わりに`ref_view<span<int, 5>>`を保持する
// sの寿命が先に尽きる場合、これはダングリング参照となる
auto lvalue = s | views::transform(f);

// spanはborrowed_rangeであるため、これはコンパイル可能
// ただ、sをコピーするのではなく、subrange<span<int, 5>::iterator>を保持する
auto rvalue = std::move(s) | views::transform(f);

左辺値の場合はダングリングの危険があり、追加の間接参照が必要となります。右辺値の場合はダングリングの危険はないものの、2つのイテレータを保持する必要からspanを直接保持する場合の倍のストレージを使用します。この値カテゴリの違いによる異なった振る舞いはどちらにせよ非効率で、固定エクステントspanviewだった場合のデメリットよりもひどいものです。

固定エクステントのspanは正当な理由でデフォルトコンストラクタを持たず、viewにするためにデフォルトコンストラクタを構築するとユーザーが気を付けてチェックしなければならない追加の状態が導入されます。spanの全ての操作に文書化の必要がある事前条件が課されることになり、これはまたすべてのviewにも当てはまっています。

この観点からもやはり、viewコンセプトのデフォルト構築要求は不要であるといえます。

P2328R0 join_view should join all views of ranges

std::ranges::join_viewの制約を緩和して、prvalueviewではないrangeを平坦化できるようにする提案。

join_viewrangerangeとなっているシーケンスを1つのrangeに平坦化するもので、他のところではflattenとも呼ばれています。

ここに1つ機能を加えて、要素(内側のrange)を変換しつつ平坦化するflat_mapというものがあります。そのまま書くと、C++20時点では次のようになるはずです。

template<std::ranges::range R, std::invocable<std::ranges::range_value_t<R>> F>
  requires std::ranges::range<std::ranges::range_value_t<R>> and
           std::ranges::range<std::invoke_result_t<F, R>>
auto flat_map(R&& r, F&& f) {
  return r | std::views::transform(f) | std::views::join;
}

このようなrangeアダプタのチェーンはほとんどの場合にコンパイルエラーとなります。

現在のjoin_viewが平坦化できるのは次の2つのどちらかです。

  • glvaluerangerange
  • prvalueviewrange

r | std::views::transform(f)の結果はfの結果のrangeによるprvalueviewとなり、fの結果のrangeprvalueviewとならない場合にコンパイルエラーとなります。少し考えてみると、これはかなり一般的なユーズケースであることがわかると思います(fの結果として範囲を返すとき、std::vectorを使いたくなりませんか?)。

現在のjoin_viewprvalueな(viewではない)rangerangeサポートが欠けていることによってこの問題は発生しています。

Range-v3ライブラリでは、views::cache1(提案されている名前はviews::cache_latest)というviewを間に挟むことでこの問題を解決し、views::flat_mapを導入しています。

cache1は元のrangeイテレータの間接参照結果をキャッシュとして持っておくことによって、prvaluerangerangeglvaluerangerangeに変換するものです。これによってjoin_viewはあらゆるケースで平坦化できるようになります。

ただ、cache1cache_latest)にはいくつかの問題があります

  • イテレータoperator*() conststd::indirectly_readbleコンセプトで要求される)は内部でキャッシュを操作するため、スレッドセーフではない。
    • 標準ライブラリのconstメンバ関数はスレッドセーフであることを表明している。
  • cache1の理解や発見は自明ではない。
    • なぜそれが必要なのかのヒントはなく、join_viewの使用を通してその存在の必要性を納得するしかない。

現在のjoin_viewは、prvalueviewrangeを処理する際に、内側のprvalueviewを内部でキャッシュしておくことによって処理しています。この提案では、cache_latestを導入する代わりに、このキャッシュ機構をprvalueな(viewではない)rangerangeに対しても行うことで上記の問題の解決を図ります。

ただし、この場合のキャッシュは伝播しません。すなわち、そのようなキャッシュを保持しているjoin_viewをコピー/ムーブすると、コピー/ムーブ先ではキャッシュは空となります。これによって、join_viewのコピー/ムーブが元のrangeの生成するものに依存しないことが保証されています。また、このようなキャッシュをしている場合のjoin_viewinput_rangeであり、begin()の呼び出しは1度しか行えません(最初のbegin()の呼び出し後にrangeとして使用不可能となる)。

P2330R0 WG21 2021-02 Virtual Meeting Record of Discussion

2月のWG21本会議における発言記録。

コア言語に関して

  • 昨年11月の本会議で採択されたP2238R0にあるコア言語IssueがC++20に対するDefect Report(DR)として扱うことを決定。
  • P1787R6の内容をDRとして扱うことを決定(バージョンは指定されていない)。
  • P2313R0の内容をDRとして扱うことを決定(バージョンは指定されていない)。

ライブラリ機能に関してはP0533R7 constexpr for <cmath> and <cstdlib>(リンクはR6)がリジェクトされた過程が記載されています。

それによれば、精度低下によるエラーが定数評価を妨げる可能性があるという問題提起があり、その解決策について十分に議論が尽くされておらず、このままだと実装が困難となるか実装間で相違が発生する可能性があるという点が懸念され、投票の結果反対および中立が多く出たため、リジェクトされたようです。

P2332R0 Establishing std::hive as replacement name for the proposed std::colony container

提案中のstd::colonyの名前をstd::hiveに変更する提案。

LEWGの議論の過程で、std::colonyという名前に関して何人かのメンバが次のような疑問を抱いているようです

  • colonyという単語は多くの意味を含んでいる
  • colonyという名前は馴染みがなく、一般的では無い

色々な名前の候補が上がった結果、hive(ミツバチの巣)が選ばれたようです。これにはLEWGの多くのメンバとstd::colonyの作者(著者)の方も同意を示しているようです。

colony(集団・居住地・村など)のお気持ちは

  • コンテナそのものがコロニー
  • 記憶ブロック(配列)が家
  • 要素の記憶域が部屋
  • 要素は人
  • 人の行き来(要素の挿入・削除)に対して家や部屋に変化はない(記憶域と配列は安定)

hiveのお気持ちは

  • コンテナは巣箱
  • 1記憶ブロック(配列)が1つの巣板
  • 要素の記憶域は6角形のセル
  • 要素はミツバチ

hiveではミツバチがセルに住んでいる訳ではないことから要素の安定性に関しての表現が足りていないとのことですが、LEWGでは名前の意味の単純化のために許容されたようです。

また、std::colonyが力を発揮する用途がイテレーション時に要素の挿入・削除が頻繁に起こるようなケースであり、蜂の巣の出入りの忙しさがこの側面を表現しているとして好まれたようです。

P2333R0 2021 Winter Library Evolution Poll Outcomes

2021年の冬(1月から3月にかけて)に行われた、LEWGの全体投票の結果。

以下の9つの提案が投票にかけられ、どれもLWGに転送されることが可決されています。

P2334R0 Add support for preprocessing directives elifdef and elifndef

#elifでマクロの定義の有無で条件分岐する糖衣構文となるプリプロセッシングディレクティブである#elifdef/#elifndefの提案。

#ifdef/#ifndef#if defined(macro_name)/#if !defined(macro_name)の糖衣構文として随分前から利用可能ですが、#elif defined(macro_name)/#elif !defined(macro_name)に対応する糖衣構文はありません。

このような構文の一貫性のなさは、一部のユーザーにとっては予測可能ではありません。

#elifdef/#elifndefを追加し一貫性を改善することで、ユーザビリティの向上を図る提案です。

#ifdef M1
...
#elif defined(M2)
...
#endif

// ↑が↓こう書ける

#ifdef M1
...
#elifdef(M2)
...
#endif

この提案はすでにC23に導入されており、C/C++間の互換性確保のためにC++に対しても提案されているものです。

P2338R0 Freestanding Library: Character primitives and the C library

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

C/C++の標準ライブラリにはシステムプログラムにおいて有用な機能が多数存在していますがそれらのライブラリ機能は必ずしもフリースタンディング指定されていないため、コンパイラ拡張などの移植性の乏しい形で利用するか、自分で実装するしかない場合が多いようです。

そのようなライブラリ機能のうち、OSのサポートや動的メモリ確保を必要とせずに実装できるものをフリースタンディングライブラリとして提供することによって、より高レベルにシステムプログラムを記述し、かつそれを移植可能にすることができます。

この提案は、フリースタンディングライブラリをOSや動的なメモリ確保に依存しない標準ライブラリの最大のサブセットとして提供することを目的とした一連の取り組みの一環です。

この提案でフリースタンディング指定を提案されているのは次のものです

  • <string>
    • std::char_traits
  • <charconv>
  • <cinttypes>
  • <cstdlib>, cmath
  • <cstring>
  • <cwchar>
  • <cerrno>, <system_error>
    • errnoを除く各種エラー定義マクロとstd::errc

この提案は同時にC標準に対しても行われています。

P2339R0 Contract violation handlers

契約プログラミングについて、契約違反を起こしたときに終了するのではなく継続する場合のユースケースについて説明した文書。

C++20でリジェクトされたコントラクト仕様にも、現在議論されているものにも、契約違反が起きた時のデフォルトの対応はstd::terminate()std::abort()を実行してその場で終了することです。

しかし、契約違反が起きた時でも実行を継続したい場合があり、C++20の仕様には違反継続モードがあり、現在の議論では例外を投げる、return;するなどが議論されているようですが、どちらも反対意見があるようです。

この文書はC++23の契約プログラミング導入にむけて、契約違反時に実行を継続するユースケースやその利点についてSG21内で共通の理解を得るためのものです。

P2340R0 Clarifying the status of the ‘C headers’

現在非推奨とされているCヘッダを相互運用の目的で使用可能なようにする提案。

ISO規格の文脈での非推奨(deprecated)という言葉は、使用されない・推奨されない・将来削除されうる、などの意味を持ちます。C++が規格化されて以降、Cのヘッダは非推奨という扱いで規定されており、C++のエコシステムとして提供はされているが使用しづらいものとなっていました。

C++におけるCヘッダの主な役割は、C言語との相互運用性およびC言語のABIとリンケージ規則を使用する他のシステムとの相互運用のためにあり、非推奨なのは相互運用を目的としないC++コードでの利用時だけなはずです。この提案は、CヘッダをCおよびその互換システムとの相互運用の目的のために利用することができるように、Cヘッダの非推奨を解除してCのヘッダ群がC++のエコシステムとして提供されることを明確にしようとする提案です。

なお、ここでのCヘッダとは<cxxx>という形式のC++におけるC互換ヘッダではなく、Cの標準ライブラリヘッダとして定義されている<xxx.h>の形式のものを指しています。

この提案の後でも、Cコードとして有効である必要がないC++コードでのCヘッダの使用は推奨されません。あくまでCのABIおよびリンケージを利用するシステムとの相互運用性確保のための変更です。

P2400R0 Library Evolution Report

2020年10月27日~2021年2月16日の間の、LEWGの活動記録。

LEWGにおいて議論されている機能の現状について、及びレビューや投票を行った提案のリストなどがあります。

Executor

P0443R14のレビューが完了し、そこで得られたフィードバックに基づいてP0443R15を準備中のようです。投稿され次第、再びレビューが行われる予定です。

コルーチンのライブラリサポート

P2168R1std::generator)の改訂版が近々提出される予定で、そのレビューはすぐに行われる予定です。

しかし、他の提案は一度のレビューの後改訂されておらず、std::generator以外のコルーチン関連のライブラリサポートの議論は止まっているようです(ただし、Executorに依存しているために止まっているものがある可能性があります)。

Networking TS

Networking Study Groupで議論が続いており、まだLEWGに提出されていません。これはExecutorに依存していますが、大きな機能であるために並行して作業が可能なはずです。

2021年春頃までにLEWGでのレビューに進めない場合、C++23に間に合わなくなる可能性があります。

標準ライブラリのモジュール化

2020年春以降、関連した活動が見られないようです。このまま議論もされなければ、C++23には間に合いません。

ranges

P2214R0をレビューし、この提案の方向性でのRangeライブラリの拡張をLEWGは支持しています。

P2210R0std::ranges::split_viewの改善)はLEWGにおける最終レビューを通過し、LWGに転送するための電子投票にかけられる予定です。前回の電子投票の期限に間に合わなかったためまだ投票は行われていませんが、2021年春頃には投票が開始される予定です。

ranges関連の提案をよく書いている著者の方が協力的かつ活発なためranges関連のレビューはスムーズに進行しており、提案は迅速に処理されているようです。

format

P2216R2std::foramtの改善)はLEWGにおける最終レビューを通過しており、2021年1月に電子投票にかけられました。この提案にはC++20に対する破壊的な変更が含まれていますが、既存実装が無いために影響は無いと判断されました。

P2093R2std::print)はまだLEWGで議論の真っ最中ですが、著者の方の応答性が高いために作業は順調に進行しているようです。早ければ2021年春頃にLWGに転送される可能性があるようです。

Text and Unicode

P1885R3がLEWGに提出されており、メーリングリストレビューおよびテレカンレビューを完了したようです。改善の必要があったため、改訂待ちをしています。

フリースタンディング

P1462R5<utility>, <ranges>, <iterator>のフリースタンディングサポート)のLEWGでの最終レビューが完了し、2021年1月にLWGに転送するための最後の電子投票にかけられました。

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

文書の一覧

採択されたものはありません、全部で58本あります。

が、SG22(C/C++相互互換性に関する研究グループ)経由でCの提案が含まれているので、そこからC++に対してのものでないものを除くと48本になります。

N4880 PL22.16/WG21 agenda: 22 February 2021, Virtual Meeting

2021年2月22日 11:00 (北米時間)に行われるWG21本会議のアジェンダです。

C++23のための2回目の全体会議です。

N4881 WG21 virtual meetings: 2021-02, -06, and -10

今年のWG21全体会議の予定表。

↑の2月のものを除くと、6月と10月に予定されています。どちらもオンラインで行われることが決定しています。

N4882 WG21 admin telecon meetings: 2021-02, -05, and -09

全体会議に共通する大まかな進行表。

N4883 WG21 February 2021 admin telecon minutes

2021年2月8日に行われた、WG21各SG等の責任者によるミーティングの議事録。

P0401R6 Providing size feedback in the Allocator interface

アロケータが実際に確保したメモリのサイズをフィードバックすることのできるメモリ確保インターフェースを追加する提案。

以前の記事を参照

このリビジョンでの変更は、LWGのフィードバックを受けて提案する文言を改善したことです。

この提案はすでにLWGでのレビューが完了しており、そこではこの提案はC++23に導入する事に全会一致でのコンセンサスが取れています。そのため、次の全体会議(2021年6月)で全体投票にかけられ、問題がなければC++23に入る見込みです。

P0448R3 A strstream replacement using span as buffer

長い間非推奨のまま代替手段のなかったstd::strstreamの代替となるstd::spanによるspanstreamを追加する提案。

std::strstreamは事前に確保された固定長のバッファを受け取りそれを利用したストリームを構築できるものでしたが、同時に可変長の内部バッファを扱う機能も持っており(コンストラクタでスイッチする)、その結果.str()から返されるchar*の指すメモリの管理責任が不明瞭になっていました。また、可変長バッファを使用する場合は.freez(false)をしてからデストラクタを呼び出す必要があるのですがわかりづらく、忘れられることが多かったようです。

このように、使いづらくメモリリークの危険性を備えていることからC++98でstd::strstreamは非推奨とされました。ただし、固定長バッファからストリームを構築し、そのバッファを文字列として参照する、という機能は有用で完全に代替するものが無かったことから削除されずに今日まで残っています。

文字列ベースのストリームという機能はstd::stringstreamが代替として利用できますが、固定長バッファによるストリームを代替する機能はありませんでした。

この提案はstd::strstreamの機能の一つだった、事前に確保された固定サイズのバッファを用いたストリームをstd::spanを利用して実現するものです。

ヘッダ<spanstrem>に以下のものが追加されます。

  • std::basic_spanbuf
    • std::spanbuf
    • std::wspanbuf
  • std::basic_ispanstream
    • std::ispanstream
    • std::wispanstream
  • std::basic_ospanstream
    • std::ospanstream
    • std::wospanstream
  • std::basic_spanstream
    • std::spanstream
    • std::wspanstream
// 入力ストリームのサンプル

char input[] = "10 20 30";

std::ispanstream is{std::span<char>{input}};

int i;
is >> i;
ASSERT_EQUAL(10,i);

is >> i;
ASSERT_EQUAL(20,i);

is >> i;
ASSERT_EQUAL(30,i);

is >>i;
ASSERT(!is);
// 出力ストリームのサンプル

char output[30]{}; // zero-initialize array

ospanstream os{span<char>{output}};
os << 10 << 20 << 30;

auto const sp = os.span();

ASSERT_EQUAL(6, sp.size());
ASSERT_EQUAL("102030", std::string(sp.data(),sp.size()));
ASSERT_EQUAL(static_cast<void*>(output), sp.data()); // ポインタの比較
ASSERT_EQUAL("102030", output);

P0849R7 auto(x): decay-copy in the language

明示的にdecay-copyを行うための構文を追加する提案。

以前の記事を参照

このリビジョンでの変更は、decltype(auto(...))decay_tとの構文比較表の追加とLWG Issue 3491に関する文言を別のところで議論することにしたこと、および関連しそうな機能(P2255R0 A type trait to detect reference binding to temporaryP0847R6 Deducing this)との相互作用についての追記です。

この提案は、ライブラリパートについてLEWGからLWGへ転送され、そこでのレビューが完了次第CWGに送られ最後のレビューを迎えます。

P1018R8 C++ Language Evolution status 🦠 pandemic edition 🦠 2020/11–2021/01

EWG(コア言語への新機能追加についての作業部会)が2020/11–2021/01の間に議論した提案やIssueのリストや将来の計画、テレカンファレンスの状況などをまとめた文書。

前回と比べると、多くのIssueに関して議論をしていたようです。

P1072R7 basic_string::resize_and_overwrite

std:stringに領域(文字長)を拡張しつつその部分を利用可能にする為のメンバ関数resize_and_overwrite()を追加する提案。

以前の記事を参照

このリビジョンでの変更は、記述の修正がメインの様です。

P1322R3 Networking TS enhancement to enable custom I/O executors

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

以前の記事を参照

このリビジョンでの変更は、AcceptableProtocol要件に入れ子::socket_for<Executor>を追加し、ip::tcp,ip::udpクラスがそれを備えるようにしたことなどの、提案する文言の調整です。

P1425R3 Iterators pair constructors for stack and queue

std::stackstd::queueに、イテレータペアを受け取るコンストラクタを追加する提案。

以前の記事を参照

このリビジョンでの変更は、アロケータ型も含めてテンプレートパラメータを推論するための推論補助を追加したことです。

この提案はLEWGからLWGへ送られ、LWGでのレビューの結果、まだ未公開のR4がC++23入りのコンセンサスを得たようです。おそらく次の全体会議(2021年6月)でC++23に採択されます。

P1659R2 starts_with and ends_with

P1659R3 starts_with and ends_with

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

以前の記事を参照

R2およびR3での変更は、提案する文言の修正がメインです。

この提案はすでにLWGでのレビューを終えており、LWGでのC++23入りのコンセンサスを得ています。おそらく次の全体会議(2021年6月)でC++23に採択されます。

P1682R3 std::to_underlying

列挙型の値からその基底の整数型への変換を行うstd::to_underlyingの提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロの修正など提案する文言の調整のみです。

この提案は2021年2月22日(米国時間)に行われた全体会議でC++23入りが承認されています。次のワーキングドラフトからC++23に反映されます。

P1885R5 Naming Text Encodings to Demystify Them

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

以前の記事を参照

このリビジョンでの変更は以下のものです。

  • エンコーディング名としてchar*を返す動機の説明の追記
  • 文言の改善と、フリースタンディング関連の文言の削除
  • systemという言葉のenvironmentへの置き換え
  • 名前の照合に問題のある古いエンコーディングを除外

LEWGでの最初のレビューの結果、提案の方向性への支持とこの提案のために作業時間をかけることへのコンセンサスが得られています。とはいえ、まだLEWGでの議論は続きます。

P1951R1 Default Arguments for pair's Forwarding Constructor

std::pair<T1, T2>forwarding constructor(要素型に変換可能な値を転送して構築するコンストラクタ)のテンプレートパラメータにデフォルト引数としてT1, T2を指定する提案。

現在のstd::pairでは次のようなコードを書くと、見えない所にオーバーヘッドを埋め込むことになります。

// std::stringとstd::vector<std::string>の一時オブジェクトが作られ、コピーされる
std::pair<std::string, std::vector<std::string>> p("hello", {});

std::pair<T1, T2>について、上記コードのコンストラクタのオーバーロード解決では、次の2つのコンストラクタが考慮されることになります。

explicit(see below) constexpr pair(const T1& x, const T2& y);

template <class U, class V>
explicit(...) constexpr pair(U&& x, V&& y);

しかし、2つ目の候補は、第二引数が{}のみであることからVの推定ができないため候補から外されます。結果1つ目の候補が選ばれ、一時オブジェクトの作成とそこからのコピー構築が行われます。

この場合に2つ目のコンストラクタを選択しようとする場合、次のように書く必要があります。

std::pair<std::string, std::vector<std::string>> p("hello", std::vector<std::string>{});

しかしこの構文は冗長でユーザーフレンドリーではありません。

この提案は2つ目のコンストラクタ(forwarding constructor)のデフォルトテンプレートパラメータとしてT1, T2を入れておくことで、最初の例のように書いた場合でも常にforwarding constructorが選択されるようにするものです。

2つ目の形式のコンストラクタは次のように変更します。

template <class U = T1, class V = T2>
explicit(...) constexpr pair(U&& x, V&& y);

たったこれだけの事で、{}の初期化子の使用が意図通りになるようになります。std::pairの構築で{}を使用している既存のコードの振る舞いが変更され、一時オブジェクトのコピーからムーブまたは完全転送による構築に切り替わります。これは明らかに望まれる動作であり、それによって壊れるコードはないものと思われます。

この提案の内容とC++23導入を目指すことはLEWGにおいて合意が取れており、LWGに転送するためにLEWGでの投票を待っている所です。

P2047R1 An allocator-aware optional type

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

Allocator Awareなオブジェクトとは、次の要件を満たすものです。

  • 一度構築されると、そのアロケータは変更されない。
  • オブジェクトのコンストラクタはuses-allocator構築で指定される2つの方法のいずれかでアロケータを受け取る。
    • これによって、コンテナの要素となるときにコンテナのアロケータが伝播される。
  • Allocator Awareな型を要素としうるオブジェクトはそれ自体がAllocator Awareであり、受け取ったアロケータを要素に伝播させなければならない。

現在のstd::optionalAllocator Awareではなく、上記ルールのいずれも満たしません。有効値を破棄して無効状態となるとそこで使用されていたアロケータを忘れてしまい、uses-allocator構築のプロトコルに従ったアロケータを受け取るコンストラクタを持っていないため、コンテナで使用されたときに要素のオブジェクトに適切にアロケータが伝播しません。

結果、現在のstd::optionalはコンテナで使用するときにアロケータを適切に管理できず、例えばpmrコンテナの不変条件である全ての要素が同じアロケータを使用する、という条件を満たすことができません(これは未定義動作につながります)。

この提案のstd::pmr::optionalAllocator Awareな要素型を持つときに上記3つのルールに従うようにすることで、自身もAllocator Awareとなるものです。そのために、構築に使用されたアロケータを内部で保持しています。

std::pmr::optionalの基本設計は単純で、全てのコンストラクタでアロケータを受け取るようにした上で、受けたアロケータを使用して要素を構築するようにします。また、そのように渡されたアロケータを内部で保持し、有効値の再構築時に使用します。

ただし、対象とするアロケータはstd::pmr::polymorphic_allocatorのみです。これによってアロケータの型がstd::optionalの型に現れることを防止します(polymorphic_allocatorC++20での改修によってこのような型に依存しない運用が可能になっています)。

これは従来のstd::optionalを置き換えるものではなく、std::optionalの振る舞いを変更するものでもありません。また、std::pmr::optionalAllocator Awareでない型を保持する場合は従来のstd::optionalに切り替わります。すなわち、std::pmr::optionalAllocator Awareな型を要素とする場合にのみ、std::optionalと異なる振る舞いをします。

std::pmr::optionalジェネリックなコードで(特にコンテナの要素型として)使用して、Allocator Awareな型を要素とする場合に適切にアロケータが伝播されるようにするものです。そして、std::pmr::optionalstd::optionalの単なるエイリアスではなく別の型として実装されることになります。

なお、std::pmr::optionalAllocator Awareな型を要素とする場合でも、そのアロケータを使用して自身のためのストレージを確保するものではありません。要素のストレージは従来通り自身の内部にあります。

P2066R5 Suggested draft TS for C++ Extensions for Minimal Transactional Memory

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

以前の記事を参照

このリビジョンの変更点は、atomicブロックでのthrow式のうち、ブロック内で処理される例外は未定義動作ではないとされたこと、ほぼ全ての標準ライブラリ関数はatomicブロック内で使用可能とされたことです。

P2093R4 Formatted output

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

前回の記事を参照

このリビジョンでの変更は、_isattyを使って処理の例示を行なっていた所をGetConsoleModeに置き換えた事です。

この提案はSG16での議論とレビューを終えて、LEWGに送られたようです。

P2128R3 Multidimensional subscript operator

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

前回の記事を参照

このリビジョンでの変更は、t[a][b]のような構文をt[a, b]と書き換えてoperator[]に委譲する機能についての議論を追加した事です。ただし、これはここでは提案されていません。

P2162R2 Inheriting from std::variant (resolving LWG3052)

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

以前の記事を参照 - P2162R0 Inheriting from std::variant (resolving LWG3052) - [C++]WG21月次提案文書を眺める(2020年5月) - P2162R1 Inheriting from std::variant (resolving LWG3052) - [C++]WG21月次提案文書を眺める(2020年8月)

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

この提案は2021年2月22日(米国時間)に行われた全体会議でC++23入りが承認されています。次のワーキングドラフトからC++23に反映されます。

P2164R4 views::enumerate

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

以前の記事を参照

このリビジョンでの変更は、views::enumrateの間接参照結果の型がindex/valueという名前のメンバを持つようにするために必要な事について追記された事です。

以前の提案では結果の型はシンプルな集成体で、views::enumrateイテレータvalue_typereferenceは同じ型とされていました。

struct result {
  count index;
  T value;
};

しかし、indirectly_readableコンセプトを満たすためにはvalue_typereferenceの間のcommon_referenceが必要であり、そのためにはそこそこ大きな追加の実装が必要となります。

このリビジョンではその実装の一部を示すとともに、std::pair/tupleを再現せずまた利用せず、かつ名前のついた(説明専用でない)新しい型を導入する方向性を提案しています。

また、以前の提案では上記result型のメンバはconstメンバでしたが、LEWGでのレビューで否定されたため、非constに修正されました。

P2195R2 Electronic Straw Polls

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

以前の記事を参照

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

P2216R3 std::format improvements

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

以前の記事を参照

このリビジョンでの変更は、提案している文言の調整がメインです。

この提案はすでにC++20に逆適用されることがほぼ決まっています。

P2231R1 Missing constexpr in std::optional and std::variant

std::optionalstd::variantをさらにconstexpr対応させる提案。

以前の記事を参照

このリビジョンでの変更は、機能テストマクロを追加した事です。

この提案はLEWGでのレビューが終わる前にLWGでのレビューが完了しており、C++23に導入するコンセンサスが得られています。LEWGのレビュー完了を待って、全体会議での投票にかけられる予定です。

P2234R1 Consider a UB and IF-NDR Audit

C++標準のUB(undefined behavior)とIF-NDRill-formed no diagnostic required)について、委員会の小さなチームによって監査されるプロセスの提案。

以前の記事を参照

このリビジョンでの変更は、R0が何を目的としているか混乱を招いたようで、文書を全体的に再構成した事です。

P2242R1 Non-literal variables (and labels and gotos) in constexpr functions

constexpr関数において、コンパイル時に評価されなければgotoやラベル、非リテラル型の変数宣言を許可する提案。

以前の記事を参照

このリビジョンでの変更は、この提案は何かを新しくconstexpr関数で実行可能にしようとするものではなく、そのことを明記した事です。

この提案はEWGでのレビューでは反対意見なくCWGへ転送されるコンセンサスが得られています。このリビジョンのEWGでの投票を待って、CWGに送られる予定です。

P2249R0 Mixed comparisons for smart pointers

スマートポインターの比較演算子に生ポインタとの直接比較を追加する提案。

スマートポインタ型はリソースの所有権を表現する方法として広く認識されており、対して生ポインタはリソースにアクセスするために用いる所有権を保有しないものとして認識されつつあります。

そして、どちらも任意のリソース(オブジェクト)のアドレスを表現するという意味論を共有しています。

その一方で、現在の標準ライブラリにあるスマートポインタ型の比較演算子は自身と同じ型(テンプレートパラメータを除いて)との間の比較演算子しか備えていません。そのため、生ポインタとスマートポインタの間で比較をするためには、一旦スマートポインタから生ポインタを取り出さなければなりません。

std::shared_ptr<object> sptr1, sptr2;
object* rawptr;

// 2つのポインタが同じオブジェクトを指しているかを調べる。
if (sptr1 == sptr2) { ... }        // OK
if (sptr1 == rawptr) { ... }       // Error
if (sptr1.get() == rawptr) { ... } // OK

この事は同値比較演算子だけではなく、大小比較を行う関係演算子でも同様です。

スマートポインタと生ポインタの比較を行うケースは一般的に発生しているため、スマートポインタに生ポインタとの比較演算子を追加する事でこのような不整合を修正しようとする提案です。

この提案では、std::unique_ptrstd::shared_ptrに対して保持するポインタと比較可能な任意の型との比較を行う==<=>を追加する事で、生ポインタとの比較を実装しています。

// ManagerはObjectを所有し、利用者にはそのポインタを貸し与える
// クライアントは特定のObjectについて作業をしてもらうために、借りたポインタをManagerに渡す事で作業を依頼する
class Manager {

  std::vector<std::unique_ptr<Object>> objects;

public:
  // Objectのポインタを取得
  Object* get_object(std::size_t index) const {
    return objects[index].get();
  }

  // 指定したObjectを削除する
  void drop_object(Object* input) {
    // 直接比較できないので述語オブジェクトを作成しなければならない
    auto isEqual = [input](const std::unique_ptr<Object>& o) {
        return o.get() == input;
    };
    erase_if(objects, input);

    // この提案の後では、次の1行で事足りる
    erase(objects, input);
  }

  // Objectのインデックスを得る
  ssize_t index_for_object(Object* input) const {
    // 先ほどと同じ理由
    // このような述語オブジェクトは様々なところで重複して頻出する可能性がある
    // 一元化すればいいのだが、多くの場合その場で書いてそのままにしがち・・・
    auto isEqual = [input](const std::unique_ptr<Object>& o) {
        return o.get() == input;
    };
    auto it = std::ranges::find_if(objects, isEqual);
    // etc.

    // この提案の後では、次の1行で事足りる
    auto it = std::ranges::find(objects, input);
  }
};

P2280R1 Using unknown references in constant expressions

定数式での参照のコピーを許可する提案。

以前の記事を参照

このリビジョンでの変更は、EWGのレビューと投票の結果を記載した事と、提案する文言を改善した事(特にthisポインタの定数式での利用を明示的に許可したこと)です。

P2281R1 Clarifying range adaptor objects

range adaptor objectがその引数を安全に束縛し、自身の値カテゴリに応じて内部状態を適切にコピー/ムーブする事を明確化する提案。

以前の記事を参照

このリビジョンでの変更は、LWGからのフィードバックを提案する文言に反映した事です。

この提案はライブラリのIssue解決の多ものものであるのでLWGからレビューが開始されています。そこではC++23に向けて導入するコンセンサスが得られており、次の全体会議(2021年6月)で採択のための投票にかけられる予定です。

P2286R1 Formatting Ranges

任意の範囲を手軽に出力できる機能を追加する提案。

以前の記事を参照

このリビジョンでの変更は、実装定義としている範囲の出力フォーマットのオプションを提示した事です。何人かの方が筆者の方にフォーマットを実装定義としていることは受け入れられないと伝えているようです。

P2287R1 Designated-initializers for base classes

基底クラスに対して指示付初期化できるようにする提案。

以前の記事を参照

このリビジョンでの変更は、R0で提案していた基底クラスを指定する構文を拡張して、{}を取り除いた形の基底クラスのメンバを直接指定した初期化を許可するようにしたことです。

struct A {
  int a;
};

struct B : A {
  int b;
};

int main() {
  // R0で提案した構文
  B b1{:A = {.a = 1}, b = 2};
  B b2{:A{.a = 1}, b = 2};
  B b3{:A{1}, .b{2}};

  // R1で追加された形式
  B b4{.a = 1, .b = 2};
  B b5{.a{1}, .b{2}};
}

P2289R0 2021 Winter Library Evolution Polls

2021年の冬(1月から3月にかけて)に予定されている、LEWGでの全体投票の予定表。

以下の9つの提案が投票にかけられる予定です。

基本的にはLEWGでの作業を完了してLWG(CWG)へ転送することを確認するための投票です。

P2290R0 Delimited escape sequences

文字・文字列定数中の8進・16進エスケープシーケンスおよびユニバーサル文字名について、その区切りが明確になるような形式を追加する提案。

現在文字列中のエスケープシーケンスには、ユニバーサル文字名(\uxx... or \Uxx...)、8進エスケープシーケンス(\ooo)、16進エスケープシーケンス(\xnn...)の3種類があります。8進エスケープシーケンスは3文字制限がありますが、16進エスケープシーケンスには長さの制限はありません。そして、どちらもエスケープシーケンス中に受け付けられない文字が出てきたらそこでエスケープシーケンスを終了するようになっています。

これによって、次のような問題が発生します。

"\17";      // 8進エスケープシーケンス、"0x0f"と等価
"\18";      // 8進エスケープシーケンスと文字、"0x01 8"の2文字
"\xabc";    // 1文字
"\xab" "c"; // 2文字

つまりどれも、エスケープシーケンスの終端(あるいは区切り)が明確ではありません。一番最後の例の様な回避策はありますが分かりづらく、この問題をよく知らない人から見ると余計なことをしているようにしか見えません。

また、ユニバーサル文字名は16進数字4桁もしくは8桁のどちらかになりますが、ユニコードのコードポイントの範囲が[0, 0x10FFFF]に制限されているため、有効なコードポイントは5桁以下の16進数字列によって書くことができます。そして、5桁のユニコード文字を書く場合は\U0001F1F8のように冗長な0が必要になってしまいます。

この提案ではこれらの問題の解決のために、\u{}, \o{}, \x{}の形の新しいエスケープシーケンス構文を提案しています。それぞれ{}の中に任意の長さの数字列(8 or 16進)を書けること以外は既存の対応するエスケープシーケンスと同様の扱いとなります。そして、この形式のエスケープシーケンスは現在コンパイルエラーとなる(\oの形式は予約されている)ために既存のコードを壊すこともありません。

なお、エスケープシーケンスの置換は文字列リテラルの連結の前に行われるため、"\x{4" "2}"のような形での使用はコンパイルエラーとすることを提案しています。

この提案の後では、先程の問題のエスケープシーケンスを次のように書くことができるようになります。

"\o{18}";     // 8進エスケープシーケンスに8は現れてはいけないのでコンパイルエラー
"\x{ab}c";    // 2文字
"\u{1F1F8}";  // 5桁のユニバーサル文字名 

この提案はC++に対してのものですがCに対しても同じ問題がある事から、WG14(C標準化委員会)に周知のためにSG22にも提出されています。

この部分の9割は以下の方のご指摘により成り立っています。

P2291R0 Add Constexpr Modifiers to Functions to_chars and from_chars for Integral Types in Header

std::to_chars, std::from_charsを整数変換に関してconstexprにする提案。

現在のC++にはコンパイル時に使用可能な文字列と数値の相互変換のためのものはありません。そのため、コンパイル時に文字列変換を行うには在野のライブラリに頼るほかありませんでした。

標準ライブラリには様々な実行時文字列変換法があり、中でもstd::to_chars, std::from_charsロケールに依存せず、例外を投げず、動的メモリ確保も行わないなど、コンパイル時の文字列と数値の相互変換に最適な候補です。そのため、この2つの関数を整数変換に限ってconstexpr対応することを目指す提案です。

constexpr std::optional<int> to_int(std::string_view s) {
  int value;
  
  if (auto [p, err] = std::from_chars(s.begin(), s.end(), value); err == std::errc{}) {
    return value;
  } else {
    return std::nullopt;
  }
}

static_assert(to_int("42") == 42);
static_assert(to_int("foo") == std::nullopt);

C++20でstd::stringコンパイル時に構築できるようになったため、この提案と合わせるとstd::formatcosntexprにすることができるようになります。

筆者の方は、MSVC STLの実装をconstexpr対応させることで実装テストを行い、その結果大きな変更や問題がなかったことを確認しています。

浮動小数点数変換のconstexpr対応を提案していないのは、実装が難しいためにサポートが進んでおらず(MSVCとGCC11が実装完了)、提案するのは時期尚早という判断のようです。

P2295R0 Correct UTF-8 handling during phase 1 of translation

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

現在のC++コンパイラが入力ソースファイルのエンコードとしてサポートしている文字コード(ソースファイル文字集合)は実装定義であり、必ずしもUTF-8をサポートしていません。それによって、UTF=8ソースファイルで書かれたmain関数だけのシンプルなプログラムですら、移植性を保証できません。

C++コンパイラがどのような文字コードを入力としてサポートするにせよUTF-8はサポートすることを義務付けることで、ソースコード移植性の問題を改善し、またユニコード関連の機能やその処理方法を(規格中でも)適切に指定し、幅広く使用できるようにしようとする提案です。

現在の主要な実装は全て、細部の差異はあれどUTF-8をサポートしているため、この提案の実装に必要なのはそれをデフォルトにすることだけです。この提案は、ユーザーのUTF-8ソースコードが適切にでコードされていることを保証する一方で、実装者はより寛大なオプションを提供できるようにするものです。例えば、UTF-8ソースファイルであることをどう決定するか(BOMの考慮)や、UTF-8として無効なコードポイントのシーケンスをどう扱うかなどは実装定義となります。

P2297R0 Wording improvements for encodings and character sets

規格の文章の~ encoding~ character setという言葉の使い方を改善し、何が何を指しているのかを一貫させる提案。

これによって、ユニバーサル文字名の扱いに関する3件のIssueが解決されます。

P2301R0 Add a pmr alias for std::stacktrace

std::basic_stacktracestd::pmrエイリアスstd::polymorphic_allocatorを用いるエイリアス)を追加する提案。

std::basic_stacktraceはテンプレートパラメータにアロケータ型を取り、std::allocatorがあてがわれたエイリアスstd::stacktraceとして定義されています。そして、std::stacktraceAllocatorAwareContainerの要件を満たしており、std::polymorphic_allocatorを用いることに障害はありません。

他のコンテナなど、アロケータモデルをサポートしデフォルトでstd::allocatorを使用するものについてstd::pmrエイリアスが用意されているように、std::basic_stacktraceにもstd::pmr::stacktraceを用意すべき、という提案です。

P2302R0 Prefer std::ranges::contains over std::basic_string_view::contains

新しいアルゴリズムとしてstd::ranges::containsを追加して、std::basic_string_view/std::basic_string.contains()メンバ関数を削除する提案。

C++23に向けたP1679R3の採択によって、std::string/string_viewにはある文字列が含まれているかを判定するための.contains()メンバ関数を備えています。

筆者の方は、このような操作の必要性は同意するがこの操作はより一般の範囲(range)に適用可能なアルゴリズムであり、メンバ関数ではなく<algorithm>に配置される非メンバアルゴリズムとして追加すべき、と述べています。

それによって、ある範囲に別の範囲が含まれているかどうか、あるいは述語を満たす部分が含まれているかどうかを調べる形に一般化でき、また他のrangeアルゴリズムに従った形で幅広い型について利用できるようになります.

std::ranges::containsの使用イメージ。

// 文字の範囲と文字を受ける
if (std::ranges::contains(haystack, 'o')) {
  // meow
}

// 文字範囲のイテレータペアと文字を受ける
if (std::ranges::contains(haystack.begin(), haystack.end(), 'c')) {
  // purr
}

// 探索する範囲のイテレータペアと探索したい範囲のイテレータペアを受ける
if (std::ranges::contains(haystack.begin(), haystack.end(), long_needle.begin(), long_needle.end())) {
  // hiss
}

// 探索する範囲と探索したい範囲を受ける
if (std::ranges::contains(haystack, long_needle)) {
  // hiss again
}

// 探索する範囲と述語を受ける
if (std::ranges::contains(haystack, long_needle, bind_back(std::modulo(), 4))) {
  // double purr
}

P2314R0 Character sets and encodings

規格文書中の ~ character setという言葉を明確に定義し直す提案。

そしてこれらの用語を用いて文言を書き直すことで、翻訳フェーズ1でユニバーサル文字名が形成されなくなり、全てのユニコード文字入力はコンパイル全体で保持されるようになります。それにより、プリプロセッサにおける文字列化演算子#)の動作が変更されます。

C++20 この提案
#define S(x) # x
const char * s1 = S(Kテカppe);      // "K\\u00f6ppe"
const char * s2 = S(K\u00f6ppe); // "K\\u00f6ppe"
#define S(x) # x
const char * s1 = S(Kテカppe);     // "Kテカppe"
const char * s2 = S(K\u00f6ppe); // "Kテカppe"

ただし、既存の実装は全てこれを実装している(エスケープされたユニバーサル文字名を返す実装はない)ために問題にはならないようです。

また、これらの変更によって現在使用されているbasic / extended source character setという言葉は使われなくなります。

P2315R0 C++ Standard Library Issues to be moved in Virtual Plenary, Feb. 2021

標準ライブラリのIsuueのうち2021年02月のオンライン全体会議の際に投票にかけられるもののリスト。

ここにあるものは投票でコンセンサスが得られればLWG Isuueとして規格に反映されることになります。

これを書いている時点で投票は完了しており、その結果ここに記載されているIssueは全て標準に適用されることになりました。

P2316R0 Consistent character literal encoding

#ifプリプロセッシングディレクティブの条件式において、文字リテラルC++の式の意味論と同等に扱えるようにする提案。

#if 'A' == '\x41'
//...
#endif

if ('A' == 0x41){}

現在の仕様では、この2つの条件式は同じ結果となる保証がありません。

#ifの条件式において文字リテラルは対応する数値に変換され処理されますが、文字リテラルをどのように解釈するか(どのエンコーディングで読み取るか)は実装定義であり、C++の式上でのそれと一致するかどうかも実装定義とされます。

筆者の方がvcpkgを用いてコードベースを調査したところ、このような#ifでの文字リテラルの比較はその環境でのナロー文字エンコーディングを取得するために使用されているようです。

sqliteより

#if 'A' == '\301'
# define SQLITE_EBCDIC 1
#else
# define SQLITE_ASCII 1
#endif

主要なコンパイラは全て期待通りに動作し、#ifでの文字リテラルをナロー文字エンコーディングにおいて対応する数値として扱うようです。

C++で文字エンコーディングを検出するより良い方法は提案中ですが現状では手段がなく、この振る舞いを標準化して上記の様なコードがユーザーの期待通りに振舞うことを保証しようとする提案です。

P2317R0 C++ - An Invisible foundation of everything

C++とは何か?(原則と理想や使用についてなど)というよく聞かれる質問に対する返答をまとめた文書。

Bjarne Stroustrup先生がC++とは何か?とかC++はまだどこでも使われてるの?などよく聞かれるらしく、その簡単な答えとその詳細を知ることのできる文書へのリンクをまとめた文書です。WG21メンバーに向けて書かれたものではないですが、WG21メンバにも役立つ可能性があるためWG21に提出された様です。

目次

  1. 目的と手段
    • C++の設計の高レベルでの目的とシステムにおけるその役割
  2. 使用
    • 基礎的な使用法に焦点を当てた、C++のいくつかの用例
  3. 進化
    • フィードバックに基づくC++開発のための進化戦略
  4. 保証、言語、ガイドライン
    • 進化、安定性、表現力、型・リソース安全を同時に達成するための戦略
    • ソフトウェア開発における人の役割の再認識
  5. 参考文献とリソース
    • C++のより深い理解につながる可能性のある参考文献への注釈付きリスト
  6. 付録
    • C++の主要なプロパティと機能の非常に簡単な概説

C++ヲタク必見です!

P2320R0 The Syntax of Static Reflection

静的リフレクションのための新しい構文の提案。

表現力・読みやすさ・柔軟さ・曖昧さがない・実装可能、の観点から提案中のものも含めた現在のリフレクションの構文を見直したものの提案です。

リフレクション

現在のリフレクションTSでは、名前からメタ情報を取得するのにreflexpr()という構文を使用しています。この提案では代わりに^を使用します。

// この提案
meta::info r1 = ˆint;   // reflects the type-id int
meta::info r2 = ˆx;     // reflects the id-expression x
meta::info r3 = ˆf(x);  // reflects the call f(x)

// N4856現在
meta::info r1 = reflexpr(int);   // reflects the type-id int
meta::info r2 = reflexpr(x);     // reflects the id-expression x
meta::info r3 = reflexpr(f(x));  // reflects the call f(x)

スプライシング

リフレクションによって取得したメタ情報から型名を取り出して利用することです。この提案では[: R :]の様な構文によって行います。

struct S { struct Inner { }; };
template<int N> struct X;
auto refl = ˆS;
auto tmpl = ˆX;

void f() {
  typename [:refl:] * x;  // OK: xはS*
  [:refl:] * x;           // error: typename必須
  [:refl:]::Inner i;      // OK
  typename [:refl:]{};    // OK: Sの一時オブジェクトをデフォルト構築
  using T = [:refl:];     // OK
  struct C : [:refl:] {}; // OK
  template [:tmpl:]<0>;   // OK
  [:tmpl:] < 0 > x;       // error: xと0の比較になる

  // N4856現在
  namespace reflect = std::experimental::reflect;
  using refl_t = reflexpr(S);
  using tmpl_t = reflexpr(X);

  reflect::get_reflected_type_t<refl_t> * x;
  typename reflect::get_reflected_type_t<refl_t>::Inner i;
  reflect::get_reflected_type_t<refl_t>{};
  using T = reflect::get_reflected_type_t<refl_t>;
  struct C : reflect::get_reflected_type_t<refl_t> {};
  // 残りの2例は対応するものがない(おそらく)
}

パックのスプライシング

リフレクションによって取得したメタ情報から型名のリストを取り出して利用することです。この提案では...[: R :]...の様な構文によって行います。

// 型エイリアス
using T = std::tuple<int, ...[:range_of_types:]..., bool>;
// 関数宣言
void f(... [:range_of_types:] ...args);

// 関数呼び出し
fn(0, 1, ...[:range:]...);  // OK: 通常引数(0, 1)の後に展開
fn(...[:range:]..., 0, 1);  // OK: 通常引数(0, 1)の前に展開
fn(...[:range:] * 2...);    // OK: rangeの要素に2をかけながら展開
fn(...[:r1:] * [:r2:]...);  // OK: ただし、r1とr2の長さは同じでなければならない

P2321R0 zip

<ranges>zip_view, adjacent_view, zip_transform_view, adjacent_transform_viewを追加する提案。

これら4つのziplikeなviewは基本的には複数の範囲を一つの範囲に変換する様に振る舞うものです。そのため、元のそれぞれの範囲の要素型(value_type)をEnとすると、間接参照の結果型(reference)はstd::pair<E1&, E2&>std::tuple<E1&, ..., En&>の様になるでしょう。

このstd::pairstd::tupleイテレータの間接参照の結果として返されるプロクシオブジェクトとして使用されるのに必要な性質を備えておらず、その議論が間に合わなかったためにC++20ではこれらのviewは採択されませんでした。

この提案は、std::pairstd::tupleの各種の問題を解決した上で、zip_view, adjacent_view, zip_transform_view, adjacent_transform_viewを追加するものです。

std:tupleへの出力

std::indirectly_writableコンセプトは、const_castを用いた制約式によって、イテレータの間接参照がprvalueを返すようなイテレータを弾く一方で、それが単にプロクシ参照である場合は許容する、という事をしています。これはstd::output_iteratorコンセプトを構成するものの一つです。

ziplikeなviewイテレータはその間接参照の結果としてstd::pair/tupleprvalueを返します。output_iteratorとして有効であるためには、それをconst化したもの(例えば、const std::tuple<...>&&なオブジェクト)に対して代入できなければなりません。

当然ながら、現在のstd::pair/tupleはそうなっておらず、プロクシイテレータの間接参照の結果型としては不足しています。

この提案では、std::pair/tupleに代入演算子constオーバーロードを追加し、std::pair/tupleconstでもその要素に代入が可能であるならばできるように変更します。

同時に、std::vector<bool>::referenceにも同様の理由から同じように代入演算子constオーバーロードを追加します。

std:tupleの読み取り

std::indirectly_readableコンセプトは、イテレータ型のvalue_type&referenceの間にcommon_referenceが存在していることを求めています。これはstd::input_iteratorコンセプトを構成するものの一つです。

ziplikeなviewイテレータにおけるvalue_type&referenceの間のcommon_referenceとは何でしょうか?

std::vector<int> vi = /* ... */;
std::vector<std::string> vs = /* ... */;

ranges::sort(views::zip(vi, vs));

例えばこの場合のzip_viewイテレータvalue_typestd::pair<int, std::string>referencestd::pair<int&, std::string&>となります。

std::pair<int&, std::string&>std::pair<int, std::string>へは変換できるため、common_referencestd::pair<int, std::string>となるでしょうか。参照ではありませんが、common_referenceに求められることは果たします。

しかし、zip対象のイテレータの要素型がコピー不可能な型になった場合、value_typereferenceはどちらをどちらにも変換できなくなるため、common_referenceは機能しなくなります。それによって、input_iteratorのモデルとなれなくなります。

この事はstd::tupleにも同じことが言えます。

この問題の解決のために、std::pair<T1, T2>に対してstd::pair<U1, U2>&, const std::pair<U1, U2>&&から構築するコンストラクタ、およびstd::tuple<Ts...>に対してstd::tuple<Us...>&, const std::pair<Us...>&&から構築するコンストラクタを追加します。

この2つのコンストラクタを追加するだけで、std::pair<std::unique_ptr<int>, std::string>からstd::pair<std::unique_ptr<int>&, std::string&>が構築できるようになります。そして、この型は参照のセマンティクスを持っています(std::tupleでも同様)。

またcommon_referenceに適合するために、std::tuplestd::pairに対してstd::basic_common_referencestd::common_typeの特殊化を追加します

これは、common_referenceとしてよりふさわしい型となります。

zip, zip_tranformvalue_type

zip_transformは与えられた範囲をどのように一つの範囲にzipするかを指定することのできるviewです。zipはデフォルトでstd::tuple/pairを用いてまとめ、その振る舞いを変更できません。対して、zip_transformはその部分を任意の関数を指定することでカスタマイズできます。

ここで問題になるのが、zip_transformvalue_typeがどうなるかという点です。

std::vector<int> vi = /* ... */;
std::vector<std::string> vs = /* ... */;

auto b = views::zip_transform([](auto&... r){
    return std::tie(r...);
  }, vi, vs);

このzip_transformzipと同じことをします。breferencestd::tuple<int&, std::string&>となり、これはzipのふるまいと一貫しています。しかし、この場合のvalue_typestd::tuple<int, std::string>としてしまう事は適切でしょうか?

zipの行う事と異なり、この場合にtupleの各要素が参照となっているのはユーザーが指定した変換の結果であり、別の変換ではこれは参照とならないかもしれません。従って、zip_transformvalue_typeは単にstd::remove_cvref_t<std::invoke_result_t<F&, std::range_reference_t<R>...>>のような型とするのが適切です。上記の例ではstd::tuple<int&, std::string&>となります。ただ、そうしてしまうとzipとの振る舞いの一貫性がなくなってしまいます。

この提案ではこの方針を採用し、zipzip_transformreference/value_typeの間の一貫性を失う事を許容することにしたようです。

これらの様な設計を選択し、C++23への導入に向けてziplikeな4つのviewの作業が開始されます。

なお、adjacent_viewとは1つの範囲のある要素についてそれに続くN - 1要素をひとまとめにしたものを要素とする範囲を生成するviewで、入力の範囲に対してその範囲の1つ目の要素を飛ばした範囲を生成して、その二つの範囲をzipしたようなviewです。adjacent_transform_viewadjacentする部分をカスタムできるものです。

std::vector<int> vi = {1, 2, 3, 4};
std::vector<std::string> vs = { "one", "two", "three", "four" };

for (auto& [n, s] : vi | std::views::zip(vs)) {
  std::cout << n << " : " << s << std::endl;
}
// 1 : one
// 2 : two
// 3 : three
// 4 : four

for (auto [n, m] : vi | std::views::adjacent<2>) {
  std::cout << n << " : " << m << std::endl;
}
// 1 : 2
// 2 : 3
// 3 : 4

P2322R0 ranges::fold

rangeアルゴリズムであるranges::foldの提案。

このranges::foldというのは関数型言語foldlと呼ばれているものに対応し、現在のC++標準ライブラリには数値に特化したイテレータ版として<numeric>std::accumulateとして存在しています。

std::ranges::accumlateP1813R0で提案されており、検討中のstd::ranges::accumlateは以下のようになっています。

template <input_range R, movable T, class Proj = identity,
          indirect_magma<const T*, projected<iterator_t<R>, Proj>, T*> BOp = ranges::plus>
constexpr accumulate_result<safe_iterator_t<R>, T>
    accumulate(R&& r, T init, BOp bop = {}, Proj proj = {});

これを良しとしないのは、特定の2項演算をデフォルトにするべきではない、戻り値型が複雑(単に結果だけが欲しい)、制約がきつすぎる(indirect_magmaコンセプトは大きすぎる)、等の理由です。

この操作は数値に限定されたものではないためより一般的な名前を付けて、より汎用的であるために過度な制約を課さないようにしたものがranges::foldであり、もはや数値のためのものではないため<algorithm>に追加することを提案しています。

提案ではさらに、ranges::foldのファミリとして、foldrに対応するranges::fold_rightと、範囲の最初の要素を初項として使用するranges::foldであるranges::fold_first、最後の要素を初項として使用するranges::fold_rightであるranges::fold_right_lastを同時に提案しています。

std::vector<int> vec = {1, 2, 3, 4, 5};

int sum1 = std::ranges::fold(vec, 0, std::ranges::plus{});
int sum2 = std::ranges::fold_first(vec, std::ranges::plus{});
// sum1 == sum2 == 15

std::vector<std::string> vec2 = {"aaa", "bbb", "ccc"};
std::string concat1 = std::ranges::fold_first(vec2, std::ranges::plus{});
// concat1 == "aaabbbccc"

std::string concat2 = std::ranges::fold_right(vec2, std::string{}, std::ranges::plus{});
std::string concat3 = std::ranges::fold_right_last(vec2, std::ranges::plus{});
// concat2 == concat3 == "cccbbbaaa"

P2324R0 Labels at the end of compound statements (C compatibility)

複合ステートメントcompound statement)の末尾にラベルを置けるようにする提案。

Cではこれが可能になっていますがC++では可能になっておらず、Cとの互換性向上のためにできるようにしようという提案です。この提案はSG22に提出されたものです。

複合ステートメントとはブロック({...})のことで、その末尾とは例えば関数の末尾の事です。

void foo(void)
{
first:  // C/C++共にok
  int x;

second: // C/C++共にok
  x = 1;

last:   // Cはok、C++はng
}

この提案はこの例のlastの様なラベルを置けるようにするものです。

P2325R0 Views should not be required to be default constructible

Viewとみなされる型にデフォルト構築可能性を要求しない様にする提案。

Viewを定義しているstd::ranges::viewコンセプトは、現在次の様に定義されています。

template <class T>
concept view =
    range<T> &&
    movable<T> &&
    default_initializable<T> &&
    enable_view<T>;

viewrangeであって少なくともムーブ可能である必要があり、viewは構文的な部分よりも意味論の部分でrangeと大きく異なるため、構文的にviewであるかどうかは明示的なオプトイン(enable_view)が必要です。

ただ、残ったdefault_initializableに関しては、C++20の<ranges>に至る議論からはviewがデフォルト構築可能である事のモチベーションは見つからず、range-v3のものを含めても本質的にデフォルトコンストラクタを必要とするviewはむしろ少数であり、default_initializableの要求はviewには不要のものに思えます。

また、デフォルト構築を要求されていることによって、関数を受け取るタイプのviewrange adaptor)は受け取った関数オブジェクトをstd::optionalsemiregular-box)に格納する必要があるため、実装を複雑化しviewのサイズを増加させています。

さらに、型がデフォルト構築可能であるという性質を持つことによって、そのような型をメンバとして持つクラスには、そのメンバが初期化されていないという追加の状態が発生します。それによって、ユーザーはそれをチェックする必要があり、またドキュメントなどにはその旨を記載する必要が出てきます。これは、現在のviewに当てはまっていることです。

デフォルト構築可能という要求がメリットをもたらさずむしろ害(型に無効状態を追加し、全てのrange adaptorにそのサイズ増加をもたらし、これが原因でviewコンセプトを満たせない場合のユーザの混乱など)をもたらすのであれば、それは取り除くべき、という提案です。

この提案では、viewコンセプトからdefault_initializableを取り除くと共に、weakly_incrementableコンセプトからも取り除くことで、input iterator/output iteratorへのデフォルト構築可能という要求を取り除くことを提案しています。

そして、一部のviewおよびイテレータ型からデフォルトコンストラクタを削除し、std::spanが常にviewとなるようにします。

多分2週間後くらい

この記事のMarkdownソース

[C++]indirectly_writableコンセプトの謎の制約式の謎

std::indirectly_writableコンセプトはイテレータによる出力操作を定義するコンセプトで、std::output_iteratorコンセプトの中核部分を成しています。

template<class Out, class T>
concept indirectly_writable = 
  requires(Out&& o, T&& t) {
    *o = std::forward<T>(t);
    *std::forward<Out>(o) = std::forward<T>(t);
    const_cast<const iter_reference_t<Out>&&>(*o) = std::forward<T>(t);
    const_cast<const iter_reference_t<Out>&&>(*std::forward<Out>(o)) = std::forward<T>(t);
  };

定義を見てみると、見慣れない構文を用いた良く分からない制約式が入ってるのが分かります。

const_cast<const iter_reference_t<Out>&&>(*o) = std::forward<T>(t);
const_cast<const iter_reference_t<Out>&&>(*std::forward<Out>(o)) = std::forward<T>(t);

常人ならばおおよそ使うことの無いであろうconst_castをあろうことかC++20のコンセプト定義で見ることになろうとは・・・

cpprefjpには

const_castを用いる制約式は、右辺値に対しても代入できるがconstな右辺値では代入できなくなる非プロキシイテレータprvalue(例えばstd::stringそのものなど)を返すイテレータを弾くためにある。これによって、間接参照がprvalueを返すようなイテレータ型はindirectly_writableのモデルとならないが、出力可能なプロキシオブジェクトを返すイテレータindirectly_writableのモデルとなる事ができる。

とあり、規格書にも似たようなことが書いてありますが、なんだかわかったような分からないような・・・

これは一体何を表しているのでしょうか、またどういう意図を持っているのでしょう?

prvalueを返すようなイテレータ

どうやらこれはrange-v3において発見された問題に端を発するようです。

struct C {
  explicit C(std::string a) : bar(a) {}

  std::string bar;
};

int main() {
  std::vector<C> cs = { C("z"), C("d"), C("b"), C("c") };

  ranges::sort(cs | ranges::view::transform([](const C& x) {return x.bar;}));

  for (const auto& c : cs) {
    std::cout << c.bar << std::endl;
  }
}

クラスCstd::vectorCの要素のstd::stringの順序によって並び変えたいコードです。コンパイルは通りますし実行もできますが、順番が並び変わることはありません。

なぜかといえば、sortに渡しているvectortransformしているラムダ式の戻り値型がstd::stringの参照ではなくprvalueを返しているからです。

割とよくありがちなバグで、戻り値型をきちんと指定してあげれば意図通りになります。

ranges::sort(cs | ranges::view::transform([](const C& x) -> std::string& {return x.bar;}));

しかし、ranges::sortはrange-v3にあるindirectly_writableコンセプトで制約されているはずで、この様なものは出力可能とは言えず、indirectly_writableを満たしてほしくは無いしコンパイルエラーになってほしいものです。

prvalueの区別

この問題は突き詰めると

std::string() = std::string();

の様な代入が可能となっているという点に行きつきます。

この様な代入操作は代入演算子の左辺値修飾で禁止できるのですが、標準ライブラリの多くの型の代入演算子は左辺値修飾された代入演算子を持っていません。メンバ関数の参照修飾はC++11からの仕様であり、C++11以前から存在する型に対して追加することは出来ず、それらの型に倣う形で他の型でも参照修飾されてはいません。

これを禁止する為の方法は、単純には間接参照の結果が常に真に参照を返すことを要求することです。

その時に問題となるのが、イテレータの間接参照でプロキシオブジェクトが得られるようなイテレータです。当然そのようなプロキシオブジェクトはprvlaueなので、出力可能であるはずでもindirectly_writableを満たさなくなってしまいます。

そうなると、プロキシオブジェクトを識別してそのprvalueへの出力は許可する必要があります。

プロキシオブジェクトはその内部に要素への参照を秘めているオブジェクトであって、自身のconst性と参照先のconst性は無関係です。従って、constであるときでも出力(代入)が可能となります。

一方、std::string等の型は当然constであるときに代入可能ではありません。

そして、イテレータoについて、decltype(*o)が真に参照を返すとき、そこにconstを追加しても効果はありません。

これらの事から、間接参照がprvalueを返すときにプロキシオブジェクト以外の出力操作を弾くためには、const_castdecltype(*o)に対して適用してconstを付加してから、出力操作をテストすれば良いでしょう。

この結果得られたのが、indirectly_writableにある謎の制約式です。

template<class Out, class T>
concept indirectly_writable = 
  requires(Out&& o, T&& t) {
    *o = std::forward<T>(t);
    *std::forward<Out>(o) = std::forward<T>(t);
    // ↓これ!
    const_cast<const iter_reference_t<Out>&&>(*o) = std::forward<T>(t);
    const_cast<const iter_reference_t<Out>&&>(*std::forward<Out>(o)) = std::forward<T>(t);
  };

std::forward<Out>の差で制約式が2本あるのは、Outに対する出力操作がその値カテゴリによらない事を示すためです。つまり、lvalueは当然として、イテレータそのものがprvalueであっても出力操作は可能であり、そうでなければなりません。これは今回の事とはあまり関係ありません。

iter_reference_tOutからその間接参照の直接の結果型(reference)を取得します。

それが真に参照ならば(その型をT&あるいはT&&とすれば)、そこにconstを追加しても何も起こらず、型はT&あるいはT&&のままとなります。しかし、iter_reference_tprvalueならば(Tとすれば)素直に追加されてconst Tとなります。

ここで起きていることはusing U = T&に対するconst Uのようなことで、これはT& const(参照そのものに対するconst修飾)となって、これは参照型には意味を持たないのでスルーされています。

最後にそこに&&を付加するわけですが、参照が得られているときはT&&& -> T&T&&&& -> T&&となります。*oprvalueを返すときはconst T&&となり、const右辺値参照が生成されます。

最後にこの得られた型を用いて*oconst_castしそこに対する代入をテストするわけですが、この過程をよく見てみれば*oが参照を返している場合は実質的に何もしておらず、すぐ上にある制約式と等価となっています。

つまり、このconst_castを用いる制約式は*oprvalueを返しているときにしか意味を持ちません。そして、const T&&なオブジェクトへの出力(代入)ができるのはTがプロキシオブジェクト型の時だけとみなすことができます。

この様にして、冒頭のコード例の様に意図せずprvalueを返すケースをコンパイルエラーにしつつ、意図してプロキシオブジェクトのprvalueを返す場合は許可するという、絶妙に難しい識別を可能にしています。

そして、これこそが問題の制約式の存在意義です。

std::vector<bool>::reference

イテレータの間接参照がプロキシオブジェクトを返すようなイテレータには、std::vector<bool>イテレータがあります。そのreferenceは1ビットで保存されたbool値への参照となるプロキシオブジェクトのprvlaueであり、まさに先ほどの議論で保護すべき対象としてあげられていたものです。

が、実際にはstd::vector<bool>イテレータstd::indirectly_writableコンセプトを構文的にすら満たしません。まさにこのconst_castを用いる制約式に引っかかります。

int main() {
  // 失敗する・・・
  static_assert(std::indirectly_writable<std::vector<bool>::iterator, bool>);
}

エラーメッセージを見ると、まさにそこを満たしていないと指摘されているのが分かります。

なぜかというと、std::vector<bool>::referenceのプロキシオブジェクトには代入演算子はあってもconst修飾されていないためです。const化してしまうと代入できなくなってしまいます。自身のconst性と参照先のそれとは無関係のはずなのに・・・

C++23に向けてここを修正する動きはあるようですが、この様なプロキシオブジェクトを用いるイテレータを作成するときは、プロキシオブジェクトの代入演算子const修飾に思いを馳せる必要があります。

参考文献

謝辞

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

この記事のMarkdownソース

[C++]ラムダキャプチャの記述順序

ラムダ式を雰囲気で書いているので、キャプチャの正しい順序が分かりません。そのため、コンパイラに怒られて直したり怒られなかったからヨシ!をしています。

正しい順序とは一体何なのでしょうか・・・?

正しいキャプチャ順序

C++20現在、キャプチャには3種類の方法があります。明確に書かれているわけではありませんがそこには確かに順序があり、次のようになります。

  1. デフォルトキャプチャ
    • &
    • =
  2. それ以外
    • 簡易キャプチャ
      • &x
      • x
      • this
      • *this
    • 初期化キャプチャ

はい、これだけです。悩む事も無いですね・・・

// 正しい順番
[&]{};
[=]{};
[=, x]{};
[=, &x]{};
[=, x = 0]{};
[=, this]{};
[=, *this]{};
[=, x, &y, z = 0, this]{};
[=, this, &x, y, z = 0]{};

// 間違った順番
[x, =]{};
[x, &]{};
[&x, &]{};
[&x, =]{};
[x = 0, =]{};
[this, &]{};
[*this, =]{};

= &によるデフォルトキャプチャが先頭にきてさえいれば、後はどういう順番でも構わないという事です。

詳細

ラムダ式の文法定義の中で、ラムダ導入子([])の中のキャプチャ(lambda-capture)は次のように構文定義されています。

lambda-capture:
  capture-default
  capture-list
  capture-default , capture-list

capture-default:
  &
  =

capture-list:
  capture
  capture-list , capture

capture:
  simple-capture
  init-capture

simple-capture:
  identifier ...(opt)
  & identifier ...(opt)
  this
  * this

init-capture:
  ...(opt) identifier initializer
  & ...(opt) identifier initializer

まず最初のlambda-captureを見てみると、capture-defaultcapture-listそれぞれ単体あるいはcapture-default , capture-listの形の列のいずれかとして定義されています。capture-defaultはその次で定義されており、= &のどちらかです。そして、capture-defaultはここ以外では出現しません。

従ってまず、capture-defaultcapture-listよりも前に来なければならない事が分かります。

ではcapture-listとは何なのかと見に行けば、captureあるいはcapture-list , captureのどちらかとして定義されています。この書き方はEBNFにおいて繰り返しを表現する定番の書き方であり、capture-listとは1つ以上のcaptureの列として定義されています。

captureはさらにsimple-captureinit-captureのどちらかとして定義され、ここには順序がありません。

simple-captureは4つのキャプチャが定義されており、上からコピーキャプチャ、参照キャプチャ、thisのコピーキャプチャ、*thisのコピーキャプチャ、が定義されています。ここにもその出現順を制約するものはありません。

init-captureはその名の通り初期化キャプチャを定義しており、コピーキャプチャと参照キャプチャの2種類が定義されています。そしてここにも順序付けはありません。

結局、lambda-captureの中で出現順が定義されているのはcapture-default , capture-listという形式だけであり、これがデフォルトキャプチャ(= &)が先頭に来て後は順不同という事を意味しています。

なお、...(opt)はパラメータパック展開のことで、これはC++20で許可されたものです。これも= &が先頭にきてさえいればどういう順番で現れても構いません。

参考文献

この記事のMarkdownソース

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

文書の一覧

採択されたものはありません、全部で30本あります。

SD-1 2021 PL22.16/WG21 document list

2016年〜2021年(1月)までの提案文書の一覧。

P0447R12 Introduction of std::colony to the standard library

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

以前の記事を参照

このリビジョンでの変更は、範囲やinitializer_listinsert()の戻り値型がvoidに変更されたこと、要素を指定した値で初期化しておくタイプのコンストラクタが非explicitに変更されたこと、reserve()の文言など標準のための文言の調整などです。

P0847R6 Deducing this

クラスのメンバ関数の暗黙のthis引数を明示的に書けるようにする提案。

以前の記事を参照

このリビジョンでの変更は、検討した他の構文の記録と、リフレクション・explicit staticvirtual・コルーチンの議論を含むセクションを再度追加した事、及び*this引数を明示した関数が非静的メンバ関数になるように規格の文書の文言を調整した事です。

P1121R2 Hazard Pointers: Proposed Interface and Wording for Concurrency TS 2

標準ライブラリにハザードポインタを導入する提案。

ハザードポインタは並行処理におけるデータ共有のための仕組みで、ABA問題を回避するためのdeferred reclamationを実装する方法の一つです。

deferred reclamationに関しては以前の記事を参照。

複数のスレッドによって共有されるデータがあり(ヒープ上にあるとします)、あるスレッドがそれを読み取っており他のスレッドも同様に読み書きができる時、読んでいるスレッドは勝手にデータを変更されたり削除されたりされたくはありません。とはいえ、高コストなロックを使いたくもありません。

そこで、全スレッドが読み取る事の出来るところにポインタを用意しておいて、読み取り中のデータのアドレスをそこに入れておきます。そのポインタに登録されたデータは誰かが見ている途中なので変更しない事、というルールを課します。このポインタのことをハザードポインタと呼びます。
ハザードポインタはある瞬間には最大1つのスレッドによって書き込みが可能とされます。読み取りは全てのスレッドから行えます。

他のスレッドが共有データを変更しようとする時、まずハザードポインタを見に行きます。何も登録されていなければ現在のデータを消すのも書き換えるのも自由です。ハザードポインタに登録がある時(そして変更しようとするデータが登録されている時)、現在のデータを維持したまま新しいデータで置き換えることでデータを更新します。維持されたデータは削除待ちとしてマークして、ハザードポインタからの登録が解除された段階で削除されます。

ハザードポインタのイメージ(P0233R6より)

ハザードポインタは単一のものを全スレッドで共有するというよりは、それぞれのスレッドがそれぞれハザードポインタを所有し、変更の際は全てのスレッドのハザードポインタをチェックする、というような実装になるようです。また、ロックフリーデータ構造の実装に使用される場合はハザードポインタは2要素程度のリストになることがあります。

この提案は、ハザードポインタを中心としたこの様な仕組みをサポートし、安全かつ簡単に利用できるようにするためのライブラリを導入しようとするものです。

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

struct Name : public hazard_pointer_obj_base<Name> {
  /* details */
};

std::atomic<Name*> name;

// 頻繁に複数スレッドから呼ばれる
void print_name() {
  // ハザードポインタを取得する
  hazard_pointer h = make_hazard_pointer();
  // nameをハザードポインタへ登録
  Name* ptr = h.protect(name);
  // 以降、*ptrには安全にアクセスできる(勝手に消えたり変更されたりしない)
}

// あんまり呼ばれない
void update_name(Name* new_name) {
  // nameを更新する
  Name* ptr = name.exchange(new_name);
  // 削除待ち登録、全てのスレッドが必要としなくなった時に削除される
  ptr->retire();
}

コメントにあるように、ハザードポインタはデータの読み取りに比べてデータの更新が稀である場合に威力を発揮するものです。更新が頻繁に起こるような場合に適した方法ではありません。

この提案はConcurrency TS v2に向けて議論が進んでいます。現在はLWGで議論中なのでそこには入りそうです。標準ライブラリに入るとしてももう少し先になりそうです。

P1425R2 Iterators pair constructors for stack and queue

std::stackstd::queueに、イテレータペアを受け取るコンストラクタを追加する提案。

std::stackstd::queueイテレータペアを受け取るコンストラクタがなく他のコンテナとの一貫性を欠いており、それによってranges::toの実装では特別扱いするかサポートしない選択を迫られていました。

この提案はこれらのコンテナアダプタと他のコンテナの間の一貫性を改善し、統一的な扱いができるようにするものです。

#include <array>
#include <stack>
#include <queue>

int main() {
  std::array<int, 4> arr = {1, 2, 3, 4};

  // C++20まで、こう書けばできた
  std::stack<int> st{{arr.begin(), arr.end()}};
  std::queue<int> qu{{arr.begin(), arr.end()}};

  // この提案
  std::stack<int> st{arr.begin(), arr.end()};
  std::queue<int> qu{arr.begin(), arr.end()};
}

この提案はLWGでのレビューをほぼ終えていますが、最近提出されたIssue(LWG 3506)との兼ね合いを調査するためのLEWGでのレビューを待っている状態です。問題がなければC++23に入るものと思われます。

P1682R2 std::to_underlying

列挙型の値からその基底の整数型への変換を行うstd::to_underlyingの提案。

多くのコードベースで、列挙値をその基底型に変換する小さな関数を見ることができます。この様な関数がよく見られる理由は単純で、static_cast<int>のように書くと列挙型から基底型に変換しているという事を見失いやすくなるためです。

この様な関数はEffective Modern C++においてもtoUtype()として紹介されており、2019年6月17日時点で、Githubなどでのto_underlying/to_underlying_type/toUtypeのヒット数は(重複を除いても)1000件を超えているようです。

この関数の使用量の増加はScott Meyersの先見性とアドバイスがあらゆる層のC++プログラマーに受け入れられていることを示しており、この様に頻繁に使用されるユーティリティに標準での名前と意味を与えることには価値があります。

また、列挙値の変換という処理は簡単でありながらも正しく書くことが難しいものでもあります

#include <cstdint>

// 基底型を後から変更した(明示的に指定した)
enum class ABCD : uint32_t {
  A = 0x1012,
  B = 0x405324,
  C = A & B,
  D = 0xFFFFFFFF // uint32_t最大値
};

// from before:

void do_work(ABCD some_value) {
  // static_castを使用していることで、コンパイラはこのキャストは意図的なものだと認識
  // 警告は発せられない
  // ABCD::Dが渡ってきた時に間違ったキャストをすることになる
  internal_untyped_api(static_cast<int>(some_value));
}

do_work(ABCD::D);と呼び出されると間違ったキャストが行われ、internal_untyped_api()には意図しないビットパターン渡されることになります。static_cast<int>はそのキャストを意図的に行っていることを宣言するものでもあるため、コンパイラはエラーも警告も発しません。

do_work()内のキャストは、正しくは次のように書く必要があります。

void do_work(ABCD some_value) {
  internal_untyped_api(static_cast<std::underlying_type_t<ABCD>>(some_value));
}

しかし、この関数の引数型を整数に変換可能な型に変更してしまった時の事を考えるとまだ問題があります。static_castを適切に修正するかABCDという型を削除しない限りこのコードはコンパイル可能であり続けます。

この提案は、頻繁に使用される列挙値から整数への変換の意図を明確にしその正しい実装を提供するために、std::to_underlying関数を追加しようとする提案です。

先ほどのdo_work()は次のように書き換えられます。

void do_work(ABCD some_value) {
  internal_untyped_api(std::to_underlying(some_value));
}

std::to_underlyingの引数の型情報は引数型としてコンパイラから渡され、列挙値以外のものが渡されるとコンパイルエラーとなります。これによって先ほどの問題を解決する事が出来ます。また、static_castではなく関数の戻り値として整数型が得られている事によって、戻り値を別の関数に直接渡す場合などにビット幅や符号のミスマッチを警告として得ることができる場合があります。

この提案はすでにLWGでのレビューを終えており、2月初め頃にある次の全体会議で投票にかけられる予定です。何事もなければそこでC++23入りが決定されます。

P1708R3 Simple Statistical Functions

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

提案されている関数は以下のものです。

  • 平均(mean
    • 算術平均
    • 幾何平均
    • 調和平均
  • 分位数(quantile
    • 分位数
    • 中央値
  • 最頻値(mode
  • 歪度(skewness
  • 尖度(kurtosis
  • 分散(variance
  • 標準偏差standard deviations
  • 重み付きの各種統計量

この提案はBoost Accumulators Libraryを参考にしており、これらの統計処理関数はstd名前空間のグローバル関数としても、Accumulator Objectという複数の統計量の同時計算用クラスとしても提供されます。

基本統計量のサンプル

#include <stats>
#include <vector>

int main() {
  std::vector vec = { 2, 3, 5, 7, 7, 11, 13, 17, 19};

  // 算術平均
  auto mean = std::mean(vec);
  
  // 中央値
  auto [m1, m2] = std::sorted_median(vec);
  
  // 最頻値
  std::vector<int> modes{};
  std::mode(vec, std::back_inserter(modes));

  // 標本分散
  auto sample_var = std::var(vec, std::sample_t);

  // 標本の標準偏差
  auto stddev = std::stddev(vec, std::sample_t);
}

このように、これらの関数はstd::rangesアルゴリズム関数と共通したインターフェースを持っており、入力としてrangeオブジェクトを受け取ります。また、射影やExecutionPolicyを取るオーバーロードも用意されています。

Accumulator Objectのサンプル

#include <stats>

int main() {
  std::vector vec = { 2, 3, 5, 7, 7, 11, 13, 17, 19};

  std::mean_accum<int> am{};
  std::geometric_mean_accum<int> gm{};
  std::harmonic_mean_accum<int> hm{};

  // シングルパスの操作によって複数の統計量をまとめて計算
  accum(vec, am, gm, hm);
}

Accumulator Objectはこのように複数の統計量を同時に計算したいときに使用でき、渡された範囲を一回だけ走査します。

P1989R1 Range constructor for std::string_view 2: Constrain Harder

std::string_viewのコンストラクタにrangeオブジェクトから構築するコンストラクタを追加する提案。

この提案の元となった提案(P1391R3)がC++20にて採択されており、std::string_viewイテレータペアを受け取るrangeコンストラクタを獲得しました。P1391R3ではrangeオブジェクトから構築するコンストラクタも提案されていたのですが、それは見送られこの提案に分離されました。

自作の型からstd::string_viewを構築するには、std::string_viewへの変換演算子を用意することで行われます。そのような型は多くの場合文字列のrangeとしてrangeインターフェースを備えていることが多く、単純にstd::string_viewrangeコンストラクタを追加してしまうと、一見どちらのコンストラクタが選択されているのか分からなくなります。

struct buffer {
  buffer() {};

  // rangeインターフェース
  char const* begin() const { return data; }
  char const* end() const { return data + 42; }

  // string_viewへの変換演算子
  operator basic_string_view<char, s>() const{
    return basic_string_view<char, s>(data, data +2);
  }

private:
  char data[42];
};

std::string_view f(const buffer& buf) {
  // string_viewにrangeコンストラクタがある時、どっちが使われる??
  std::string_view strview{buf};

  return strview;
}

このように、場合によっては既存のコードの振る舞いを変えてしまうことになります。

この事に対する検討のために、P1391R3からはrangeオブジェクトを取るコンストラクタは分離され、この提案に引き継がれました。

この提案では、rangeオブジェクトを取るコンストラクタを次のように定義する事でこれらの問題を回避しています。

namespace std {

  template <typename T, typename Traits>
  concept has_compatible_traits = !requires { typename T::traits_type; }
    || ranges::same_as<typename T::traits_type, Traits>;

  template<typename charT, typename traits = std::char_traits<char>>
  struct basic_string_view {

    //...

    template <ranges::contiguous_range R>
      requires ranges::sized_range<R>
        && (!std::is_convertible_v<R, const charT*>)
        && std::is_same_v<std::remove_cvref_t<ranges::range_reference_t<R>>, charT>
        && has_compatible_traits<R, traits>
        && (!requires (std::remove_cvref_t<R> & d)
          {
            d.operator ::std::basic_string_view<charT, traits>();
          })
    basic_string_view(R&&);
  }
}

まず、rangeオブジェクトを取るコンストラクタは型remove_cvref_t<R>が自分と同じstd::basic_string_viewへの変換演算子を持っている場合は選択されないようにしています(一番最後のrequires式)。

次に考慮されているのは、型remove_cvref_t<R>Traits型だけが異なるstd::basic_string_viewへの変換演算子を持っている場合です。その場合今までは(型変換演算子による構築では)コンパイルエラーとなっていました。この提案によるrangeオブジェクトを取るコンストラクタはそのような場合でも、remove_cvref_t<R>::traits_typeを持っていないなら呼び出されるようになっています。

この提案はP1391R3の議論の過程でこの問題以外の部分のレビューをほぼ終えているため、現在はLWGでの最後のレビューを待っている状態です。

P2036R1 Changing scope for lambda trailing-return-type

ラムダ式の後置戻り値型指定において、初期化キャプチャした変数を参照できるようにする提案。

現在の仕様の下では、次のコードはコンパイルできません。

auto counter1 = [j=0]() mutable -> decltype(j) {
  return j++;
};

ラムダ本体のjは初期化キャプチャした変数jを参照しますが、後置戻り値型のdecltype(j)にあるjはキャプチャしたものではなく外のスコープの名前を探しに行きます。これは、初期化キャプチャした変数名はラムダ式の本体内部でしか変数名として参照できないためです。

このコードはコンパイルエラーとなるのでまだいいですが、もしjが存在していたらどうなるでしょう・・・

int j = 0;

auto counter1 = [j=0.0]() mutable -> decltype(j) {
  return j++;
};

この場合コンパイルは恙なく完了し、このラムダの戻り値型はdoubleではなくintになります。この様な暗黙変換が静かに起こっていると思わぬバグとなる可能性があります。

この問題は初期化キャプチャで最も顕著になりますが、通常のコピーキャプチャでも問題になる可能性があります。

template <typename T>
int bar(int&, T&&);        // #1

template <typename T>
void bar(int const&, T&&); // #2


int i;

auto f = [=](auto&& x) -> decltype(bar(i, x)) {
  return bar(i, x);
}

f(42); // コンパイルエラー

ラムダの後置戻り値型指定ではiは外で宣言されたint型の変数iを参照し、bar()は#1が選択され戻り値型はintと推論されます。しかし、ラムダ式内部でのiはコピーキャプチャしたconst int型の変数iを参照し、return文のbar()は#2が選択され戻り値型はvoidと推論されます。これは当然コンパイルエラーとなります。

とはいえ、この種の問題は非常に出会いにくいものであり、サンプルコードを思いつくことは出来ても実際に見かけることはなく、筆者の方によるコードベースの調査でも見つけることは出来なかったようです。

ただ、このラムダ式の本体と後置戻り値型における同じ名前の異なる解釈は、ラムダ式の簡易構文の提案(P0573R2)が拒否された理由の一つでした。この様な同じ名前の非常に近しいコンテキストでの異なる解釈はバグであると思われ、C++の将来の進化を妨げていることからこの様なコーナーケースは排除すべき、という提案です。

この事を修正するにしても少し問題があります。

int i;
[=]() -> decltype(f(i))
{/* ... */}

この様な場合、iがキャプチャされるかどうかはラムダ式の本体を全て見ないと分かりません。現在は外側の変数iを見に行きますが、先程の問題の解決のためにはキャプチャされているならラムダ内部の変数を見に行く必要があります。

この提案では、この様な場合は常にその名前はキャプチャされた変数であるとして扱って推論を行う事を提案しています。

そして、この変更は初期化キャプチャかコピーキャプチャをしていて、後置戻り値型にキャプチャした変数名を使用している場合に既存のコードを壊す可能性があります。特に、次の様なコードのコンパイル結果が変わります。

auto f(int&)       -> int;
auto f(int const&) -> double;

int i;

auto should_capture = [=]() -> decltype(f(i)) {
    return f(i);
};

auto should_not_capture = [=]() -> decltype(f(i)) {
    return 42;
};

現在、この二つのラムダ式の戻り値型はintとなりますが、この提案以降では両方共doubleとなります。

筆者の方の調査でもこの変更で壊れるコードは見つからなかったことから、既存のコードを壊す可能性は極低いものと思われます。EWGにおけるレビューでは、この問題をC++の欠陥として扱うことに合意が取れているようです。

P2072R1 Differentiable programming for C++

C++微分可能プログラミング(Differentiable Programming)サポートを追加するための検討の文書。

微分可能プログラミングとは、従来の計算に微分可能という性質を加えた形で処理を記述するプログラミングスタイル・言語・DSLを指し、その実態は自動微分であるようです。
微分可能プログラミングは機械学習ディープラーニング)の分野で興った概念で、ニューラルネットワークの出力部分の処理を微分可能にすることで、ニューラルネットの学習時にその出力処理も含めて学習を行うものです。それによって、ニューラルネットに出力処理を組み込むことが可能になります。例えば、微分可能レンダリングというものがあります。

微分可能プログラミングサポートを追加するというのは、C++で自動微分サポートを追加するという意味なので、その恩恵は機械学習だけではなく数値最適化や物理シミュレーションなど様々な分野に及びます。

この文書はC++に最も適した形での自動微分サポートの議論のために、微分を計算するための方法や自動微分についてを解説し、ライブラリ・言語サポート・将来の言語機能によるサポートなど、自動微分を実装するために可能なアプローチについてを概説したものです。

この文書では、ライブラリサポートよりも言語サポートが望ましく、浮動小数点数型に適用可能でさえあればテンプレートである必要もなく、特に既存のコードに自動微分を適用可能であることが望ましいと述べています。ようするにコードをコンパイルする過程で処理のグラフを解析し、自動でその勾配を算出していくものです。

そのような既存実装の一つであるEnzymeというLLVM IRを解析してリバースモード自動微分を実現するコードトランスパイラによるサンプルです。

// 行列の差の二乗和(自動微分を意識していない普通のコード)
__attribute__((noinline))
static double matvec(const MatrixXd* __restrict W, const MatrixXd*
__restrict M) {
  MatrixXd diff = *W-*M;
  return (diff*diff).sum();
}

int main(int argc, char** argv) {
  // 行列の初期化
  MatrixXd W = Eigen::MatrixXd::Constant(IN, OUT, 1.0);
  MatrixXd M = Eigen::MatrixXd::Constant(IN, OUT, 2.0);
  MatrixXd Wp = Eigen::MatrixXd::Constant(IN, OUT, 0.0);
  MatrixXd Mp = Eigen::MatrixXd::Constant(IN, OUT, 0.0);

  // matvecによる処理の実行と自動微分の計算
  // EnzymeがLLVM IRを解析することでこの関数の導関数を求め、計算するコードを出力する
  __enzyme_autodiff((void*)matvec, &W, &Wp, &M, &Mp);
  
  // ...
}

このコードは、ClangによってLLVM IRに変換されたあとでEnzymeによって導関数LLVM IRとして求められ、その結果をClangによって実行ファイルへとコンパイルすることで実行可能なプログラムを得ます。この文書の示すC++自動微分サポートの方向性は、__enzyme_autodiffの部分を言語サポートによって簡易な構文に置き換えつつ、このコンパイル過程を通常のコンパイルで行おうとするものです。

P2093R3 Formatted output

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

前回の記事を参照

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

  • std::printという名称が他言語の出力機能との互換性の点で有利であることの説明を追加
  • P1885を使用してリテラルエンコーディングを取得することで実装を簡素化
  • 様々な言語でのユニコード処理の比較結果を追記
  • 提案文書の文言と、規格のための文言の調整

などです。

P2168R1 generator: A Synchronous Coroutine Generator Compatible With Ranges

Rangeライブラリと連携可能なT型の要素列を生成するコルーチンジェネレータstd::generator<T>の提案。

前回の記事を参照

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

  • コンパイラでのベンチマーク結果を追加
  • 再帰したジェネレータにおける変換の曖昧さの解消のためにelements_of()を追加
  • アロケータサポートを追加
  • Symmetric transferが様々なアロケータ/値のジェネレータで機能することを追記
  • イテレータ->を削除
  • 提案しているstd::generatorを新ヘッダ<generator>に配置するように変更
  • Valueテンプレートパラメータの利点を強調するために例を追加

などです。

P2210R1 Superior String Splitting

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

前回の記事を参照

このリビジョンでの変更は、const-iterationの説明を修正し現在のviews::splitの上により適切なsplitを構成できるようにしたこと、現在のviews::splitの機能を別の意味論に基づく名前で維持するようにしたことです。

この提案では、現在の超汎用splitstd::ranges::lazy_split_view/std::views::lazy_splitと名前を変更し、新しいより直感的なsplitstd::ranges::split_view/std::views::splitとすることを提案しています。

P2216R2 std::format improvements

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

以前の記事を参照

このリビジョンでの変更は、提案している機能の効果についての調整がメインです。

この提案はLEWGでの投票において 提案する2つの事項(コンパイルフォーマットチェック、バイナリサイズ削減)についてC++20へのDefact Reportとすることで合意が取れ、C++20への逆適用のためにLWGで先行してレビューされました。そこでもC++20へのDRとすることで合意が取れ、LEWGでのレビューと投票を待ってどうやら2021年の夏ごろにはC++23に採択されそうです。その場合この変更はC++20にさかのぼって適用されます。

P2232R0 Zero-Overhead Deterministic Exceptions: Catching Values

任意の値を投げることのできる静的例外機能の提案。

P0709R4にて議論されているstd::errorによる静的例外では、例外オブジェクトとして特定のものだけをthrowすることができます。しかし、既存の例外を使用するコードにおいては独自の例外型が使用されていることがあり、その場合はstd::errorに移行することはできず、結果としてC++におけるエラーハンドリングの複雑さを増加させることになってしまいます。

この提案はそれを防止するため、値によるキャッチを使用する場合に任意の型の例外オブジェクトを効率的に転送するためのP0709R4とは別のアプローチを提案するものです。

まずこの提案による任意の値のthrowでは、次の2つの仮定を置きます

  • 値のcatchのみを使用する
    • catch(E& e)ではなく、catch(E e)
  • catch(E e)のセマンティクスを変更する
    • 動的な型ではなく、静的な型によって例外オブジェクトとマッチさせる

この過程を置くと、例外オブジェクトはtry-catchの範囲内でスタック領域を使用できるようになります。

try
{
    f(); // Throws
}
catch( E1 e1 )
{
    // Use e1
}
catch( E2 e2 )
{
    // Use e2
}

今、E1, E2のそれぞれの例外オブジェクトの型とそのサイズは静的に決定できるため、スタック上の領域を予約しておくことができます。すると、キャッチした側ではE1, E2のどちらの値が投げられてきたのかを知る仕組みが必要となります。この判定のために、std::optional<E1>, std::optional<E2>の領域をスタック上に確保しておきます。

次に、f()内のthrow E1{}/throw E2{}がそのスタック領域を使用できるようにする効率的な仕組みが必要となります。これには、スレッドローカルストレージを使用します。

f()を囲うtryブロックでは、f()を呼び出す前にe1, e2用に予約されたsスタック上のstd::optional<E1>, std::optional<E2>の領域を指すようにスレッドローカルストレージにポインタpE1, pE2を初期化します。現在の例外処理の実装もスレッドローカルストレージを使用しているので、それを利用することに問題はありません。

f()が例えばをthrow E1{}したとき、スレッドローカルポインタpE1にアクセスし、次のどちらかを実行します

  • pE1nullであれば、catch(E1 e1)ステートメントが現在のコールスタックで使用できないことを意味しており、従来の例外機構による処理に切り替える
  • それ以外の場合、pE1の指す領域にE1のオブジェクトを構築する

次に、スタックを最上位の例外スコープ(最も内側にあるtry-catchブロックの位置)まで巻き戻します。この実装はP0709にあるものと同じものが利用でき(詳細は不明・・・)、代替戻り値としては、例外オブジェクトの代わりに失敗か成功かを表す1ビットのフラグを返します。

try-catchブロックに到着する(巻き戻る)と、あらかじめ確保しておいたstd::optional<E1>, std::optional<E2>(スタック上)を調べます。catchブロックの順番にstd::optionalオブジェクトがチェックされ、空でない最初のstd::optionalに対応するcatchブロックが実行されます。ここでの説明の例では、e1 -> e2の順でチェックされ、catch(E1 e1)スコープが選択されます。適切なブロックが見つからなければスタック巻き戻しを続行し、最終的には適切なcatchで捕捉されるか従来の例外機構にスイッチするかのどちらかで完了します。

これによって、スタックの巻き戻しが高速になりcatchが動的型ではなく静的な型でチェックされるようになるため、P0709と比較しても効率がさらに向上しているとのことです。

P2244R0 SG14: Low Latency/Games/Embedded/Finance/Simulation Meeting Minutes

SG14のミーティングの議事録。

SG14はゲームハードや組み込みシステムなどのリソースが制限されている環境や金融やシミュレーションなど低遅延が重視される環境などにおけるC++についてを議論・研究するグループです。

P2245R0 SG19: Machine Learning Meeting Minutes

SG19のミーティングの議事録。

SG19はC++における機械学習サポートについて議論・研究するグループです。

P2246R1 Character encoding of diagnostic text

コンパイル時にメッセージを出力するものについて、ソースコードエンコーディングが実行時エンコーディング(出力先のエンコーディング)で表現できない場合にどうするかの規定を修正する提案。

以前の記事を参照

このリビジョンでの変更は、提案する文言の調整(shallshouldへ変更)がメインです。

この提案はSG16で議論されていましたが、この提案の方向性についての合意が取れたたためEWGへ転送されました。

P2259R1 Repairing input range adaptors and counted_iterator

iterator_categoryが取得できないことから一部のrange adoptorのチェーンが機能しない問題と、counted_iteratorの問題を修正する提案。

以前の記事を参照

このリビジョンでの変更は、elements_viewiterator_categoryを修正しiterator_conceptを定義した事です。そのiterator_conceptは受けているrangeの満たすC++20的性質を、iterator_categoryは受けているイテレータC++17的性質を受け継いで決定されます。これはこの提案で変更されている他のViewも同様の方向性で調整されています。

この提案はIssue解決のためのものだったこともありLWGで議論されており、LWGでは全会一致でアクセプトされました。次の全体会議で全体投票にかけられ問題が無ければC++23に採択される予定です。

P2266R0 Simpler implicit move

return文における暗黙のムーブを改善する提案。

C++20での欠陥改善(P1825R0)により、関数のreturnにおいては右辺値参照型のローカル変数からでも暗黙的にムーブ(implicitly move)を行うことができるようになります。しかし、この改善されたはずの規格の文書にはまだ欠陥があり、関数が参照を返す場合に暗黙ムーブが行われないようです。

struct Widget {
  Widget(Widget&&);
};

struct RRefTaker {
  RRefTaker(Widget&&);
};

// 次の3つのケースではreturnで暗黙ムーブされる

Widget one(Widget w) {
  return w;  // OK、C++11以降
}

RRefTaker two(Widget w) {
  return w;  // OK、C++11以降(CWG1579解決後)
}

RRefTaker three(Widget&& w) {
  return w;  // OK、C++20以降(P0527による)
}

// 暗黙ムーブされてほしい、されない・・・
Widget&& four(Widget&& w) {
  return w;  // Error!
}

この様に関数が右辺値参照を返す場合には暗黙のムーブが行われず、このケース(four())では、関数ローカルの左辺値wを右辺値参照型Widget&&に暗黙変換できずにエラーとなります。

戻り値が参照である場合に、同様の事が起きます。

struct Mutt {
  operator int*() &&;
};
struct Jeff {
  operator int&() &&;
};

// 暗黙ムーブされ、Mutt&&からの暗黙変換によってreturn
int* five(Mutt x) {
  return x;  // OK、C++20以降(P0527による)
}

// 暗黙ムーブされず、int&へ変換できずエラー
int& six(Jeff x) {
  return x;  // Error!
}
template<class T>
T&& seven(T&& x) { return x; }

void test_seven(Widget w) {
  // Widget& seven(Widget&)
  Widget& r = seven(w);               // OK
  // Widget&& seven(Widget&&)
  Widget&& rr = seven(std::move(w));  // Error
}

関数の戻り値型がオブジェクト型ではないとき、暗黙ムーブが行われない事によってこれらのような問題が起きています。

この提案は、関数から返されるmove-eligibleな式は常にxvalueであると言うように指定する事でこれらの問題の解決を図るものです。

P2276R0 Fix std::cbegin(), std::ranges::cbegin, and cbegin() for span (fix of wrong fix of lwg3320)

メンバ関数cbegin()とフリー関数のstd::cbegin()/std::ranges::cbegin()の不一致を修正し、std::spancbegin()サポートを復活させる提案。

std::cbegin()/std::ranges::cbegin()はともに、const引数で受け取ったオブジェクトに対して使用可能なbegin()を呼び出そうとします。標準のコンテナならばconstオブジェクトに対するbegin()メンバ関数const_iteratorを返すため、メンバcbegin()と同様の結果を得ることができます(cend()も同様)。

しかし、これはつまりメンバとしてcbegin()/cend()を用意していてもstd::cbegin()/std::ranges::cbegin()はそれを呼び出さない事になり、クラスによってはstd::cbegin()/std::ranges::cbegin()で期待されるread-onlyなイテレータアクセスを提供しない可能性があります。

C++20当初のstd::spanがまさにその問題に引っかかっておりLWG Issue 3320にて一応解決されました。しかし、この修正はstd::span::cbegin()メンバ関数std::cbegin()/std::ranges::cbegin()の戻り値型の不一致の是正に重きを置いていたため、std::spanからcbegin()/cend()メンバ関数を削除する事でその不一致を解消していました。

一方、std::spanbegin()メンバ関数constオブジェクトに対するオーバーロードを提供しておらず、std::cbegin()/std::ranges::cbegin()から呼び出された時でもmutableイテレータを返してしまいます。結局std::spanconst_iteratorを提供しないため、std::cbegin()/std::ranges::cbegin()を用いてもread-onlyなイテレータアクセスはできません。

std::vector<int> coll{1, 2, 3, 4, 5};
std::span<int> sp{coll.data(), 3};

for (auto it = std::cbegin(sp); 
          it != std::cend(sp); ++it) 
{
  *it = 42; // コンパイルエラーにならない・・・
}

for (auto it = std::ranges::cbegin(sp);
          it != std::ranges::cend(sp); ++it)
{
  *it = 42; // コンパイルエラーにならない・・・
}

for (auto it = sp.cbegin(); // コンパイルエラー!
          it != sp.cend(); ++it)
{
  // ...
}

この提案では、次の2つの変更によってこの問題の解決を図ります。

  • std::cbegin()/std::ranges::cbegin()は引数に対してそのメンバ関数.cbegin()が呼び出し可能ならばそれを使用する
  • その上で、std::spanメンバ関数cbegin()/cend()を追加し、const_iteratorサポートを復活させる

1つ目の変更はcend()/crbegin()/crend()に対しても同様の変更を提案しています。

P2277R0 Packs outside of Templates

パラメータパックをテンプレートではないところでも使えるようにする提案について、実装難度がメリットを上回っていないかどうかの検討を促す文書。

現在の進行中の提案のうち、パラメータパックをより活用しようとするものには次の4つがあります。

これらの提案では、可変長テンプレートでないところでも、あるいは非テンプレートの文脈でもパラメータパックを活用しようとしています。

template <typename... Ts>
struct simple_tuple {
  // データメンバ宣言時のパック展開(P1858)
  Ts... elems;
};

int g(int);

// 非関数テンプレート
void f(simple_tuple<int, int> xs) {
  // 構造化束縛でのパック導入(P1061)
  auto& [...a] = xs;
  int sum_squares = (0 + ... + a * a);
  
  // パラメータパックでないもののパック展開(P1858)
  int product = (1 * ... * g(xs.elems));

  // テンプレートパラメータのreflection-rangeを構築する(P1240)
  // これはint型のリフレクション2つを含むvector
  constexpr auto params = std::meta::parameters_of(reflexpr(decltype(xs)));
  
  // reflection-rangeの展開1(P1240)
  // decltype(ys) is simple_tuple<int, int>
  simple_tuple<typename(...params)> ys = xs;

  // reflection-rangeの展開2(P2236)
  // decltype(zs) is simple_tuple<int, int>
  simple_tuple<|params|...> zs = xs;
}

P1240とP2236の二つのリフレクションベースの提案におけるreflection-rangeの展開は似ていますが、P1240が単なる展開しかできないのに対して、P2236は展開しつつ変換することもできます。例えば、simple_tuple<int&, int&>構成しようとするとそれぞれ次のように書くことができます。

// P1240の方法
constexpr auto refs = params
                    | std::views::transform(std::meta::add_lvalue_reference);
simple_tuple<typename(...refs)> ys_ref{a...};

// P2237の方法
simple_tuple<|params|&...> zs_ref{a...};

これらの提案のうち一部のものについては実装の複雑さとコンパイル時間増大の懸念が示されています。

ある名前がパック展開の対象となるか否かは一連の式を最後まで見る必要があります。...は接尾辞であり、しかもパック名の直後だけではなくその後の任意の場所にあらわれる可能性があるためです。ただし、現在のところこのような配慮は可変長テンプレートの中でだけ行えばよく、他のところではこれを考慮する必要はありません。

しかし、これらの提案の機能の一部には可変長テンプレートではない場所でパック展開が発生するものがあります。これがもし導入されると、可変長テンプレートではないC++の全てのところでパック展開が出現する可能性を考慮する事になり、これはコンパイル時間を増大させます。

P1240R1はこのことを考慮して注意深く設計されているようですが、他の提案にはこの問題があります。

この問題への対処には次の3つの方法があります。

  1. テンプレートの外でのパック展開を許可し、そのために発生するコスト(コンパイル時間増大)を受け入れる
  2. テンプレート外でのパック展開には、接頭辞によって行う何かを考え出す。パック展開にはその仕様を推奨する。
  3. 可変長テンプレートの外でのパック展開は許可しない

筆者の方は1を推奨しているようです。

この文書は、EWGがこれらの提案の議論の前に上記選択肢のどれを選択するのか?あるいはテンプレートの外でのパック展開は利点がコストを上回っているのか、なるべく早期にその方向性を決定することを促すものです。

P2278R0 cbegin should always return a constant iterator

std::ranges::cbegin/cendを拡張して、常にconst_iteratorを返すようにする提案。

先程P2276の所でも言っていたように、std::ranges::cbegin/cendconstオブジェクトに対するstd::ranges::begin/endを呼び出すため、必ずしもconst_iteratorを返しません。

C++11でメンバcbegin/cendと非メンバstd::begin/endが追加され、その後std::cebgin/cendが追加された(LWG Issue 2128)ときは、コンテナオブジェクトcに対するstd::as_const(c).begin()c.cbegin()の結果が異なるコンテナは(少なくとも標準ライブラリの中には)存在しておらず、CPOの様なものも発明されていなかったので、std::cbegin()が実質的にstd::as_const(c).begin()のように定義されても問題はなく、仕方ない所がありました。

その後もC++17までは何事もありませんでしたが、C++20にてstd::spanが追加されるとstd::cbegin/endの振る舞いが問題になりました。std::spanは別の範囲を参照するものでしかなく、それ自身のconst性と参照先のconst性が同期していません。したがって、std::as_const(c).begin()c.cbegin()の結果が一致しません。例えば、std::spanでは参照先のconst性を表すのはconst span<T*>ではなくspan<const T*>です。従って、それらのメンバbegin/endconstオーバーロードconst_iteratorを返すのはセマンティクスにあっていません。

しかし一方で、std::cbegin/endspanのメンバbegin/endconstオーバーロードを呼び出してしまうので、std::cbegin/endを使って取得したイテレータconst_iteratorではありません。ただし、spanのメンバcbegin/cendはきちんとconst_iteratorを返していたため、非メンバstd::cbegin/endとメンバcbegin/cendの間で結果が異なることになります。最終的に、一貫性のためにspanのメンバcbegin/cendは削除されました(LWG Issue 3320)。

この問題は現在のところstd::spanでしか起きてないようですが、C++20で追加されたRangeライブラリの各種Viewの中にはそもそもconst-iterableではないためにメンバcbeginもメンバbeginconstオーバーロードも提供していないものがあります。また、これから追加されるであろう他のViewでも同様あるいはspanと同様の問題が発生しうるものがあるようです。

この提案ではこの問題の解決のために、イテレータ/センチネルのペアをラップしてconst_iterator化するmake_const_iteratorようなものを追加し、std::ranges::cbegin/cendでは、それを用いて引数のrangeオブジェクトから得られるイテレータ/センチネルのペアをラップして、std::ranges::cbegin/cendが常にconst_iteratorを返すように変更することを提案しています。また、それを用いてviews::const_rangeアダプタを追加することも提案しています。

ただし、std::cbegin()/cend()後方互換性維持のために手を付けていません。

P2279R0 We need a language mechanism for customization points

C++に適切なカスタマイゼーションポイントを提供するための言語サポートを追加する検討を促す提案。

現在のC++言語機能の範囲内で利用可能なカスタマイゼーションメカニズムには次のようなものがあります。

  • 仮想関数
  • クラステンプレートの特殊化
  • ADL
  • カスタマイゼーションポイントオブジェクト(CPO)
  • tag_invoke

過去に提案されていたカスタマイゼーションメカニズムには次のようなものがあります。

  • カスタマイゼーションポイント関数
  • コンセプトマップ

それらにRustのTraitを含めて比較すると、それぞれ次のような特性を持ちます

✔️は可能であること、❌は不可能であること、 🤷 は部分的には可能だが完全ではないことをそれぞれ表しています。

各行の意味はそれぞれ

  • Interface visible in code
    • カスタマイズ可能な(あるいはその必要がある)インターフェース(関数など)がコードで明確に識別できる
  • Providing default implementations
    • デフォルト実装を提供し、なおかつオーバーライド可能
  • Explicit opt-in
    • インターフェースを明示的にオプトインできる(インターフェースへのアダプトが明示的)
  • Diagnose incorrect opt-in
    • インターフェースに意図せずアダプトしない
  • Easily invoke the customization
    • カスタマイズされたものを簡単に呼び出せる
    • デフォルト実装がある場合、必ずカスタマイズされたものを呼び出す
  • Verify implementation
    • ある型がインターフェースを実装していることを簡単に確認できる(機能がある)
  • Atomic grouping of functionality
    • インターフェースにアダプトするために必要な最小の機能グループを提示でき、早期にそれを診断できる
  • Non-intrusive
    • 非侵入的(その型を所有していない人が後からカスタマイズできる)
  • Associated Types
    • 関連する型をまとめて扱える(個別の型ごとにインターフェースにアダプトする必要が無い)
    • 例えば、イテレータ型に対するカスタマイゼーションポイントを提供する時、イテレータの要素の型ごとにカスタマイズ処理を書く必要が無い。

そして追加で、Customization Forwardingという要求も検証しています。例えばCPOやtag_invokeなら、それそのものを呼び出し可能オブジェクトとして他の関数などに渡すことができます。一方、コンセプトマップは(Rustのtaritも?)それそのものは呼び出し可能ではありません。

この提案は、理想的にはこれらの要件をすべて満足するようなカスタマイズメカニズムをC++の言語機能としてサポートすることを目指して、その議論の出発点となるべく書かれたものです。

P2280R0 Using unknown references in constant expressions

定数式での参照のコピーを許可する提案。

C++では生配列のサイズを求めるのにstd::sizeを使用できます。が、constexpr関数では不可解なコンパイルエラーに遭遇することがあります。

template <typename T, size_t N>
constexpr auto array_size(T (&)[N]) -> size_t {
  return N;
}

void check(int const (&param)[3]) {
  int local[] = {1, 2, 3};

  constexpr auto s0 = array_size(local); // ok
  constexpr auto s1 = array_size(param); // error
}

この提案は、この問題を解決し二つ目の呼び出しが適格となるようにするものです。

これはルールとしては定数式で禁止されている事項に引っかかっているためにコンパイルエラーとなります。

N4861 7.7 Constant expressions [expr.const]/5.12より

  • 参照に先行して初期化されていて次のどちらかに該当するものを除いた、変数の参照または参照型データメンバであるid-expression
    • 定数式で使用可能である
    • その式の評価の中でlifetimeが開始している

[expr.const]/5では定数式で現れてはいけない式が列挙されています。これはそのうちの一つです。

定数式ではあらゆる未定義動作が許可されないため、コンパイラはすべての参照が有効であることを確認する必要があります。先程のarray_sizeが機能するためには配列の参照を定数式で読み取る(コピーする)必要がありますが、関数引数の参照は直接的には有効性が判定できないものであり、上記のルールに抵触するタイプの参照となります。ただ、ここでは単に参照のコピーが必要なだけで参照そのものに関心はないはずです。

この事はポインタを取るように変更する事でより明快になります。

template <typename T, size_t N>
constexpr size_t array_size(T (*)[N]) {
  return N;
}

void check(int const (*param)[3]) {
  constexpr auto s2 = array_size(param); // error
}

ここでのarray_sizeの呼び出しではparamのコピーが発生しています。定数式中で関数にコピー渡しする場合、コピー元も定数式でなければなりませんが、関数の引数は定数式ではありません(cosntexpr/consteval関数の中であっても)。ローカルの配列(のポインタ)の場合はその参照(ポインタ)が有効であることをコンパイラが認識できるので問題にはなりません。

ただし、この様な問題は参照(ポインタ)でのみ起きます。値を渡す場合、例えばstd::arrayを用いるとコンパイルエラーは起こりません。

void check_arr_val(std::array<int, 3> const param) {
  std::array<int, 3> local = {1, 2, 3};

  constexpr auto s3 = std::size(local); // ok
  constexpr auto s4 = std::size(param); // ok
}

提案では、この様に参照そのものと関係なく動作するケースにおいて、定数式内での参照の読み取り(すなわちそのコピー)を許可しようとするものです。参照そのものに依存するような操作は引き続き禁止されます。

EWGでのレビューでは、この問題をC++23までに解決することとC++11までの欠陥報告とする事に合意が取れており、EWGでの次の投票を受けてCWGに転送される予定です。

P2281R0 Clarifying range adaptor objects

range adaptor objectがその引数を安全に束縛し、自身の値カテゴリに応じて内部状態を適切にコピー/ムーブする事を明確化する提案。

GCCはRangeライブラリの全ての部分を、MSVCは一部を既に実装していますが、そこには次のような相違点があるようです。

template<class F>
auto filter(F f) {
  // GCC : fを参照で保持する
  // MSVC : fを常にコピーして保持する
  return std::views::filter(f);
}

std::vector<int> v = {1, 2, 3, 4};

// 状態を持つCallableオブジェクトを渡す
auto f = filter([i = std::vector{4}] (auto x) { return x == i[0]; });

// GCCの場合、fにダングリング参照が含まれている
// MSVCは無問題
auto x = v | f;

明らかにMSVCの動作が望ましいのですが、adaptor(args...)の様に呼ばれたときにadaptorargs...をどのようにキャプチャするのかは規定がありません。また、adaptor(range, args...)adaptor(args...)(range)が同じ振る舞いをするという要件は、ここでargs...をコピーするという選択を排除している可能性があります。

結果的に現在の記述では、左辺値の範囲に対するパイプラインを再利用しても安全であることを保証していません。つまり、上記におけるxの再利用が安全であるかは未規定に近く、v | fでは(fの状態がなんであれ)fからムーブされることが無いことを示唆しています。

この提案では、range adaptor objectが行う部分適用がstd::bind_frontの行う事と同等であることが明確になるように現在の規格書の記述を変更しています。

それによって、上記のMSVC実装が合法かつ推奨される実装となります。これは、range adaptor objectに渡された引数が参照によって束縛されず常に値によって束縛され、パイプラインに渡す時にはその値カテゴリに応じて束縛されたものを適切にコピー/ムーブする、という事でもあります。

疑似コードで書くと次のようになります。

auto c = /* 任意のrange */;
auto f = /* コピーコストが高い関数オブジェクト */;

c | transform(f); // fをコピーして結果のviewにムーブする

auto t = transform(f);  // fをコピーする
c | t;            // tから再度fをコピーする
c | std::move(t); // tからfをムーブする

またこれに伴って、customizetion point objectrange adaptor objectはCPOでもある)がコピーやムーブされたときにも同じ引数に対して同じ結果を返すことを保証するように文面を明確化しています。

P2283R0 constexpr for specialized memory algorithms

<memory>にある未初期化領域に対する操作を行う各関数をconstexprにする提案。

未初期化領域に対する操作を行う各関数とは、<memory>にあるstd::uninitilized_~という関数群の事です。

これらの関数はstd::vectorの内部実装に使用されており、std::vectorconstexpr化する際にこれらの関数のconstexpr化が必要となることが発覚し、提案に至ったようです。

その際に問題になるのが、std::uninitialized_default_constructの効果で、C++20で追加されたconstexprな配置newを行うstd::construct_atを使ってしまうとデフォルト初期化ではなくゼロ初期化になってしまうためstd::construct_atを利用できないのですが、現状それ以外にconstexprな配置newを行う方法がありません。

std::construct_atは未初期化領域へのポインタpだけが与えられると::new(p) T()相当の初期化を行い、これはゼロ初期化され、Tのコンストラクタが無い時その領域がゼロ埋めされます。一方std::uninitialized_default_constructは、その領域に対して::new(p) T相当の初期化を行い、これはデフォルト初期化され、Tのコンストラクタが無い時なにもしません。)

これを回避するために、この提案ではconstexprな配置newによるデフォルト初期化を行うstd::default_construct_at関数を追加し、それを利用してstd::uninitialized_default_constructconstexpr化することを提案しています。

なお、ExecutonPolicyを取るオーバーロードconstexpr対応されません。

これはMSVC STLにおいてstd::vectorconstexpr化する過程で発覚し、既に実装されているようです。

P2285R0 Are default function arguments in the immediate context?

関数テンプレートのテンプレートパラメータに依存するデフォルト引数のインスタンス化の失敗をSFINAEできるようにする提案。

現在の規格では関数テンプレートのデフォルト引数のインスタンス化に失敗した時、そこがSFINAEできる文脈(immediate context)であるかどうかが規定されていないようです。例えば、次のようなコードで多様な結果を得ることができます。

template <typename T, typename Allocator>
struct container {

  // アロケータ型をデフォルト構築しているデフォルト引数
  template <std::ranges::range Range>
  explicit container(Range r, Allocator a = Allocator()) {}

};

// デフォルト構築できないアロケータ型
struct Alloc {
  Alloc() = delete;
  // ...
};

int main() {
  // Clang, ICCはここでコンパイルエラー
  constexpr bool c = std::constructible_from<container<int, Alloc>, std::vector<int>>;

  // GCC : 0 (false), MSVC : 1 (true)
  std::cout << c << std::endl;
}

この提案はこれを明確化し、デフォルト引数のインスタンス化の失敗はSFINAEできる文脈であると規定しようとするものです。

これによって、コードを次のように改善できるようになります。

現在 この提案
template<class Hash, class Equal, class Allocator>
struct Map {

  template<class Range>
  explicit Map(Range&&, Hash, Equal, Allocator);

  template<class Range>
  explicit Map(Range&& c, Hash h, Equal e)
   requires default_initializable<Allocator>
   : Map(c, h, e, Allocator()) {}

  template<class Range>
  explicit Map(Range&& c, Hash h)
   requires default_initializable<Equal>
         && default_initializable<Allocator>
   : Map(c, h, Equal(), Allocator()) {}
  template<class Range>
  explicit Map(Range&& c)
   requires default_initializable<Hash>
         && default_initializable<Equal>
         && default_initializable<Allocator>
   : Map(c, Hash(), Equal(), Allocator()) {}
};
template<class Hash, class Equal, class Allocator>
struct Map {

  template<class Range>
  explicit Map(Range&&,
               Hash = Hash(),
               Equal = Equal(),
               Allocator = Allocator());
};

P2286R0 Formatting Ranges

任意の範囲を手軽に出力できる機能を追加する提案。

例えば文字列を分割してその結果をコンソール出力しようと思った時、C++20では次のように書くことができます。

#include <iostream>
#include <string>
#include <ranges>
#include <format>

int main() {
  // 文字列の分割(右辺値stringをsplitできない)
  std::string s = "xyx";
  auto parts = s | std::views::split('x');
  
  // これは出来ない
  std::cout << parts;
  
  // P2093のstd::print、これもできない
  std::print("{}", parts);


  std::cout << "[";
  char const* delim = "";
  for (auto part : parts) {
    std::cout << delim;
    
    // これもできない
    std::cout << part;
    
    // std::print、当然できない
    std::print("{}", part);
    
    // これはできる!
    std::ranges::copy(part, std::ostream_iterator<char>(std::cout));
    
    // これもできる!!
    for (char c : part) {
        std::cout << c;
    }
    delim = ", ";
  }
  std::cout << "]\n";
}

{fmt}ライブラリを使うと、次のように書けます。

#include <ranges>
#include <string>
#include <fmt/ranges.h>

int main() {
  std::string s = "xyx";
  auto parts = s | std::views::split('x');

  fmt::print("{}\n", parts);
  fmt::print("[{}]\n", fmt::join(parts, ","));

  // 出力
  // {{}, {'y'}}
  // [{},{'y'}]
}

しかしこれはstd::formatには含まれていません。

この提案は、{fmt}ライブラリのこの機能をstd::formatに追加しようとするものです。

範囲も含めて以下のものに対してフォーマッタの特殊化を追加することを目指しています。

  • value_typereferenceがフォーマット可能な任意の範囲
  • 2つの型が共にフォーマット可能なstd::pair<T, U>
  • 全ての型が共にフォーマット可能なstd::tuple<Ts...>
  • std::vector<bool>::referenceboolと同じようにフォーマットする

フォーマットの方法(範囲やpair/tuple[...]/{...}のどちらでフォーマットするかなど)は実装定義とし、fmt::joinに倣ったstd::format_joinの様なものを追加することも提案しています。 

P2287R0 Designated-initializers for base classes

基底クラスに対して指示付初期化できるようにする提案。

C++20にて、集成体型に対する指示付初期化が出来るようになりましたが、それは直接のメンバに対してのもので、基底クラスのメンバに対しては行えませんでした。

struct A {
  int a;
};

struct B : A {
  int b;
};

int main() {
  A a = { .a = 1 };  // ok
  B b1 = { {2}, 3 }; // ok
  B b2 = { 2, 3 };   // ok

  B b3 = { .a = 4, .b = 5 };   // ng
  B b4 = { {6} .b = 7 };       // ng
  B b5 = { { .a = 8} .b = 9 }; // ng
}

基底クラスのメンバを指定できないため、指示付初期化と通常の初期化が混在してはいけないというルールを守ることができず、結果として継承している集成体では指示付初期化できなくなっています。

この提案は、基底クラスを指定して指示付初期化できるようにしようとするものです。

その際問題になるのが、基底クラスをどうやって指定するのかという事です。現在のC++には基底クラスを明示的に指定するような構文は存在していません。

この提案では、集成体初期化子の先頭で、:に続いて基底クラス名を指定することで指示子とする事を提案しています。

template <typename T>
struct C { 
  T val;
};

struct D : C<int>, C<char> {};

int main() {
  B b1 = { :A = { .a = 1}, b = 2 };
  B b2 = { :A{ .a = 1}, b = 2 };

  D d = { :C<int>{.val = 1}, :C<char> = {.val=`x`} };
}

この提案では基底クラスを指定するための方法を追加することだけが目的で、指示付初期化の他の部分を変更していません。従って、指示子の有無が混在することやその順番が宣言順と異なることも許可されません。

2週間後くらいかな・・・

この記事のMarkdownソース

[C++]推論補助(deduction guide)にexportはいるの?

A : いりません

根拠

モジュールにおけるexport宣言は、そのモジュールのインターフェース単位でのみ行うことができます。export宣言は名前を導入するタイプの宣言の前にexportを付けることで行い、その名前に外部リンケージを与える以外は元の宣言と同じ効果を持ちます。

モジュールのimport宣言は指定したモジュールをインポートするもので、書かれている場所と異なるモジュールを指定した場合はそのモジュールのインターフェースをインポートする事になります。

モジュールのインターフェースをインポートすると、インポートされたモジュールでexportされている名前がインポートした側での名前探索において可視となり、インポートされたモジュールのインターフェースで宣言されているものが全て到達可能(reachable)となります。

推論補助そのものはテンプレートの宣言の一種であり、名前を導入するものなのでexportすることができます。

推論補助にexportがいるかどうかというのは、推論補助が名前探索を通じて発見されるのかどうか?という事でもあります。推論補助はどのようにして発見されているのでしょうか・・・?

N4861 13.7.2.3 Deduction guides [temp.deduct.guide]に明確に書いてあります。

Deduction guides are not found by name lookup. Instead, when performing class template argument deduction ([over.match.class.deduct]), all reachable deduction guides declared for the class template are considered.

推論補助は名前探索では見つからず、代わりにクラステンプレートの引数推論時にそのクラステンプレートに対して 到達可能 な全ての推論補助が考慮される。みたいに書いてあります。

ここでの到達可能(reachable)とは、import宣言によってインターフェースにあるものが到達可能になる、と言っていたところの到達可能と同じ意味です。

つまり、推論補助はモジュールのインターフェースにありさえすればそのモジュールをインポートした側で使用可能となります。exportする必要はありません。

/// mymodule.cpp
export module mymodule;

import <ranges>;

export
template<std::input_or_output_iterator I, std::sentinel_for<I> S>
class iter_pair {
  I it;
  S se;

public:

  template<typename R>
  iter_pair(R&& r); // (1)

  iter_pair(I i);   // (2)
};

// (1)に対する推論補助1
template<typename R>
iter_pair(R&&) -> iter_pair<std::ranges::iterator_t<R>, std::ranges::sentinel_t<R>>;

module : private;
// プライベートモジュールフラグメントの内側はインポートした側から到達可能とならない


// (2)に対する推論補助2
template<std::input_or_output_iterator I>
  requires std::sentinel_for<std::default_sentinel_t, I>
iter_pair(I) -> iter_pair<std::remove_cvref_t<I>, std::default_sentinel_t>;


// コンストラクタ定義、暗黙エクスポート
template<std::input_or_output_iterator I, std::sentinel_for<I> S>
template<typename R>
iter_pair<I, S>::iter_pair(R&& r)
  : it(std::ranges::begin(r))
  , se(std::ranges::end(r))
{}

template<std::input_or_output_iterator I, std::sentinel_for<I> S>
iter_pair<I, S>::iter_pair(I i)
  : it(std::move(i))
  , se{}
{}
/// main.cpp

import <iterator>;
import mymodule;

int main() {
  int ar[3] = {1, 2, 3};
  
  iter_pair ip(ar);   // ok、推論補助1は到達可能

  std::counted_iterator ci{std::ranges::begin(ar), 2};
  
  iter_pair ip2(ci);  // ng、推論補助2は到達可能ではない
}

参考文献

この記事のMarkdownソース