[C++]inline名前空間の使途

inline名前空間C++11から追加された機能で、その中にあるものは透過的に(名前空間がないかのように)アクセスすることができます。一見使いどころがなく見られがちですが、うまく使えばとても便利に活用することができます。

1. using namespaceの範囲を限定する

これは標準ライブラリではユーザー定義リテラルの定義と利用でよく利用されます。

例えば、std::string_viewを簡易利用するためのsvリテラルは次のように宣言されています。

namespace std {

  // string_view本体
  template <class CharT, class Traits = char_traits<CharT>>
  class basic_string_view;

  inline namespace literals {
    inline namespace string_view_literals {

      // svリテラル
      constexpr std::string_view operator""sv(const char* str,   std::size_t len) noexcept;
    }
  }
}

これによって、using namespace std;という広すぎる範囲をusingすることなく、次のいずれかによってこのsvリテラルを使用することができます。

#include <string_view>

int main() {
  {
    // 1、標準ライブラリの定義する全リテラルだけが使用可能になる
    using namespace std::literals;

    auto str = "literals"sv;
  }
  {
    // 2、string_viewのsvリテラルだけが使用可能になる
    using namespace std::string_view_literals;

    auto str = "string_view_literals"sv;
  }
  {
    // 2、std名前空間の神羅万象が利用可能になる
    using namespace std;

    auto str = "std"sv;
  }
}

std::literals名前空間は標準ライブラリの定義するすべてのユーザー定義リテラルs, h, m, s, iなどなど)が定義されているinline名前空間であり、それをusing namespaceするとそれらのリテラルの全てがそのスコープで見えるようになります。
std::string_view_literalssvリテラルだけが定義されているinline名前空間であり、using namespaceしてもsvリテラル以外のものは見えません。
そして、これらのユーザー定義リテラルstd名前空間直下からも参照できます。

このようにライブラリの提供するものをinline名前空間である程度グループ化しておくことで、使う側は適宜必要な範囲だけをusing namespaceすることができるようになります。

2. APIのバージョニング

これは多分最もポピュラーなinline名前空間の使い方でしょうか。

例えば、既に利用している関数があり、その関数のAPI(インターフェース)は変えないけれども内部実装を変更したい時を考えます。

namespace mylib {
  int f(int a) {
    return a;
  }
}

この処理を、2倍してから返すようにしたいとします。

namespace mylib {
  int f(int a) {
    return 2 * a; // 2倍して返すようにしたい
  }
}

しかし、この関数は既に至るところで使われており、変更するならしっかりテストしてからにしたいし、使われているとこを書き換えて回るのは嫌です。そんな時、変更前のバージョンをinline名前空間で、変更後のバージョンを名前空間で囲っておきます。

namespace mylib::inline v1 {

  // 現在の処理
  int f(int a) {
    return a;
  }
}

namespace mylib::v2 {

  // 変更後の処理
  int f(int a) {
    return 2 * a; // 2倍して返すようにしたい
  }
}

そして、新しい関数mylib::v2::f()の実装とテストが完了してすべてのf()を新バージョンへ切り替えることができるようになったら、それぞれの名前空間inline指定を逆にします。

// 古いバージョンを非inlineに
namespace mylib::v1 {

  // 現在の処理
  int f(int a) {
    return a;
  }
}

// 新しいバージョンをinlineに
namespace mylib::inline v2 {

  // 変更後の処理
  int f(int a) {
    return 2 * a; // 2倍して返すようにしたい
  }
}

inline名前空間は透過的です。そのため、これだけで、f()を使用しているところを一切書き換えることなくその処理内容をアップデートできます。もし古いバージョンを使いたい場合はmylib::v1::f()のように名前空間を明示的に指定してやればよく、古いバージョンを使用していることも分かりやすくなります。

3. ABIのバージョニング

inline名前空間APIでは省略可能ですが、ABI(マングル名)では通常の名前空間と同じ扱いをされ、常にマングル名に表示されていますし、参照するときも省略できません。inline名前空間の効果は純粋にC++の意味論上だけのものです。

この性質によって、APIは変わらないけれどABIを破壊するような変更がなされたときにその影響を軽減することができます。

/// header.h

namespace mylib {
  class S {
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

namespace mylib {

  int S::get_m() const {
    return this->m;
  }
}
/// main.cpp

#include <iostream>

#include "header.h"

int main() {
  mylib::S s{};
  
  std::cout << s.get_m();
}

例えばこんなクラスとそのメンバ関数がありヘッダと実装が分かれているとき、これを利用するmain.cppが同じコンパイラを使ってsource.cppコンパイルしている間は何も問題はありません。

しかし、source.cppを静的ライブラリや動的ライブラリの形であらかじめコンパイルしてから利用しているときに、このmylib::SにABIを破壊する変更がなされてしまったとします。

/// header.h

namespace mylib {
  class S {
    float f = 1.0;  // メンバを追加した
    int m = 10;
  public:

    int get_m() const;
  };
}

このような変更はAPIに何も影響を及ぼしませんが、クラスのレイアウトが変更されているのでABIから見ると重大な変更です。ABIを破壊しています。

このとき、source.cppコンパイルした静的or動的ライブラリを再コンパイルせずに使い続けて(リンクして)いたとしてもコンパイラは何も言わないでしょう。この場合の変更によってはマングル名は変化しておらず、コンパイルもリンクもつつがなく完了します。

/// main.cpp

#include <iostream>

// これは最新のものを参照しているとする
#include "header.h"
// source.cppは10年前にコンパイルしたものをリンクして使い続けているとする

int main() {
  mylib::S s{};
  
  std::cout << s.get_m(); // 未定義動作!
}

古いS::get_m()関数の定義はsource.cppコンパイルした外部ライブラリにあり、新しいmylib::Sのレイアウトを知りません。したがって、レイアウト変更後のクラスのどこかの領域をint型のメンバS::mとして読みだした値を返してくれるでしょう(たぶん実行時エラーも起きないのではないかと思われます)。これは紛う事なき未定義動作です・・・

これは稀によくあるビルド済みバイナリをリンクして利用する時の問題で、C++でヘッダオンリーライブラリが好まれる傾向にある事の一つの理由でもあります。

こんな時、inline名前空間を利用することでこの問題の軽減を図れます。

リンクエラーにする

解決策の一つ目は、変更後のコードをそれを表すinline名前空間で囲ってしまう事です。

/// header.h

// inline名前空間を追加する
namespace mylib::inline v2 {
  class S {
    float f = 1.0;  // メンバを追加した
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

// inline名前空間を追加する
namespace mylib::inline v2 {

  int S::get_m() const {
    return this->m;
  }
}

inline名前空間APIC++コード上)からは透過的ですがABI(マングル名)には表示されます。従って、この変更後のmylib::Sおよびそのメンバ関数を利用するコードに変更は必要ありませんが、そのマングル名は変更前のものと異なっています。
結果、再コンパイルしないで用いている変更前ソースによるビルド済みバイナリからはそのようなシンボルが見つからずリンクエラーによってコンパイル時に気づくことができます。

/// main.cpp

#include <iostream>

// これは最新のものを参照しているとする
#include "header.h"
// source.cppは10年前にコンパイルしたものをリンクして使い続けているとする

int main() {
  mylib::S s{};
  
  std::cout << s.get_m(); // リンクエラー、シンボルが見つからない
}

ABI互換性を確保する

もう一つの方法は初めからinline名前空間を利用していた場合にのみ利用可能となります。例えば先程のサンプルは初めから次のようにinline名前空間に囲まれていたとします。

/// header.h

// inline名前空間に包まれている
namespace mylib::inline v1 {

  class S {
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

// inline名前空間に包まれている
namespace mylib::inline v1 {

  int S::get_m() const {
    return this->m;
  }
}

このコードに対して先程の変更がなされたとしましょう。その際、古いバージョンのinline名前空間を非inlineにし、新しいバージョンのinline名前空間名を変更しておきます。

/// header.h

// 古いバージョン、inline名前空間ではなくする
namespace mylib::v1 {

  class S {
    int m = 10;
  public:

    int get_m() const;
  };
}

// 最新版
namespace mylib::inline v2 {

  class S {
    float f = 1.0;  // メンバを追加した
    int m = 10;
  public:

    int get_m() const;
  };
}
/// source.cpp

#include "header.h"

// 古いバージョン、inline名前空間ではなくする
namespace mylib::v1 {

  int S::get_m() const {
    return this->m;
  }
}

// 最新版
namespace mylib::inline v2 {

  int S::get_m() const {
    return this->m;
  }
}

こうしておいた状態で、さっきまでのように使用しようとすれば正しくリンクエラーになります。

今回のようにした場合は、逆に変更前のバージョンのものを参照し続けているようなプログラムに最新のsource.cppをビルドした(変更が反映された)バイナリをリンクした時でも、リンクエラーも未定義動作も起こさずに使用することができます。

/// main.cpp
// 10年前のコードを参照ヘッダも含めて変更せずに使い続けているとする

#include <iostream>

// 変更前のものを参照している
#include "header.h"
// source.cppはさっきコンパイルした最新のものをリンクしているとする

int main() {
  // mylib::v1::Sを参照している
  mylib::S s{};

  // mylib::v1::S::get_m()を参照している
  std::cout << s.get_m(); // 10
}

inline名前空間はマングル名レベルでは名前空間と区別なく扱われます。すなわち、ABIからは名前空間名がinlineであるかどうかは分かりません。したがって、このように変更を追記し古いバージョンを維持しておけば、古いバージョンを利用しているプログラムに対しても古いバージョンを提供し続けることができます。これによって、ABI互換性を維持し、ABIを保護することができます。

GCCやclangの最近の標準ライブラリの実装では、ABI保護のためにinline名前空間が多用されています。

4. 名前の衝突を回避する

これは特にCPO(Customization Point Object)の定義で利用されています。

CPOはその呼び出しに当たって自身と同名の非メンバ関数をADLで探索するようになっていることがよくあります。これはあるクラスに対してHidden friendsと呼ばれるfriend関数を探し出すものです。

一方で、標準ライブラリにあるCPOは一部のものを除いてstd名前空間のすぐ下に定義されることになります。

すると、標準ライブラリにあるクラスに対するHidden friends関数とCPOとで名前が衝突してしまいます。

namespace mystd {
  
  namespace cpo_impl {
    
    // swap CPOの実装クラス
    struct swap_cpo {
      
      template<typename T, typename U>
      void operator()(T&, U&) const;
    };
  }

  // swap CPO #1
  inline constexpr cpo_impl::swap_cpo swap{};
  
  struct S {
    
    // Hidden friendsなswap関数 #2
    friend void swap(S& lhs, S& rhs);
  };
}

この例では#1と#2の異なる宣言が同じ名前空間にあるためにコンパイルエラーになっています。

このように、CPOと同じ名前空間にあるものがそのCPOにアダプトしようとすると名前衝突してしまうわけです。

この場合にCPOの定義をinline名前空間で囲ってやることでこの問題を解決できます。しかも、呼び出す際に名前空間名が増えることもありません。

namespace mystd {
  
  namespace cpo_impl {
    
    // swap CPOの実装クラス
    struct swap_cpo {
      
      template<typename T, typename U>
      void operator()(T&, U&) const;
    };
  }

  // CPO定義をinline名前空間で囲う
  inline namespace cpo {
    // swap CPO #1
    inline constexpr cpo_impl::swap_cpo swap{};
  }
  
  struct S {
    
    // Hidden friendsなswap関数 #2
    friend void swap(S& lhs, S& rhs);
  };
}

こうしてもmystd::swapという名前でswapCPOを参照できますし、#1と#2のswapは別の名前空間にいるために名前は衝突していません。
このため、標準ライブラリにあるCPOはほとんどのものがinline名前空間に包まれています。

ここでは説明のために変な名前空間と適当なクラスを用意しましたが、mystdstdmystd::Sstd::vectorとかに読み替えるとつかみやすいかもしれません。

参考文献

この記事のMarkdownソース

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

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

提案文書で採択されたものはありません。全部で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++() { ... }
};
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();
  std::erase(x.begin(), x.end(), a);
}
void pop_front_alike(Container auto& x) {
  std::erase(x.begin(), x.end(), auto(x.front()));
}

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>を追加する提案。

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

onihusube.hatenablog.com

このリビジョンでの変更は、名前が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 = {...};

auto vec = list
  | std::ranges::to<std::vector<int>>(list);

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

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

現在 この提案
std::iota_view v{0, 1024};
std::vector<int> vec;
std::copy(v, std::back_inserter(vec));

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

std::vector<int> vec(iota.begin(), iota.end());
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型への暗黙の縮小変換を定数式で許可する提案。

onihusube.hatenablog.com

このリビジョンでの変更は、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ならintWindowsなら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++字句規則の規定を明確にする提案。

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

onihusube.hatenablog.com

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

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

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

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

onihusube.hatenablog.com

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

P2093R2 Formatted output

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

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

onihusube.hatenablog.com

このリビジョンでの変更は、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ベースに更新する提案。

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

P2187R5 std::swap_if, std::predictable

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

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

onihusube.hatenablog.com

onihusube.hatenablog.com

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

P2192R3 std::valstat - Returns Handling

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

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

onihusube.hatenablog.com

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

P2198R1 Freestanding Feature-Test Macros and Implementation-Defined Extensions

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

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、どのワーキングドラフトをベースとするか明示されたこと、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

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

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、この変更が生文字列リテラルに影響を与えないことを明記した事と、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, {}))
  {}
};
void Engine::maybeRunOnce() {
  if (std::exchange(m_shouldRun, false)) {
    run();
  }
}
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;
  }
};
class C {
  Data *data;
public:
  // idiomatic, C++2?
  C(C&& other) noexcept
    : data(std::take(other.data))
  {}
};
void Engine::maybeRunOnce() {
  if (std::take(m_shouldRun, false)) {
    run();
  }
}
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-NDRill-formed no diagnostic required)について、委員会の小さなチームによって監査されるプロセスの提案。

UBとIF-NDRC++の多くの所に潜んでおり、特に文書化されておらず、出会ってしまうとプログラムのデバッグをより困難にしてしまいます。このことは、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などコンパイル時にあれこれするための機能についてどう使うかや何に役立つかなど多岐にわたって述べられています。
なお、ここに上がっているものはまだ提案中のものばかりです。

11月半ばごろ?

この記事のMarkdownソース

[C++]std::exchangeによるmoveしてリセットするイディオムの御紹介

2020年10月公開分の提案文書を眺めていたら良さげなものを見つけたので宣伝です。そのため、この記事の内容は次の提案文書を元にしています。

もくじ

moveしてリセット!

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, {});

std::exchangeC++14で追加された関数で、第二引数の値を第一引数に転送し、第二引数の元の値を戻り値として返すものです。

template <class T, class U=T>
constexpr T exchange(T& obj, U&& new_val);

ストリーム的に見ると、第二引数から戻り値まで右から左へその値が玉突き的に流れていくように見えます。

第二引数の型も個別のテンプレートパラメータになっており、デフォルトでは第一引数の型が推論されます。そのため、第二引数に{}を指定したときはT{}と指定したのと同等になるわけです。
そして、そのようにデフォルト構築(正確には値初期化(Value initialization))された値が第一引数にmoveされ、第一引数の元の値が戻り値として返されます。その際、可能であればすべてmoveされます。

このイディオムには次の2つの利点があります。

1. 操作の複合化

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

struct MyPtr {
  Data *d;

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

  // BETTER, 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, 読みづらい
    std::swap(d, newData);
    if (newData) {
      dispose(newData);
    }

    // BETTER, 意図は分かりやすい
    Data *old = d;
    d = newData;
    if (old) {
      dispose(old);
    }

    // GOOD, 1行かつストリーム的
    if (Data *old = std::exchange(d, std::exchange(newData, {}))) {
      dispose(old);
    }
  }
};

このように、値が右から左へストリーム的に流れていくように見ることができ、そして移動とリセットの操作が合成されているために、意図が明確でミスも起こしにくいコードを書くことができます。

2. move後状態の確定

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

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

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

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

とはいえ完全にmoveと同等ではなくいくつか違いがあります。

move(old_obj) exchange(old_onj, {})
例外を投げる? 通常ムーブコンストラクタはnoexcept デフォルトコンストラクタ次第
処理後のold_objの状態は? 有効だが未規定 デフォルト構築状態
呼び出しのコストは? ムーブコンストラクタ次第 ムーブ/デフォルトコンストラクタ次第
obj = xxxx(obj);は何をする? 実装依存 例外を投げないと仮定すると、何もしない
old_objをその後使用しない場合に最適な書き方? Yes No

いくつかのサンプル

class C {
  Data *data;
public:
  // ムーブコンストラクタの改善
  C(C&& other) noexcept
    : data(std::exchange(other.data, {}))
  {}
};
template <typename K, typename V, template <class...> class C =  std::vector>
class flat_map {
  C<K> m_keys;
  C<V> m_values;

public:

  // ムーブ後の有効だが未規定な状態を達するにはデフォルトmoveに任せられない
  // Cのムーブ操作によってはflat_mapの不変条件が破られ有効ではなくなってしまう可能性がある
  // 言い換えると、有効だが未規定な状態は合成されない
  // そのため、明示的なリセットが必要
  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;
  }
};
void Engine::processAll() {
  // m_dataを消費しつつループする
  for (auto& value : std::exchange(m_data, {})) {
      // この処理はm_dataを変更する可能性がある
      // イテレータ破壊を回避する
      processOne(value);
  }
}
void ConsumerThread::process() {
  // pendingDataをmutexの保護の元で取得し
  // 現在のスレッドがそれを安全に使用できるようにする
  Data pendingData = [&]() {
      std::scoped_lock lock(m_mutex);
      return std::exchange(m_data, {});
  }();

  for (auto& value : pendingData)
      process(value);
}
// 一度だけ実行される関数
void Engine::maybeRunOnce() {
  if (std::exchange(m_shouldRun, false)) {
    run();
  }
}
// Dataのオブジェクトを貯めておくクラス
struct S {
    // C++ Core Guideline F.15に基づいたオーバーロードの提供
    void set_data(const Data& d);
    void set_data(Data&& d);
} s;

Data d = ~~~;

// dをため込むが、明示的にデフォルト状態にする
s.set_data(std::exchange(d, {}));

assert(d == Data());

参考文献

この記事のMarkdownソース

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

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

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

P0288R7 : any_invocable

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

先月の記事を参照

onihusube.hatenablog.com

前回からの変更は、規格書に追加するための文言を調整しただけの様です。

P0443R14 : A Unified Executors Proposal for C++

処理をいつどこで実行するかを制御するための汎用的な基盤となるExecutorライブラリの提案。

以前の記事を参照。

onihusube.hatenablog.com

R13からの変更は、いくつかEditorialなバグを修正した事です。

P0881R7 : A Proposal to add stacktrace library

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

先月の記事を参照。

onihusube.hatenablog.com

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++にはifswitchという二つの条件分岐用の構文があります。ifは複雑なboolの式を扱うことができますが、switchは整数値しか扱えず、ifは分岐条件を一つづつ書いて行くのに対してswitchは条件を羅列した形で(宣言的に)書くことができるなど、は2つの構文の間にはギャップがあります。
パターンマッチングはそれらの良いとこどりをしたような構文によって、特に代数的データ型の検査と分岐を書きやすく、読みやすくするものです。

この提案ではinspect式によって構造化束縛宣言を拡張する形でswitch的な書き方によってパターンマッチングを導入しています。これを提案中ではstructured inspection(構造化検証?)と呼んでいます。

std::string

`if`文 `inspect`式
if (s == "foo") {
  std::cout << "got foo";
} else if (s == "bar") {
  std::cout << "got bar";
} else {
  std::cout << "don't care";
}
inspect (s) {
  "foo" => { std::cout << "got foo"; }
  "bar" => { std::cout << "got bar"; }
  __ => { std::cout << "don't care"; }
};

std::tuple

`if`文 `inspect`式
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 (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`文 `inspect`式
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 (v) {
  <int> i => {
    strm << "got int: " << i;
  }
  <float> f => {
    strm << "got float: " << f;
  }
};

polymorphicな型

継承ベースの動的ポリモルフィズム `inspect`式
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;
  }
};
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::foodetail::C::barという修飾名を持ちます。

しかしDR2061による修正によれば、2つ目の名前空間detailの宣言は一つ目の名前空間A::detailと同一視されます。その結果、#3, #4はそれぞれA::detail::fooA::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つのことをサジェストしています。

  1. DR2061を元に戻す
  2. 標準ライブラリ内でユーザーが特殊化することを定義されているものについて、修飾名で特殊化すること、という規定を追加する。

つまり、先程の特殊化は次のように書くことを規定するということです。

template<>
constexpr bool std::ranges::disable_sized_range<MyType> = true;

こうすれば、名前空間の宣言を伴わないため上記のような問題に悩まされることはありません。

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)の構文において、不可視のゼロ幅文字や制御文字の使用を禁止する提案。

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

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンの変更点は、この提案によって変更されるものと変更されないものを明記したのと、いくつかのサンプルを追加した事です。

P2013R3 : Freestanding Language: Optional ::operator new

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

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

このリビジョンの変更点は、8月のEWGの電話会議での投票結果を記載した事と、いくつかの文言の修正、機能テストマクロについてはP2198の将来のリビジョンで検討することになったことです。

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

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

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

このリビジョンの変更点は、電話会議の結果を受けて提案している文言を調整しただけの様です。

P2066R3 : Suggested draft TS for C++ Extensions for Transaction Memory Light

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

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

このリビジョンでの変更は、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というメンバ型を持っていれば有効化されます。

P2138R3 : Rules of Design <=> Specification engagement

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

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、対象とする読者への説明、提案文書作成時に文言の作成に支援を求める方法、Tentatively Readyステータスをスキップできる条件を明確にしたことと、Tentatively Ready for Plenaryというステータスは次のミーティングの本会議の採決にかける事を意味していることを明確にしたことです。

P2145R1 : Evolving C++ Remotely

コロナウィルスの流行に伴ってC++標準化委員会の会議がキャンセルされている中で、リモートに移行しつつどのように標準化作業を進めていくのかをまとめた文章。

onihusube.hatenablog.com

このリビジョンでの変更は、以前のリビジョン時からの状況変化を反映した様です。

P2164R2 : views::enumerate

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

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、既存のviewと同様にvalue_typeを非参照にしたこと、説明と文言間の矛盾を正したこと、サンプルコードに関連ヘッダ(<ranges>)を追記した事です。

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

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

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、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

宣言以降使用されず追加情報を提供するための名前をつける必要もない変数を表すために_を言語サポート付きで使用できる様にする提案。

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更はP2011での_の利用(プレイスホルダーとしての利用)との構文の衝突について追記されたことです。

int _ = 0;      // この提案の用法、プレイスホルダーではない
f() |> g(_, _); // P2011の用法、プレイスホルダー

この提案での_の特別扱いは変数宣言にのみ作用するため、上記P2011の用法による場合は常にプレイスホルダーとして扱えば良いと述べられています。

P2192R2 : std::valstat -Transparent Returns Handling

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

以前の記事を参照

onihusube.hatenablog.com

onihusube.hatenablog.com

このリビジョンでの変更は、動機やメタステートの説明、付録のサンプルコードなどを改善・書き足しした事と、タイトルが"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_pointClockテンプレートパラメータに対する要件を弱める提案。

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、提案する既存規格の文言の変更についてフィードバックを受けて修正した事です。

P2215R1 : "Undefined behavior" and the concurrency memory model

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

以前の記事を参照

onihusube.hatenablog.com

このリビジョンでの変更は、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_tostd::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::optionalvalue_or()メンバ関数をベースとした、利便性向上のための新しいメンバ関数を追加する提案。

value_or()メンバ関数std::optionalが有効値を保持していればその値を返し、無効値を保持している場合は指定された値を代わりに(std::optionalの要素型に変換して)返すものです。
value_or()関数は有効値からのフォールバックという性質上、多くの場合その型のデフォルト値が使用されます。その場合リテラルを持つ組み込み型ならばあまり困る事も無いのですが、コンストラクタを持つユーザー定義型では意図通りにならない事があります。

例えばstd::optional<std::string>value_orstd::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を与えておくことで、UTから推論することができるようになります。ただし、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を用いてP1393require/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/preferCPOそのものだけではなく、プロパティを表現する型やプロパティサポートを問い合わせるためのquery、プロパティを受け入れるための実装方法などもtag_invokeを使うように変更されています。結果として、requireは消えているようです。

元のrequire/preferはユーザーから見れば効率的で柔軟なプロパティ指定の方法でしたが、それを受け入れるようにする実装は少し大変でした。しかし、tag_invokeを利用することで実装者の負担がかなり軽減されています。

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::executorCPOにアダプトする例を比較してみます。

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をいくら増やしてもユーザーコードではそれを気にせずに同じ名前を使用することができるようになります。

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_scheduleExecutorex上で、入力の処理prologueshapeによって表現される範囲内でバルク実行をし、かつその実行は遅延可能であるものです。その戻り値はそれらバルク実行のそれぞれの処理の開始を表す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が担うようになり、バルク処理のそれぞれに後続の処理をチェーンさせることと、バルク処理の全体に後続の処理をチェーンさせることをより明確に書くことができるようになっています。

多分2週間後くらい

この記事のMarkdownソース

[C++]モジュールにおける所有権モデル

モジュールにおける所有権とは、名前付きモジュールに属しているエンティティの名前に関する所有権の事で、次のようなコードがどう振舞うかを決定するものです。

/// moduleA.ixx
export module moduleA;

export int munge(int a, int b) {
    return a + b;
}
/// moduleB.ixx
export module moduleB;

export int munge(int a, int b) {
    return a - b;
}
/// libA.h

int libA_munge(int a, int b);
/// libA.cpp

#include "libA.h"
import moduleA;

int libA_munge(int a, int b) {
    return munge(a, b);
}
/// main.cpp

#include "libA.h"
import moduleB;

// ここでは、moduleAはインポートされていない

int main() {
  // それぞれ、どちらが呼ばれる?
  int a = munge(1, 2);      // moduleBのものが呼ばれる?
  int b = libA_munge(1, 2); // moduleAのものが呼ばれる?
}

こちらのサンプルコードを改変しています)

C++の意味論としてはこれは当然明白なことで、コメントにある通りに呼ばれるはずです。しかし、このプログラムはコンパイルされリンクされた結果として一つの実行ファイルになります。C++20の名前付きモジュールはそれそのものが1つの翻訳単位をなすので、そこには2つのモジュール(moduleAmoduleB)をコンパイルした2つのオブジェクトファイルが含まれます。
その時、翻訳単位main.cpplibA.cppはそれぞれのコードの意味論から期待されるモジュールからの関数を呼び出すでしょうか?

これはつまりODR違反が起きている状態です。C++17まではこれは未定義動作であり、リンカがオブジェクトファイルをリンクする順番に依存して結果が変わります。そして、モジュールファイルでもこれは未定義動作とされています。

モジュールの所有権とはこの未定義動作をどう実装するか?という問題の解です。そこには、2つの選択肢があります。

なお、同じ場所で2つの異なるモジュールからの同じ宣言が見えている場合は確実にコンパイルエラーとなります、今回のケースはリンカからしかそれが見えていません。

import moduleA;
import moduleB;

int main() {
  int a = munge(1, 2);  // コンパイルエラー!
}

弱い所有権(weak ownership)モデル

弱い所有権(weak ownership)はこの問題を解決しないモデルです。すなわち、C++17以前と同じくこれは未定義動作であり、リンカがリンクする順番に依存します。

/// main.cpp

#include "libA.h"
import moduleB;

// ここでは、moduleAはインポートされていない

int main() {
  // 弱い所有権モデルの下では未定義動作!!
  int a = munge(1, 2);
  int b = libA_munge(1, 2);
}

強い所有権(strong ownership)モデル

強い所有権(strong ownership)はこれを解決するモデルです。すなわち、異なるモジュールからエクスポートされているものは、その宣言が完全に同一であったとしても、リンク時にもモジュール名によって別のものとして扱われます。

/// main.cpp

#include "libA.h"
import moduleB;

// ここでは、moduleAはインポートされていない

int main() {
  // 強い所有権モデルの下では期待通りに呼ばれる
  int a = munge(1, 2);      // -1(moduleBのものが呼ばれる)
  int b = libA_munge(1, 2); // 3(moduleAのものが呼ばれる)
}

モジュールリンケージ

C++20モジュールにおいてはリンケージ区分にもう一つモジュールリンケージが追加されました。モジュールリンケージは名前付きモジュール内で外部リンケージを持ち得る名前のうち、エクスポートされていないものが持つリンケージです。モジュールリンケージをもつ名前は同じモジュール内からしか参照できません。つまりは、内部リンケージを同じモジュール内部の範囲まで拡張したものです。

export module moduleA;

// モジュールリンケージ
int func1(int a, int b) {
  return a + b;
}

// 外部リンケージ
export int func2(int a, int b) {
  return a + b;
}

// 内部リンケージ
static int func3(int a, int b) {
  return a + b;
}

リンケージの観点からは、2つの所有権モデルの差異は外部リンケージを持つ(エクスポートされている)名前をモジュールが所有するかどうか?という問題とみることができます。

弱い所有権モデルでは、内部リンケージとモジュールリンケージを持つ名前だけをモジュールが所有し、エクスポートされている名前をモジュールは所有しません。

強い所有権モデルでは、エクスポートされている名前を含めたモジュールに属するすべての名前がモジュールによって所有されます。

所有権と名前マングリング

この2つの所有権モデルのどちらを選択するか?という問題は、実装から見ると名前マングリングの問題となるでしょう(他の実装もあり得ますが)。

1つのモジュールは複数の翻訳単位から構成されうるので、リンカは少なくともモジュールリンケージを識別する必要があります。そのため、モジュールに所有されている名前のマングル方法にはいくつかの選択肢が生まれます。そして、現在の実装では次の2つが存在しています。

  1. モジュールリンケージを持つ名前のマングル方法を変更して、それだけをリンカが特別扱いする
    • エクスポートされている名前のマングル方法は従来通り(現在の外部リンケージを持つ名前と同じ)
  2. エクスポートされている名前のマングル方法を変更して、リンカがモジュールを認識することでリンケージを処理する
    • モジュールリンケージを持つ名前のマングル方法は従来通り

1つ目の方法では、エクスポートされている名前は既存の外部リンケージを持つ名前と同じマングルとすることができ、それ以外はこれまで通りです。また、モジュールリンケージの処理に関してもモジュールを構成する時だけ考慮したうえで、モジュールと他の翻訳単位をリンクするときは内部リンケージと同様に扱うことができ、リンカにかかる負担はかなり軽くなります。また、リンク時の扱いもこれまでと大きく変わらなさそうです。

2つ目の方法では、まずリンカはオブジェクトファイルがモジュールファイルなのかどうかを認識し、そこにある名前を見てエクスポートされている名前かどうかを識別します。この方法ではモジュール毎に名前を識別することができますが、コンパイラだけでなくリンカにもそれなりの変更が必要です。

そして、1つ目の方法はまさに弱い所有権モデルの実装であり、2つ目の方法は強い所有権モデルの実装です(他にも方法はあるでしょうが、現状はこの2つに1つです)。外部リンケージを持つ名前に対してマングル名で区別をつけないということは、リンク時に別々のモジュール(翻訳単位)からの同じ名前を区別できません。

名前マングリングの観点から見ると、所有権の強弱はモジュールリンケージを持つ名前とエクスポートされている名前のどちらのマングリング方法を変更するのか?という問題とみることもできます。そしておそらく、そのマングル方法とは単にマングル名にモジュール名とモジュールであることを示す文字列が入るだけでしょう。

実装の選択

現在のC++処理系の主要3実装は既にある程度モジュールを実装しています。そこでは、GCC\Clangは弱い所有権モデルを採用しており、MSVCは強い所有権モデルを採用しています。

例えばGCC/Clangで次のようなモジュールコードをコンパイルすると、外部リンケージを持つ名前はこれまで通りのマングルであるのに対して、内部リンケージとモジュールリンケージを持つ名前のマングル名にはモジュール名が含まれるようになっていることが分かります。また、モジュールリンケージと内部リンケージはマングル名レベルでは同じ扱いをされていることも分かります。

export module moduleA;

// モジュールリンケージ
int func1(int a, int b) {
  return a + b;
}

// 外部リンケージ
export int func2(int a, int b) {
  return a + b;
}

// 内部リンケージ
static int func3(int a, int b) {
  return a + b;
}

GCCのモジュール実装に関するWikiによれば、GCCとClangは互いの相互運用性のために連携してモジュールの実装を進めており、その結果として双方共に弱い所有権モデルを採用しているようです。

一方、MSVCは単独ですでにC++モジュールの実装をいったん完了しており、そこでは強い所有権モデルを採用していると明言されています。

この実装間の差異は、おそらくリンカの扱いから来ています。MSVCは1つの社内でリンカとコンパイラを開発しており、ターゲットはWindowsのみで、MSVCコンパイラは自社のリンカ(link.exe)以外を考慮する必要はありません。そしてそれはリンカ側から見ても同様です。そのため、リンカとコンパイラをより密に連携することができ、リンカに対して思い切った変更を加えることができます。

一方GCC/ClangはMSVCに比べるとリンカとコンパイラの開発体制には距離があります。また、それぞれ使われうるリンカは特定の一つではありません。例えば、GNU ld、lld、goldなどいくつかの実装があります。また、ClangとGCCはマングル名に関してお互いに強く互換性を意識しており、片方でコンパイルされたバイナリをもう片方でコンパイルされたバイナリとリンクすることができます(標準ライブラリ周りは別として)。そのため、モジュールの大部分を実装するコンパイラとしてはなるべくリンカに影響を与えないアプローチをとらざるを得なかったものと思われます。
また、既存のABIを大きく変更しないという方針を取ったものでもあるようです。FFIで他言語から呼び出すときにも、モジュール前後で変更が必要ないはずです。

(この辺りのことは完全に私の想像なので正しくないかもしれません)

明確にこの所有権が問題となるコードを書くことはないと思いますが、結果としてこの問題にあたってしまうことはあり得ます。その時に、実装のこの差異はモジュールコードのポータビリティに問題を与えるかもしれません。

なお、GCC/Clangはモジュールを実装中なので、このことは最終的には変化する可能性があります(低いと思わますが)。

参考文献

この記事のMarkdownソース

[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ソース

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

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

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

P1068R4 : Vector API for random number generation

<random>にある既存の分布生成器にイテレータ範囲を乱数で初期化するAPIを追加する提案。

既存の分布生成器はスカラーAPI(1度に1つの乱数しか取得できないAPI)しか備えておらず、乱数エンジンが状態を持つことと合わさってコンパイラによる最適化を妨げており、1度に多量の乱数を取得する場合に効率的ではなくなることがあります。一方で、既存の擬似乱数生成アルゴリズムには多数の乱数生成の際にSIMDを利用するような形に書き換えて効率化する事ができるものが多数あります。
そのような用途のためのベクターAPI(1度に多数の乱数を取得できるAPI)を別途追加し、実装がその中で各種分布生成器やエンジンの特性によって最適な実装を行えるようにし、またコンパイラに最適化の機会を提供可能にすることがこの提案の目的です。

当初はSIMDタイプのAPIも同時提案していたようですが、途中でそれは別の提案に分離されたため、この提案ではイテレータによるベクターAPIの追加のみを提案しています。

std::array<float, N> array; 

std::mt19937 gen(777);  // 乱数エンジン
std::uniform_real_distribution dis(1.f, 2.f); // 分布生成器

// 範囲を乱数列で初期化する、現在はこう書くしかない
for (auto& el : array) {
  el = dis(gen);
}

// この提案によるAPI、イテレータ範囲を乱数列で初期化する
dis(array.begin(), array.end(), gen);

これは必ずしも対応するforループによる範囲の要素毎の初期化コードと同じ効果とはならず、より効率的に実装される可能性があります。

P1184R2 : A Module Mapper

モジュール(特にそのインターフェース)の名前とその中間生成物の間の名前のマッピングやモジュールのビルドについてGCCのTS実装に基づいて書かれた報告書。

C++20のモジュールはそのモジュール名とファイル名の間には何ら関連付けの規則が無く、ビルドの際に必要とされるコンパイル済みモジュールインターフェース(モジュール名とexportされているエンティティ名を記録した中間生成物)の命名に関しても当然何ら規則がありません。この論文はGCCのモジュールTSに基づく実装経験から得られたモジュール名と中間生成物の名前のマッピングのための1つのプロトコルについて説明されているものです。

P1272R3 : Byteswapping for fun&&nuf

バイトスワップ(バイトオーダー変換)のための関数std::byteswap()の提案。

C++20より<bit>ヘッダが導入されpopcountやbit rotation等の基本的なビット操作のための関数が標準で用意されるようになりました。しかし、そこにはバイトスワップ(バイトを逆順にする操作)はありません。現代のほとんどのCPUはバイトスワップのための命令を持っており、それが無い環境でも使用可能なビット演算手法が存在しています。それらを標準で用意しておくことで、よりポータブルかつ効率的に(結果として1命令以下で)バイトスワップを行えるようにしようという提案です。

// 提案されている宣言
namespace std {
  constexpr auto byteswap (integral auto value) noexcept;
}

int main() {
  auto r = std::byteswap(0x1234CDEF);
  // r == 0xEFCD3412
}

LWGでの合意は反対無しで取れていてほとんどC++20入りが決まっていたようでしたが、パディングビット(C++標準としては1byte=8bitではなく、奇数幅整数型の実装がありうる)の扱いについてCWGでの議論が必要となりその文言調整に手間取った結果C++20には間に合わなかったようです。おそらくC++23には入りそうです。

P1478R4 : Byte-wise atomic memcpy

アトミックにメモリのコピーを行うためのstd::atomic_load_per_byte_memcpy()/std::atomic_store_per_byte_memcpy()の提案。

この提案では、主にSeqlockと呼ばれるロック手法におけるユースケースを主眼にそこで有用となるアトミックなmemcpyについて提案しています。

// Seqlockにおける読み手の処理の一例
// ロック対象のshared_dataをdataに読み取る
do {
  // クリティカルセクション開始時点のシーケンス番号取得
  int seq1 = seq_no.load(std::memory_order_acquire);

  // データの読み取り
  // shared_dataがatomic変数ではない場合、この読み取り操作はフェンスを越えてseq2の後に変更されうる
  data = shared_data;

  atomic_thread_fence(std::memory_order_acquire);  // seq2の読み取り順序はここより前に変更される事はない

  // クリティカルセクション終了時点のシーケンス番号取得
  int seq2 = seq_no.load(std::memory_order_relaxed);

  // 開始時点と終了時点のシーケンス番号が異なる、あるいは開始時点のシーケンス番号が奇数であるならクリティカルセクション中に書き込みが起きている
} while (seq1 != seq2 || seq1 & 1);

// dataはshared_dataからアトミックにロード完了している事が期待されるが・・・

shared_dataはロック対象である事もあり普通はatomicでは無いはずで、その場合はこのループが正常に終了した場合でもdata変数の内容はデータ競合を起こした結果を読み取っている可能性があります。つまり、データ競合を回避するためにクリティカルセクション中で読み取られたデータは書き込みが起きている場合に破棄されるようになっているにも関わらず、このクリティカルセクション中の読み取り操作はアトミックに行われなければなりません。

多くの場合Seqlock対象のデータはtrivially copyableなオブジェクトであり、その場合そのコピーにはmemcpyが使用されます。しかし、memcpyはアトミックでは無いので、対象オブジェクトをロックフリーなアトミックサブオブジェクトに分解した上でそれらを1つづつコピーすることになります。

// 上記クリティカルセクション中のコピーは例えばこうなる
for (size_t i = 0; i < sizeof(shared_data); ++i) {
  reinterpret_cast<char*>(&data)[i] =
      std::atomic_ref<char>(reinterpret_cast<char*>(&shared_data)[i]).load(std::memory_order_relaxed);
}
std::atomic_thread_fence(std::memory_order_acquire);

この提案によるatomic_load_per_byte_memcpyはこのようなコピー(およびフェンス)を1文で自明に行うものです。

Foo data;  // trivially copyableだとする

do {
  int seq1 = seq_no.load(std::memory_order_acquire);  // このロードより後のあらゆる読み書き操作はこの前に順序変更されない

  // データの読み取り
  std::atomic_load_per_byte_memcpy(&data, &shared_data, sizeof(Foo), std::memory_order_acquire); // このロードより後のあらゆる読み書き操作はこの前に順序変更されない

  int seq2 =  seq_no.load(std::memory_order_relaxed); // 順序変更について制約はないが、先行する2つのacquireロードを超えて順序変更されることはない
} while (seq1 != seq2 || seq1 & 1);

// dataはshared_dataからアトミックにロード完了している

atomic_load_per_byte_memcpyはメモリオーダー指定を受け取りコピー元に対してアトミックにアクセスする事以外はmemcpyと同様に動作します。これを用いると最初の問題のあるSeqlockの読み取り操作はより単純にそして正しく書く事ができるようになります。誤解の種でもあったatomic_thread_fenceを使用する必要も無くなります。

atomic_store_per_byte_memcpyはこのケースの読み取りに関して双対的なもので、memcpyにおいてコピー先へのアクセスをアトミック化します。Seqlockの書き手側の処理などで使用することを想定しているようです。

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

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

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

このリビジョンでの変更は、機能テストマクロが別の提案に分離されたこと、P1641によるフリースタンディング機能指定の文言への変更、uses-allocator構築に関する2つの機能の追加、などです。

P1659R1 : starts_with and ends_with

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

C++20にてstd::stringstd::string_viewに対してstarts_with/ends_withメンバ関数が追加され、文字列の先頭(末尾)が指定の文字列で始まっている(終わっている)かをbool値で得られます。それらをより一般化して、任意の範囲について同じ事ができるようにするのがこの提案です。

// 共にtrueとなる
bool s = std::ranges::starts_with(std::views::iota(0, 50), std::views::iota(0, 30));
bool e = std::ranges::ends_with(std::views::iota(0, 50), std::views::iota(20, 50));

また、これらは第3引数に述語オブジェクトを渡す事で行われる比較をカスタマイズできます。例えば、先頭や末尾がある値以下であるか?のようなチェックができるようになります。

// 共にtrueとなる
bool s = std::ranges::starts_with(std::views::iota(0, 50), {-1, 0, 1, 2, 3}, std::ranges::greater);
bool e = std::ranges::ends_with(std::views::iota(0, 50), {46, 47, 48, 49, 50}, std::ranges::less);

また、さらにその後ろに各範囲の要素についての射影(Projection)を渡すこともできます。ただ残念なことに、これは普通の関数なので|で繋ぐことが出来ません。

この提案は既にLWGでのレビューをほとんど終えていて、どうやら次のリビジョンとそのレビューでもってC++23入りが確定する様子です。

P1679R3 : String Contains function

std::string, std::string_viewに、指定された文字列が含まれているかを調べるcontains()メンバ関数を追加する提案。

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

このリビジョンでの変更は機能テストマクロのの追加と大文字小文字の取り扱いについての説明の追記です。

また、LWGでの全会一致でC++23への導入が承認されたようなので何事もなければC++23に入りそうです。

P1726R4 : Pointer lifetime-end zap

Pointer lifetime-end zapと呼ばれる問題の周知とそれについてのフィードバックを得るための報告書。

Pointer lifetime-end zapとは、現在のC++のポインタセマンティクスが寿命が尽きたオブジェクトを指しているポインタの存在を認めない(その再利用が禁止されている)事で、これによってポインタ再利用を用いると効率的に書けるタイプのアルゴリズム(特に並行アルゴリズム)が未定義動作に陥っている問題の事のようです。

この文書は、Pointer lifetime-end zapは規格としてどのように表現されているかに始まり、なぜそうなっているのか?どこで問題になるのか?そして、この問題をどう解決するのか?を記したものです。
主目的は委員会のメンバに向けてこの問題の周知を図り問題そのものや解決についてのフィードバックを得ることで、何かを提案するものではないですが、この文書を受けてSG1(Concurrency Study Group)はこれを削除することを目指しており、LWGもこの問題の解決に興味を持っているようです。

(内容は私には難しいので識者の方には是非解読をお願いしたい所存)、

P1864R0 : Defining Target Tuplets

ビルドシステムがそのターゲットとなる環境を決めるための名前の列であるTarget Tupletsについて、基礎的な定義を標準に追加する提案。

ビルドシステムの担う最も困難な仕事の一つは、あるプラットフォームに向けてコンパイルする際にどのアーキテクチャ、ツールチェーン、OS、そして実行環境を使用するのかを指定することです。この名前は実装によって好き勝手に選択されており、ツールのベンダーによって異なっています。
この提案は、それらの中でも多少広く使用されているclangの定めるTarget Tripleを基にしてその標準的なものを定義することを目指すものです。

Target Tupletsはビルドのために必要となる次の4つの情報の列として規定されます。

  • Architecture
    • CPUの命令セット(ISA)
    • x86_64, armv8など
  • Vendor
    • C++ツールチェーンを提供する事業体名(組織、個人、PC、なし)
    • apple, nvidia, noneなど
  • Operating system
    • linux, win32など
  • Environment
    • 実行ファイルの形式、ABI、ランタイムのいずれか
    • macho, elfgnu, eabiandroidなど

これらの具体的な名前をこの順番に-で繋いだ文字列がTarget Tupletsとなります。

  • Windows環境の一例 : x86_64-ms-win32-msvc
  • Linux環境の一例 : x86_64-none-linux-elf
  • CUDA環境の一例 : nvptx64-nvidia-cuda-cuda

ただし、標準規格としてTarget Tupletsを定義はせず、具体的なものは別のドキュメントに委ねるようです。規格書に記述してしまうと更新のために時間と手間がかかるためです。

P2000R2 Direction for ISO C++

C++の標準化の方向性を記した文書。

C++標準化の目的や課題、次のバージョンに向けて重視する機能など、より大域的な視点でC++標準化の道筋を示す文書です。今回コロナウィルス流行に伴う社会環境の変化を反映する形で変更されたようです。

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

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

この提案は以下の3つのIssueを解決するものです。

この提案では、既存の実装をベースとして字句規則をより明確に修正することによってこれらの問題の解決を図ります。

  • Core Issue 411
    • 単一の文字として表現できないユニバーサル文字名のエンコーディングは文字リテラルと文字列リテラルで異なる振る舞いをする、と規定する。
    • 例えば実行文字集合UTF-8である時、'\u0153'は(サポートされる場合)int型の実装定義の値を持つが、"\u0153"\xC5\x93\x00の長さ3の文字配列となる。
  • Core Issue 1656
  • Core Issue 2333
    • UTF-8リテラル中での数値エスケープ文字使用には価値があるので、明確に振る舞いを規定する。

問題範囲としては大きくないのですが、該当する文言をほとんど書き直しているため変更範囲が広くなっています。しかし、よく見るとより明確かつシンプルにこれらのことが表現されるようになっています。

P2075R1 : Philox as an extension of the C++ RNG engines

<random>に新しい疑似乱数生成エンジンを追加する提案。

疑似乱数生成エンジンは次のように様々な側面から特徴付けられます。

  • 乱数の質
  • 生成速度
  • 周期
  • 並列化(ベクトル化)可能性

例えば線形合同法std::linear_congruential_engine)は周期が短く乱数の質が低いのに対して、メルセンヌツイスターstd::mersenne_twister_engine)は周期が長く乱数の質は高いのですが、非常に大きなサイズの状態に依存しており並列化を妨げています。また、準モンテカルロ法をサポートできるような超一様分布を生成するエンジンもありません。

これらの観点から見ると現在のC++に用意されている乱数エンジンだけでは乱数のユースケースを満たすことは出来ず不十分であるので、最新の研究成果に基づいていくつかを検討し追加しようというのが(大本のP1932R0の)狙いです。

この提案ではPhiloxと呼ばれるGPU等のハードウェアによって並列化しやすい特性を持つエンジンに絞って提案されています。

Philoxはカウンタベースと呼ばれるタイプのエンジンで、カウンターベースのエンジンは総じて小さい状態と長い周期が特徴です(Philox4x32はそれぞれ40 [byte]2^130)。Philoxは状態が小さい事と演算が単純であることからSIMDGPUによる並列化が容易であり、intelnVidiaAMDによってそれぞれのハードウェアに最適化されたライブラリ実装が提供されており、それら実装は統計的なテストをパスしています。また、そのような特性を生かして大規模な並列乱数生成が求められる金融や非決定性有限オートマントンのシミュレーションなど、すでに広い使用実績があります。
このように、Philoxエンジンは導入および実装のハードルは低く標準化による利得は大きいため、標準に追加することを提案しています。

そして、Philoxエンジンを導入するに当たっては次の2つのAPIのどちらかを選択することを提案しています。

  • Philoxだけに着目したAPI
    • 既存の乱数エンジンを参考に、1つのPhiloxエンジンのベースとなるクラステンプレートと、そのパラメータ定義済みのいくつかのエイリアスstd::philoxNxM)だけを追加する。
  • カウンタベースエンジンのAPI

現状ではまだどちらを選択するかは議論中のようですが、どちらのAPIが選択されたとしてもユーザーからはstd::philox4x64std::philox4x64_rのような名前で既存の乱数エンジンと同様のインターフェースによって使用可能となります。

P2093R1 : Formatted output

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

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

このリビジョンでの変更は、wchar_tおよびcharN_tオーバーロードの追加に関してを別の提案に分離して、ユニコードテキストのフォーマットについてと共に議論することが明記されたことです。

P2128R2 : Multidimensional subscript operator

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

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

このリビジョンでの変更は、[]が少なくとも1つの引数を取る必要があるという制約を削除したことと、C配列に対するサポートや既存ライブラリに追加することを目指さないことを明言したことです。 C配列や既存のライブラリ機能(std::valarray)でそれらを使用したい場合は、C++26以降に向けて別の提案で議論することを推奨しています。

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

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

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

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

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

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

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

  • std::streambufやCのI/O APIの使用例を追加
  • design decisionsの書き直し
  • 参照文書の変更
  • エンディアンを考慮してRIFFファイルを読み取るサンプルコードの追加
  • std::filebufベンチマークを追加

などです。

P2161R2 : Remove Default Candidate Executor

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

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

このリビジョンでの変更は、1引数の​defer, dispatch​, ​postがその引数のCompletionTokenから(色々やって)型::executor_typeを取得できることを要求し、それが同様に(色々やって)取得できるget_executor()の戻り値型と一致することを要求するようになった事などです。

P2165R1 : Compatibility between tuple and tuple-like objects

std::pairと2要素std::tupleの間の非互換を減らし比較や代入をできるようにする提案。

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

このリビジョンでの変更はstd::tupleの推論ガイドを変更しないことにしたことと、std::tuple_catもより広いtuple-likeな型全般で使用可能となるようにしたことです。

以前のリビジョンではstd::tupleをtuple-likeな型からその要素を分解して構築できるように推論ガイドを変更していたのですが、std::tuple{std::array<int, 2>{}}(この型はstd::tuple<std::array<int, 2>>になるが、変更の下ではstd::tuple<int, int>になってしまう)の様な既存のコードの振る舞いを破壊する事になっていいたためです。ただ、std::pairは以前から特別扱いされていたので変更なしでも2要素tupleへ(現在でも)変換可能です。

P2169R1 : A Nice Placeholder With No Name

宣言以降使用されず追加情報を提供するための名前をつける必要もない変数を表すために_を言語サポート付きで使用できる様にする提案。

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

このリビジョンでの変更は_がクラスメンバでも同じような意味をもつようにしたことと、同じスコープで複数回_を使う場合の扱いが変更されたことです。

結局、この特別な_は次の場所で使用することができます。

  • 変数名
  • クラスの非静的データメンバ名
  • 関数の引数名
  • ラムダ式のキャプチャ名
  • 構造化束縛宣言の変数名

そして、前回掲載したサンプルコードは次のように動作が微妙に変わります。

/// ここはモジュール実装単位内ではないとする

namespace a {
  auto _ = f(); // Ok, [[maybe_unused]] auto _ = f();と等価
  auto _ = f(); // NG: _の再定義、モジュール実装単位内ならばok
}

void f() {
  auto _ = 42; // Ok、[[maybe_unused]] auto _ = 42;と等価
  auto _ = 0;  // Ok、再定義(参照しない限りOK)

  {
    auto _ = 1; // Ok, 単に隠蔽する
    assert( _ == 1 ); // Ok
  }

  assert( _ == 42 ); // NG、再定義された名前の参照
}

ただし、この様な再定義が許可されるのは_という名前の変数だけです。他の名前の変数は今まで通り定義が一つでなければなりません。

P2178R1 : Misc lexing and string handling improvements

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

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

このリビジョンでの変更は、FAQを追加した事と、生文字列リテラル末尾の空白やBOM、ill-formedなcode units sequence、文字列リテラルの連結についての追記が行われたことです。

P2183R0 : Executors Review: Properties

C++23への導入を目指してレビューが進んでいるExecutorライブラリ(P0443R13)におけるプロパティ指定(P1393R0)の利用に関する報告書。

P0688R0によってExecutorに対して導入された組み合わせ爆発を抑えるプロパティ指定の方法は、Executorにとどまらず将来のライブラリに対しても有用であると判断され、よりジェネリックなプロパティ指定のためのAPIとしてExecutorから分離されました(P1393R0)。
この提案は分離されたことによって若干変化したAPIをExecutorがどのように利用するのかをLEWGにおけるレビューのためにまとめたもので、boost.asioにおける先行実装を用いたサンプルコードがいくつか掲載されています。また、その際に発見された小さないくつかの問題についても同時に報告されています。

P2186R0 : Removing Garbage Collection Support

ガベージコレクタサポートのために追加された言語とライブラリ機能を削除する提案。

C++11にて、C++処理系としてGC実装を許可するための最小の文言とライブラリサポートが導入されました。2020年現在、BoehmGCというC/C++向けのGCライブラリ(not処理系)があるほか、主要ブラウザのJavascriptエンジンなどGCをサポートする言語がC++で実装され、GCにより管理されているオブジェクトとC++オブジェクトが密接に連携できるなど、C++から利用可能なGCの実装は一定の成功を収めています。ただしC++の処理系としてのGC実装はなく、これらのC++から利用可能なGC実装はC++11で導入されたGCのための仕組みを利用していません。

また、そのような標準で用意されているGC実装のための仕組みにはいくつも問題があり、実際のGC実装のためにはほとんど役に立ちません。実際、既存のC++から利用可能なGC実装が依存しているのは、現在の規定とは異なる要因に対してです。そして、GCのためのライブラリ機能を使用しているコードはGCCLLVM(つまり実装者)以外には見つからなかったそうです。

C++実装としてGCサポートを認めないわけではなく、少なくとも現在の仕様とライブラリ機能は役に立たないので削除しようという提案です。これらの文言と機能は実装される事も使用されることも無く、非推奨にすることには意味が無いためすぐに削除することを提案しています。

P2187R3 : std::swap_if, std::predictable

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

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

R1とR2は公開されておらず、前回のR0からいきなりR3に飛んでいます。

変更点はまず、入力となるTのサイズによってswap_ifを使うかどうかを静的に分岐するようにしたことです。そのための、is_trivially_swappable_vcheaply_swappableコンセプトを追加して以下の様な実装をします。

template <typename T>
constexpr bool is_trivially_swappable_v = std::is_trivially_copyable_v<T>;

template <typename T>
concept cheaply_swappable = (std::is_trivially_swappable_v<T> && sizeof(T) <= N); // ここのNは実装定義

template <typename T>
  requires (cheaply_swappable<T> || std::swappable<T>)
constexpr bool swap_if(bool c, T& x, T& y) noexcept(cheaply_swappable<T> || std::is_nothrow_invocable_v<decltype(std::swap<T,T>), T&, T&>) {
  if constexpr (cheaply_swappable<T>) {
    struct alignas(T) { char b[sizeof(T)]; } tmp[2];

    std::memcpy(tmp[0].b, &x, sizeof(x));
    std::memcpy(tmp[1].b, &y, sizeof(y));
    std::memcpy(&y, tmp[1-c].b, sizeof(x));
    std::memcpy(&x, tmp[c].b, sizeof(x));

    return c;
  }

  // Tのコピーコストが大きいとき、普通に条件分岐してswapする
  if (c) std::swap(x, y);
  return c;
} 

この様にすることで、Tのコピーコストが高くつく場合にも自動的に最適な実装に切り替えることができます。
また、is_trivially_swappable_vはカスタマイゼーションポイントになっていて、trivially copyableとみなされないが安全にmemcpyできるような型をswap_ifにアダプトできます。

そして最後に、イテレータに対して直接swap_ifするiter_swap_ifが追加されています。

このswap_ifが使用されるのはstd::sortなどのアルゴリズム中であり、そこではイテレータを介して実際の値にアクセスします。swap_ifの引数はイテレータを間接参照して渡す事になり、わずかではありますがボイラープレートが発生します。iter_swap_ifイテレータを直接受け取り内部で間接参照しつつswap_ifに渡す簡易なラッパーです。

template <typename Flag, typename I>
bool iter_swap_if(Flag c, I p, I q) {
  return swap_if(c, *p, *q);
}

P2188R1 : Zap the Zap: Pointers are sometimes just bags of bits

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

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

EWGでのレビューを受けて、不完全だった提案の文言をいったん削除し、代わりに著者の方が重視している側面について追記されています。

1. 等しいポインタは交換可能であるべき

ポインタはValue Semanticsを持っており、コピーすると元の値と同じになる。従って、2つの値が等しければ、それらは交換して扱うことができるはず。

// pとqは初期化されているとする
void f(int* p, int* q){
  *q = 99;
  bool same = false;

  if (p == q) {
    // ポインタの値が等しいならば、pとqは同じ
    *p = 42;
    same = true;
    assert(*q == 42);
  }

  assert(same ? (*q == 42) : (*q == 99));
}

このコードが動作しないとすれば、ポインタの等値性が壊れていることを意味する。

2. ポインタの等値性は一貫しているべき

同じ2つのポインタの等値性は比較される場所によらないはず、これはValue Semanticsの基本的な側面にすぎない。

bool compare(int* const p, int* const q){
  return p == q;
}

// pとqは初期化されているとする
void f(int* const p, int* const q){
  bool const same = (p == q);
  g(p, q);
  assert(same == (p == q));       // ここがtrueならば
  assert(same == compare(p, q));  // ここもtrue
}

このコードはg()がなんであるかや、翻訳単位やインライン化の有無などに関わらず動作するはず。

3. コンパイラは特定の状況ではエイリアスが無いと仮定しなければならない

他の場所にその参照を渡されることがない関数内のローカル変数は、他の場所から変更されることは無いと仮定できるようにしなければならない。

void f(){
  int x = 42;
  g();
  assert(x == 42);  // 常にパスする
}

アサートが起動することはあってはならない。xへのポインタは存在しないので、g()の中で使用されるいかなるポインタと等しくなることはありえない。

4. 無効なポインタのデリファレンスは未定義動作

そのことを変更するつもりはない(ただし、次の場合は除く)。

5. ポインタの有効性は比較によって伝染する

ポインタpを、有効である事が分かっているポインタqと比較した結果等しければ、pも有効でなければならない(上記1に従う)。

void f(){
  int * const p = new int(42);
  delete p;
  int * const q = new int(99);

  if (p == q) {
    assert(*p == 99); // p == qならば指す先は同じ
  }
}

実装がこの場合のp == qが成り立たないようにすることを推奨するが、もしp == qであるならば、pqは同じオブジェクトを指していなければならない。

逆に言うと、現在のC++はこれらの事を必ずしも保証できていないという事です。

P2191R0 : Modules: ADL & GMFs do not play together well (anymore)

グローバルモジュールフラグメントのあらゆる宣言が、モジュール外からのテンプレートのインスタンス化時に参照されないようにする提案。

グローバルモジュールフラグメントとは、モジュール内部に用意される主としてヘッダをインクルードするための領域です。その領域にあるものがそのモジュール内から参照されなかった場合、モジュールのコンパイルの段階で破棄されます。モジュールのビルドはテンプレートを残す必要があるため完全に完了するわけではありませんが、破棄された宣言はその後のテンプレートインスタンス化時には一切参照できなくなります。これはモジュールのインターフェースの肥大化を抑えつつ、モジュール内で#includeを行うための仕組みです。

一方、従来の#includeディレクティブはヘッダユニットのインポートとして書くことができ、コンパイラによって自動的に置換される可能性があります。グローバルモジュールフラグメント内でもヘッダユニットのインポートが発生する可能性があり、その場合はヘッダファイルが一つのモジュールとしてインターフェース依存関係を構築するため、そのimportが破棄されることはありません。すると、同じヘッダを(グローバルモジュールフラグメント内で)#includeしたときと異なり、あらゆる宣言は破棄されません。

そして、モジュール内で定義されエクスポートされているテンプレートがモジュール外でインスタンス化されたとき、そのインスタンス化経路(テンプレートのインスタンス化に伴ってインスタンス化される関連テンプレートを繋いでいったグラフのようなもの)上からグローバルモジュールフラグメント内にあって外部リンケージを持ち破棄されていない宣言はADLによって参照することができます。
この時、コンパイラによってグローバルモジュールフラグメント内の#includeimportに置換されていた場合、ADLによって参照可能な宣言が異なることになります。どのようなヘッダをヘッダユニットとして扱い、いつ#includeimportに置換するのかは実装定義とされているため、このことはコンパイラによって異なる可能性があります。また、ユーザーがそれを制御する手段はありません。

他の問題として、このようなグローバルモジュールフラグメントの仕様によって、実装はモジュールの第一段階のコンパイル後にもグローバルモジュールフラグメントの中身と依存関係グラフを保持し続けなければならず、モジュールパーティションによって複数のグローバルモジュールフラグメントが間接的に外部にエクスポートされる時、それをどのように統合すべきか?という問題が生じます。また、それによってはモジュール内部のパーティションへの分割が外部から観測可能となってしまうことになり得ます。

/// header.hpp

#include <iterator> // このインクルードは実装によってimportに変換されるかもしれない
/// mymodule.cpp
module;
// グローバルモジュールフラグメント

#include "header.hpp" // このインクルードは実装によってimportに変換されるかもしれない

export module mymodule;

template<typename C>
export void f(C&& container) {
  auto it = begin(container);  // 依存名なのでここではstd::beginは参照されているとみなされない
}

// 説明のために実装単位などを省略
/// main.cpp
import mymodule;

#include <vector>

int main() {
  std::vector<int> v{};

  f(v);
  // mymoduleのグローバルモジュールフラグメント内の#includeがそのままならばこれはエラー
  // mymoduleのグローバルモジュールフラグメント内で#includeがimportになっているとコンパイルは通る
}

この提案では、これらの問題を解決するためにグローバルモジュールフラグメント内のあらゆる宣言は、モジュールに属するテンプレートのインスタンス化時のADLにおいて可視とならない、と規定を変更することを提案しています。
これによって、グローバルモジュールフラグメントは完全にモジュール内部で完結するものになり、モジュール外部からは気にしなくてもよくなります。また、実装はモジュールのコンパイル後にグローバルモジュールフラグメント内のものを残すかどうかを自由に選択できるようになります。

ただし、グローバルモジュールフラグメント内の宣言を参照しているようなモジュールに属するテンプレートの利用のためには、モジュール内でusingしておくか、そのモジュールを使う側で必要な宣言が含まれているヘッダをインクルードしなければならなくなります。

/// mymodule.cpp
module;
// グローバルモジュールフラグメント
// この提案の下では、ここの宣言は全て外部から参照されることは無い

#include <vector>
#include "header.hpp"

export module mymodule;

template<typename C>
export void f(C&& container) {
  using std::begin; // beginを後から使用するためにはこうする (1)

  auto it = begin(container);
}

// 説明のために実装単位などを省略
/// main.cpp
import mymodule;

#include <vector>
#include <iterator> // mymodule内部のstd::beginを有効化するために明示的に追加する (2)

int main() {
  std::vector<int> v{};

  f(v); // (1)か(2)のどちらかをしておかないとコンパイルエラー
}

P2192R0 std::valstat - function return type

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

現在のC++にはいくつものエラー報告のための方法がありますが、エラーを報告する側と受け取り側にとって単純かつ柔軟な方法はなく、戻り値の形でエラー報告を行う際の標準的な1つの型もありません。

  • 例外機構
    • 例外を報告する最も簡易な方法ではあるが、受け取る側では何が飛んでくるか分からない
    • オーバーヘッドが大きい
  • errnoあるいはstd::errc
    • エラーしか報告できない
  • std::error_code
    • 動的メモリ確保を伴うなどいくつか問題がある(P0824R1
    • エラーしか報告できない
  • outcome<T>/expected<T, E>
    • これらは非常に複雑な型となり、必要以上の複雑さを導入することになる(例えば、Boost.outcomは7223行)
    • エラーかそうでないかの2つの情報しか表現できない

一方、C++標準としてはエラー報告については次のようなコンセンサスがあるようです。

  • 処理(関数)の結果はエラーかそうでないかの2値ではない
    • 例えば非同期処理のキャンセルなど
  • エラーかそうでないかのバイナリロジックを回避すれば複雑さが低減される
    • 早期リターンが失敗ではない場合のエラーがある
  • (エラー報告の結果の)戻り値を消費することは、必然的に複雑となる
    • 戻り値型は多様なので、そのような消費ロジックは複雑にならざるをえない
    • 統一的な戻り値消費処理を決定し、それに従うようにすることが有益

valstatはこれらのコンセンサスに基づいた、戻り値でエラーハンドリングを行うためのコンセプト(概念)とそれに従うものの総称です。次のように使用することができます。

// valstatは何か特定の戻り値型ではない
auto [ value, status ] = valstat_enabled_function();

// valstatは戻り値として4つの状態を表現する
if (   value &&   status )  { /* info */ }
if (   value && ! status )  { /* ok   */ }
if ( ! value &&   status )  { /* error*/ }
if ( ! value && ! status )  { /* empty*/ } 

valstat(に適合する戻り値型)は処理結果と処理のステータスのペアであり、その組み合わせによってMeta Stateを表現します。そして、その状態は!&&によって判別できます。

Meta Stateは処理結果と処理のステータスの有無によって次の4つの状態の1つを表現します

処理結果 \ 処理ステータス 有り 無し(空)
有り Info OK
無し(空) Error Empty
  • OK
    • 関数は期待される値を返し完了した
  • Error
    • 関数は期待通りに完了しなかった(失敗した)
    • ステータスには関連情報が格納される
  • Info
    • 関数は期待される値を返し完了した
    • ステータスには追加の情報が格納されている
  • Empty
    • 戻り値は空
    • (これがどのように使われるのかは提案文書に書いてない、多分重要なのは上3つ)

これはvalstatのコンセプトの一部であり、列挙値などで表現されるものではなく、valstatの任意の実体がこれを明示的に実装するわけではありません。valstatにアダプトした型は必然的にその状態によってこのMeta Stateを表現することになります。

この提案としてはこのようなコンセプトとそれを表現するための標準実装であるstd::valstatを標準ライブラリに導入することを提案しています。それは次のように実装可能です。

#include <optional>

namespace std {
  template<tpyename T, typename S>
  struct [[nodiscard]] valstat {
    using value_type = T;
    using status_type = S;

    optional<T> value;
    optional<S> status;
  };
}

ただし、valstatは特定の型に縛られるものではなく、std::valstatと同じように使用可能なvalstatコンセプトに適合する任意の型によって実装可能です。それは純粋なC言語においてもポインタを利用することで実装でき、そのようなライブラリをC++から利用するときにはvalstatコンセプトに従って統一的にエラー報告を処理できるようになります。

P2193R0 : How to structure a teaching topic

R1が同時に出ているので↓でまとめて。

P2193R1 : How to structure a teaching topic

C++を教えるにあたってのカリキュラムを書く際のガイドラインとなる文書。

ある1つのトピック(C++の言語・ライブラリ機能)についてそこに何をどのように書くか等を説明し、カリキュラム中のトピック説明においての共通の文書構造を提供するものです。

P2196R0 : A lifetime-extending forwarder

std::forwardのように与えられた引数を完全転送し、CV修飾を記憶しつつその生存期間を延長し、必要になった場所で完全転送しつつ取り出すことのできるクラステンプレートであるstd::forwarderの提案。

おおよそ次のように動作するものです(提案文書からの引用。一部正しくないコードが含まれていますが、イメージを掴む分には問題ないのでそのままにしています)。

// オブジェクトの値カテゴリによって呼ばれるメンバ関数が異なる
struct object {
  void test() const & {std::clog << "void test() const &\n";}
  void test() const && {std::clog << "void test() const &&\n";}
};

// Forwarding referenceで入力を受ける
template <class T>
auto function(T&& t) {
  // 引数tの型を記憶し生存期間を延長する
  auto fwd = forwarder<T>(t);
  
  /∗ ... ∗/

  return fwd;
}

int main(int argc, char∗ argv[]) {
  object x;
  auto fwd0 = function(x);
  auto fwd1 = function(object{});
  
  /∗ ... ∗/

  // 構築時の値カテゴリとCV修飾を復元して取り出す
  // (提案文書にはこのように書かれているが実際この呼び出しは出来ない)
  fwd0().test(); // void test() const &
  fwd1().test(); // void test() const &&
  return 0;
}

オブジェクトの値カテゴリとCV修飾を保存したまま持ち運び、必要な時にそれを復元しつつ取り出すことができます。ただし、提案ではメンバ関数は型変換演算子しか定義されておらず、operator.オーバーロード不可能なので実際には上記の.によるメンバ呼び出しみたいなことはできません。

筆者の方は別の提案であるP1772R1(いわゆるoverloadedの提案)の作業中にこのような完全転送と生存期間延長を行うラッパー型の必要性に遭遇し、それがより広範に適用可能なものであったため独立した提案として提出したとのことです。

P2198R0 Freestanding Feature-Test Macros and Implementation-Defined Extensions

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

P1641R3P1642R3の以前のリビジョンで同時に提案されていたものを分離して一つにまとめた提案です。

P2199R0 : Concepts to differentiate types

2つの型が同じではない、2つの型は類似の型(CV修飾を無視して同じ型)である、あるいは2つの型は類似していない、という事を表現する3つの新しいコンセプトを追加する提案。

それぞれ、std::different_from, std::similar_to, std::distinct_fromという名前が当てられています。

2つの型が異なる、CV修飾の違いを除いて異なるとき、のようなテンプレートパラメータに対する制約は標準ライブラリの中でも頻出しています(例えばstd::optionalstd::variantの変換コンストラクタや代入演算子など)。この様なテンプレートパラメータに対する制約はよくある物なのでユーザーコードでも当然頻出し、ユーザーは各々同じコンセプトを最発明することになります。標準でこれらを定義しておくことでそのような手間を省くことができ、また標準ライブラリに対する制約もこれらのコンセプトを直接用いて簡略化することができます。

提案ではそれぞれ次のように定義されます。

namespace std {
  template<typename T, typename U>
  concept different_from = not same_as<T, U>;

  template<typename T, typename U>
  concept similar_to = same_as<remove_cvref_t<T>, remove_cvref_t<U>>;

  template<typename T, typename U>
  concept distinct_from = not similar_to<T, U>;
}

この定義だけを見てもよく使いそうなのは分かるでしょう。

この提案では同時に、different_fromdistinct_fromによって置換可能な標準ライブラリ内の制約条件を書き換えることも行っています。
また、differentdistinctの使い分けが曖昧なので、LWG/LEWGの判断次第では名前が入れ替わる可能性があるとのことです。

P2201R0 : Mixed string literal concatenation

異なるエンコードプレフィックスを持つ文字列リテラルの連結を禁止する提案。

文字列リテラルが2つ並んでいるとき、それらはコンパイラによって連結され1つの文字列リテラルとして扱われます。

int main() {
  auto str = "abc" "def" "ghi"; // "abcdefghi"として扱われる
}

このとき、そのエンコード指定(u8 L u U)が異なっている場合の動作は実装定義で条件付きでサポートされるとされており、実際には主要なC++コンパイラはエラーとします。ただ、UTF-8文字列リテラルのその他のリテラルとの連結は明示的に禁止されています。

int main() {
  auto str1 = L"abc" "def" U"ghi";   // 実装定義
  auto str2 = L"abc" u8"def" U"ghi"; // u8がある場合はill-formed
}

ただ、SDCC(Small Device C Compiler)というCコンパイラはこれをサポートしており、最初のエンコード指定で連結するようです。

このような異なるエンコード指定の文字列リテラル連結のユースケースはないので、明示的にill-formedであると規定しようという提案です。

P2202R0 : Senders/Receivers group Executors review report

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

Executor提案はレビューの効率化のために構成要素に分割したうえで、それぞれ別々の人々によってレビューされているようです。

P2203R0 : LEWG Executors Customization Point Report

Executor提案(P0443R13)のカスタマイゼーションポイント周りのレビューのまとめと、生じた疑問点についての報告書。

この文書は筆者(レビュワー)の方々がP0443のカスタマイゼーションポイント(オブジェクト)について読み込み、どんなカスタマイゼーションポイントがあるかやそれらの枠割についてまとめた部分が主となっています。Executorライブラリのカスタマイゼーションポイントについて比較的簡単にまとめられているので興味のある人は見てみるといいかもしれません。

onihusube.hatenablog.com

この記事のMarkdownソース