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

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

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

N4862 : Business Plan and Convener's Report

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

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

P0288R6 : any_invocable

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

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

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

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

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

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

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

#include <any_invocable>

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

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

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

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

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

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

P0881R6 : A Proposal to add stacktrace library

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

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

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

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

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

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

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

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

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

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

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

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

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

P1787R5 : Declarations and where to find them

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

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

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

P1875R1 : Transactional Memory Lite Support in C++

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

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

onihusube.hatenablog.com

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

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

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

onihusube.hatenablog.com

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

絵文字について

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

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

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

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

GCCUTF-8文字関連のバグ

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

P2013R2 : Freestanding Language: Optional ::operator new

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

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

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

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

P2053R1 : Defensive Checks Versus Input Validation

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

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

P2079R1 : Parallel Executor

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

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

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

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

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

P2096R2 : Generalized wording for partial specializations

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

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

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

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

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

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

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

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

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

P2187R4 : std::swap_if, std::predictable

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

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

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

P2192R1 : std::valstat - function return type

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

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

P2197R0 : Formatting for std::complex

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

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

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

P2205R0 : Executors Review - Polymorphic Executor

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

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

  • bad_executor
  • any_executor
  • prefer_only

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

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

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

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

P2207R0 : Executors review: concepts breakout group report

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

P2209R0 : Bulk Schedule

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

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

using namespace std::execution;

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

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

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

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

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

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

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

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

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

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

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

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

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

P2210R0 : Superior String Splitting

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

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

std::string s = "1.2.3.4";

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

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

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

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

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

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

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

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

std::string s = "1.2.3.4";

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

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

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

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

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

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

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

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

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

P2212R0 : Relax Requirements for time_point::clock

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

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

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

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

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

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

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

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

P2213R0 : Executors Naming

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

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

P2215R0 : "Undefined behavior" and the concurrency memory model

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

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

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

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

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

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

  return 0;
}

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

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

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

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

多分2週間後くらい

この記事のMarkdownソース