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

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

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

P1401R3 : Narrowing contextual conversions to bool

constexpr ifstatic_assertの引数でのみ、整数型からbool型への暗黙の縮小変換を定数式で許可する提案。

現在定数式での整数の暗黙変換では、文脈的なbool変換も含めて縮小変換が禁止されており次のようなコードはコンパイルエラーになります。

enum Flags { Write = 1, Read = 2, Exec = 4 };

template <Flags flags>
int f() {
  if constexpr (flags & Flags::Exec) // 縮小変換が起きるためコンパイルエラー
  // if constexpr (bool(flags & Flags::Exec)) とするとok
    return 0;
  else
    return 1;
}

int main() {
  return f<Flags::Exec>();  // コンパイルエラー
}
template <std::size_t N>
class Array {
  static_assert(N, "no 0-size Arrays"); // 縮小変換が起きるためコンパイルエラー
  // static_assert(N != 0); とするとok

  // ...
};

Array<16> a;  // コンパイルエラー

一方対応する実行時コードは普通にコンパイルでき、期待通りに動作します。

if (flags & Flags::Exec) // ok
  {}

assert(N); // ok

このような一貫しておらず直感的ではない挙動を修正するために、constexpr ifstatic_assertの条件式に限って、文脈的なbool変換時の縮小変換を許可しようというものです。

そもそもこれらの条件式でさえもboolへの縮小変換が禁止されていたのは、noexcept式での縮小変換を禁止した時に巻き込まれてしまったためのようです。関数f()の例外仕様が別の関数g()と同じ(あるいはそれに従う)場合、noexceptを二つ重ねて書きます。しかし、その場合に書き間違えて関数名だけを書いたり、1つにしてしまってg()constexpr関数だったりすると思わぬバグを生みます。

int f() noexcept(noexcept(g()));  // 書きたいこと、noexcept指定指の中にnoexcept式を書く

int f() noexcept(g);    // 関数ポインタからの暗黙変換、でもこう書きたさもある・・・
int f() noexcept(g());  // 定数評価の結果bool値へ変換されると・・・

このような些細な、しかし気づきにくいバグを防ぐために定数式での文脈的なbool変換の際は縮小変換を禁止することにしました。しかし、noexcpet以外のところではこれによって(最初に上げたような)冗長なコードを書くことになってしまっていました。

この欠陥報告(CWG 2039、C++14)を行ったRichard Smithさんによると、本来はnoexcept式にだけ適用するつもりで、static_assertには表現の改善のみで縮小変換禁止を提案してはいなかったそうですが、その意図に反して両方で縮小変換が禁止されてしまいました。結果、おそらくその文言を踏襲する形でconstexpr ifexplicit(bool)にも波及したようです。

P1450R3 : Enriching type modification traits

型の修飾情報などを操作するためのいくつかの新しいメタ関数の提案。

主に次の2種類のものが提案されています。

  • remove_all_pointers
  • 型に付いている情報をコピーするcopy_*メタ関数
using remove_ptr = std::remove_all_pointers_t<int************>;     // int
using copy_ptr1 = std::copy_all_pointers_t<int***, double>;         // double***
using copy_ptr2 = std::copy_all_pointers_t<int***, double*>;        // double****
using copy_r1 = std::copy_reference_t<int&, double>;                // double&
using copy_r2 = std::copy_reference_t<int&, double&>;               // double&&
using copy_r3 = std::copy_reference_t<int&, double&&>;              // double&
using copy_const1 = std::copy_concst_t<const int, double>;          // const double  
using copy_const2 = std::copy_const_t<const volatile int, double>;  // const double  
using copy_cvr = std::copy_cvref_t<const volatile int&&, double>;   // const volatile double&&

この他にもcopy_volatileとかcopy_extentcopy_pointerなどが提案されています。

copy_*系メタ関数の引数順は<From, To>になっており、全てに_t付きのエイリアスが用意されています。また、対象の修飾がコピー先にすでに付いている場合はそれはそのままに追加でコピーする形になり、コピー元に対象の修飾がない場合は何もコピーしません。

筆者の方々の経験からプロクシクラス作成やカスタムオーバーロードセットを構築するツールの実装に有用であった型特性を提案しているそうです。

P1467R4 : Extended floating-point types and standard names

C++コア言語/標準ライブラリに拡張浮動小数点型のサポートを追加する提案。

機械学習(特にディープラーニング)では多くの場合それほど高い精度が求められないため、float(32bit浮動小数点数)よりもより小さい幅の浮動小数点型(時には整数型)を利用することでその時間的/空間的なコストを抑えることが行われており、それを支援する形でハードウェア(GPU/CPU)やソフトウェア(CUDA/LLVM-IR)でのサポートが充実してきています。

現在のC++には3種類の浮動小数点型だけが定義されておりそれ以外のものは何らサポートがあリません。そのため、拡張浮動小数点型は算術・変換演算子オーバーロードして組み込み型に近い挙動をするようなクラス型を定義することでサポートされています。しかし、そのような方法は完全ではなく面倒で、効率化のためにインラインアセンブラコンパイラ固有のサポートが必要とされます。
これらの問題はユーザー定義のライブラリで解決できるものではなく、コア言語でのサポートが必要です。そして、拡張浮動小数点型が求められる場所ではC++が使用される事が多くこれらの問題を解決するに足る価値(ポータビリティや効率性の向上など)があるので、拡張浮動小数点型の言語/ライブラリサポートを追加しようという提案です。

ただし、現在のところ拡張浮動小数点型のスタンダードとなるものは確定しておらず、将来どれが使われていく(あるいは廃れる)のか予測することは困難であるため、何がいくつ定義されるかは実装定義とされます。そのため、bfloat16とかfloat16みたいな具体的な型は提供されませんが、代わりに似た形のエイリアスが実装定義で提供されます。

変更は既存の浮動小数点型の振る舞いを保ったままで追加の浮動小数点型をより安全に利用可能かつ拡張可能である(ハードウェアに依存しない)ようにされています。

  • 拡張浮動小数点型はdoubleに昇格されない
  • 拡張浮動小数点型の関わる暗黙変換では縮小変換を許可しない
    • ただし、定数式では許可される
  • 式のオペランドとなっている2つの浮動小数点型を統一する算術型変換(Usual arithmetic conversions)では、どちらのオペランドももう片方の型に変換できない場合はill-formed
    • float, double等は従来のルール通りにより幅の広い型に自動昇格する
float f32 = 1.0;
std::float16_t f16 = 2.0;
std::bfloat16_t b16 = 3.0;

f32 + f16; // OK、f16はfloatに変換可能、結果の型はfloat
f32 + b16; // OK、b16はfloatに変換可能、結果の型はfloat
f16 + b16; // NG、2つの型の間に互換性がなく、どちらの型ももう片方の型に値を変更することなく変換できない

std::float16_t x{2.1};  // OK、2.1は2進浮動小数で表現可能ではないため縮小変換が発生している、定数式なのでok

// <charconv>が使用可能
char out[50]{};
if (auto [ptr, ec] = std::to_chars(out, std::end(out), f16); ec == std::errc{}) {
  std::cout << std::string_view(out, ptr - out) << std::endl;

  std::float16_t outv;
  if (auto [_, ec2] = std::from_chars(out, ptr, outv); ec2 == std::errc{}) {
    std::cout << outv << std::endl;
  }
}

// ユーザー定義リテラルが用意される
using namespace std::float_literals;

// complexも規定される
std::complex<std::bfloat16_t> z = {1.0bf16, 2.0bf16};

これら型エイリアスが定義されるヘッダには<fixed_float><stdfloat>という名前を提案しているようですが、筆者の方々はあまり気に入っていないようでより良い名前やふさわしい既存のヘッダを考慮中のようです。いい名前が思いついたらコントリビュートチャンスです。

P1468R4 : Fixed-layout floating-point type aliases

この提案文書は拡張浮動小数点型の既知のレイアウトとその名前についての提案でしたが、今回P1467R4(1つ前のの拡張浮動小数点型に対する提案)にマージされたため、内容は空です。それが行われたことを記しておくために存在しているようです。

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

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

[utility]は範囲が広いですが、ほとんどpair, tupleを対象にしています。また、動的メモリ確保を必要としたり例外を送出しうるもの、iostreamのようにOSのサポートを必要とするものは当然含まれていません。<utility>, <tuple>, <ratio>ヘッダの全てと、特筆する所ではstd::unique_ptrstd::functionのフリースタンディング化が提案されています。
<ranges><iterator>からは(i|o)stream_iterator(i|o)streambuf_iteratoristream_view等に関わるもの以外の全てを追加することが提案されています。

また、これらのものには機能テストマクロが用意されています。

筆者の方は、<optional><variant>などを今後別の提案で詳細に検討していくつもりのようです。

ライブラリ機能のフリースタンディング化に慎重な検討が必要になるのは、フリースタンディング処理系とホスト処理系とである関数呼び出し時のオーバーロードセットが変化することで暗黙のうちに動作が変わってしまうことを防ぐためです。変わったとしてもちゃんとエラーになるのかや選択されるオーバーロードが変化しないかなどを慎重に検討せねばならないようです。とても大変そうです・・・

P1944R1 : Add Constexpr Modifiers to Functions in cstring and cwchar Headers

<cstring><cwchar>の関数にconstexprを追加する提案。

これらのヘッダに定義されている文字列操作関数をconstexprにすることを意図しています。ただし、ロケールやグローバルオブジェクトの状態に依存したり、スレッドローカルな作業域を持つような関数は除外しています。また、std::memcpystd::memmoveなどのメモリ操作系の関数もconstexprにすることが提案されています。これらは引数にvoidポインタを取るためそのままだと定数式で使えないのですが、コンパイラマジックにより定数式で使用可能にしてもらうようです。

<cstring><cwchar>C++として独自実装している処理系とCのコードを流用している処理系が存在しているようですが、前者はそのままconstexprを付加し、後者はコンパイラの特別扱いによってABIを破損することなくconstexpr対応できるだろうということです。

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

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

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

前回(R3)との変更点は文書にSummaryが追加されたことと、提案する字句トークン(プリプロセッシングトークン)のEBNF定義の修正だけのようです。

この提案はすでにCWGでの文言レビューを待つだけになっており、C++23に導入される可能性は高いです。その場合、C++においては識別子(クラス・変数・関数等の名前)に絵文字を使用できなくなります(欠陥報告になる可能性があるので以前のバージョンも含めて)。

P1990R1 : Add operator[] to std::initializer_list

std::initializer_listに添字演算子[]data()関数を追加する提案。

std::initializer_listは軽量な生配列のプロキシクラスではありますが、要素アクセスにはイテレータを使用するしかないなど少し使いづらい所があります。その解消のために、添字アクセスと先頭領域へのポインタ取得をサポートしようという提案です。

struct Vector3 {
  int x, y, z;

  // C++20現在
  Vector3(std::initializer_list<int> il) {
    x = *(il.begin() + 0);
    y = *(il.begin() + 1);
    z = *(il.begin() + 2);
  }

  // 提案
  Vector3(std::initializer_list<int> il) {
    x = il[0];
    y = il[1];
    z = il[2];
  }
};


void f(std::initializer_list<int> il) {
  // C++20 現在
  const int* head = il.begin();

  // 提案
  const int* head = il.data();
}

P2025R1 : Guaranteed copy elision for return variables

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

C++17からはRVO(Return Value Optimization)によるコピー省略が必須となり、関数内で戻り値型をreturnステートメントで構築する場合にコピーやムーブを省略し、呼び出し元の戻り値を受けている変数に直接構築するようになっています。一方、関数内で変数を構築してから何かしてその変数をreturnする場合のコピー省略(NRVO)は必須ではなく、コンパイラの裁量で行われます(とはいえ、主要なコンパイラは大体省略します)。

// コピーが重いクラス
struct Heavy {
  int array[100]{};

  Heavy() {
    std::cout << "default construct\n"
  }

  Heavy(const Heavy& other) {
    std::copy_n(other.array, 100, array);
    std::cout << "copy construct\n"
  }

  Heavy(Heavy&& other) {
    std::copy_n(other.array, 100, array);
    std::cout << "move construct\n"
  }

};

Heavy rvo() {
  return Heavy{}; // RVOが必ず行われる
}

Heavy nrvo() {
  Heavy tmp{};

  for (int i = 0; i < 100; ++i) {
    tmp.array[i] = i;
  }

  return tmp; // NRVOはオプション
}

int main() {
  Heavy h1 = rvo();   // 結果はh1に直接格納され、デフォルトコンストラクタが一度だけ呼ばれる
                      // コピーやムーブコンストラクタは呼ばれない。

  Heavy h2 = nrvo();  // NRVOが行われない場合、デフォルト・ムーブコンストラクタが一回づつ呼ばれる
                      // 正確にはデフォルト構築→コピー→ムーブとなるが、returnでのコピー後のprvalueはRVOの対象なので最後のムーブコンストラクタは省略される
                      // NRVOが行われた場合、デフォルトコンストラクタが一度だけ呼ばれる
}

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

(言語バージョンをC++14にすると完全にコピー省略のない世界を見ることができます。また、-fno-elide-constructorsを外すとコピー省略された結果を見ることができます。)

このような場合のnrvo()の呼び出しのようにNRVOが可能なケースではNRVOを必須にしよう、という提案です。NRVo可能なケースというのは簡単に言うと全てのreturn文が同じオブジェクトを返すことが分かる場合の事で、提案文書にはこの提案によっていつNRVOが保証されるかのいくつかのサンプルが掲載されています。

この提案はCWGでの文言調整フェーズに進んでおり、C++23に入る可能性が高そうです。

P2034R2 : Partially Mutable Lambda Captures

ラムダ式の全体をmutableとするのではなく、一部のキャプチャだけをmutable指定できるようにする提案。

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

前回との差分は一部のサンプルコードが変更されたことと初期化キャプチャ時パック展開でのmutable指定が提案に含まれた事、EWGIの議論で示された懸念事項が追記された事です。

C++20よりラムダ式の初期化キャプチャ時にパラメータパックをキャプチャ出来る様になっているので、そこでもmutableが出来るようにしようとしています。これを用いるとパラメータパックだけをラムダ式中で再ムーブする時に、全部をmutableにしなくても良くなります。

template <class... Args>
auto delay_invoke_foo(Args... args, State s) {
  return [s, mutable ...args = std::move(args)] {
    return foo(s, std::move(args)...);
  };
}

追加された懸念事項は、明示的なconstキャプチャをする場合に、ラムダ式のムーブで暗黙にコピーが行われるようになる事です。クラスのメンバにconstメンバがあってもムーブコンストラクタ自体は使用可能ですが、constメンバはコピーされます。コピーコンストラクタは多くの場合例外を投げうるので、これによって思わぬところで例外が発生するようになってしまう可能性があります。

auto l1 = [const str = std::string{"not movable"}](){return str;};
auto l2 = std::move(l1);  // キャプチャしたメンバstrはコピー構築される、場合によっては例外を投げうる

この提案によってもたらされるラムダ式の対称性と一貫性の向上による効用と、このような足を撃ち抜く可能性を導入することによる弊害のどちらがより大きいのかは解決されておらず、より議論が必要となりそうです。

P2037R1 : String's gratuitous assignment

std::stringの単一の文字代入を非推奨とする提案。

std::stringにはchar1文字を受け取る代入演算子が定義されています。

// char1文字を代入する
constexpr basic_string& operator=(charT c);

しかし、この代入演算子は特に制約されておらず、charに暗黙変換可能な型に代入を許します。その代表的なものはintdoubleの数値型です。

std::string s{};

s = `A`;  // s == A
s = 66;   // s == B
s = 67.0; // s == C

すなわち、intdoubleへの暗黙変換を実装している任意のユーザー定義型も代入可能です。

そもそもstd::stringchar1文字を代入できる必要があることが疑わしい上にコンストラクタのインターフェースとも一貫しておらず、この様な変換が起きることはほとんどの場合意図したものではなくバグの原因であるので非推奨にしようという主張です。
ただし、削除することまでは提案されていません。

ほかの選択肢としては

  • 削除する
    • その場合、nullptrの代入が可能になってしまうのでケアする必要がある
  • コンセプトによる制約を行う cpp template<same_as<charT> T> constexpr basic_string& operator=(T c) ;
  • intからの変換だけを許可するようにして、他の変換は不適格とする。
    • 筆者の方が見てきたこれらの変換が問題となっていたケースはほぼ全てintからの変換だったので解決策としては弱いだろう、とのこと

R0の際に行われたLEWGでの投票では、非推奨とすることに合意が取れていて、今回はそれを受けて標準のための文言を追加したようです。

P2093R0 : Formatted output

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

C++20で導入されたstd::formatはフォーマットを指定しつつ文字列を構成できるものですが、その結果はstd::stringで得られ出力機能は備えていません。そのままiostreamを使えば出力できますが、一時オブジェクトの確保が必要になる上、iostreamによってフォーマット済み文字列を再びフォーマットすることになり非効率です。

// 一時オブジェクトが作成され、内部で再フォーマットされ、バッファリングされうる
std::cout << std::format("Hello, {}!", name);

// nameはnull終端されていなければならない、型安全ではない
std::printf("Hello, %s!", name);

// 一時オブジェクトが作成される、c_str()と個別I/O関数の呼び出しが必要になる
auto msg = std::format("Hello, {}!", name);
std::fputs(msg.c_str(), stdout);

この提案では、このような場合に一時オブジェクトを作成せず、フォーマットとI/Oで別の関数を呼び出す必要もなく、より効率的な出力を行うstd::print関数を提案しています。

// 一時オブジェクトは作成されず、フォーマットは一度だけ、直ちに出力する
std::print("Hello, {}!", name);

これはすなわちiostreamに変わる新しい出力ライブラリとなります。このライブラリは次のことを目標にしています。

これはすでに{fmt}にて実装されていて、その実装により得られたベンチマーク結果が掲載されています。既存のI/Oと比較すると速度とバイナリフットプリントの両面で良好な結果を得られているようです(ただ、純粋なフットプリントだけはprintfに及ばないようです)。

P2138R2 : Rules of Design<=>Wording engagement

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

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

このリビジョンでの変更は、CWG/LWGにおける標準のための文言レビューと本会議での投票の間に、最終確認のためのTentatively Readyという作業フェーズを追加することを提案している点です。

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

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

この提案は規格書中のAnnex.Dというセクションに記載されている機能だけを対象としていて、そこにあるもの以外を削除するわけでもなく、そこに新しく追加する機能について検討するものでもありません。

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

P2141R0 : Aggregates are named tuples

集成体(Aggregate)を名前付きのstd::tupleであるとみなし、標準ライブラリにおけるstd::tupleのサポートを集成体に拡張する提案。

std::tupleは任意個数の型をひとまとめにして扱える大変便利なものではありますが、コア言語のサポートが無く全てをライブラリ機能によって実現しているため使いづらい事が多くあります。一方、集成体はC言語から引き継がれたいくつかの条件を満たした構造体で、std::tupleを利用するシーンでは集成体を利用した方が便利だったりする事が多々あります。

// 集成体
struct auth_info_aggreagte {
  std::int64_t id;
  std::int64_t session_id;
  std::int64_t source_id;
  std::time_t valid_till;
};

// std::tuple
using auth_info_tuple = std::tuple<
  std::int64_t,
  std::int64_t,
  std::int64_t,
  std::time_t
>;

template <class T>
constexpr bool validate() {
    static_assert(std::is_trivially_move_constructible_v<T>);
    static_assert(std::is_trivially_copy_constructible_v<T>);
    static_assert(std::is_trivially_move_assignable_v<T>);
    static_assert(std::is_trivially_copy_assignable_v<T>);
    return true;
}

// std::tupleは特殊メンバ関数をほぼ自前定義しているので、trivialではない
constexpr bool tuples_fail = validate<auth_info_tuple>();
constexpr bool aggregates_are_ok = validate<auth_info_aggreagte>();

ただ、集成体には言語サポート(集成体初期化、必然的なtrivial性など)がある代わりに、ほぼライブラリサポートがありません。std::getなどを利用できず、ジェネリックなコードにおいては少し使いづらい事があります。

namespace impl {
  // ストリームから読みだしたデータでtupleを初期化する
  template <class Stream, class Result, std::size_t... I>
  void fill_fileds(Stream& s, Result& res, std::index_sequence<I...>) {
    (s >> ... >> std::get<I>(res)); // 集成体はstd::getを使用できないため、コンパイルエラー
  }
}

template <class T>
T ExecuteSQL(std::string_view statement) {
  std::stringstream stream;

  // ストリームにデータを入力するステップ、省略

  T result;
  impl::fill_fileds(stream, result, std::make_index_sequence<std::tuple_size_v<T>>());
  return result;
}

constexpr std::string_view query = "SELECT id, session_id, source_id, valid_till FROM auth";

const auto tuple_result = ExecuteSQL<auth_info_tuple>(query); // ok
const auto aggreagate_result = ExecuteSQL<auth_info_aggreagte>(query); // error!

std::getstd::tupleに対するライブラリサポートはtuple-likeな型(例えばstd::pairstd::array)ならば利用可能であるので、一般の集成体をtuple-likeな型として利用可能にすることで集成体にライブラリサポートを追加しよう、という提案です。

tuple-likeな型の条件はstd::tuple_sizeによってその長さが、std::tuple_elementによってその要素型が、そしてstd::getによってインデックスに応じた要素を取得できる事です。標準ライブラリにおいて、任意の集成体に対してこれらを用意(あるいは自動生成?)しておくようにする事で集成体にライブラリサポートを追加します。コア言語に変更は必要ありませんが、コンパイラによるサポートは必要そうです。
そして、それによってstd::tupleを用いている既存のコードは一切変更する事なく集成体でも利用できるようになります。

constexpr std::size_t elems = std::tuple_size<auth_info_aggreagte>::value;  // 4
using e2_t = std::tuple_element_t<2, auth_info_aggreagte>;  // std::int64_t

auth_info_aggreagte a = { 1, 2345, 6789, {}};
auto& e3 = std::get<3>(a);  // 6789

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

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

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

このリビジョンでの変更は、いくつかの機能の追加とそれを用いた既存機能の修正などです。

P2152R0 : Querying the alignment of an object

alignofを型だけではなくオブジェクトに対しても使用出来るようにする提案。

alingasによってオブジェクトと型に対してアライメントを指定する事ができますが、alignofでアライメントを取得できるのは型だけです。この挙動は一貫しておらず、GCCではオブジェクトに対してもalignof出来るようになっているためC++標準としても正式に許可しようとする提案です。

struct alignas(32) S {};  // ok

int main () {
  alignas(64) S obj{};  // ok

  std::size_t type_alignment = alignof(S);    // ok、32
  std::size_t  obj_alignment = alignof(obj);  // 現在はエラー
}

さらに、既存のアライメントに関しての空白部分やC言語との非互換性を改善する提案も同時に行なっています。

オブジェクトの型のアライメントとオブジェクトのアライメント指定について。

// 32バイト境界にアラインするように指定 in C
typedef struct U U;
struct U {
}__attribute__((aligned (32)));
// C++での等価な宣言
// struct alignas(32) U {};

int main() {
  // C言語の挙動
  _Alignas(16) U u; // ng、型のアライメント要求よりも弱いアライメント指定
  _Alignas(64) U v; // ok
  _Alignof(v);      // GNU拡張、64

  // 等価なはずのコードのC++での挙動
  alignas(16) U u;  // GCCとMSVCはok、Clangはエラー (1)
  alignof(u);       // GCCのみok、16
  alignas(64) U v;  // ok
  alignof(v);       // GNU拡張、64 MSVCはエラー (2)
}
  • (1) : 型よりも弱いアライメントを指定するalignasではオブジェクトを定義できないはずだが、C++にはこの場合の規定がない
    • 型のアライメント要求よりも弱いアライメント指定はエラーと明確に規定する
  • (2) : オブジェクト型に対するalignofは現在許可されていない
    • この提案のメインの部分によって許可する

型のアライメントとメンバ変数のアライメントについて。

typedef struct V V;
typedef struct S S;
typedef struct U U;

struct V {} __attribute__((aligned (64)));
struct S {} __attribute__((aligned (32)));
struct U {
  S s;
  V v;
} __attribute__((aligned (16))); // GGCおよびclangはこのアライメント要求を無視する

/*
C++での等価な宣言
struct alignas(32) S {};
struct alignas(64) V {};
struct alignas(32) U {  // GCCはこのアライメント要求を無視、clangはエラー、MSVCは警告 (1)
  S s;
  U u;
};
*/

int main() {
  // C言語の挙動
  _Alignof(U);  // ok、64

  // 等価なはずのコードのC++での挙動
  alignof(U);   // GCCとMSVCではok、64  (2)
}
  • (1) : 型へのアライメント要求がそのメンバのアライメント要求よりも弱い場合の規定がC++にはない
    • 型へのアライメント要求がそのメンバのアライメント要求よりも弱い場合はエラーと明確に規定する
  • (2) : (1)の場合にアライメントをどうするのかの規定もない(ただし、構造体のアライメントはメンバのアライメントによって制限されるということを示す記述はある)
    • エンティティ(型)のアライメントはそのメンバと同じかそれよりも強くなければならない、と明確に規定する。

こうしてみると、C言語がしっかりとしている一方でC++は深く考えてなかった感があります・・・

P2161R1 : Remove Default Candidate Executor

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

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

このリビジョンでは、単純にassociated_executorからsystem_executorを削除してしまうと、Networking TS内にある別の機能である​defer, dispatch​, ​postが深刻な影響を受けてしまうようで、それについての問題点と対策が追記されています。他には、5月に行われたSG4でのレビューについて追記されています。

P2164R1 : views::enumerate

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

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

このリビジョンでは、インデックスの型の指定が変更されました。以前は1つ前の範囲の差分型(difference type)をインデックスの型に使用していましたが、1つ前の範囲のranges::size()の返す型が取得できる場合はそれを、できない場合は差分型と同じ幅の符号なし整数型を使用する、という風に変更されました。要は常に符号なし整数型を使用するようになったという事でしょう。

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

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

std::stringstd::string_viewにはconst char*を受けるコンストラクタ用意されており、nullptrを直接渡すとそのコンストラクタが選択され、未定義動作に陥ります。こんなコードは書かないだろうと思われるのですが、筆者の方の調査によればLLVMを含む少なくとも7つのプロジェクトでこのようなコードが発見されたそうです。

実装によっては実行時アサーションによってエラーにするものもあるようですが、std::nullptr_tを受けるコンストラクタをdeleteする事でそのような診断をコンパイル時に行おうとするものです。

P2176R0 : A different take on inexpressible conditions

契約プログラミングにおいて事前/事後条件をC++コードとして記述する際、チェックされない条件を記述する構文についての提案。

現在C++標準ライブラリでは処理の事前条件や事後条件を文章で指定していますが、契約プログラミングによってそれらをC++コードとして記述することが出来るようになります(予定)。その際、実行時であってもそのチェックが難しいか出来ない条件については、注釈という形で書いておくことが出来るようになっています。例えば、文字列のnull終端要求や、イテレータendへの到達可能性などがあります。

bool is_null_terminated(const char *); // 定義しない

// 文字列はnullでなくnull終端されている、という2つの事前条件が契約されている
void use_str(const char* s)
  [[expect: s != nullptr]]                  // この条件はチェックされる
  [[expect axiom: is_null_terminated(s)]];  // この条件は注釈であり、チェックされない

// 文字列はnullであるかnull終端されている、という事前条件が契約されている
void use_opt_str(const char* s)
  [[expect axiom: s == nullptr || is_null_terminated(s)]]; // この条件全体は注釈であり、チェックされない

この様に、axiomと指定された条件は注釈であり実行時にチェックされません。

この提案はこの構文を変更し、事前・事後条件に注釈であることを書くのではなく、関数宣言の方に注釈のためのものであることを表示するようにするものです。

// axiomをこっちに付ける
axiom is_null_terminated(const char *); // 定義なし

void use_str(const char* s)
  [[expect: s != nullptr]]           // この条件はチェックされる
  [[expect: is_null_terminated(s)]]; // この条件は注釈であり、チェックされない

void use_opt_str(const char* s)
  [[expect: s == nullptr || is_null_terminated(s)]]; // nullチェックは行われるが、is_null_terminatedは注釈でありチェックされない

このようにする事で、注釈となる条件とそうでないものを混ぜて書きながら実行可能な条件をチェックしてもらう事が出来るようになリます。OR条件の場合は条件を複数に分割して書く訳にもいかないので特に有用です。

axiomとマークされた関数は契約の構文の中でのみ使用でき、何らかの述語としてbool値を返す関数だがチェックが困難であることを表現し、実行時には単にtrueを返す条件として扱われます。それ以外はほとんど通常の関数と同様に扱えるものです。ただし、そのために記述する順番には制約がかかります。

void use_opt_str(const char* s)
  [[expect: is_null_terminated(s) || s == nullptr]];  // ng、チェック可能な条件を先に書く必要がある

P2178R0 : Misc lexing and string handling improvements

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

現在のC++の字句規則はユニコード以前の世界で定義されたもので、文字コードを具体的に指定せずに構成されています。しかし、それによって実装定義の部分が広くなり実装による差異が多く発生していたり、そもそも人間に理解しづらかったりしています。
この提案はそれらを改善しユニコードの振る舞いをより明確にしつつ、実装間の差異をなるべく縮小することを目指したものです。全部で12個の提案が含まれています。

C++コンパイラ書く人とかC++コンパイラになりたい人は読んでみると面白いかもしれません。

P2179R0 : SG16: Unicode meeting summaries 2020-01-08 through 2020-05-27

SG16(Unicode Study Group)のミーティングにおける議論の要旨をまとめた文書。

例えば先ほど出てきていたP1949: C++ Identifier Syntax using Unicode Standard Annex 31などの提案やIssue等についての議論の様子が記載されています。

P2181R0 : Correcting the Design of Bulk Execution

進行中のExecutor(P0443)提案中のbulk_executeのインターフェースを改善する提案。

bulk_executeはバルク実行のためのカスタマイゼーションポイントオブジェクトで、カスタムされたbulk executorを使用することによってハードウェアやOSが提供するバルク実行API(例えば、SIMDやスレッドプール)によって効率的なバルク処理を行う事を可能にするためのものです。

// P0443R13より、サンプル
// ex = executor, f = バルク処理, rng = バルク処理の対象となるデータのシーケンス
template<class Executor, class F, class Range>
void my_for_each(const Executor& ex, F f, Range rng) {
  // バルク実行を要求し、senerを取得する
  // ここで、exにカスタムbulk executorを渡せばバルク実行をカスタマイズできる
  sender auto s = execution::bulk_execute(ex, [=](size_t i) {
    f(rng[i]);
  }, std::ranges::size(rng));

  // 実行を開始し処理の完了を待機
  execution::sync_wait(s);
}

ただ、bulk_executeは提案の初期から存在しており、P0443は途中で遅延実行のためにsender/recieverによるアプローチを採用しましたが、bulk_executeはそれらの変更に追随しておらずインターフェースが一貫していませんでした。この提案はそれを解決するものです。主に以下の3点を変更します。

  • 既存のexecute(CPO)とセマンティクスを統一し、bulk_executeは与えられた作業を即座に実行する実行用インターフェースとする
  • 遅延実行用bulk_executeであるbulk_schedule(CPO)を導入する(executeに対するscheduleと同様)
  • bulk_scheduleによって返されるsenderに対する要件を制約し明確化するmany_receiver_ofコンセプトを導入する
    • このsenderではset_value()が繰り返し呼び出される事を許可する

これらの変更の提案はP0443R13に対してのもので、現在のExecutorライブラリの要件やコンセプト、セマンティクスを大きく変更しません。bulk_executeexecute/schedulesender/recieverとのセマンティクスの一貫性を改善し、Executorライブラリをより使いやすくするものです。

namespace std::execution {
  // bulk_executeの宣言
  void bulk_execute(executor auto ex,
                      invocable<executor_index_t<decltype(ex)> auto f,
                      executor_shape_t<decltype(ex)> shape);
}

// 任意のexecutorと処理対象データ列
auto executor = ...;
std::vector<int> ints = ...:

// intのvectorを変更する作業をexecutorに投入する、ただし実行タイミングは実装定義
bulk_execute(executor,
             [&](size_t idx) { ints[i] += 1; },
             vec.size());

// ここでintsを他の処理に使用する場合、同期等の配慮が必要になるかもしれない
namespace std::execution {
  // bulk_scheduleの宣言
  sender auto bulk_schedule(executor auto ex,
                            executor_shape_t<decltype(ex)> shape,
                            sender auto prologue);
}

// 任意のexecutorと処理対象データ列
auto executor = ...;
std::vector<int> ints = ...:

// intのvectorを変更する作業を構成する、まだ実行はされない
auto increment =
    bulk_schedule(executor, vec.size(), just(ints)) |
    transform([](size_t idx, std::vector<int>& ints) {
        ints[i] += 1;
    });

// ここでのintsの変更は安全

// 作業を開始する、ここでは処理をハンドルしないのでnull_receiverに接続する
execution::submit(increment, null_receiver{});

// ここでは処理はすべて終了している

P2182R0 : Contract Support: Defining the Minimum Viable Feature Set

C++20で全面的に削除されたContractsのうち、議論の余地がなく有用であった部分と削除の原因となった論争を引き起こした部分とに仕分けし、有用であった部分だけを最初のC++ Contractsとして導入する事を目指す提案。

C++20において最終的にContractsが削除されることになってしまったのは、主に以下の機能が議論を巻き起こし合意が取れなくなったためです。

  • 継続モード
  • ビルドレベル
  • 上記も含めた、制御がグローバルであること
  • Literal semantics(in-source controls)
    • 個々の契約に対して個別にチェックするか否かを指定したり、それがグローバルフラグの影響を受けないようにしていた
  • Assumption
    • (上記の事によって)axiomではないのにチェックされていない契約条件の存在が想定される

提案ではC++20Contractsからこれらの部分を除いた広く合意の取れていた有用な部分をMVP(Minimum Viable Product)と呼称し、MVPを最初のContractsとして導入し、そうでない部分(上記の5項目)についてはより時間をかけて議論し、追加の機能として導入していくことを提案しています。

P2184R0 : Thriving in a crowded and changing world: C++ 2006-2020

2020年6月のHistory Of Programming Languages (HOPL) で発表されるはずだったBjarne StroustrupさんによるC++の歴史をまとめた論文の紹介文書。

6月のHOPLカンファレンス延期されましたが論文は公開されているようです。英文PDF168Pの超大作ですが、とても興味深そうな内容です(翻訳お待ちしております)。

HOPLは15年毎に開催されるようで、C++はHOPLで3回紹介されたただ一つの言語になり、BjarneさんはHOPLで3回論文を書いたただ一人の人になったようです。次は2035年ですが、C++はそこでも登場することができるでしょうか・・・?

P2185R0 : Contracts Use Case Categorization

Contractsユースケースを「何のために使用するか」と「どうやって使用するか」2つにカテゴライズし、報告されている既存のユースケースをカテゴライズする文書。

これは提案文書ではなく、SG21(Contracts Studt Group)での議論のための報告書です。

P2187R0 : std::swap_if, std::predictable

新しい標準アルゴリズムであるstd::swap_ifstd::predictableの提案。

std::sortに代表される標準ライブラリ中の多くのアルゴリズムには次のような典型的な条件付きswapが頻出します。

if (*right < pivot) {
  std::swap(*left, *right);
  ++left;
} 

この様なswap-if操作は実装を少し変更するだけで、分岐予測のミスによるパイプラインストールを回避しパフォーマンスを2倍以上改善できるらしく、std::swap_ifはそのためのより効率的なswap-if操作を提供するものです。

次のような実装になるようです。

template <movable T>
bool swap_if(bool c, T& a, T& b) {
  T tmp[2] = { move(a), move(b) };
  b = move(tmp[1-c]), a = move(tmp[c]);
  return c;
}

bool値がfalse == 0true == 1であることを利用して、条件分岐を配列のインデックスに帰着させています。
これを用いると先ほどの典型的な操作は次のように書けます。

left += swap_if(*right < pivot, *left, *right);

ただし、現在のC++コンパイラはこの様なコードに対して必ずしも最適な(cmovを使った)コードにコンパイルすることができず、せいぜい次善のコードを出力する場合が多いようです。ただ、その場合でも通常のswap-ifによるstd::sortよりも高速なので、標準ライブラリとしてstd::swap_ifを規定し効率的な実装が提供されるだけでも典型的なswap-if操作の性能向上が図れます。

また、std::swap_ifを規定することはコンパイラによるのぞき穴最適化の機会を提供することに繋がり、将来的に多くのコンパイラが最善のコードを出力できるようになるかもしれません。

ただし、std::swap_ifの上記の様な実装は多くのケースでは高速ですが、特定のデータに対してはかえって低速になります(例えば、ほとんどソート済みの配列のようなデータ列など)。それが事前に予測できる場合、通常の分岐によるswap-if操作にフォールバックできる必要があります(現在のハードウェアでは、その閾値は90%以上の確度が必要)。

2つ目のstd::predictableはそのための述語ラッパー型です。

template <predicate Predicate, bool is = true>
struct predictable {
  std::remove_reference<Predicate>::type pred; // 名前は自由

  explicit predictable(Predicate&& p) : pred(p) {}

  template <typename... Args>
  constexpr bool operator()(Args&&... args) { 
    return ::std::invoke(p, args...);
  }
};

// predictableを検出する変数テンプレート
template <typename>
constexpr bool is_predictable = false;

template <predicate P, bool is>
constexpr bool is_predictable<predictable<P,is>> = is;

標準ライブラリの述語を引数に取るアルゴリズムでは、これを用いて述語をラップして渡し、アルゴリズム中でそれを検出してstd::swap_ifを使用するかをコントロールします。

auto v = std::vector{ 3, 5, 2, 7, 9 };

std::sort(v.begin(), v.end()); // swap_ifを使用する
std::sort(v.begin(), v.end(),  // swap_ifを使用しない
          std::predictable([](int a, int b) { return a > b; }));

std::predictableは単なる述語ラッパーであるため、従来の述語を取るアルゴリズムは何ら変更することなくこれを受け入れ、使用できます。一方で、std::swap_ifを使用しかつ述語を取るアルゴリズムでは、これを検出することで最適な実装を選択できるようになります。
これによって、標準ライブラリにstd::swap_ifを使用するかしないかを選択するための従来のアルゴリズム名それぞれに対応する新しい名前を導入したり、既存のアルゴリズムの規定を変更したりすることなく、標準アルゴリズムの多くでパフォーマンス向上と最適な実装の選択を同時に達成できるようになります。

P2188R0 : Zap the Zap: Pointers should just be bags of bits

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

[basic.stc] p4には、「Any other use of an invalid pointer value has implementation-defined behavior.(無効なポインタ値の他の使用には実装定義の振舞がある)」とあり、その注釈には「Some implementations might define that copying an invalid pointer value causes a system-generated runtime fault.(一部の実装では、無効なポインタのコピーを行うとシステム生成の実行時エラーが発生する、と定義している場合がある)」とあります(これらの規定のことをpointer zapと呼んでいるようです)。
一方でこの事は、[basic.types] p3にある規定及びポインタ型がtrivially copyableであることと明らかに矛盾しています。

提案はいくつかの例を示すとともに、これら規定を削除して無効なポインタはtrivially copyableであり比較可能と規定するか、ポインタ型はtrivially copyableではないと規定するか、どちらかを選択すべきと主張しています。提案としては前者が提案されています。

#include <assert.h>
#include <string.h>

int main() {
  int* x = new int(42);
  int* y = nullptr;
  
  // ポインタの値(参照先ではない)をx -> yへコピーする
  memcpy(&y, &x, sizeof(x));

 // ポインタyは有効化される
  assert(x == y);
  assert(*y == 42);
}

[basic.types] p3にある例をint*に特殊化したコードで、ポインタ型はtrivially copyableであるためこのコードは有効であり、yxと同じものを指すようになります。

#include <assert.h>
#include <string.h>

int main() {
  int* x = new int(42);
  int* y = nullptr;
  unsigned char buffer[sizeof(x)];

  //ポインタの値(参照先ではない)をbufferを介してx -> yへコピーする
  memcpy(buffer, &x, sizeof(x));
  memcpy(&y, buffer, sizeof(x));

  // ポインタyは有効化される
  assert(x == y);
  assert(*y == 42);
}

先ほどのサンプルを中間bufferを介して行ったもの。[basic.types] p2にあるように、ポインタ型はtrivially copyableであるためこのコードは有効です。

#include <assert.h>
#include <string.h>
#include <stdint.h>

int main() {
  int* x = new int(42);
  int* y = nullptr;

  // ポインタ値を対応する数値表現に変換したうえでx -> yにコピーする
  uintptr_t temp = reinterpret_cast<uintptr_t>(x);
  y = reinterpret_cast<int*>(temp);

  // ポインタyは有効化される
  assert(x == y);
  assert(*y == 42);
}

[expr.reinterpret.cast] p5にあるように、ポインタ値を整数型にキャストしてから再びポインタ値に戻した場合でもポインタとしては有効であり続けます。

他にも込み入った例が全部で10パターン紹介されています。しかしここで見ただけでもわかるように、標準は少なくとも有効なポインタから無効なポインタへのその値のコピーは有効であることを示しており、([basic.stc] p4にあるような)無効なポインターのコピーが実装定義であるという規定を削除すべきという主張のようです。

onihusube.hatenablog.com

この記事のMarkdownソース