WG21月次提案文書を眺める(2020年9月)
文書の一覧
提案文書で採択されたものはありません。全部で32本あります。
P0288R7 : any_invocable
ムーブのみが可能で、関数呼び出しのconst
性やnoexcept
性を指定可能なstd::function
であるstd::any_invocable
の提案。
先月の記事を参照
前回からの変更は、規格書に追加するための文言を調整しただけの様です。
P0443R14 : A Unified Executors Proposal for C++
処理をいつどこで実行するかを制御するための汎用的な基盤となるExecutorライブラリの提案。
以前の記事を参照。
R13からの変更は、いくつかEditorialなバグを修正した事です。
P0881R7 : A Proposal to add stacktrace library
スタックトレースを取得するためのライブラリを追加する提案。
先月の記事を参照。
LWGでのレビューが終了し、この提案は本会議での投票に向かうようです。このリビジョンはLWGでのレビューを受けて最終的な文言調整を反映したものです。多分C++23入り確定でしょう。
P0958R2 : Networking TS changes to support proposed Executors TS
Networking TSのExecutorの依存部分をP0443のExecutor提案の内容で置き換える提案。
現在のNetworking TSは以前のBoost.asioをベースにしており、asioの持つExecutorはP0443のものとは異なっているので、Networking TSのExecutor部分もまたP0443のものとは異なった設計となっています。
Networking TSはExecutorに強く依存しており、C++23に導入される予定のExecutorはP0443のものがほぼ内定しているので、Networking TSもそれに依存するように変更するものです。
Boost.asioライブラリは既にP0443ベースのExecutor周りを実装し終えて移行しており、その実装をベースとする形で書き換えています。その経験によれば、一部のものを除いて殆どのアプリケーションでは追加の変更を必要としなかったとのことです。
P1322R2 : Networking TS enhancement to enable custom I/O executors
Networking TSのI/Oオブジェクトをio_context
だけではなく、任意のExecutorによって構築できるようにする提案。
Networking TSのio_context
はBoost.asioではio_service
と呼ばれていたもので、P0443のExecutorにいくつかの機能が合わさったものです。
io_context
は実行コンテキストを抽象化し表現するという役割がありますが、実行コンテキストを変更する事は単体ではできません。P0443ではexecutor
コンセプトによってそれらExecutorと実行コンテキストを表現します。
Networking TSのI/Oオブジェクト(socket, acceptor, resolver, timer
)はio_context
を構築時に受け取り、それを利用して実行します。そこに任意のExecutorを渡せるようにすることでユーザーが用意したものやプラットフォームネイティブの実行コンテキストでそれらの処理を実行できるようにしようとする提案です。
また、各I/OオブジェクトのクラスのテンプレートパラメータにExecutorを指定できるようにし、それらの動作をカスタマイズ可能としています。
この提案はおそらくNetworking TS仕様のExecutorをベースとしており、P0443ベースで書かれていないようです。
P1371R3 : Pattern Matching
オブジェクトの実際の状態に応じた分岐処理のシンタックスシュガーであるパターンマッチング構文の提案。
現在のC++にはif
とswitch
という二つの条件分岐用の構文があります。if
は複雑なbool
の式を扱うことができますが、switch
は整数値しか扱えず、if
は分岐条件を一つづつ書いて行くのに対してswitch
は条件を羅列した形で(宣言的に)書くことができるなど、は2つの構文の間にはギャップがあります。
パターンマッチングはそれらの良いとこどりをしたような構文によって、特に代数的データ型の検査と分岐を書きやすく、読みやすくするものです。
この提案ではinspect
式によって構造化束縛宣言を拡張する形でswitch
的な書き方によってパターンマッチングを導入しています。これを提案中ではstructured inspection(構造化検証?)と呼んでいます。
std::string
if
文
if (s == "foo") {
std::cout << "got foo";
} else if (s == "bar") {
std::cout << "got bar";
} else {
std::cout << "don't care";
}
inspect
式
inspect (s) {
"foo" => { std::cout << "got foo"; }
"bar" => { std::cout << "got bar"; }
__ => { std::cout << "don't care"; }
};
std::tuple
if
文
auto&& [x, y] = p;
if (x == 0 && y == 0) {
std::cout << "on origin";
} else if (x == 0) {
std::cout << "on y-axis";
} else if (y == 0) {
std::cout << "on x-axis";
} else {
std::cout << x << ',' << y;
}
inspect
式
inspect (p) {
[0, 0] => { std::cout << "on origin"; }
[0, y] => { std::cout << "on y-axis"; }
[x, 0] => { std::cout << "on x-axis"; }
[x, y] => { std::cout << x << ',' << y; }
};
std::variant
if
文
struct visitor {
void operator()(int i) const {
os << "got int: " << i;
}
void operator()(float f) const {
os << "got float: " << f;
}
std::ostream& os;
};
std::visit(visitor{strm}, v);
inspect
式
inspect (v) {
<int> i => {
strm << "got int: " << i;
}
<float> f => {
strm << "got float: " << f;
}
};
polymorphicな型
継承ベースの動的ポリモルフィズム
struct Shape {
virtual ~Shape() = default;
virtual int Shape::get_area() const = 0;
};
struct Circle : Shape {
int radius;
int Circle::get_area() const override {
return 3.14 * radius * radius;
}
};
struct Rectangle : Shape {
int width, height;
int Rectangle::get_area() const override {
return width * height;
}
};
inspect
式
struct Shape {
virtual ~Shape() = default;
};
struct Circle : Shape {
int radius;
};
struct Rectangle : Shape {
int width, height;
};
int get_area(const Shape& shape) {
return inspect (shape) {
<Circle> [r] => 3.14 * r * r;
<Rectangle> [w, h] => w * h;
};
}
この例だけを見てもかなり広い使い方が可能であることが分かると思います。しかし、この例以上にinspect
式による構文は柔軟な書き方ができるようになっています(ここで説明するには広すぎるので省略します。そこのあなたm9!解説記事を書いてみませんか??)。
P1701R1 : Inline Namespaces: Fragility Bites
inline
名前空間の名前探索に関するバグを修正する提案。
当初(C++11)のインライン名前空間と名前空間名の探索には次のようなバグがありました。
namespace A {
inline namespace b {
namespace C {
template<typename T>
void f();
}
}
}
namespace A {
namespace C {
template<>
void f<int>() { } // error!
}
}
当初の名前空間名の探索は宣言領域 (declarative region) という概念をベースに行われていました。宣言領域とは簡単にいえば、ある宣言を囲む宣言の事です。
宣言領域の下では、2つ目の名前空間A::C
の宣言は1つ目のA::b::C
とは宣言領域が異なるため、それぞれの名前空間C
は同一のものとはみなされません。
しかし、これはテンプレートの特殊化を行う際に問題となるため、DR 2061によって規格書の定義としては修正されました。その修正方法は、名前空間名を宣言するとき、そのコンテキストからネストしたinline
名前空間も考慮したうえで到達可能な名前空間名を探索し、同じ名前が見つかった場合は同一の名前空間として扱い、見つからない場合にのみ新しい名前空間名を導入する。という感じです。
これによって先程のバグは解決されましたが、筆者の方がそれをGCCに実装する時に問題が浮かび上がったようです。
inline namespace A {
namespace detail { // #1
void foo() {} // #3
}
}
namespace detail { // #2
inline namespace C {
void bar() {} // #4
}
}
DR2061以前は2つ目の名前空間detail
は新しい名前空間を導入し、#3, #4
はそれぞれA::detail::foo
とdetail::C::bar
という修飾名を持ちます。
しかしDR2061による修正によれば、2つ目の名前空間detail
の宣言は一つ目の名前空間A::detail
と同一視されます。その結果、#3, #4
はそれぞれA::detail::foo
とA::detail::C::bar
という修飾名を持つことになります。
このことはヘッダファイルやモジュールのインポートを介すことで、意図しない名前空間名の汚染を引き起こすことになります。
これによって、ヘッダと実装でファイルを分けている場合、実装ファイルで名前空間名の指定が意図通りにならず、最悪別の名前に対する実装を行ってしまうかもしれません。また、inline
名前空間をusing
することでAPIの一部だけを有効化するような手法をとるライブラリでは、意図しないものがスコープ中にばらまかれることにもなりかねません。
namespace component {
inline namespace utility {
namespace detail {
// component::utility::detail
}
}
}
namespace component {
namespace detail {
// DR2061以前は component::detail
// DR2061の後は component::utility::detail
}
}
一方、DR2061が無いとC++20以降は特に困ったことが起きます。
namespace std {
namespace ranges {
template<>
constexpr bool disable_sized_range<MyType> = true;
}
}
std::range::disable_sized_range
は変数テンプレートであり、ユーザー定義の型について特殊化することでstd::range::sized_range
コンセプトを無効化するものです。C++20のrange
ライブラリ周りではこのようなオプトアウトのメカニズムがほかにもいくつかあります。
現在、多くの実装はABI保護のためにstd
名前空間内の実装には何かしらの形でインライン名前空間を挿入しています。すると、このコードで行っているような特殊化はDR2061による変更が無ければ特殊化としてみなされなくなります。
この提案ではこれらの問題の解決として次の2つのことをサジェストしています。
- DR2061を元に戻す
- 標準ライブラリ内でユーザーが特殊化することを定義されているものについて、修飾名で特殊化すること、という規定を追加する。
つまり、先程の特殊化は次のように書くことを規定するということです。
template<>
constexpr bool std::ranges::disable_sized_range<MyType> = true;
こうすれば、名前空間の宣言を伴わないため上記のような問題に悩まされることはありません。
- CWG Issue 2061. Inline namespace after simplifications
- Bug 90291 - [8/9/10/11 Regression] Inline namespace erroneously extends another namespace - GCC Bugzilla
- P1701 進行状況
P1885R3 : Naming Text Encodings to Demystify Them
システムの文字エンコーディングを取得し、識別や出力が可能なライブラリを追加する提案。
C++の標準は文字エンコーディングを参照する時にロケールを介して参照しています。これは歴史的なものですが、ユニコードの登場によってもはや機能しなくなっています。そして、C++はシステムがどのエンコーディングを使用し、また期待しているかを調べる方法を提供していないためそれを推測しなければならず、文字列の取り扱いを満足に行う事ができません。
この提案は、現在取得する方法のないシステムやコンパイラの使用するエンコーディングを取得・識別できるようにし、また、エンコーディングを表現する標準的なプリミティブを提供することを目指すものです。
なお、ここでは文字コード変換のための標準機能の追加を目指してはいません。
#include <text_encoding> // 新ヘッダ
#include <iostream>
int main() {
// char文字(列)リテラルのエンコーディングを取得
std::cout << std::text_encoding::literal().name() << std::endl;
// wchar_t文字(列)リテラルのエンコーディングを取得
std::cout << std::text_encoding::wide_literal().name() << std::endl;
// システムのマルチバイト文字エンコーディングを取得
std::cout << std::text_encoding::system().name() << std::endl;
// システムのワイド文字エンコーディングを取得
std::cout << std::text_encoding::wide_system().name() << std::endl;
}
この提案による全てのものはstd::text_encoding
クラスの中にあります。上記の4つの関数はその環境で対応する文字エンコーディングを表すstd::text_encoding
のオブジェクトを返します。
std::text_encoding
は非static
メンバ関数name(), mib(), aliases()
によってその名前、IANAのMIBenum、文字エンコーディング名の別名(複数)を取得することができます。
また、システムの文字エンコーディングが特定のエンコーディングであるかを判定する機能も提供されています。
int main() {
assert(std::text_encoding::system_is<std::text_encoding::id::UTF8>());
assert(std::text_encoding::system_wide_is<std::text_encoding::id::UCS4>());
}
ただ、残念ながらこれらはconstexpr
ではありません。
std::text_encoding::id
は各種文字エンコーディングを表すenum class
で、IANAの定義するCharacter Setにある名前が列挙値として登録されています。この列挙値か、対応する文字コード名文字列をによってもstd::text_encoding
クラスを構築することができます。
また、std::text_encoding
のオブジェクト同士を==
で比較することもできます。
P1949R6 : C++ Identifier Syntax using Unicode Standard Annex 31
識別子(identifier)の構文において、不可視のゼロ幅文字や制御文字の使用を禁止する提案。
以前の記事を参照
- P1949R3 : C++ Identifier Syntax using Unicode Standard Annex 31 - 地面を見下ろす少年の足蹴にされる私
- P1949R4 : C++ Identifier Syntax using Unicode Standard Annex 31 - 地面を見下ろす少年の足蹴にされる私
- P1949R5 : C++ Identifier Syntax using Unicode Standard Annex 31 - 地面を見下ろす少年の足蹴にされる私
このリビジョンの変更点は、この提案によって変更されるものと変更されないものを明記したのと、いくつかのサンプルを追加した事です。
P2013R3 : Freestanding Language: Optional ::operator new
フリースタンディング処理系においては、オーバーロード可能なグローバル::operator new
を必須ではなくオプションにしようという提案。
以前の記事を参照
このリビジョンの変更点は、8月のEWGの電話会議での投票結果を記載した事と、いくつかの文言の修正、機能テストマクロについてはP2198の将来のリビジョンで検討することになったことです。
P2029R3 : Proposed resolution for core issues 411, 1656, and 2333; escapes in character and string literals
文字(列)リテラル中での数値エスケープ文字('\xc0'
)やユニバーサル文字名("\u000A"
)の扱いに関するC++字句規則の規定を明確にする提案。
以前の記事を参照
このリビジョンの変更点は、電話会議の結果を受けて提案している文言を調整しただけの様です。
P2066R3 : Suggested draft TS for C++ Extensions for Transaction Memory Light
現在のトランザクショナルメモリTS仕様の一部だけを、軽量トランザクショナルメモリとしてC++へ導入する提案。
以前の記事を参照
このリビジョンでの変更は、EWGのフィードバックを受けて、完全式(full-expression)の定義にatomicブロック(atomic
ステートメント)の開始と終了という定義を追加した事と、トランザクションの並行処理に関するメモの追加や、atomic
ステートメントの導入キーワードのatomic do
への変更と、atomic
ステートメントで使用可能なライブラリ関数のリストが追加されたことなどです。
int f() {
static int i = 0;
// atomicブロック、atomicトランザクションを定義
// このブロックは全てのスレッドの間で1つづつ実行される
atomic do {
++i;
return i;
}
}
int main() {
std::array<std::thread, 100> threads{};
// 関数f()の呼び出し毎に一意の値が取得される
// 同じ値を読んだり、更新中の値を読むことはない
for (auto& th : threads) {
th = std::thread([](){
int n = f();
});
}
for (auto& th : threads) {
th.join();
}
}
このatomicブロックで使用可能なものには以下が挙げられています。
std::memset, std::memcpy, std::memmove
std::move, std::forward
- 基本型(組み込み型)に対する
std::swap, std::exchange
-
std::array<T>
の全てのメンバ関数-
T
に対する必要な操作がatomicブロックで使用可能である場合のみ
-
-
std::list<T>, std::vector<T>
-
T
に対する必要な操作がatomicブロックで使用可能である場合のみ
-
- TMPとtype traits
<ratio>
-
std::numeric_limits
の全てのメンバ - placement
new
と対応するdelete
P2077R1 : Heterogeneous erasure overloads for associative containers
連想コンテナに対して透過的な要素の削除と取り出し方法を追加する提案。
C++20では、非順序連想コンテナに対して透過的な検索を行うことのできるオーバーロードが追加されました。「透過的」というのは連想コンテナのキーの型と直接比較可能な型については、一時オブジェクトを作成することなくキーの比較を行う事が出来ることを指します。これによって、全ての連想コンテナで透過的な検索がサポートされました。
一方、削除の操作に関しては順序/非順序どちらの連想コンテナも透過的な削除をサポートしていませんでした。この提案は、全ての連想コンテナのerase()
とextract()
に対しても同じ目的のオーバロードを追加しようとするものです。
std::unordered_map<std::string, int> map = {{"16", 16}, {"1024", 1024}, {"65536", 65536}};
const std::string key{"1"}
// C++20より、どちらも一時オブジェクトを作成しない
map.contains(key);
map.contains("1024");
map.erase(key); // 一時オブジェクトを作成しない
map.erase("1024"); // stringの一時オブジェクトが作成される
この提案によるオーバロードを有効化する手段は透過的な検索と同様の方法によります。
順序連想コンテナではその比較を行う関数オブジェクトの型がis_transparent
というメンバ型を持っていて、渡された型がイテレータ型に変換されない場合に有効になり、非順序連想コンテナではそれに加えてハッシュ関数オブジェクトの型がis_transparent
というメンバ型を持っていれば有効化されます。
- 非順序連想コンテナのルックアップ操作で、key_type と比較可能な型を変換せずに使えるように (P0919R3), (P1690R1) - cppmap
- P0919R3 Heterogeneous lookup for unordered containers
- P2077 進行状況
P2138R3 : Rules of Design <=> Specification engagement
CWGとEWGの間で使用されているwording reviewに関するルールの修正と、それをLWGとLEWGの間でも使用するようにする提案。
以前の記事を参照
- P2138R1 : Rules of Design <=> Specification engagement - 地面を見下ろす少年の足蹴にされる私
- P2138R2 : Rules of Design <=> Specification engagement - 地面を見下ろす少年の足蹴にされる私
このリビジョンでの変更は、対象とする読者への説明、提案文書作成時に文言の作成に支援を求める方法、Tentatively Readyステータスをスキップできる条件を明確にしたことと、Tentatively Ready for Plenaryというステータスは次のミーティングの本会議の採決にかける事を意味していることを明確にしたことです。
P2145R1 : Evolving C++ Remotely
コロナウィルスの流行に伴ってC++標準化委員会の会議がキャンセルされている中で、リモートに移行しつつどのように標準化作業を進めていくのかをまとめた文章。
このリビジョンでの変更は、以前のリビジョン時からの状況変化を反映した様です。
P2164R2 : views::enumerate
元のシーケンスの各要素にインデックスを紐付けた要素からなる新しいシーケンスを作成するRangeアダプタviews::enumrate
の提案。
以前の記事を参照
このリビジョンでの変更は、既存のview
と同様にvalue_type
を非参照にしたこと、説明と文言間の矛盾を正したこと、サンプルコードに関連ヘッダ(<ranges>
)を追記した事です。
P2166R1 : A Proposal to Prohibit std::basic_string
and std::basic_string_view
construction from nullptr
std::string
、std::string_view
をnullptr
から構築できないようにする提案。
以前の記事を参照
このリビジョンでの変更は、googleのProtbufで見つかったコード例の追加と類似した問題へのリンクが追加されたことです。
string GetCapitalizedType(const FieldDescriptor* field) {
switch (field->type()) {
// handle all possible enum values, but without adding default label
}
// Some compilers report reaching end of function even though all cases of
// the enum are handed in the switch.
GOOGLE_LOG(FATAL) << "Can't get here.";
return NULL;
}
この関数はswitch
でハンドルされなかった場合にNULL
から構築されたstring
を返しています。ただ、実際にはこのコードは到達可能ではないらしく、この提案を妨げるものではないようです。
P2169R2 : A Nice Placeholder With No Name
宣言以降使用されず追加情報を提供するための名前をつける必要もない変数を表すために_
を言語サポート付きで使用できる様にする提案。
以前の記事を参照
- P2169R0 : A Nice Placeholder With No Name - 地面を見下ろす少年の足蹴にされる私
- P2169R1 : A Nice Placeholder With No Name - 地面を見下ろす少年の足蹴にされる私
このリビジョンでの変更はP2011での_
の利用(プレイスホルダーとしての利用)との構文の衝突について追記されたことです。
int _ = 0; // この提案の用法、プレイスホルダーではない
f() |> g(_, _); // P2011の用法、プレイスホルダー
この提案での_
の特別扱いは変数宣言にのみ作用するため、上記P2011の用法による場合は常にプレイスホルダーとして扱えば良いと述べられています。
P2192R2 : std::valstat -Transparent Returns Handling
関数の戻り値としてエラー報告を行うための包括的な仕組みであるvalstatの提案。
以前の記事を参照
- P2192R0 : std::valstat -Transparent Returns Handling - 地面を見下ろす少年の足蹴にされる私
- P2192R1 : std::valstat -Transparent Returns Handling - 地面を見下ろす少年の足蹴にされる私
このリビジョンでの変更は、動機やメタステートの説明、付録のサンプルコードなどを改善・書き足しした事と、タイトルが"std::valstat - transparent return type"から"std::valstat -Transparent Returns Handling"に変更されたことです。
P2194R0 : The character set of the internal representation should be Unicode
C++標準の想定するコンパイル中のC++ソースコードの表現について、ユニコード以外を認める必要が無いという根拠を示す文書。
これはSG16(Unicode Study Group)での議論のために書かれた報告書です。
C++20では、翻訳フェーズ1以降のソースコードの規格的な表現(エンコーディング)はユニコードです。SG16での議論の中で、そこにユニコードのスーパーセットたるエンコーディングを許可しようというアイデアが提出されたらしく、この文書はそのアイデアを採用する根拠がない事を説くものです。
P2195R0 : Electronic Straw Polls
委員会での投票が必要となる際に、メールまたは電子投票システムを用いて投票できるようにする提案。
これは標準化のプロセスについてのもので、C++言語そのものに対する提案ではありません。
コロナウィルスの流行によって対面での会議が行えなくなったため、現在のC++標準化委員会の作業は主に電話会議で行われており、投票が行われる場合も電話会議でリアルタイムに行われていました。
より議論と作業を効率化させるために、この投票を電子メールなどによって非同期的、かつ定期的(四半期ごと)に行うことを提案しています。
また、CWGやEWGでは現状の体制でも特に問題が無かったため、この提案の対象はLWG/LEWGに絞られています。
他にも、標準化の作業の投票やコンセンサスがどのように決定されるかなどについて詳細に書かれています。
P2206R0 : Executors Thread Pool review report
Executor提案(P0443R13)のstatic_thread_pool
周りのレビューの結果見つかった問題についての報告書。
P2212R1 : Relax Requirements for time_point::clock
std::chrono::time_point
のClock
テンプレートパラメータに対する要件を弱める提案。
以前の記事を参照
このリビジョンでの変更は、提案する既存規格の文言の変更についてフィードバックを受けて修正した事です。
P2215R1 : "Undefined behavior" and the concurrency memory model
out-of-thin-airと呼ばれる問題の文脈における、未定義動作とは何かについての報告書。
以前の記事を参照
このリビジョンでの変更は、memory_order_load_store
の順序の問題が逐次一貫性のあるload
にも適用されることを明確にしたことと、"Consequences"セクションが追加されたことです。
P2216R0 : std::format improvements
std::format
の機能改善の提案。
主に、次の二点を改善しようとするものです。
コンパイル時にフォーマット文字列をチェックするようにする
C++20のstd::format
では、フォーマットエラーは実行時にstd::format_error
例外を投げることで報告されます。
std::string s = std::format("{:d}", "I am not a number");
例えばこのコードは、:d
が通常の文字列に対するフォーマットとして不適切なため、実行時に例外を投げます。
この提案では、このようなフォーマットのエラーをコンパイル時にコンパイルエラーとして報告することを提案しています。
ただし、言語と実装の特別なサポートなくしてはこのままのコードでコンパイル時のフォーマットチェックを行うことはできません。そのような特別扱いを嫌う場合、ユーザー定義リテラルや静的な文字列型など何かしらの手段が必要となります。
std::vformat_to
のバイナリサイズの削減
std::vformat_to
はstd::format_to
の引数型を型消去したもので、フォーマット文字列とフォーマット対象を受けとり、任意の出力イテレータにフォーマット済み文字列を出力します。主にformat
ライブラリの内部で使用されるものです。
次のように宣言されています。
template<class Out, class charT>
using format_args_t = basic_format_args<basic_format_context<Out, charT>>;
template<class Out>
Out vformat_to(Out out, string_view fmt,
format_args_t<type_identity_t<Out>, char> args);
ここで問題となるのは、第二引数の型format_args_t<type_identity_t<Out>, char>
が出力イテレータ型Out
に依存していることによって、異なるOut
毎に個別のformat_args_t
がインスタンス化されてしまい、コードサイズが肥大化する原因となることです。これはフォーマットしない引数型に対しても引き起こされうるので、使用しないものに対して余計なコストを支払うことになりかねません。
また、std::format/vformat
は内部バッファを介してイテレータ型を型消去しており、同じ方法をとればこの特殊化は必要ありません。
そこで、シグネチャを次のように変更することを提案しています。
template<class Out>
Out vformat_to(Out out, string_view fmt, format_args args);
format_args
はあらかじめ定義されているイテレータ型を型消去し引数を保持しておくものです。これによって、std::format/vformat
と同様の方法でイテレータ型の型消去を行うようになり、不要なテンプレートのインスタンス化を防ぐことができます。
なお、これらの事は明らかにC++20の<format>
ライブラリに対する破壊的変更となりますが、<format>
は現在のところ誰も実装していないので壊れるコードは無い、と筆者の方は述べています(個人的にはDRにしてほしい気持ちです・・・)。
P2217R0 : SG16: Unicode meeting summaries 2020-06-10 through 2020-08-26
SG16(Unicode Study Group)の会議の議事録の要約。
P2218R0 : More flexible optional::value_or()
std::optional
のvalue_or()
メンバ関数をベースとした、利便性向上のための新しいメンバ関数を追加する提案。
value_or()
メンバ関数はstd::optional
が有効値を保持していればその値を返し、無効値を保持している場合は指定された値を代わりに(std::optional
の要素型に変換して)返すものです。
value_or()
関数は有効値からのフォールバックという性質上、多くの場合その型のデフォルト値が使用されます。その場合リテラルを持つ組み込み型ならばあまり困る事も無いのですが、コンストラクタを持つユーザー定義型では意図通りにならない事があります。
例えばstd::optional<std::string>
のvalue_or
でstd::string
のデフォルト値(空文字列)を返そうとしてみると
std::optional<int> oi{};
std::optional<bool> ob{};
std::optional<std::string> os{};
// 無問題、意図通り
std::cout << oi.value_or(0) << std::endl;
std::cout << ob.value_or(false) << std::endl;
// 実行時エラー
std::cout << os.value_or(nullptr) << std::endl;
// パフォーマンス的に最適ではない
std::cout << os.value_or("") << std::endl;
// コンパイルエラー
std::cout << os.value_or({}) << std::endl;
// 正解、でも型名を省略したい
std::cout << os.value_or(std::string{}) << std::endl;
特に3番目の様な書き方ができないのがとても不便です。要素型がさらに複雑な型になるとより面倒になります。
この原因はvalue_or()
のテンプレートパラメータが要素型とは無関係になっていることにあります。そのため、要素型からの推論を行う事が出来ません。
template<typename T>
class optional {
// おおよそこんな感じの実装になっている
template<typename U>
requires (is_copy_constructible_v<T> and is_convertible_v<U&&, T>)
constexpr T value_or(U&& u) const & {
return this->has_value() ? this->value() : static_cast<T>(std::forward<U>(u));
}
};
そこで、このテンプレートパラメータU
のデフォルト引数として要素型T
を与えておくことで、U
をT
から推論することができるようになります。ただし、std::optional
の要素型はconst
修飾を行っておくことが可能なので、それは除去しておきます。
template<typename T>
class optional {
// おおよそこのような実装だが、おそらくこのままだとエラーになる
template<typename U = remove_cv<T>>
requires (is_move_constructible_v<T> and is_convertible_v<U&&, T>)
constexpr T value_or(U&& u) const & {
return this->has_value() ? this->value() : static_cast<T>(std::forward<U>(u));
}
};
これによって、既存の振る舞いを壊すことなく全ての要素型においてvalue_or({})
の様な呼び出しが出来るようになります。
次に、value_or()
を発展させて、無効値を保持している場合に指定された引数で要素を構築して返すvalue_or_construct()
関数を追加することを提案しています。これは、あくまでその場で構築して返すだけで、そのstd::optional
の無効状態を変更するものではありません。
std::optional<std::string> os{};
// デフォルト構築したstd::stringを返す
os.value_or_construct();
// 指定した文字列で構築したstd::stringを返す
os.value_or_construct("left value");
最後に、value_or_construct()
をさらに発展させて、構築のための引数の評価を遅延させるvalue_or_else()
関数を提案しています。
using namespace std::string_literals;
std::optional<std::vector<std::string>> ovs{};
// 呼び出し時点でstd::initializer_list<std::string>が構築される
ovs.value_or_construct({"Hello"s, "World"s});
// 呼び出し時点ではstd::vector<std::string>>は構築されない
// 無効値を保持している場合にのみラムダ式の呼び出しを通して構築される
ovs.value_or_else([]{ return std::vector{"Hello"s, "World"s}; });
value_or_construct()
とvalue_or_else()
は一見似通ったものに見えますが、それぞれ異なる欠点があり使いどころが異なっているため、両方追加することを提案しています。
P2219R0 : P0443 Executors Issues Needing Resolution
Executor提案(P0443R13)のレビューの結果見つかった解決すべき問題点の一覧。
地味に63個も問題があり、これの解決後にさらなるレビューが必要となるので、もう少し時間がかかりそうです・・・
P2220R0 : redefine properties in P0443
Executor提案(P0443R13)における、Executorに対するプロパティ指定の方法を別に提案中のtag_invoke
によって置き換える提案。
CPO(Custmization Point Object)は呼び出された引数に対して定義されたいくつかの方法のうちの一つが可能であれば、それによってその目的を達します。中でもほぼ必ず、引数に対してそのCPO自身と同名の関数をADLで探索することが行われます。この時、意図せずに同じ名前の関数を同じ名前空間に持つようなクラス型に対して誤った呼び出しを行ってしまう可能性があります。それを防ぐためにコンセプトによって制約されていますが、それすらもすり抜けるケースが無いとは言えません。
tag_invoke
はCPOのその部分(ADLによる探索)をより安全に置き換えるためのものです。CPOの型そのものをタグとして用い、std::tag_invoke
(これ自身もCPO)を介して、ユーザー定義型に対してADLでtag_invoke
という名前の関数を呼び出します。CPOにアダプトするユーザーはtag_invoke
という名前の関数を例えばHidden friendsとして定義し、対象のCPOの型をタグとして受け取れるようにした上で、そこにCPOによって呼び出された時の処理を記述します。要するに少し複雑なタグディスパッチを行うものです。
この提案は、そんなtag_invoke
を用いてP1393のrequire/prefer
によるプロパティ指定の方法をより簡潔に定義し直そうとするものです。
require/prefer
は主にExecutorライブラリのexecutor
に対してプロパティを設定するもので、CPOとして定義されています。呼び出すとメンバ関数またはADLによってrequire/prefer
という名前の関数を探し処理を委譲します。require/prefer
によってプロパティ指定可能なクラスを作成する際は、そのように呼ばれるrequire/prefer
関数内でプロパティ指定の処理を行い、その後プロパティ指定済みのオブジェクトを返します。
// 何かしらのexecutor
executor auto ex = ...;
// 実行にはブロッキング操作が必要という要求(require
executor auto blocking_ex = std::require(ex, execution::blocking.always);
// 特定の優先度pで実行することが好ましい(prefer
executor auto blocking_ex_with_priority = std::prefer(blocking_ex, execution::priority(p));
// ブロッキングしながら実行、可能ならば指定の優先度で実行
execution::execute(blocking_ex_with_priority, work);
require
は必ずそのプロパティを設定するという強い要求を行い、設定できない場合はコンパイルエラーとなります。対して、prefer
は弱い要求を行い、そのプロパティ指定は無視される可能性があります。
require/prefer
は処理の実行方法などのプロパティ(ブロックするかしないか、優先度など)と、処理の実行のインターフェース(std::execution::execute
などのCPO)とを分離し、プロパティ指定毎にその実行用インターフェースが増加することを回避するための仕組みです。
この提案ではrequire/prefer
の大部分をtag_invoke
を使って実装するように変更しています。先程のコードは次のように変化します。
// 何かしらのexecutor
executor auto ex = ...;
// 実行にはブロッキング操作が必要という要求(require
executor auto blocking_ex = execution::make_with_blocking(ex, execution::always_blocking);
// 特定の優先度pで実行することが好ましい(prefer
executor auto blocking_ex_with_priority = std::prefer(execution::make_with_priority, blocking_ex, p);
// ブロッキングしながら実行、可能ならば指定の優先度で実行
execution::execute(blocking_ex_with_priority, work);
require/prefer
CPOそのものだけではなく、プロパティを表現する型やプロパティサポートを問い合わせるためのquery
、プロパティを受け入れるための実装方法などもtag_invoke
を使うように変更されています。結果として、require
は消えているようです。
元のrequire/prefer
はユーザーから見れば効率的で柔軟なプロパティ指定の方法でしたが、それを受け入れるようにする実装は少し大変でした。しかし、tag_invoke
を利用することで実装者の負担がかなり軽減されています。
- P1393R0 A General Property Customization Mechanism
- P1895R0 tag_invoke: A general pattern for supporting customisable functions
- P2216 進行状況
P2221R0 : define P0443 cpos with tag_invoke
Executor提案(P0443R13)で提案されているCPO(Custmization Point Object)を別に提案中のtag_invoke
によって定義する提案。
先程も出て来たtag_invoke
は、CPOの行う呼び出しのうちCPOと同名の関数をADLによって探索する部分を置き換えるためのものです。
CPO等によってカスタマイゼーションポイントを導入することは実質的にその関数名をグローバルに予約してしまう事になります。CPOはコンセプトによるチェックを行うとは言え、意図せずチェックを通ってしまう型が無いとも限りません。tag_invoke
はCPOの型をタグとしてstd::tag_invoke
という1つのCPOだけを通してADLによる関数呼び出しを行います。その結果、グローバルに予約する名前はtag_invoke
一つだけになります。
Executorでは、execute
など全部で9つのCPOが用意されており、全て同名の関数をADLで探索します。従って、9つの名前を実質的に予約してしまうことになります。tag_invoke
を使ってその部分の定義だけを置き換える事で、これらの名前を予約することを避けようとする提案です。
この提案の後でもCPOを使う側は一切何かする必要はありません。CPOにアダプトしようとする場合にtag_invoke
を利用することになります。例えば、std::execution::executor
CPOにアダプトする例を比較してみます。
P0443
struct my_executor {
template<typename F>
friend void executor(const my_executor& ex, F&& f) {
// オリジナルのexecutorの処理、略
}
auto operator<=>(const my_executor&) = default;
};
この提案
struct my_executor {
template<typename F>
friend void tag_invoke(std::tag_t<std::execution::execute>, const my_executor& ex, F&& f) {
// オリジナルのexecutorの処理、略
}
auto operator<=>(const my_executor&) = default;
};
my_executor ex{};
// 上記のどちらが採用されるにせよ、こう呼び出せる
std::execution::execute(ex, [](){ /* 何か処理*/ });
これらのfriend
関数はHidden friendsというイディオムであり、同じ名前空間のフリー関数として定義していても構いません。しかしどちらにせよ、そこの関数名にCPO名ではなくtag_invoke
という名前を使うようになることで、CPOをいくら増やしてもユーザーコードではそれを気にせずに同じ名前を使用することができるようになります。
- [C++]カスタマイゼーションポイントオブジェクト(CPO)概論 - 地面を見下ろす少年の足蹴にされる私
- P1895R0 tag_invoke: A general pattern for supporting customisable functions
- P2221 進行状況
P2223R0 : Trimming whitespaces before line splicing
バックスラッシュ+改行による行継続構文において、バックスラッシュと改行との間にホワイトスペースの存在を認める提案。
CとC++では行末にバックスラッシュがある場合に、その行と続く行を結合して論理的に1行として扱います。これはプリプロセスより前に処理されるため、マクロを複数行で定義する時などに活用されます。
しかし、バックスラッシュと改行の間にホワイトスペースしかない場合に問題が潜んでいます。
int main() {
int i = 1
// \
+ 42
;
return i;
}
3行目のバックスラッシュの後にはスペースが挿入されています。この時、このバックスラッシュを行継続のマーカーとして扱うかどうかが実装によって異なっています。EDG(ICC),GCC,Clangはホワイトスペース列を除去して行継続を行い、結果1を返します(+ 42
はコメントアウトされる)。MSVCはバックスラッシュの直後に改行が無いことから行継続とは見なさず、結果43を返します。
これはどちらも規格的に正しい振る舞いで、実装定義の範疇です。
ただ、この振る舞いは直感的では無いため、バックスラッシュ後のホワイトスペース列は除去した上で行継続を行うように規定しようとする提案です。つまり、MSVCの振る舞いに修正が必要となります。
このような振る舞いに依存しているコードはバックスラッシュの後の見えない空白を維持し続けているはずですが、それを確実に保証することはできずその有用性も無いため、このような振る舞いをサポートし続ける必要はないだろうという主張です。
他にも次のようなコードで影響があります。
auto str = "\
";
MSVC以外ではstr
は空文字列で初期化されますが、MSVCは\
がエスケープシーケンスとして有効ではないためコンパイルエラーとなります。
P2224R0 : A Better bulk_schedule
P2181R0にて提案されているbulk_schedule
のインターフェース設計の改善提案。
現在のbulk_schedule
は次のようなインターフェースになっています。
template<executor E, sender P>
sender auto bulk_schedule(E ex, executor_shape_t<E> shape, P&& prologue);
bulk_schedule
はExecutorex
上で、入力の処理prologue
をshape
によって表現される範囲内でバルク実行をし、かつその実行は遅延可能であるものです。その戻り値はそれらバルク実行のそれぞれの処理の開始を表すsenderで、呼び出し側はそこから各バルク実行の処理本体(あるいは後続の処理)を表現するsenderを構築する義務を負います(つまり、そのsenderに処理をチェーンする形でバルク実行の本体を与える)。
この場合、そのようなsenderを作成しバルク実行後、そこにどのように後続の処理をチェーンさせるかは不透明であったため、それを解決するためにbulk_join
操作が検討されていました。
この提案では、bulk_schedule
のインターフェースと担う役割を変更しそのような問題の解消を目指します。提案されているインターフェースは次のようになります。
template<scheduler E, invocable F, class... Ts>
sender_of<Ts...> auto bulk_schedule(sender_of<Ts...> auto&& prologue, E ex, executor_shape_t<E> shape, F&& factory);
この戻り値はバルク処理全体を表現するsenderで、検討されていたbulk_join
の戻り値に近いものになります。
最後の引数にあるfactory
は、バルク実行の一つ一つの処理を表現するsenderを構築する責任を担うsender factoryで、次のようなインターフェースを持ちます。
auto factory(sender_of<executor_shape_t<E>, Ts&...>) -> sender_of<void>
sender factoryは個別の処理(prologue
)の開始を表すsenderを一つ受け取ります。このsenderは接続されたreceiverに各処理の固有番号(インデックス)と処理の結果(あれば)を渡すものです。そして、このsender factoryはそれら1つ1つの処理の全体を表すsender_of<void>
を返します。
このsender factoryへの引数は元の(この提案の対象の)bulk_schedule
が返していたsenderに対応しており、この提案によるbulk_schedule
が返すsenderは元のbulk_schedule
の呼び出し元が構築する義務を負っていた(bulk_join
が返していた)senderに対応しています。
結局、これらの変更によってbulk_schedule
を使った処理のチェーンは次のように変化します。
// 元のbulk_schedule
// N個の処理(prologue)に継続して処理Aをバルク実行し、bulk_joinによって統合後処理B(非バルク実行)を実行する
auto S = bulk_schedule(ex, N, prologue) | ..A.. | bulk_join() | ..B..;
// この提案のbulk_schedule
// 上記と同じことを行うもの
auto S = bulk_schedule(prologue, ex, N, [](auto begin) { return begin | ..A..; }) | ..B..;
ユーザーが負っていた責任の多くをbulk_schedule
が担うようになり、バルク処理のそれぞれに後続の処理をチェーンさせることと、バルク処理の全体に後続の処理をチェーンさせることをより明確に書くことができるようになっています。
Discussion