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

38 min読了の目安(約34900字TECH技術記事

文書の一覧

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

N4863 Agenda for Fall Virtual WG21/PL22.16 Meeting

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

C++23への機能追加のための投票も行われると思われるので、ようやくC++23入りするものが出てきそうです。

N4864 WG21 virtual meeting: Autumn 2020

↑のWG21本会議周知のための文章?

中身は日付とzoomのURLがあるだけです。

N4865 Response to Editorial Comments: ISO/IEC DIS 14882, Programming Language C++

C++20のDIS(Draft international standard)に対して寄せられた各国の委員会からのコメントのまとめ。

N4866 WG21 admin telecon meeting: Pre-Autumn 2020

2020年11月9日に行われるWG21本会議のスケジュール表。先程のN4863よりも少し詳しく書かれています。

N4867 Editors' Report - Programming Languages - C++

↓の更新されたWorking Draftの差分をまとめたもの。

今回は新しい機能の追加はありません。

N4868 Working Draft, Standard for Programming Language C++

C++23のWorking Draft第二弾。↑のEditors' Reportにあるように、新規追加された機能はなく、文言の調整などのみの変更です。

P0847R5 Deducing this

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

現在のC++では、メンバ関数のCV修飾と参照修飾によって暗黙のthisパラメータのconst/volatile性と値カテゴリを指定したオーバロードを行うことができます。それはクラスのオブジェクトの実際の状態に応じて処理内容を切り替えるのに必要ではありますが、ほぼ同じ処理をいくつも(大抵は2×2)書くことになります。おおよそ次の3つ方法のどれかによって実装されます。

  1. 4つのメンバ関数それぞれに処理を記述する
  2. どれか1つに委譲するようにする
  3. 4つ全てで別の実装関数に委譲する

例えばstd::optionalvalue()関数はまさにconst有無と参照修飾で4つのオーバーロードを提供しています。おおよそ1つ目の方法で実装されており、次のようになります。

template <typename T>
class optional {
  // ...
  constexpr T& value() & {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  constexpr T const& value() const& {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  constexpr T&& value() && {
    if (has_value()) {
      return move(this->m_value);
    }
    throw bad_optional_access();
  }

  constexpr T const&& value() const&& {
    if (has_value()) {
      return move(this->m_value);
    }
    throw bad_optional_access();
  }
  // ...
};

この様にほぼ同じ実装を微妙に異なって複数書かなければいけない事はバグを誘発しやすく、また保守性も低下します。

一方でこれがもしメンバ関数ではなかったとしたら、次のように簡潔な実装を選択できます。

template <typename T>
class optional {
  // ...
  template <typename Opt>
  friend decltype(auto) value(Opt&& o) {
      if (o.has_value()) {
          return forward<Opt>(o).m_value;
      }
      throw bad_optional_access();
  }
  // ...
};

この1つの関数テンプレートでさきほどの4つのメンバ関数と全く同じ動作をさせることができます。ただ、これはメンバ関数ではないのでopt.value()のように呼び出すことは出来ません。

この2種の関数の差は、thisに相当する引数を明示的に書けるかどうかという事から来ています。明示的に書くことができれば、フォワーディングリファレンスと完全転送によって4つの実装を1つに圧縮できます。

この提案は、このような問題を解決するためにメンバ関数でも非メンバ関数のようにthisに相当する引数を明示的に取れるようにしつつ、呼び出し側は従来通りに呼び出せるようにするものです。

非静的メンバ関数の第一引数にthisによって注釈をつけておく事でそれ以外のものと区別します。その場合はCV/参照修飾を行えなくなります。

struct X {
  // void foo(int i) const & 相当の宣言
  void foo(this X const& self, int i);

  // フォワーディングリファレンスによる宣言
  template <typename Self>
  void bar(this Self&& self);
};

struct D : X { };

void ex(X& x, D const& d) {
  x.foo(42);      // selfはxを束縛し、iに42が渡される
  x.bar();        // SelfはX&に推論され、X::bar<X&>が呼ばれる
  move(x).bar();  // SelfはXに推論され、X::bar<X>が呼ばれる

  d.foo(17);      // selfはdを束縛する
  d.bar();        // SelfはD const&に推論され、X::bar<D const&>が呼ばれる
}

この引数のことをexplicit object parameterと呼びます。

.によってメンバ関数呼び出しされた時、explicit object parameterには呼び出したオブジェクトが渡されます。それ以降は通常の関数引数と同じ扱いとなり、テンプレートの恩恵を受けることができます。

これによって、先ほどのstd::optional<T>::value()の実装は次のように改善されます。

template <typename T>
class optional {
  // ...
  template <typename Self>
  constexpr auto&& value(this Self&& self) {
    if (!self.has_value()) {
      throw bad_optional_access();
    }

    return forward<Self>(self).m_value;
  }
  // ...
};

また、これはラムダ式においても使用する事ができます。

std::vector captured = {1, 2, 3, 4};
[captured](this auto&& self) -> decltype(auto) {
  // forward_like<T>(U u)はTのCV修飾と値カテゴリをUにコピーした上でuを転送するもの
  return forward_like<decltype(self)>(captured);
}

[captured]<class Self>(this Self&& self) -> decltype(auto) {
  return forward_like<Self>(captured);
}

これが可能になる事によって例えばCR抜きのCRTPができるようになります。

// CRTPによる前置インクリメント演算子の導出
template <typename Derived>
struct add_postfix_increment {
  Derived operator++(int) {
    auto& self = static_cast<Derived&>(*this);

    Derived tmp(self);
    ++self;
    return tmp;
  }
};

struct some_type : add_postfix_increment<some_type> {
    some_type& operator++() { ... }
};
// explicit object parameterで同じことをする
struct add_postfix_increment {
  template <typename Self>
  auto operator++(this Self&& self, int) {
    auto tmp = self;
    ++self;
    return tmp;
  }
};

struct some_type : add_postfix_increment {
    some_type& operator++() { ... }
};

他にも、再帰ラムダ、値によるメンバ関数、SFINAE-friendlyで完全なCall wrapperなど、新しいイディオムへの道が開けるようです。

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

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

decay-copyというのは関数テンプレートに引数を渡すときに行われる変換のことです。関数テンプレートのテンプレートパラメータによる引数に値を渡すとき、左辺値は右辺値に、配列はポインタに、CV修飾を除去しつつ変換されます。

template<typename T>
void f(T t);

std::vector<int> vec{};
const std::vector<int> cvec{};

// 全て T = std::vector<int>
f(vec);                 // コピーされる
f(cvec);                // コピーされる
f(std::vector<int>{});  // ムーブされる

int arr[] = {1, 2, 3};

// T = int*
f(arr);

std::decaydecay-copyの型の変換をシミュレートするものです。このような振る舞いはauto copy = value;のように書くことで再現できますが、この提案はその意図を明確にするためにもワンライナーで書くことができるようにするものです。

auto(value)という構文でvalueをコピーしたprvalueを生成するもので、例えば次のように利用できます。

// Containerはコンセプトとする
void pop_front_alike(Container auto& x) {
  auto a = x.front(); // decay-copyする
  std::erase(x.begin(), x.end(), a);
}
void pop_front_alike(Container auto& x) {
  std::erase(x.begin(), x.end(), auto(x.front()));  // auto(x)の利用
}

std::eraseは指定されたイテレータ範囲から、第3引数で渡された値と同じものを削除する関数です。イテレータ範囲に含まれている要素を削除するときは、その操作の最中でダングリング参照とならないようにあらかじめコピーする必要があります。その際に、auto()によるdecay-copy構文を使用できます。

struct S {
  S(const S&) {
    /**/
  }
  S& operator=(S&& other) {
    /**/
  }

  // コピー構築とムーブ代入を利用したコピー代入演算子の簡易定義
  S& operator=(const S& other) {
    if (this != &other) {
      auto copy = other;        // コピーして
      *this = std::move(copy);  // ムーブ代入
    }

    return *this;
  }
}
struct S {
  S(const S&) {
    /**/
  }
  S& operator=(S&& other) {
    /**/
  }

  S& operator=(const S& other) {
    if (this != &other) {
      *this = auto(other);  // コピーしてムーブ代入
    }

    return *this;
  }
}

auto a = x.front();のようなコピーは変数宣言構文であり、ここでの主目的であるコピーは変数宣言の持つプロパティの一つでしかありません。一方、auto(x.front())は明確にコピーという操作を表しています。

関数キャストT(x)Tautoに置き換えることによって、auto(x)の構文は関数キャストの亜種であると見ることができます。クラステンプレートの引数推論を考慮すれば、この構文には次のような直交・一貫性があります。

変数定義 関数キャスト new
auto v(x) auto(x) new auto(x)
auto v{x} auto{x} new auto{x}
ClassTemplate v(x) ClassTemplate(x) new ClassTemplate(x)
ClassTemplate v{x} ClassTemplate{x} new ClassTemplate{x}

ライブラリサポートではなく言語サポートすることによって、このように変数宣言や関数スタイルキャストなどの構文との一貫性を向上することができます。

P0870R4 A proposal for a type trait to detect narrowing conversions

Tが別の型Uへ縮小変換(narrowing conversion)によって変換可能かを調べるメタ関数is_convertible_without_narrowing<T, U>を追加する提案。

以前の記事を参照

このリビジョンでの変更は、名前がis_narrowing_convertible<T, U>からis_convertible_without_narrowing<T, U>に変更されたことと、使用例のサンプルコードが追加されたことです。

P1048R1 A proposal for a type trait to detect scoped enumerations

scoped enumenum class)を識別するためのメタ関数であるstd::is_scoped_enum<T>の提案。

SFINAEによって、型がenumなのかenum classなのかで処理を分けたいときにあると有用なので追加しようというものです。
筆者の方は、古いC++ライブラリをアップデートする際にそこに含まれるenumからenum classへの移行を追跡するためのテストにおいて活用したそうです。

#include <type_traits>

enum E1{};

enum class E2{};

int main() {
  bool b1 = std::is_enum_v<E1>; // false
  bool b2 = std::is_enum_v<E2>; // true
}

これは例えば次のように実装できます。

template<class T, bool = is_enum_v<T>>
struct is_scoped_enum_helper : false_type {};

template<class T>
struct is_scoped_enum_helper<T, true> : public bool_constant<!is_convertible_v<T, underlying_type_t<T>>> {};

template<class T>
struct is_scoped_enum : public is_scoped_enum_helper<T> {};

この提案は次の本会議での投票にかけられる予定で、C++23入りがほぼ確実そうです。

P1206R2 ranges::to: A function to convert any range to a container

任意のrangeをコンテナへ変換/実体化させるためのstd::ranges::toの提案。

std::list<int> list = {...};

// イテレータペアを渡して範囲をコピーして構築
std::vector<int> vec(list.begin(), list.end());
std::list<int> list = {...};

// ranges::toアダプタによるrangeからコンテナへの変換
auto vec = list
  | std::ranges::to<std::vector<int>>(list);

このようなコンテナの変換はとても基本的な操作ですが、この提案のメインはこれではなくViewの実体化を簡易化することにあります。

標準コンテナは上記のようにイテレータペアを受け取るrangeコンストラクタを持っていますがそのイテレータペアは同じ型となる事を前提としています。ところが、<ranges>Viewは多くがそのようなcommon_rangebegin()/end()のイテレータ型が同じrange)ではありませんので、少し遠回りをしなければなりません。そこにranges::toを用いると簡潔に書くことができるようになります。

// iota_viewからvectorを構築したい
std::iota_view v{0, 1024};
std::vector<int> vec;

// イテレータを活用
std::copy(v, std::back_inserter(vec));

// あるいはcommon_viewを活用
auto iota = std::views::iota(0, 1024)
  | std::views::common;

std::vector<int> vec(iota.begin(), iota.end());
// ranges::toアダプタ!
auto vec = std::views::iota(0, 1024)
  | std::ranges::to<std::vector<int>>(list);

これによって、range adoptorのチェーンから任意のコンテナへの変換が簡単に行えるようになります。

std::ranges::toによる変換は指定するコンテナによって最も効率的な方法で実装されます。例えば、reserve可能な標準コンテナに関しては、変換元のrangeがその距離を効率的に求められればreserveしてから代入されます(ただし、この振る舞いはとりあえず標準コンテナのみとなるようです)。

また、std::ranges::toにはクラステンプレートの実引数推定によって値型を推定してもらうことのできるオーバーロードが提供されています。

std::list<int> list = {...};

// std::vector<int>を推論してくれる
auto vec = list
  | std::ranges::to<std::vector>(list);

P1401R4 Narrowing contextual conversions to bool

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

このリビジョンでの変更は、EWGでの指摘を受けてサンプルをいくつか追加した事と、提案する文言を調整した事です。

P1525R1 One-Way execute is a Poor Basis Operation

Executor提案(P0443R14)におけるstd::execution::execute及びstd::execution::executorコンセプトは、Executorライブラリにおける基本的なものとしては不適格であるという報告書。

std::execution::executeは任意のexecutorと引き数なしで呼び出し可能な処理を受け取って、そのexecutorの実行コンテキストで処理を即座に実行します。その戻り値はvoidであり、処理の結果やキャンセル、エラーを受け取ったり、処理をチェーンする方法も提供しません。つまりは処理を投げたらその処理について何かする方法が一切ありません。

int main() {
  std::execution::executor auto ex = ...; // 任意のexecutor
  std::invocable auto f = [ - 地面を見下ろす少年の足蹴にされる私]() { /*何か処理*/ };

  // 処理fをexの実行コンテキストで即座に実行し、何も返さない
  std::execution::execute(ex, f);
}

このために、execute()による実行は処理の発行時、発行と実行の間、実行中のそれぞれで発生するあらゆるエラーをハンドリングする方法を提供せず、それは実装定義となりexecutorによって異なる事になります。
そのため、ジェネリックなコードでは非同期に発生するエラーに対応するポータブルな方法が無く、柔軟なエラー処理を必要とする高レベルな非同期アルゴリズムをexecute()上で構築する事を妨げています。

さらに、execute()は処理の実行そのものが何らかの理由でキャンセルされた事を伝達するためのチャネルも持たず、execute()による非同期タスクの実行ではその状態のための動的なアロケーションが必要ですが、そのアロケーションを制御する方法もありません。

一方で、schedule()およびsender/receiverによる設計ではそれらの問題は全て解決されています。

int main() {
  std::execution::executor auto ex = ...; // 任意のexecutor
  std::invocable auto f = [ - 地面を見下ろす少年の足蹴にされる私]() -> int { /*何か処理*/ };

  // 実行のスケジューリング、senderを返す
  std::execution::sender auto s1 = std::execution::schedule(ex);
  // 処理の登録
  std::execution::sender auto s2 = std::execution::then(s1, f);
  // 処理をチェーン
  std::execution::sender auto s3 = std::execution::transform(s2, [ - 地面を見下ろす少年の足蹴にされる私](int n) { return std::to_string(n); });

  // receiverは単なるコールバック
  // 処理の結果、エラー、完了(キャンセル)を受ける3つのチャネルを持つ
  std::execution::receiver auto r = ...;  // 任意のreceiver

  // senderにreceiverを接続する
  std::execution::operation_state auto state = std::execution::connect(s3, r);

  // 処理の実行
  // senderとreceiverの実装によって、実行中のアロケーションを制御できる
  std::execution::start(state);
}

この文書では、execute()及びexecutorコンセプトよりもschedule()及びschedulerコンセプトの方が、Executorライブラリの基本的な操作とコンセプトとして相応しいと述べています。

P1759R3 Native handles and file streams

標準ファイルストリームに、OSやプラットフォームネイティブのファイルを示すものを取得する方法およびその型エイリアスを追加する提案。

この提案の対象の標準ファイルストリームとは以下のものです。

  • basic_filebuf
  • basic_ifstream
  • basic_ofstream
  • basic_fstream

例えば開いているファイルの最終更新日を取得したい場合に、ファイルストリームからそれを取得する手段はありません。標準ライブラリでそれを行うには、例えばstd::filesystem::last_write_timeを利用しますが、これは引数としてstd::filesystem::pathをとります。そのため、どうしてもファイルオープンと最終更新日取得のタイミングは開いてしまう事になり、同じpathが同じファイルを指していなかったり、そもそもファイルがない可能性があります。
また、標準ライブラリにはないファイル操作を行いたい場合は、ファイルストリームを必要になるタイミングで再構築するかプラットフォーム依存のコードを書くかの選択になります。

// 最終更新日を取得する
std::chrono::sys_seconds last_modified(int fd) {
  ::stat s{};
  int err = ::fstat(fd, &s);
  return std::chrono::seconds(s.st_mtime.tv_sec);
}

int main() {
  // ファイルストリームの再オープン
  {
    // 最終更新日をまず取得
    int fd = ::open("~/foo.txt", O_RDONLY); // CreateFile on Windows
    auto lm = last_modified(fd);
    ::close(fd); // CloseFile on Windows

    // このパスは本当に同じファイルを指している?
    std::ofstream of("~/foo.txt");
    of << std::chrono::format("%c", lm) << '\n';
  }

  // プラットフォーム固有APIを常に使用
  {
    int fd = ::open("~/foo.txt", O_RDWR);
    auto lm = last_modified(fd);
  
    auto str = std::chrono::format("%c\n", lm);
    ::write(fd, str.data(), str.size());
  
    // 閉じるのを忘れずに!
    ::close(fd);
  }
}

この提案は、このような場合のためにOSネイティブのファイルハンドル(POSIXならファイルディスクリプタ、Windowsならファイルハンドル)を取得できるようにし、標準ファイルストリームを使用しつつ、必要な時にプラットフォーム固有のファイル操作を行えるようにするものです。

int main() {
  std::ofstream of("~/foo.txt");
  // ネイティブファイルハンドルの取得
  auto lm = last_modified(of.native_handle());
  of << std::chrono::format("%c", lm) << '\n';
}

この例の他にも、ファイルロックやステータスフラグの取得、Vectored I/Onon-blocking I/Oなどのユースケースがあります。

これはstd::threadstd::mutexなどがすでに持っているnative_handle()と同じものです。同じように、ネイティブファイルハンドルの型を示すエイリアスnative_handle_typeがファイルストリームのクラスに入れ子型として追加されます(POSIXならint、WindowsならHANDLEvoid*))。

P1938R2 if consteval

constevalstd::is_constant_evaluated()にある分かりづらい問題点を解決するためのconsteval ifステートメントの提案

constevalstd::is_constant_evaluated()を組み合わせた時、あるいはstd::is_constant_evaluated()そのものの用法について、次の2つの問題があります。

constexpr関数でのconsteval関数の条件付き呼び出し

consteval関数は即時関数と呼ばれ、その呼び出しは必ずコンパイル時に完了しなければならず、コンパイル時に実行できないような呼び出しはコンパイルエラーとなります。

consteval int f(int i) { return i; }

constexpr int g(int i) {
  if (std::is_constant_evaluated()) {
      return f(i) + 1; // ng
  } else {
      return 42;
  }
}

consteval int h(int i) {
  return f(i) + 1;  // ok
}

g()h()を実行時にも呼び出し可能なように拡張したものです。一見、このコードは何の問題もなく意図通りに動作しそうに思えます。しかし、h()は問題ありませんがg()でコンパイルエラーが発生します。

f()consteval関数でありその引数は定数式でなければなりません。g()で呼ばれるf()の引数iは単にconstexpr関数の引数であり定数式ではありません。従って、このf()呼び出しはstd::is_constant_evaluated()の結果に関わらず常に失敗します。
一方、h()で呼ばれるf()h()consteval関数であるのでこの制約を受けません。

しかし、g()内のf()の呼び出しが例えばf(42)の様になっているとその呼び出しは成功し、コンパイルエラーは起きません。

この問題は即時関数が呼ばれるコンテキストの問題ですが、constexpr ifの特性を知っている人はif (std::is_constant_evaluated())のようにすればg()が実行時評価されたときにはf(i)の呼び出しはコンパイルされないので行ける!と思うかもしれません・・・

if constexpr (std::is_constant_evaluated())

std::is_constant_evaluated()はコンパイル時に呼び出されたときにtrueを返し、実行時に呼ばれるとfalseを返す関数、と単純に説明されることが多いです。するとおそらく誰もが考えるでしょう、実行時にまでifを残したくないのでif constexprを使おう!と。

#include <type_traits>

constexpr int f() {
  if constexpr (std::is_constant_evaluated()) {
    return 20;
  } else {
    return 0;
  }
}

int main() {
  // コンパイル時
  constexpr int n = f();
  // 実行時
  int m = f();
  
  std::cout << n << '\n' << m << std::endl;
  // 20
  // 20
}

std::is_constant_evaluated()の正確な効果は、コンパイル時実行されることが確実な特定のコンテキストで呼び出されたときにのみtrueを返し、それ以外の場合はfalseを返す、というものです。
特に、if constexprの条件式で呼び出されたときは常にtrueを返します。

std::is_constant_evaluated()if文と組み合わせて使うのが正しい用法です。

この関数の呼び出しはおそらく常にコンパイル時に行われます。その際、特定のコンテキストにある呼び出しのみがtrueを返しそれ以外はfalseとなります。実際の所、普通のifと共に使ったとしてもその条件分岐が実行時まで残ることは無いでしょう。

if consteval

とはいえ、この2つの振る舞いは非直感的であり、特に2つ目の方は罠になり得ます。この提案は新しくif constevalという条件分岐構文を追加することでこの解消を図る物です。

constexpr int f() {
  if consteval {
    return 20;  // コンパイル時の処理
  } else {
    return 0;   // 実行時の処理
  }
}

if constevalは次の事を除くと、殆どif (std::is_constant_evaluated())のシンタックスシュガーです。

  • <type_traits>のインクルードが必要ない
  • 構文が異なるため、誤用や誤解のしようがない
    • コンパイル時に評価されているかをチェックする適切な方法についての混乱を完全に解消できる
  • if constevalを使用してconsteval関数を呼び出すことができる
consteval int f(int i) { return i; }

constexpr int g(int i) {
    if consteval {
        return f(i) + 1; // ok!
    } else {
        return 42;
    }
}

consteval int h(int i) {
    return f(i) + 1;  // ok
}

if constevalのコンパイル時評価ブロック内では、consteval関数の呼び出しが特別扱いされて、定数式ではない引数を受けていても呼び出すことができるようになります。

このように、if constevalの導入によってC++20で導入されてしまった2つの非自明な点を解消できます。

この提案はEWGでの議論をほぼ終えていて、CWGへ転送するための投票待ちをしています。CWGでの議論次第ではありますがC++23に入る可能性は高そうです。

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

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

以前の記事を参照

このリビジョンの変更点は、提案している文言を調整したことです。

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

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

以前の記事を参照

このリビジョンの変更点は、atomicブロックでのthrow式が未定義動作であると変更されたことです(以前は実装定義)。

P2093R2 Formatted output

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

前回の記事を参照

このリビジョンでの変更は、std::printlnFILE*std::ostream&を取るオーバーロードを追加した事とstd::ostream&を取るオーバーロードについて<ostream>へ移動したことで<format><ostream>に依存しなくなった事、vprint_*関数の有用性を明確にしたこと及び提案する文言の調整です。

std::printが可変長テンプレートで任意個数の引数を受け取り出力を行うのに対して、std::vprint_unicode()/std::vprint_nonunicode()は型消去された引数参照の配列であるformat_argsオブジェクトを引数に取る非テンプレートの関数です。
std::print等他のものは内部でこれらに委譲して実装することで、余分なテンプレートのインスタンス化を減らしてバイナリサイズを削減することができます。

P2148R0 Library Evolution Design Guidelines

C++に新しいライブラリ機能を提案する際の設計のガイドラインの提案。

型やコンセプトなどの命名、関数オーバーロードの追加方法、クラスにおける特定のメンバ関数や変換、例外についてが簡単にまとめられています。自分でライブラリを書く際にも参考にできそうな内容です。

P2171R1 Rebasing the Networking TS on C++20 (revision 1)

P2171R2 Rebasing the Networking TS on C++20 (revision 2)

現在のNetworking TS(N4771)のベースとなっている規格はC++14なので、C++20ベースに更新する提案。

以前の記事(参照するほどの事は書いてない)

P2187R5 std::swap_if, std::predictable

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

前回の記事を参照

前回からの変更は、標準への影響を説明するセクションと報告された既知の問題点についてのセクションが追加されたことと、機能テストマクロが追加されたことです。

P2192R3 std::valstat - Returns Handling

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

以前の記事を参照

このリビジョンでの変更は、サンプルコードを明確にしたことです。

P2198R1 Freestanding Feature-Test Macros and Implementation-Defined Extensions

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

以前の記事を参照

このリビジョンでの変更は、どのワーキングドラフトをベースとするか明示されたこと、P1642が採択されることに依存している部分があることを明示したこと、P2013R3に関する機能テストマクロを追加した事です。

P2214R0 A Plan for C++23 Ranges

C++23に向けてのrangeライブラリの拡張プランについてまとめた文書。

コロナウィルスの流行によって対面のミーティングが行えなくなったため、委員会のメンバーにrangeライブラリ周りでC++23に向けて何をすべきかを共有するために書かれた文書のようです。

この文書では、機能に三段階の優先度を設けた上で、機能をView adjunctsViewsAlgorithmActionsに分けてそれぞれについて解説しています。

かなり膨大な数の新規機能がリストアップされていますが、最優先のものだけを列挙してみます。

  • ranges::to
  • std::formatによるviewのフォーマット
  • range adopter
    • views::cache_latest
    • views::cartesian_product
    • views::chunk
    • views::group_by
    • views::iter-zip-transform<V> (exposition-only)
    • views::iter-adjacent-transform<V> (exposition-only)
    • views::index-view<S, D> (exposition-only)
    • views::join_with
    • views::slide
    • views::stride
    • views::transform_maybe
    • views::enumerate
    • views::flat_map (renamed to… something)
    • views::zip
    • views::zip_transform
    • views::adjacent
    • views::adjacent_transform
  • range algorithm
    • ranges::iota
    • ranges::fold

使い道や必要性はわかりますが、これだけでもかなり巨大です。この優先度最高のものはC++23を目指して議論されるようです。

P2223R1 Trimming whitespaces before line splicing

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

以前の記事を参照

このリビジョンでの変更は、この変更が生文字列リテラルに影響を与えないことを明記した事と、CWG Issue 1698の修正をここではしない事にしたことです。

P2226R0 A function template to move from an object and reset it to its default constructed state

std::exchangeによるmoveしてリセットするイディオムを行う新しいCPO、taketake_assignの提案。

std::exchangeによるmoveしてリセットするイディオムとは次のようなものです。

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

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

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

struct MyPtr {
  Data *d;

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

  // BETTER, use std::exchange
  MyPtr(MyPtr&& other) : d(std::exchange(other.d, nullptr)) {}

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


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

    // BETTER, 読みやすい
    Data *old = d;
    d = newData;
    if (old) {
      dispose(old);
    }

    // GOOD, 合理的
    if (Data *old = std::exchange(d, newData)) {
      dispose(old);
    }
  }
};

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

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

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

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

このイディオムは名前がついていたわけではありませんが、既存の大規模なC++コードベース(Boost, Qt, firfox, Chromium等)で広く使われており、かつ有用性も明らかです。これらのパターンに名前を付けてイディオムとして広めることはC++コミュニティ全体にとって有益であり、その名前が明確かつ簡潔であれば、std::exchangeによる物よりもイディオムの意図が明快になります(std::exchange(old, {})というのは一見すると分かり辛いです)。そのような理由からtake/take_assignCPOとして提案に至ったようです。

ムーブコンストラクタでの利用

class C {
  Data *data;
public:
  // idiomatic, C++14
  C(C&& other) noexcept
    : data(std::exchange(other.data, {}))
  {}
};
class C {
  Data *data;
public:
  // idiomatic, C++2?
  C(C&& other) noexcept
    : data(std::take(other.data))
  {}
};

一度しか実行されない関数のフラグ管理

void Engine::maybeRunOnce() {
  if (std::exchange(m_shouldRun, false)) {
    run();
  }
}
void Engine::maybeRunOnce() {
  if (std::take(m_shouldRun, false)) {
    run();
  }
}

「有効だが未規定」状態を保証する - flat_mapを例にして

template <
  typename K, typename V,
  template <class...> class C = std::vector
>
class flat_map {
  C<K> m_keys;
  C<V> m_values;

public:

  flat_map(flat_map&& other) noexcept(/**/)
    : m_keys(std::exchange(other.m_keys, {})),
      m_values(std::exchange(other.m_values, {}))
  {}

  flat_map &operator=(flat_map&& other) noexcept(/**/) {
    m_keys = std::exchange(other.m_keys, {});
    m_values = std::exchange(other.m_values, {});
    return *this;
  }
};
template <
  typename K, typename V,
  template <class...> class C = std::vector
>
class flat_map {
  C<K> m_keys;
  C<V> m_values;

public:

  flat_map(flat_map&& other) noexcept(/**/)
    : m_keys(std::take(other.m_keys)),
      m_values(std::take(other.m_values))
  {}

  flat_map &operator=(flat_map&& other) noexcept(/**/) {
    std::take_assign(m_keys, other.m_keys);
    std::take_assign(m_values, other.m_values);
    return *this;
  }
};

P2227R0 Update normative reference to POSIX

現在のC++標準規格が参照しているPOSIX規格への参照を更新する提案。

現在のC++規格は「ISO/IEC 9945:2003 (POSIX.1-2001 または、The Single UNIX Specification, version 3)」を主に標準ライブラリの定義中で現れるPOSIX関数のために参照しています。
ただ、これは古い規格であり、現在のC++標準ではそこに載っていない関数を参照していることがあるようです。

そのため、POSIX規格の参照を最新の「ISO/IEC/IEEE 9945:2009 (POSIX.1-2008 aka SUSv4)」に更新しようとするものです。

P2228R0 Slide Deck for P1949 EWG Presentation 20200924

P1948R6 C++ Identifier Syntax using Unicode Standard Annex 31のプレゼンの際に使われたスライド。

EWGで行われたP1948の内容を解説するプレゼンの際に使用された資料のようです。

P2231R0 Add further constexpr support for optional/variant

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

C++20では共用体のアクティブメンバの切り替えplacement new(std::construct_atが定数式で可能となりました。std::optionalstd::variantはこれらを実装に利用しているため、いくつかの関数をさらにconstexpr対応させることができるようになっています。
この提案はそれに従ってconstexprを付加するだけで対応可能なものにconstexprを追加するものです。

どちらに対しても、コピー/ムーブコンストラクタや代入演算子。emplace(), swap()constexpr対応が提案されています。

P2233R0 2020 Fall Library Evolution Polls

LEWGが2020年秋に投票を行うことが予定されている提案についてのリスト。

Executor提案の調整や、いくつかの提案をLWGに転送することを決める投票がメインです。C++23に何かを導入するものではありません。

P2234R0 Consider a UB and IF-NDR Audit

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

UBとIF-NDRはC++の多くの所に潜んでおり、特に文書化されておらず、出会ってしまうとプログラムのデバッグをより困難にしてしまいます。このことは、C++に深く精通していないプログラマがC++プログラムについて推論することを妨げています。

この提案の目的は、多くのUBとIF-NDRの全てについて専門家の小さなグループによって監査し、より良い振る舞いを規定できるものを特定し、その変更の方法や影響範囲を見積もることを継続的に行っていくことです。

この提案ではUBを改善可能なものとして、nullptrや使用できないポインタに対するサイズ0のmemcpyの動作や副作用のない無限ループを挙げています。

P2235R0 Disentangling schedulers and executors

現在のExecutor提案(P0443R14)について、schedulerexecutorの設計を簡素化し、schedulerexecutorの絡み合いをほどく提案。

P0443のexecutorコンセプトによって定義されるexecutorは引数も戻り値もないCallableオブジェクトを受け取って即座に実行する能力しかありません。schedulerコンセプトによって定義されるschedulerはそれに加えて実行の遅延と、sender/receiverと組み合わせた結果の受け取りやエラーハンドリング、そして処理のチェーンをサポートします。
schedulersender/receiverと共に、C++ Executorライブラリ上での多彩なジェネリックアルゴリズムの実装をサポートします。

schedulerexecutorの持つ能力を包含していますが、executorはそうではありません。schedulerからexecutorへの変換は縮小変換の様なもので、ソースコード上の見えないところで変換が起きた場合静かなバグの源となり得ます。にもかかわらず、現在のP0443は相互の暗黙変換をサポートしています。

一方、C++ Executorライブラリが非同期並行処理のための基盤となるものであることを考えると、どこかから渡されてきたschedulerexecutorとして扱うことも避けるべきです。これは不可逆変換ではありませんが、広い契約を持つ関数が中でより狭い契約を持つ関数に丸投げしているようなもので、広い契約を期待する呼び出し元の期待は満たされません。
schedulerexecutorとして扱ってexecuteCPOに投入してしまうと、まず処理のスケジューリングの機会がありません。そして、スケジューリングエラー(この場合、executeCPOが処理を受け取り実行環境に投入してから実際に実行されるまでの間のエラー)をハンドルする機会もありません。ユーザーがschedulerをカスタマイズしてスケジューリングエラーをハンドルする仕組みを備えていたとしても、schedulerexecutorとして扱ってしまえばそれが活かされる機会はありません。

このように、この2つのものは混ざり合いません。一方を他方として扱うとすれば、それは目に見える形で明確に細心の注意を払って行われるべきです。

この提案では次の変更によってこの絡み合いを解消し、問題の解決を図ります。

  • scheduleCPOはschedulerのみを受け付ける
  • executeCPOはexecutorのみを受け付ける
  • connectCPOなどのsenderreceiverに対する操作はsenderreceiverのみを受け付ける
  • executorからexecutorへの一方向の明示的な変換を追加する
    • 双方向の暗黙変換を削除する
  • schedulesenderの単純なfire-and-forget実行(executeの行うような実行)を可能にする個別のアルゴリズムは、execute以外の名前を使用するようにする

これは既に次のLEWGの投票にかけられることが決まっていて、そこでコンセンサスを得られればすぐにP0443に適用されることになります。

P2236R0 C++ Standard Library Issues to be moved in Virtual Plenary, Nov. 2020

標準ライブラリのIsuueのうち2020年11月のオンライン投票にかけられるもののリスト。

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

P2237R0 Metaprogramming

C++23以降に予定されている、あるいは現在提案中のメタプログラミングサポートに関連する機能に関するサーベイ論文。

リフレクション、メタクラス、expansion statementstemplate引数、コンパイル時I/Oなどコンパイル時にあれこれするための機能についてどう使うかや何に役立つかなど多岐にわたって述べられています。
なお、ここに上がっているものはまだ提案中のものばかりです。