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

C++標準化委員会の論文(提案文書)公開がコロナウィルスの影響もあって月1になり量がお手頃になったので、4/20公開の提案文書をさらっと見てみます。

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

提案文書で採択されたものは今回はありません。

N4858 : Disposition of Comments: SC22 5415, ISO/IEC CD 14882

C++20 CD (committee draft)の投票時に各国委員会およびそのメンバーから寄せられたコメントとその対応および理由の総覧です。

N4859/N4860/N4861

N4860/N4861のN4849との差分を記したEditors' Report。新たに採択された提案文書の一覧、解決されたIssueの一覧、Github上での軽微な修正コミットの一覧、などが載っています。

C++20のDIS (draft international standard)。この後FDIS (final draft international standard)を経てIS (international standard)へと至ります。

残念ながら委員会のメンバーしか見られないようです・・・

C++23のWD (working draft)第一弾。でもC++23向けに導入されたものはないはず。

N4860との差異は、表紙とヘッダ、フッダ、C++17規格とのクロスリファレンスの有無(無い)だけのようで、内容としてはDIS(N4860)と同一とのこと。C++17(N4659)も最終的に公開されているのはDIS相当のWDなので、これがC++20規格として参照されることになりそうです。

P0533R6 : constexpr for <cmath> and <cstdlib>

<cmath><cstdlib>の一部の関数をconstexpr対応する提案。

<cmath>からは、logb()modf()scalbn()abs()/fabs()ceil(),floor()等丸め系関数、fmod()copysign(), nextafter()fmax()/fmin()/fdim()fma()fpclassify(),isunordered()等数値分類・数値比較系関数、等が対象です。
<cstdlib>はなぜかそっちに含まれている数学関数(abs()とかdiv())だけが対象です。

筆者は、<cmath>の関数群は全てconstexpr指定できるはずだけど、コンパイラ/標準ライブラリベンダーの過度な負担とならない一部だけをconstexpr対応させる、と述べています。その一部には、sin(),cos()等の数学関数は含まれません・・・

また、これらの関数はerrnoや丸めモードなどグローバルフラグに依存し、またそれらを更新します。errnoをセットすべき時(定義域エラーやゼロ割)は単にコンパイルエラーを発生させ、グローバルフラグの状態はコンパイル時には変更しない、と言うようにすれば良いのですがC++17以前ではそれは少し難しい実装を要求していました。
しかし、C++20にてstd::is_constant_evaluated()が導入されたことでこの問題は解決されるため、単純な実装によって多くの<cmath>関数を追加でconstexpr対応させられるようになりました。

丸めモードに関しては色々議論があるようで、この提案では丸めモードへの依存が強い(変更することで精度が1%以上変化しうる)関数を除外しています。

P0870R2 : A proposal for a type trait to detect narrowing conversions

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

これは例えば、std::optionalstd::variantのようなラッパー型において、縮小変換が起こる場合に変換や構築を禁止する制約をかけるのに利用できます。

意図しない縮小変換の発生は実行時において浮動小数点数の精度低下などの発見しづらいバグにつながります。縮小変換(発生の可能性)をコンパイル時に検出し禁止しておくことで、C++の型システムをユーザーの手によって多少ロバストにして運用することができます。

提案されている宣言。

namespace std {
  template <class From, class To>
  struct is_narrowing_convertible;

  template <class From, class To>
  inline constexpr bool is_narrowing_convertible_v = is_narrowing_convertible<From, To>::value;
}

これは例えば、次のように実装できます(提案文書より)。

// そもそも変換不可能な型のペアのためのプライマリテンプレート
template<class From, class To>
inline constexpr bool is_narrowing_convertible_v = false;

// 縮小変換を検出する
// To t[] = { std::declval<From>() };のような式がエラーとなるかによって縮小変換が起こるかを調べている
template<class T, class U>
concept construct_without_narrowing = requires (U&& x) {
  { std::type_identity_t<T[]>{std::forward<U>(x)} } -> std::same_as<T[1]>;
};

// 変換可能な型のペアはこちらを利用
template<class From, class To> requires std::is_convertible_v<From, To>
inline constexpr bool is_narrowing_convertible_v<From, To> =
  !construct_without_narrowing<To, From>;

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

P1255R6 : A view of 0 or 1 elements: views::maybe

std::optionalやポインタ等のmaybeモナドな対象を、その状態によって要素数0か1のシーケンスに変換するRangeアダプタviews::maybeの提案。

例えば、std::optionalのシーケンスを無効値を持つかによってフィルタする処理をわざわざ書く必要がなくなったり、std::optionalの状態をチェックして中身を取り出して・・・といったお決まりのコードを隠蔽することができます。

{
  auto&& opt = possible_value();  // optionalを返す関数
  if (opt) {
      
      // 数十行の処理が挟まっていたとすると・・・

      use(*opt); // ここでのデリファレンスは有効かがすぐに分からなくなりがち
  }
}

// ↑これが↓こう書ける

for (auto&& opt : views::maybe(possible_value())) {
  
  // 数十行の処理が挟まっていたとしても・・・

  use(opt); // すでにデリファレンスされており、有効値が得られている
}

views::maybeを通した場合、possible_value()が無効値を返した場合はループが実行されません(シーケンスが空なので)。

std::vector<int> v{2, 3, 4, 5, 6, 7, 8, 9, 1};

auto test = [](int i) -> std::optional<int> {
  switch (i) {
    case 1:
    case 3:
    case 7:
    case 9:
      return i;
    default:
      return {};
  }
};


auto&& r = v | ranges::views::transform(test)
             | ranges::views::filter([](auto x){return bool(x);})
             | ranges::views::transform([](auto x){return *x;})
             | ranges::views::transform(
                [](int i) {
                  std::cout << i;
                  return i;
                }
               );

// ↑これが↓こう書ける

auto&& r = v | ranges::views::transform(test)
             | ranges::views::transform(views::maybe) //0か1要素のシーケンスのシーケンスになる
             | ranges::views::join                    //シーケンスのシーケンスを1つのシーケンスに平滑化する
             | ranges::views::transform(
                [](int i) {
                  std::cout << i;
                  return i;
                }
               );

P1315R5 : secure_clear

特定のメモリ領域の値を確実に消去するための関数secure_clear()の提案。

パスワード等のセキュアなデータを扱う場合、不用になったらすぐにその内容を消し去り、コアダンプ等によってキャプチャ可能な時間を少しでも短くする必要があります。このことは、近年の脆弱性(MeltdownやSpectre等)の影響によって重要度が増しています。

しかし、単純にメモリ領域をクリアするだけの処理はその領域がその後使用されない事からコンパイラの最適化によって削除される可能性があります。

void f()
{
  constexpr std::size_t size = 100;
  char password[size];

  // セキュアなデータの取得
  getPasswordFromUser(password, size);

  // 取得したデータの仕様
  usePassword(password, size);

  // 取得したデータの削除
  std::memset(password, 0, size);
}

この様な問題(すなわちコンパイラ最適化)を回避するのにはいくつもの方法がありますが、それらの方法は非自明であったり、移植性が無く容易に利用できるものではなかったりします。

そのような機能を標準によって提供しポータブルかつ容易に利用できるようにするために、secure_clear()関数を提案しています。

namespace std {
  template <class T>
    requires is_trivially_copyable_v<T>
        and (not is_pointer_v<T>)
  void secure_clear(T & object) noexcept;
}

効果は上に示した通り、受け取った参照先のオブジェクトの占めるメモリ領域をゼロクリアします。

なお、この提案は同時にC標準に対しても行われているようです(N2505)。

void secure_clear(void * data, size_t size);

こちらはポインタとゼロクリアする領域サイズを取ります。

C++からはstd::secure_clear()としてこの2つのオーバーロードが利用可能になります(採択されれば)。

P1641R3 : Freestanding Library: Rewording the Status Quo

フリースタンディング処理系に要求されるライブラリ機能についての文言を変更する提案。

現在はライブラリヘッダ毎にフリースタンディングで要求されるかを規定していますが、ヘッダの一部分の機能および対応する機能テストマクロを個別にフリースタンディング指定することができるように文言を追加・変更しようというもの。主に<cstdlib>の文言を改善するのが目的っぽい?

P1654R1 : ABI breakage - summary of initial comments

ABIの破損問題について、委員会メンバからのコメントをまとめた報告書。

C++標準がABI破損を伴う変更を受け入れるのか、どのように受け入れるのかについて、次の4つのケースが考えられます。

  1. ABIを絶対に壊さない
    • 実行パフォーマンスが低下するが、もっとも安定している。何も懸念がなければこれを選択すべき
  2. ケースバイケース(例 : std::stringのSSO)
    • 以前に行ったことがあるが、ユーザーはそれが行われることを予測できない
  3. 特定のリリースを境界としてABI破壊を許可する(例えば12年毎など)
    • 試したことはない、適切な期間とは何か?
  4. 任意のタイミングで自由にABIを破壊する
    • 一番素早く動けて実行パフォーマンスを高められるが、安定性がもっとも低い

どれを選ぶのかを慎重に検討するために委員会メンバからのコメントを募集し、過去に行われたABI破壊や、ABI破壊を理由に採択されなかった提案について、また将来必要になるかもしれないABI破壊などについてまとめられています。

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

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

現在C++では識別子に使用可能なUnicode文字列をコードポイントの範囲として規定していますが、その中にはゼロ幅文字など人間の目で見て区別できない文字が含まれてしまっており、万が一使用されればバグの元となりえます。そのため、それらの使用を禁止しそのような文字列が使用されていた場合はコンパイルエラーにすることを提案しています。

Unicode Standard Annex 31というのはどうやら、プログラミング言語において汎用的に識別子として使用可能な文字列および文字列の形式を定めたものです。C++11時点ではこれは安定しておらず使用されませんでしたが、現在は安定しており後々のUnicodeの規格で破壊的な変更が行われないことが保証されるようになっているようです。

そのため、それを参照して識別子の構文を規定することで識別子として適切な文字だけが使用できるように標準を変更します。

Unicode Standard Annex 31で規定されている識別子の構文規則(EBNF)は次のようになります。

<Identifier> := <Start> <Continue>* (<Medial> <Continue>+)*

ここで、<Start>XID_Startという特定の文字(コードポイント)の集合、<Continue>XID_Continueという特定の文字の集合、<Medial><Continue>の文字の間に現われることができる文字の集合です。

C++では、<Start>_(U+005F、アンダーバー)を追加し<Medial>は空になります(<Continue>はそのまま)。上記の文法に照らせば、次のようになります。

<Identifier> := <Start> <Continue>*
<Start> := XID_Start + U+005F
<Continue> := <Start> + XID_Continue

XID_Startにどんな文字が含まれているのか及びXID_Continueにどんな文字が含まれているのかは正直良く分からないくらい大量の文字がありますが、多分制御文字やゼロ幅文字はないはずで、絵文字も含まれていないようです。

また、採択されたとしたら、これらのことは欠陥報告としてC++20以前のバージョンに遡って適用されることになりそうです。

P2011R1 : A pipeline-rewrite operator

x |> f(y);f(x, y);と評価する新しい演算子|>の提案。

この演算子オーバーロード可能ではなく、右辺の値を左辺の関数呼び出しの第一引数に渡すように式全体を書き換えるだけです。

Unified Function Call Syntax(UFCS)に近いものに見えますが、この演算子による書き換えは常に非メンバ関数を呼び出します。

x->f(y);    // メンバ関数f()を呼び出す  
x.f(y);     // メンバ関数f()を呼び出す
x |> f(y);  // 非メンバ関数f()を呼び出す

一見すると何の意味があるのか分からない演算子ですが、Rangeのパイプライン演算子|にまつわる以下の様な諸問題を解決するためのものです。

Rangeのパイプライン演算子がやっていることは要するに、左辺の式の結果オブジェクトを右辺の関数の第一引数に渡すようなもので、それによって関数呼び出しのネストを分解しています。

#include <iostream>
#include <vector>
#include <ranges>

int main()
{
  std::vector v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  
  // 偶数を取り出して、2倍して、逆順にする
  // 適用順と逆順になるうえ、やることが多くなるとネストしまくり可読性がしぬ
  auto&& range = std::views::reverse(
                   std::views::transform(
                     std::views::filter(v, [](auto n){ return n % 2 == 0;}),
                     [](auto n) { return n * 2;}
                   )
                 );
  
  for (auto e : range)
  {
    std::cout << e << std::endl;
  }
}

//↑これを↓のように書ける

int main()
{
  std::vector v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  
  // パイプラインスタイル
  // 適用順と同じ順番で縦に並べられるので見やすい!
  for (auto e : v | std::views::filter([](auto n){ return n % 2 == 0;})
                  | std::views::transform([](auto n) { return n * 2;})
                  | std::views::reverse)
  {
    std::cout << e << std::endl;
  }
}

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

ここで、2つのスタイルの例に現れているfiltertransform等の関数はそれぞれ異なるオーバーロードが使用されています(例えば、filter(Rng&&, Pred&&)filter(Pred&&))。|演算子はあくまで演算子オーバーロードでありその呼び出しよりも引数に与えられた式の評価が先になるので、パイプラインスタイルの時に|演算子の右辺に来る関数(filter(Pred&&))は渡された関数オブジェクトを|に引き渡す為のラッパを生成するだけの処理になります。一方、第一引数にrangeオブジェクトが直接渡っている最初の例(filter(Rng&&, Pred&&))では受け取った処理の適用準備の済んだrange viewオブジェクトを返します。

どちらが読みやすいかを考えるとパイプラインスタイルの威力は圧倒的ですが、この裏側では大量の黒魔術が発動しています・・・

このような|の行なっていることを式の書き換えによって行う|>演算子を言語サポートすることで、パイプライン演算子を使用するためのそのような黒魔術コードを削減することができ、それに伴う諸問題の解決を図ることができます。

|>演算子でも|と同様に書けます。

#include <iostream>
#include <vector>
#include <ranges>

int main()
{
  std::vector v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  
  for (auto e : v |> std::views::filter([](auto n){ return n % 2 == 0;})
                  |> std::views::transform([](auto n) { return n * 2;})
                  |> std::views::reverse)
  {
    std::cout << e << std::endl;
  }
}

|>演算子の場合はこれを一番最初の関数呼び出しネストコードに書き換えることによって|演算子と同じことを達成します。これによって演算子オーバーロードもそれに対応するためにfilter等に不要なオーバーロードを追加する必要もなくなります。

また、|>の両辺は書き換え前に評価されません。つまり、他の演算子とは少し振る舞いが異なります。オーバーロード不可能とされているのはこの性質によります。

P2013R1 : Freestanding Language: Optional ::operator new

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

フリーストア(ヒープ)を持たないかその使用が著しいオーバーヘッドとなる環境では、意図しない::operator newが使用された場合にコンパイルエラー(リンクエラー)となってほしい場合があります。また、OSのカーネルの動作環境のように、メモリ割り当てを正しく行う方法が無い環境でも同様です。このような環境ではすでに::operator newの実装を提供できておらず、結果としてそれらの環境のC++ユーザーは::operator newを使わないか、(使うために)独自実装をするかの2択を迫られているのが現状です。そのため、あえて::operator newを定義しないという選択肢を標準化し、その場合の振る舞いを規定する必要がある、というのが要旨です。

提案では、オーバーロード可能なグローバル::operator newの提供を実装定義とし、提供するならば全てのオーバーロードを提供する必要があるが、提供しない場合は全てのオーバーロードを提供しない、という規定を追加します。結果として、::operator newが提供されない場合、プログラム中でのそれらの使用はill-formedであり、おそらくリンカエラーを引き起こします。
ちなみにそのような場合には、<coroutine>ヘッダはグローバル::operator newに依存しているので存在そのものがill-formedになります(#include or importしなければok)。

なお、::operator deleteは仮想デストラクタにおいて参照されるのでそのままであり、constexpr newは使用可能となるように文言が調整されています。

記載されているEWG等での投票結果を見るに受け入れられそうな雰囲気です。

P2034R1 : Partially Mutable Lambda Captures

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

パフォーマンスが求められるコールバック関数オブジェクトではその内部に個別に利用するローカルメモリを持つことがあります。また、mutex等の参照をメンバに持つこともあるでしょう。それらは外部から観測不可能な内部状態であり、そのオブジェクトは意味論的にはimmutableです。

struct MyRealtimeHandler {
private:
  const Callback callback_;
  const State state_;
  mutable Buffer accumulator_;

public:
  void operator()(Timestamp t) const {
    callback_(state_, accumulator_, t);
  }
}; 

struct MyThreadedAnalyzer {
private:
  const State& state_;
  std::mutex& mtx_;

public:
  void operator()(Slice slice) const {
    std::lock_guard<std::mutex> lock{mtx_};
    analyze(state_, slice);
  }
};

例えばこの様な典型的な型はラムダ式を使えば定義を必要とせずに簡単に書くことができますが、現在はこの様に部分的にmutable/非constなメンバを持つようなラムダ式を書くことが出来ません。全部constが全部mutableかの二者択一です。

提案では、次のようにキャプチャを個別にmutable指定できるようにします。

auto a = [mutable x, y]() {}; 

// ↑は↓と等価

struct A {
  mutable X x;  // Xが参照型(xが参照キャプチャ)なら単に非const参照になる
  const Y y;

  void operator()() const {}
} a; 

この様に、その物理的な状態を変更したとしてもそのオブジェクトの論理的(意味論的)な状態を変更しないような不変オブジェクト(に対する操作)の事をlogical constと呼びます。

さらに、std::any_invocableというCV修飾やnoexceptを指定できるstd::functionが議論されており、それを踏まえるとlogical constラムダ式はより必要とされます。

また、この提案では更なる議論を前提としていて、他にも以下の様な書き方を可能にすることが提案されています。

// constキャプチャとmutable呼び出し
auto b = [x, const y]() mutable {}; 

// 参照のconstキャプチャ
auto b = [&x, const &y]() {};

// const呼び出し(コンパイルエラーにならないようにする)
auto c = [x]() const {}; 

// constキャプチャとconst呼び出し
auto c = [const x]() const {};

// mutableキャプチャとmutable呼び出し
auto c = [mutable x]() mutable {}; 

P2044R2 : Member Templates for Local Classes

ローカルクラスでメンバテンプレートを使用できるようにする提案。

ローカルクラスとは関数の中で定義されたクラスで、関数テンプレートと利用するとインターフェースの自動実装を行えたりとなかなか便利なやつです。ローカルクラスのスコープはその関数の中に閉じられ、関数外部からクラス名を参照することはできないため名前が衝突したりせず、インターフェースクラスを継承して自動実装する場合はアップキャストされるのを完全に防止できます。なお、ローカルクラスのメンバは普通に外から参照できます。

しかし、ローカルクラスでは囲む関数テンプレートのテンプレートパラメータなど囲む関数からアクセスできるものは全てアクセスできますが、メンバテンプレートを持つことができないなどいくつかの制限があります(ローカルクラス自体がテンプレートになることはできます)。

ローカルクラスがメンバテンプレートを持つことができるようになると、あるインターフェースを別のインターフェースへ変換するアダプタの自動生成などをローカルクラスで書くことができるようになります。

// operator()をcall()に変換するアダプタを生成する
template<typename Callable>
auto callable_to_call(Callable&& f) {
  // ローカルクラス
  struct call_impl {
    Callable m_f;

    // 今はこれができない・・・
    template<typename... Args>
    auto call(Args&&... args) {
      return m_f(std::forward<Args>(args)...);
    }
  };

  return call_impl{std::forward<Callable>(f)};
}

実装クラスを関数外部に持っておいてもいいのですが、クラス名が漏洩しないというのと、見た目的にも気持ち的にもコードがコンパクトになるのが個人的お好みポイントです。

MSVCの実装者はこの変更は問題ないと言っているそうですが、Clangは熱心にテンプレートの実体化を行う結果思わぬコンパイルエラーが起こる可能性があるとのことです。しかし、すでにジェネリックラムダが限定的とはいえ同じことが可能になっているのであまり壁は高くなさそうです。

P2096R1 : Generalized wording for partial specializations

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

現在の書き方だと変数テンプレートの部分特殊化についてが不透明なので、クラステンプレートの部分特殊化に関する文言を一般化して変数テンプレートの部分特殊化を規定するように文言を調整しています。これが通ったとしても多くのユーザーにとっては関係ない話です。パッと見では、クラステンプレートの部分特殊化やプライマリクラステンプレート、などと書かれていたところからクラステンプレートというワードが消されています。仮に採択された場合はこの辺を読むときは注意しないと分かりづらいかもしれません。

P2098R1 : Proposing std::is_specialization_of

std::complex<T>std::complex<double>のように、ある型Tが別の型Pの特殊化となっているかを調べるメタ関数is_specialization_of<T, P>の提案。

例えばテンプレートの文脈で、std::complex<T>std::optional<T>std::vector<T>など、その要素型はともかくとして型がその特殊化であるかを知りたい!という場合はよくあります。幸いこれを判定するメタ関数を書くのは難しくないのでテンプレート好きな人は多分1度は書いたことがあるでしょう。そのように、よく利用されるものであるので標準に追加しようという提案です。ただし、あるテンプレート毎に個別にそのようなメタ関数を追加するわけにはいかないので、より一般化した任意の型のペアの間でそれを判定するものを追加します。

template<class T, template<class...> Primary>
struct is_specialization_of;

template<class T, template<class...> Primary>
inline constexpr bool is_specialization_of_v = is_specialization_of<T,Primary>::value;

例えば次の用に使います。

// そのまま使う
static_assert(std::is_specialization_of_v<T, std::optional>);

// 特定のテンプレート用に特殊化
template< class T >
inline constexpr bool is_complex_v = is_specialization_of_v<T, std::complex>;

ただ、std::array<T, N>のように非型テンプレートパラメータを取るものは判定できません。それは諦めているようです。また、これはクラスの継承関係を判定するものではありません。

P2138R1 : Rules of Design<=>Wording engagement

(タイトルの<=>は宇宙船演算子ではありません)
CWGとEWGの間で使用されているwording reviewに関するルールの修正と、それをLWGとLEWGの間でも使用するようにする提案。

C++標準会員会の作業プロセスの改善に関するお話なので、完全にユーザーには関係ありません。CWGとかEWGとかは次の図参照。

WG21 組織図

コア言語の提案はEWG(Evolution Working Group)で基礎設計が詰められてからCWG(Core Working Group)へ送られ、CWGでは標準としての文言の確認と調整を行います。その際、EWGである程度設計に基づく文言が整っていることが要求されますが、設計を文言が表現しきれていなかったり、議論していない文言が含まれていたり、とそうなっていない事があったようです。
そのため、EWGとCWGの間ではそう言う事が無いようにするためのルールが設けられていました。とはいえ、そのルールは文書化されたものではなかったためか、CWGに送られた段階でしっかりと文言が整っていない事がまだあるようです。

EWGが設計とそれを表現する文言を決定しCWGは文言を確認するだけ、という役割分担を明確にしCWGの時間を無駄にしないようにするためにルールを変更し、同じことをLWG(Libraly Working Group)とLEWG(Libraly Evolution Working Group)の間でも行うようにする。というのが提案の要旨です。

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

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

C++20現在、バイナリファイルのIOをやろうとするとiostreamを使用することになりますが、iostreamもベースにあるCのIO関数もテキストストリームへの入出力前提なものをバイナリモードという特殊な状態にしたうえでバイナリIOに使用することになるので、使いづらく、また非効率です。

#include <fstream>

int my_value = 42;
{
  std::ofstream stream{"test.bin", std::ios_base::out |    std::ios_base::binary};
  stream.write(reinterpret_cast<const char*>(&my_value), sizeof(my_value));
}

int read_value;
{
  std::ifstream stream{"test.bin", std::ios_base::in |  std::ios_base::binary};
  stream.read(reinterpret_cast<char*>(&read_value), sizeof(read_value));
}

assert(read_value == my_value)

これには以下の欠点があります。

  • std::byte非対応のため、reinterpret_cast<const char*>が必要
  • バイト数を明示的に指定しなければならない
  • バイトの読み書きにエンディアンを考慮してくれない(するようにできない)
  • std::char_traitsが使われるがバイナリIOには不要、std::ios::pos_typeは多くのIO操作に必要だが使いづらい。
  • バイナリIOに必要なのは常にstd::ios_base::binary、オープンモード指定は不用
  • ストリームオブジェクトはテキスト形式フラグをいくつも持っているが、バイナリIOには不要。メモリの無駄
  • デフォルトのストリームは例外を投げない。これはストリーム状態を調べて例外を発生させるラッパーコードを追加する手間の元
  • メモリ内で完結するIOのためにstd::stringを使用するstd::stringstreamが用意されているが、無駄なコピーが発生するなど使いづらい。バイナリデータはほとんどの場合std::vector<std::byte>が適当であり、spanで参照すれば十分
  • 現行のiostreamには、バイナリIOとシリアライズのためのカスタマイゼーションポイントが無い

これらの欠点をすべて解決したバイナリIOのための新ライブラリの導入を目指すのがこの提案です。

生バイト列のIOサンプル

#include <io>
#include <iostream>

int main() {
  
  // 書き込むバイト列
  std::array<std::byte, 4> initial_bytes{
    std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}
  };

  {
    // 書き込み用にファイルオープン
    std::io::output_file_stream stream{"test.bin"};
    // 書き込み
    std::io::write_raw(initial_bytes, stream); 
  } // RAIIによってストリームが閉じられる

  // 読み込み用バイト列
  std::array<std::byte, 4> read_bytes;
  
  {
    // 読み込みのためにファイルオープン
    std::io::input_file_stream stream{"test.bin"};
    // 読み込み
    std::io::read_raw(read_bytes, stream); 
  } // RAIIによってストリームが閉じられる

  // 読み込んだバイト列の比較
  if (read_bytes == initial_bytes) { 
    std::cout << "Bytes match.\n"; 
  } else { 
    std::cout << "Bytes don't match.\n"; 
  }
}

カスタマイゼーションポイントによる任意クラスのカスタムシリアライズエンディアン指定のサンプル。

#include <io>
#include <iostream>

struct MyType {
  int a; 
  float b;

  void read(std::io::input_stream auto& stream) {
    // ビッグエンディアンでメンバ変数にストリームから値を読み出す 
    std::io::default_context context{stream, std::endian::big};
    std::io::read(a, context);
    std::io::read(b, context);
  }

  void write(std::io::output_stream auto& stream) const {
    // ビッグエンディアンでメンバ変数の値をストリームに書き出す
    std::io::default_context context{stream, std::endian::big}; 
    std::io::write(a, context);
    std::io::write(b, context);
  }
};

int main() {
  MyType my_object{1, 2.0f};
  std::io::output_memory_stream stream;

  // std::io::writeはカスタマイゼーションポイントオブジェクト
  // メンバ関数か同じ名前空間の非メンバ関数のwrite()を探して呼び出す
  // 対になるstd::io::readも同様
  std::io::write(my_object, stream);

  // ストリームのバッファを取得し、内容をバイト列として書き出す
  const auto& buffer = stream.get_buffer();
  for (auto byte : buffer) {
    std::cout << std::to_integer<int>(byte) << ' ';
  }
  std::cout << '\n'
}

他にも、spanやメモリのためのI/Oストリームが用意されていたり(これらはconstexpr対応!)、エンディアンを途中で切り替え可能だったり、整数型の特殊なフォーマット(LEB128など)をサポート可能だったり、ISO 60559以外もサポート可能な浮動小数点数バイナリフォーマット変換も考慮されていたり(ドロップされそうですが)、コンセプトベースだったりとイケてる雰囲気のライブラリです。

筆者の方が並行してリファレンス実装を作っています。なかなか本気のようです。

P2149R0 : Remove system_executor

Networking TSからsystem_executor​system_contextを削除する提案。

system_context::get_executor()はデフォルト構築したsystem_executor​を返して、そのメンバ関数であるsystem_executor::context()は静的記憶域期間に配置された(つまりグローバル変数の)system_context​オブジェクトへの参照を返します(これは必ずしもMeyer’s singletonではないかもしれない、つまり本物のグローバル変数かもしれない)。

しかも、そのようなグローバルなsystem_context​オブジェクトはmutableです。

グローバルなオブジェクトであるがゆえに、それを利用するユーザーのコンポーネントのRAIIとは無縁の所で動いています。プログラム、あるいはコンポーネントの終了時にそのグローバルsystem_context​に何かしなければいけないかどうかは、system_context​とやり取りをしたコンポーネントが自分も含めて存在しているかによって決まります。また、この様なグローバルなオブジェクトにはその構築と破棄の順序の不定性など様々な問題があります。

Networking TSの仕様ではsystem_contextを直接使用するのはsystem_executorだけで、system_executorassociated_executor(_t)の仕様においてフォールバックExecutorとして使用されています。

従って、グローバルな状態に依存しないような代わりのexecutorを用意して、現在のsystem_executor​system_contextを削除しよう、という事のようです(良く分かりません・・・)
本質的には、グローバル変数として複雑な状態を持ってしまっていることが問題のようです。

P2150R0 : Down with typename in the library!

標準ライブラリのパートから不用なtypenameを消し去る提案。

C++20からいくつかの場所でtypenameが不用になったのに伴って(P0634R3 : Down with typename!)、標準ライブラリの規定部分からも取り除こうという話です。

どこで不要になるかはこのページを参照。

P2155R0 : Policy property for describing adjacency

進行中のExecutor(簡単に言えばスレッドプールサポートライブラリ)に関するもので、NUMAのようなアーキテクチャ向けに、スレッドとそこで使用するメモリを同じノード内で確保しバインドするように指示するポリシーを追加する提案。

NUMAでは1つのプロセッサとそこに接続されたローカルメモリを1ノードとして、複数のノードで構成されることになりますが、そのシステム上での論理スレッド(OS上プロセスのスレッド)はOSによって任意のノードの物理スレッド(CPUコア)に割り当てられる可能性があり、また、そのスレッド内で確保し使用しているメモリはそのスレッドを実行している物理スレッドの属するノードとは別のノードに属するメモリを使用している可能性があります。

OSのスケジューリングによってこれはほとんど予測不可能となりますが、ノードを超えたスレッドスケジュールやメモリアクセスは当然ノード内で行われるよりも高コストになり、全体のパフォーマンスに影響を与えます。この様な実行スレッドに対する割り当てメモリの位置の事をメモリアフィニティ(memory afinity)、あるいは単にアフィニティと呼びます。

このようなことが起こりえる場合にもパフォーマンスを向上させるための1つの方法は、ある論理スレッドを物理スレッドとそのローカルメモリにバインドしスケジューリングやメモリ割り当てをあるノード内で完結するように強制してしまう事です。

NUMAの様なシステムにおいてC++開発者が現在および将来のアーキテクチャに渡って最高のパフォーマンスを得るためには、この様なスレッドとメモリの配置の制御をC++標準機能としてネイティブサポートする必要がある、というのが提案の要旨です。

次のようなadjacencyプロパティグループを定義しておき、これを実行ポリシーに与えることで、Excecutor実装に対してアフィニティ制御に関するヒントを提供できるようにします。

namespace std {
namespace experimental {
namespace execution {

  struct adjacency_t {
    struct no_implication_t;
    struct constructive_t;
    struct destructive_t;
  
    // デフォルト、普通にアフィニティ制御をしてほしい
    constexpr no_implication_t no_implication;

    // 以下二つは、隣接するワークアイテム(スレッド?)を離した上でアフィニティ制御を行うかを指定する
    // キャッシュラインの配置までコントロールするか否か?

    // 実行する処理はconstructive interferenceの恩恵を受けうる
    // すなわち、参照局所性が重要
    constexpr constructive_t constructive;

    // 実行する処理はdestructive interferenceの恩恵を受けうる
    // すなわち、false sharingが問題になる
    constexpr destructive_t destructive;

  };

  constexpr adjacency_t adjacency;

} // execution
} // experimental
} // std

このように、アフィニティ制御をどのように行うかを指定するポリシーを渡すことで実装へのヒントとし、実装の抽象化度を維持し移植性を持たせたまま必要なら高パフォーマンスな実装を選択できるようになります。

提案文書よりサンプルコード(Executor分からないから読めない

// bulk algorithmの各インデックスについて、そこで使用されるメモリ領域用のポインタ列
std::vector<std::unique_ptr<float>> data{}; data.reserve(SIZE); 
 
// NUMA対応Executorの作成
numa_executor numaExec; 
 
// bulk algorithmの各実行に与えるインデックス
auto indexRng = ranges::iota_view{SIZE}; 
 
// std::par実行ポリシーに加えてadjacency.constructiveプロパティを要求する新しい実行ポリシーを作成
// 実装に対して、実行する処理はconstructive interferenceの恩恵を受けうることをヒントとして与える
// adjacencyプロパティはここで指定する
auto adjacencyPar = std::execution::require(std::par, adjacency.constructive); 
 
// bulk algorithmの各実行毎に初期化を行うCallableオブジェクト
auto initialize = [=](size_t idx, std::vector<unique_ptr<float>> &value) {
  value[idx] = std::make_new<float>(0.0f);
}; 

// 実行する処理内容
auto compute = [=](size_t idx, std::vector<unique_ptr<float>> &value) {
  do_something(value[idx]);
};
 
// 入力となるdataを受けて、NUMA対応Executorを使用してスケジューリングし、
// indexed_forによって、まず初期化を行いその後で計算を行うsenderを作成
// 実行ポリシーはここで指定する
auto sender = std::execution::just(data)
            | std::execution::via(numaExec)
            | std::execution::indexed_for(indexRng, adjacencyPar, initialize)
            | std::execution::indexed_for(indexRng, adjacencyPar, compute); 
 
// senderをExecutorへ送り、結果を待機
std::execution::sync_wait(sender, std::execution::sink_receiver{}); 

P2156R0 : Allow Duplicate Attributes

属性指定時に同じ属性を重複して指定しても良いようにする提案。

現在の規定では、一つの属性指定[[]]の中で同じ属性が複数回現れることは出来ません。しかし、属性指定を複数に分割すれば同じ属性が何回重複してもokです。

// ng
[[noreturn, carries_dependency, deprecated, noreturn]]
void f();

// ok
[[noreturn]] [[carries_dependency]] [[deprecated]] [[noreturn]]
void g();

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

この挙動は一貫していないので、属性指定の重複を認める(上記NGの例f()を適格にする)方向に変更すべし、という提案です。

EWGの見解としては、属性指定を分ければ重複可能なのはマクロによって属性を条件付きで追加していくことをサポートするためのもので、一つの属性指定のなかでそれを行う事はレアケースなのでこの制限を解除する必要はない、という事。

しかし、これをそのままにしておくと、重複不可能な属性を標準に追加するたびにその旨を一々記述しておく必要があり、逆に重複可能な属性に対しては重複した時の振る舞いを記述しておく必要が生じます。これは明らかに標準を太らせ望ましくないので重複可能をデフォルトにするべき、というのが筆者の主張です。また、これは欠陥として過去のバージョンにさかのぼって適用されるのが望ましいとも述べています。

onihusube.hatenablog.com

この記事のMarkdownソース

[C++]to_chars()とfrom_chars()ってはやいの??

<charconv>ヘッダはC++17から導入されたヘッダで、ロケール非依存、動的確保なし、例外なげない、などを謳ういいことづくめで高速な文字列⇄数値変換を謳う関数が提供されています。現在フルで実装しているのはMSVCだけですが、実際速いってどのくらいなの?既存の手段と比べてどうなの??という辺りが気になったので調べてみた次第です。

計測環境

  • Core i7 7700T HT有効OCなしTBあり 16GBメモリ
  • Windows 10 1909 18363.778
  • VisualStudio 2019 update 6 preview 3

一応電源プランを高パフォーマンスにして、VS以外を終了させた状態で計測。ビルドは/std:c++latestを追加したリリースモードで行っています。

std::to_chars()

std::to_chars()は数値を文字列へ変換する関数です。出力はcharの文字列限定で、戻り値を調べることでエラーの有無と文字列長が分かります。

これとの比較対象は以下のものです。これらは同じく数値→文字列への変換を行います。

  • std::to_string()
  • std::stringstream
  • snprintf()

測定方法

100万件のランダムな数値を用意してそれを1つづつ全件変換にかけ、それにかかる時間を計測します。それを10回繰り返して各種統計量で見てみることにします。整数型(64bit整数型)と浮動小数点型(double)それぞれで実験を行います。

コードは以下のようになります。

#include <iostream>
#include <charconv>
#include <vector>
#include <random>
#include <chrono>
#include <type_traits>
#include <cassert>
#include <thread>
#include <numeric>
#include <string>
#include <sstream>

template<typename NumericType>
auto make_data(unsigned int N) -> std::vector<NumericType> {
  
  auto rng = []() {
    if constexpr (std::is_integral_v<NumericType>) {
      return std::uniform_int_distribution<NumericType>{};
    } else if constexpr (std::is_floating_point_v<NumericType>) {
      return std::uniform_real_distribution<NumericType>{};
    } else {
      static_assert([] { return false; }, "You have to specify a number type, right?");
    }
  }();
  
  std::mt19937_64 urbg{ std::random_device{}() };
  
  std::vector<NumericType> vec;
  vec.reserve(N);

  for (auto i = 0u; i < N; ++i) {
    vec.emplace_back(rng(urbg));
  }

  return vec;
}

template<typename Container>
void report(Container&& container) {
  const auto first = std::begin(container);
  const auto last = std::end(container);
  const auto N = std::size(container);

  using value_type = typename std::iterator_traits<std::remove_const_t<decltype(first)>>::value_type;

  std::sort(first, last);

  const auto max = *(last - 1);
  const auto min = *first;

  std::cout << "min : " << min.count() << " [ms]" << std::endl;
  std::cout << "max : " << max.count() << " [ms]" << std::endl;

  const auto medpos = first + (N / 2);
  std::cout << "median : " << (*medpos).count() << " [ms]" << std::endl;
  
  const auto sum = std::accumulate(first, last, value_type{});
  const auto ave = sum.count() / double(N);
  std::cout << "average : " << ave << " [ms]" << std::endl;

  const auto var = std::inner_product(first, last, first, 0ll, std::plus<>{},
    [](auto& lhs, auto& rhs) {return lhs.count() * rhs.count(); }) / N - (ave * ave);
  std::cout << "stddev : " << std::sqrt(var) << "\n" << std::endl;
}

template<typename NumericType, typename F>
void profiling(const char* target, F&& func) {
  using namespace std::chrono_literals;

  constexpr int trialN = 10;
  constexpr int sampleN = 1'000'000;

  std::chrono::milliseconds results[trialN]{};

  for (int i = 0; i < trialN; ++i) {
    //データの準備
    auto input = make_data<NumericType>(sampleN);

    //計測開始
    auto start = std::chrono::steady_clock::now();

    for (auto v : input) {
      func(v);
    }

    //計測終了
    auto end = std::chrono::steady_clock::now();
    results[i] = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::this_thread::sleep_for(200ms);
  }

  std::cout << target << std::endl;
  report(results);
}

int main()
{
  char buf[21];
  auto first = std::begin(buf);
  auto last = std::end(buf);

  profiling<std::int64_t>("to_chars() int64", [first, last](auto v) {
    auto [ptr, ec] = std::to_chars(first, last, v);
    if (ec != std::errc{}) throw new std::exception{};
  });
}

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

全部入りのコードはこちら

整数(std::int64_t

単位は全てms

方法 最小値 最大値 中央値 平均値 標準偏差
std::to_chars() 27 36 28 29.2 2.71293
std::to_string() 79 98 81 82.4 5.31413
std::stringstream 525 602 533 543.9 22.8427
snprintf() 236 246 240 240 2.44949

浮動小数点数double

方法 最小値 最大値 中央値 平均値 標準偏差
std::to_chars() 45 51 46 47.3 1.92614
std::to_string() 481 517 488 492.8 10.6846
std::stringstream 617 761 628 639.1 41.1241
snprintf() 245 264 250 250.4 4.98397

圧倒的じゃないか!というくらいにぶっちぎりでto_chars()最速です。

std::from_chars()

std::from_chars()は文字列から数値へ変換するものです。入力はcharの文字列限定で出力は引数に取った数値型変数への参照で返します。戻り値の扱いなどはto_chars()と似た感じです。

これとの比較対象は以下のものです。これらは同じく文字列→数値への変換を行います。

  • std::stoll()/std::stod()
  • std::stringstream
  • sscanf()
  • strtoll()/strtod()

測定方法

100万件のランダムな数値をto_chars()で文字列に変換しておき、それを全件変換にかけかかる時間を計測します。それを10回繰り返して各種統計量で見てみることにします。整数型(64bit整数型)と浮動小数点型(double)それぞれで実験を行います。

先ほどの処理と似たようなことになります。

#include <iostream>
#include <charconv>
#include <vector>
#include <random>
#include <chrono>
#include <type_traits>
#include <cassert>
#include <thread>
#include <numeric>
#include <string>
#include <string_view>

template<typename NumericType>
auto make_data(unsigned int N) -> std::pair<std::vector<char>, std::vector<std::string_view>> {
  
  auto rng = []() {
    if constexpr (std::is_integral_v<NumericType>) {
      return std::uniform_int_distribution<NumericType>{};
    } else if constexpr (std::is_floating_point_v<NumericType>) {
      return std::uniform_real_distribution<NumericType>{};
    } else {
      static_assert([] { return false; }, "You have to specify a number type, right?");
    }
  }();
  
  std::mt19937_64 urbg{ std::random_device{}() };
  
  std::vector<char> buffer(N * 21);
  auto* pos = buffer.data();
  std::vector<std::string_view> vec;
  vec.reserve(N);

  for (auto i = 0u; i < N; ++i) {
    const auto num = rng(urbg);
    const auto [end, ec] = std::to_chars(pos, pos + 21, num);
    if (ec != std::errc{}) {
      --i;
      continue;
    }
    
    const std::size_t len = end - pos;
    vec.emplace_back(pos, len);
    pos += (len + 1);
  }

  return {std::move(buffer), std::move(vec)};
}

/*
report()関数は変更ないので省略
*/

template<typename NumericType, typename F>
void profiling(const char* target, F&& func) {
  using namespace std::chrono_literals;

  constexpr int trialN = 10;
  constexpr int sampleN = 1'000'000;

  std::chrono::milliseconds results[trialN]{};

  for (int i = 0; i < trialN; ++i) {
    //データの準備
    auto [buf, input] = make_data<NumericType>(sampleN);

    //計測開始
    auto start = std::chrono::steady_clock::now();

    for (auto sv : input) {
      func(sv);
    }

    //計測終了
    auto end = std::chrono::steady_clock::now();
    results[i] = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::this_thread::sleep_for(200ms);
  }

  std::cout << target << std::endl;
  report(results);
}


int main()
{
  std::int64_t v;

  profiling<std::int64_t>("to_chars() int64", [&v](auto sv) {
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.length(), v);
    if (ec != std::errc{}) throw new std::exception{};
  });
}

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

全部入りのコードはこちら

整数(std::int64_t

単位は全てms

方法 最小値 最大値 中央値 平均値 標準偏差
std::from_chars() 24 32 26 26.3 2.07605
std::stoll() 129 159 132 134.5 8.23104
std::stringstream 409 438 412 416.5 9.26013
sscanf() 181 196 185 186.6 5.14198
strtoll() 53 57 55 54.7 1.38203

浮動小数点数double

方法 最小値 最大値 中央値 平均値 標準偏差
std::from_chars() 163 196 168 174.2 12.2213
std::stod() 287 316 297 296 8.42615
std::stringstream 445 531 456 464.6 23.5338
sscanf() 339 352 348 346.6 3.66606
strtod() 195 200 197 196.9 1.17898

以外にstrtoll()/strtod()が検討していますが、こちらもfrom_chars()最速です。しかし浮動小数点数変換は際どい・・・

なお、実装をチラ見するにstd::stoll()/std::stod()は対応するstrtoll()/strtod()によって変換を行っているだけなので、その速度差はstd::stringのオブジェクト構築と動的メモリ確保のオーバーヘッドから来るもののようです(std::stringstreamも結局は同じようにstrto~に投げていますが、こっちは更にもう少し色々してるみたいです)。

グラフで見てみる

先程のprofiling()を少し変更して処理時間をCSVに吐き出してグラフ化してみましょう。

template<typename NumericType, typename F>
auto profiling(const char* target, F&& func) -> std::vector<std::chrono::milliseconds> {
  using namespace std::chrono_literals;

  constexpr int trialN = 10;
  constexpr int sampleN = 1'000'000;

  std::vector<std::chrono::milliseconds> results(trialN, 0);

  for (int i = 0; i < trialN; ++i) {
    //データの準備
    auto [buf, input] = make_data<NumericType>(sampleN);

    //計測開始
    auto start = std::chrono::steady_clock::now();

    for (auto sv : input) {
      func(sv);
    }

    //計測終了
    auto end = std::chrono::steady_clock::now();
    results[i] = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::this_thread::sleep_for(200ms);
  }

  std::cout << target << std::endl;

  return results;
}

template<std::size_t N>
void output(const char* filename, std::vector<std::chrono::milliseconds>(&array)[N]) {

  std::ofstream ofs{ filename , std::ios::out | std::ios::trunc};
  //BOM付加
  unsigned char bom[] = { 0xEF, 0xBB, 0xBF };
  ofs.write(reinterpret_cast<char*>(bom), sizeof(bom));

  auto datanum = array[0].size();

  for (auto i = 0u; i < datanum; ++i) {
    for (auto j = 0u; j < N; ++j) {
      ofs << array[j][i].count() << ", ";
    }
    ofs << "\n";
  }
}

int main() {
  std::int64_t v;

  std::vector<std::chrono::milliseconds> res_array[5]{};

  res_array[0] = profiling<std::int64_t>("from_chars() int64", [&v](auto sv) {
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.length(), v);
    if (ec != std::errc{}) throw new std::exception{};
  });
  
  output("from_chars_int64.csv", res_array);
}

グラフは箱ひげ図で見てみます。
各箱は100万件の整数/浮動小数点数値に対する文字列⇄数値変換にかかった処理時間とばらつきを表しており、各箱に含まれるデータ数は10です。

箱の上辺は第三四分位点、下辺は第一四分位点、中央の線が中央値、×点が平均値を表しています。ひげの上辺は最大値、下辺は最小値を表し、それらから外れた孤立点は外れ値を表しています。

これによって、各処理方法毎のおおよその処理時間とそのばらつきを視覚的に確認・比較できます。

なお、データはここまでの実験とは別に取りなおしたものなので数値は上の表と一致していません。しかし、大まかな傾向に変化はないはずです。

std::to_chars()

std::to_chars()の計測結果

std::from_chars()

std::from_chars()の計測結果

to_chars()/from_chars()が他よりも明らかに速い事が改めて確認できます。また、全体の傾向として整数変換よりも浮動小数点数変換の方が重いことも分かります。

相対比較

代表値として平均値を採用し、to_chars()/from_chars()の処理時間を1としたときの他の方法の処理時間の比を見てみます。これによって、系統的な誤差要因を無視したうえで他の方法がto_chars()/from_chars()の何倍遅いか?を見ることができます。

データは上の表にある平均値を利用します。

std::to_chars()

方法 \ 数値型 int64_t double
std::to_chars() 1.00 1.00
std::to_string() 2.82 10.4
std::stringstream 18.6 13.5
snprintf() 8.21 5.29

std::from_chars()

方法 \ 数値型 int64_t double
std::from_chars() 1.00 1.00
std::stoll()/std::stod() 5.11 1.70
std::stringstream 15.8 2.67
sscanf() 7.09 1.99
strtoll()/strtod() 2.08 1.13

例えばこれをもって、std::stringstreamによる数値→文字列変換はstd::to_chars()に比べて、整数で18.6倍、浮動小数点数で13.5倍遅い!などという事ができます。

結論

使えるならto_chars()/from_chars()使いましょう!ちょっぱやです!!

注意点

これはあくまでMSVCの実装における結果なので、GCCやclang等ではまた違った結果になるかもしれません。C++標準はto_chars()/from_chars()の実装については何も規定していないためです・・・

GCCやclangは良い環境が手元に無いのと<charconv>浮動小数点型対応がまだなので実験していません。

参考文献

この記事のMarkdownソース

[C++]コンソール出力にchar8_t文字列を出力したい!

Windows

おそらくほとんどの場合、非Windows環境ではcharエンコードUTF-8なのでそのまま出力できるはずです。しかし、C++20では標準出力ストリームに対するchar8_t char16_t char32_toperator<<deleteされているため、そのままではコンパイルエラーになります。でもまあcharUTF-8なのですから、こう、ちょっとひねってやれば、無事出力できます・・・

#include <iostream>
#include <string_view>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  std::cout << reinterpret_cast<const char*>(u8str.data()) << std::endl;
}

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

問題なのはWindowsさんです・・・

Windowsのコンソール出力と標準出力

C言語ではI/Oをファイルとそれに対するデータのストリームとして抽象化しています。ファイルの読み書きによって動作環境との通信(すなわちI/O)を制御し、規定しています。標準出力(stdout)や標準入力は(stdin)は標準によって予め開くファイルが規定されているストリームで、これらにおいてのファイルとはコンソール(端末)です。

C++もCからこれらのことを継承し、多くのI/O関数はCのものを参照しているため、この辺りの標準I/Oストリームに関することは共通しています。

従って、C/C++の範囲から見た標準出力とはとりあえずは何かのファイルに対する出力として考えることができます。特に、標準出力が受け取った文字列をどう表示するかというところはファイル出力の先の話であり、C/C++が感知するところではありません。

標準IOストリームのモード

C言語においてのファイルストリームには3つのモードがあり、ファイルオープンに使う関数種別fopen/wfopenおよびその引数、もしくはそのファイルストリームに対して最初に使用した関数で決定されます。

テキストモードではストリームへの入力データをテキストデータ(マルチバイト文字列 : char文字列)だとして処理します。改行コードの変換やロケール対応がここで行われます。標準入出力のデフォルトはこのモードです。この時、ワイド文字列版のI/O関数(std::wcoutなど)を使用すると、内部でマルチバイト文字列へ変換されたうえでストリームへ入力されます。

ユニコードモードではテキストモード時の入力データをワイド文字列(wchar_t文字列)として扱います。それ以外はテキストモードと同様ですが、マルチバイト文字列(char文字列)を処理するI/O関数(std::coutなど)が使用できなくなります。

バイナリモードはその名の通り入力データをバイト列として扱います。ワイド文字、マルチバイト文字版何れの関数でもその入力データをバイト列として扱い、何の変換も行われません。

ここでのマルチバイト文字列/ワイド文字列のエンコードは規定されていません。WindowsではそれぞれANSI/UTF-16になります。

出力だけに注目すると、最終的にファイルにはモードに応じて次のように書きこまれます。

  • テキストモード
    • char : そのまま書き込み
    • wchar_t : charに変換されて書き込み
  • ユニコードモード
    • char : 使用不可
    • wchar_t : そのまま書き込み
  • バイナリモード
    • 何れの場合もそのまま書き込み

コンソールのコードページ

ここからはWindowsのお仕事です。

Windowsにおける標準出力として設定されているファイルの中はコンソール出力へ繋がっています。C/C++のI/O関数としてはこのファイルに対してテキストモードではcharエンコード、すなわちWindows環境ごとのANSI(日本語ならShift-JIS)エンコードで文字列を書きこんでおり、ユニコードモードならばUTF-16で文字列を書きこんでいます。

文字列を表示するためには、その環境の言語毎に最適なエンコードを選択して文字列をそれに変換したうえで表示する必要があります。例えば、ANSI文字列と言っても言語設定によってその解釈に使用すべき文字コードは変化します。
Windowsのコンソールにおいてそれを指定しているのがコードページです。日本語環境ならばCP932というコードページがデフォルトであり、その文字コードはShift-JiSが利用されます。

おおよその場合デフォルトのコードページはその言語環境に合わせたANSIを示すコードページになっているはずなので、テキストモードではコードページに合わせた何かをする必要はありません。バイナリモードの場合も、ファイル出力されてきたバイト列をコードページに従ったエンコードで解釈するだけです。

しかし、ユニコードモード時はそのコードページに対応するエンコードへ変換する必要があります。無駄に思われるかもしれませんが、標準ストリームのモードとコードページは別なのです。コードページはOSで指定されているものなので、表示に当たってはそちらが優先されます。

そのため、ここのコードページを変更してやればユニコードモードにおいてはUTF-16を無変換で通すこともできるかもしれません。

スクリーンバッファ

コードページに従ったエンコードに変換された文字列は最後にスクリーンバッファに出力され、そのままフォントレンダラに渡され表示されます。

このスクリーンバッファ1文字辺りはCHAR_INFO構造体1つによって表現されます。定義を見るに、1文字はwchar_t1つかchar1つのどちらかです。これはおそらくVSプロジェクト設定にある文字セットの設定によってどちらが使われるか決定されると思われます。

あえて変更しなければ今時はユニコードになっているはずなので、スクリーンバッファの文字コードひいてはコンソール最終出力の文字コードUTF-16になっています。従って、コードページのエンコードからスクリーンバッファのエンコードへ再び変換され、スクリーンバッファへと出力・表示されることになります。

CHAR_INFO1つがコンソールスクリーン上の1文字に当たり、それはwchar_t1つ分なので、コンソール出力ではサロゲートペアや合字をそのまま扱えなさそうなことがうかがえます・・・

1. 素直に変換してstd::coutする

一番簡便かつ確実な方法は、UTF-8文字列をANSI(Shift-JIS)文字列へ変換してstd::coutへ出力することです。

std::codecvtC++17で非推奨化してしまったので変換にはWinAPIを利用することにしますが、UTF-8 -> Shift-JISの変換を実はそのままできません。MultiByteToWideCharchar* -> wchar_t*へ、WideCharToMultiBytewchar_t* -> char*へ変換するので、どうしても型を合わせられないのです・・・

なのでこれらを連続適用して、UTF-8 -> UTF-16 -> Shift-JISという2段階変換することになります。

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  //UTF-8 -> UTF-16
  auto length = ::MultiByteToWideChar(CP_UTF8, 0, 
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    nullptr, 0);
  
  std::wstring temp(length, '\0');

  auto res = ::MultiByteToWideChar(CP_UTF8, 0,
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    temp.data(), temp.length());

  //UTF-16 -> Shift-JIS
  length = ::WideCharToMultiByte(CP_ACP, 0,
    temp.data(), static_cast<int>(temp.length()),
    nullptr, 0,
    nullptr, nullptr);

  std::string result(length, '\0');

  res = ::WideCharToMultiByte(CP_ACP, 0,
    temp.data(), static_cast<int>(temp.length()),
    result.data(), static_cast<int>(result.length()),
    nullptr, nullptr);

  std::cout << result;
}

出力結果

変換の実装を信用すれば、UTF-8 -> UTF-16の変換で文字が落ちることはありませんが、UTF-16 -> Shift-JISの変換では当然Shift-JISでは受けきれないものが出てきます(絵文字とか)。それはWideCharToMultiByteがシステムデフォルト値(どうやら??)で埋めてくれます。

後面倒なのでしてませんが、コード内resで受けてる変換結果が0だとエラーが起きてるのでケアした方が良いでしょう。

1.2 UTF-16に変換してstd::wcoutする

しかしとはいえ、二段階変換はさすがに気になりますし、途中でバッファ(wstring)を確保しなければいけないのも少し気になります。むしろ、std::wcoutUTF-16出力したくなりますよね。しかし、そのままだとなぜかAscii範囲外の文字が出力されません・・・

std::wcoutと言えども出力先はstd::coutと一緒です。すなわち、wchar_tを内部でcharに変換してから出力しています。そしてどうやら、std::wcoutのデフォルトはCロケールになっており、Cロケールでは変換時に非Ascii範囲の文字をスルーしてくれるようです。華麗です・・・

つまりは、明示的にロケールを指定してあげればいいのです。何を指定すればいいのかさっぱりですが、幸いWindowsではstd::locale("")とするとその環境のシステムデフォルトのロケールが取得できます。これはWindows限定でポータブルで、外国語環境に行っても適切にその環境のデフォルトロケールを取得することができます。後はこれをstd::wcoutにセットしてやればいいのです。

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEANv
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  //UTF-8 -> UTF-16
  auto length = ::MultiByteToWideChar(CP_UTF8, 0, 
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    nullptr, 0);

  std::wstring result(length, '\0');

  auto res = ::MultiByteToWideChar(CP_UTF8, 0,
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    result.data(), static_cast<int>(result.length()));

  // wcoutにシステムデフォルトのロケールを設定(Cロケールから変更
  std::wcout.imbue(std::locale(""));

  // 出力
  std::wcout << result;
}

出力結果

絵文字は消えましたが日本語出力は出来ているように見えます。しかし、これ以降同じプログラム内でstd::wcoutに何か出力しようとしても何も出てきません。

絵文字が消えているまさにそれが問題で、Shift-JISは絵文字を表現できないので絵文字の変換の際に内部でエラーとなってしまい、それ以降fail状態となり何も出てこなくなるのです。これはfail()によって検出でき、clear()によって回復できます。

  // wcoutにシステムデフォルトのロケールを設定
  std::wcout.imbue(std::locale(""));

  // 出力
  std::wcout << result;

  // fail状態なら状態を復帰する
  if (std::wcout.fail()) {
    std::wcout.clear();
  }

std::wcoutで出力したとしてもその内部でコードページに従った変換(結局Shift-JISへの変換)が走っているうえに、変換エラーによって出力できなくなるというのはこれはこれでイケてないですね・・・

2. UTF-16に変換してWriteConsoleW()する

Windowsにおいて、スクリーンバッファに直接出力するためのAPIWriteConsoleW()関数です。この関数はUTF-16文字列を受け取り、指定されたコンソールのスクリーンバッファに直接出力します。

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  //UTF-8 -> UTF-16
  auto length = ::MultiByteToWideChar(CP_UTF8, 0, 
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    nullptr, 0);

  std::wstring result(length, '\0');

  auto res = ::MultiByteToWideChar(CP_UTF8, 0,
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    result.data(), static_cast<int>(result.length()));

  // 出力
  ::WriteConsoleW(::GetStdHandle(STD_OUTPUT_HANDLE), result.data(), result.length(), nullptr, nullptr);
}

出力結果

WriteConsoleW()関数は指定されたコンソールのスクリーンバッファに対して指定されたUTF-16文字列を直接書き込む関数です。スクリーンバッファはコンソールの出力そのもので、ここに書き込まれているデータがフォントレンダラによって表示されます。

出力結果をコピペしてみると分かるのですが、絵文字列は表示出来ていないだけでコピペ先が表示できるもの(VSCodeとか)ならばちゃんと表示されます。すなわち、文字コードとしては出力までUTF-16で行われています。絵文字が出ないのはおそらくコンソールの表示部分がサロゲートペアを扱えないのに起因していると思われます。

WriteConsoleW()関数は名前の通りコンソール出力専用の関数なので、起動したプログラムにコンソールが割り当てられていない場合に失敗します。すなわち、この関数による出力ではリダイレクトができません。

3. 標準出力をユニコードモードにする

冒頭で説明したように、ユニコード出力だけを使うのであれば標準ストリームをユニコードモードにしてしまえばいいでしょう。Windowsでは_setmode()関数によってストリームのモードを後から変更できます。

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  //UTF-8 -> UTF-16
  auto length = ::MultiByteToWideChar(CP_UTF8, 0, 
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    nullptr, 0);

  std::wstring result(length, '\0');

  auto res = ::MultiByteToWideChar(CP_UTF8, 0,
    reinterpret_cast<const char*>(u8str.data()), static_cast<int>(u8str.length()),
    result.data(), static_cast<int>(result.length()));

  // 標準出力をユニコードモードにする
  ::_setmode(_fileno(stdout), _O_U16TEXT);
  // 出力
  std::wcout << result;
}

出力結果

この方法実は、ユニコードモードといいつつユニコード直接出力出来ているわけではありませんので、コピペしてみると表示できないものは表示できない事が分かるでしょう。内部でUTF-16 -> Shift-JIS -> UTF-16変換が行われています。変換に失敗した文字列はスペースが当てられているのでしょうか。試してませんが、UTF-16コードページに変更すればあるいは・・・

なお、この方法だとstd::coutが使用できなくなります。出力するとエラー吐いて止まります・・・。他人の書いたライブラリを使っているときなどはログ出力にstd::coutが使用されている可能性があるので注意が必要です。

4. コンソールのコードページを変更してUTF-8バイト列を直接流し込む

C/C++I/O関数の範囲内においてはバイナリモードで出力しておき、コンソールのコードページをUTF-8に変更してしまえば、スクリーンバッファへの出力時のUTF-16変換一回で済みそうです。これならばUTF-8文字列をなるべく変換させず、文字が落ちることもほぼないはず・・・

#include <iostream>
#include <string_view>

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  // 出力先コンソールのコードページをUTF-8にする
  ::SetConsoleOutputCP(65001u);
  // 標準出力をバイナリモードにする
  ::_setmode(_fileno(stdout), _O_BINARY);
  // バイナリ列として直接出力
  std::cout.write(reinterpret_cast<const char*>(u8str.data()), u8str.length());
}

出力結果

最後の方がダブってるのはなんでしょうか、3バイト以上の文字が悪さをしているのでしょうか・・・

この方法でも、VSCodeなどにコピペしてみれば絵文字が正しく表示されるので意図通りになっているようです。また、Ascii範囲内の文字ならばstd::coutは依然として使用可能ですが、std::wcoutは文字化けします。

コードページを変更してあるので、コンソールはまず入ってきたバイト列をUTF-8文字列として解釈します。UTF-8はAscii文字と下位互換性があるのでstd::coutはAscii範囲内に限って使用可能となります。しかし、std::wcoutは通常wchar_tUTF-16)を受け付けますが、バイナリモードでは無変換でコンソール入力へ到達し、そこでのコードページに従った解釈の際、UTF-16文字列をUTF-8文字列だと思って処理してしまうため、文字化けします・・・

ただし、コンソールのスクリーンバッファへの出力は通常UTF-16なので、UTF-8がそのまま出力されているわけではなく、スクリーンバッファへの出力にあたってはUTF-8 -> UTF-16の変換が行われます。

5. Boost.Nowideを使用する

boost1.73から追加されたBoost.Nowide<iostream><fstream>UTF-8対応をポータブルにするライブラリです。非Windows環境に対してはcharエンコードUTF-8だと仮定しそのまま、Windows環境ではUTF-8 -> UTF-16変換してWriteConsoleW()などWindowsユニコード対応APIで出力します。

残念ながらchar8_t対応はされていない(おそらく厳しい)のですが、これを利用すれば一番最初に紹介した方法がポータブルになります。

#include <string_view>
#include <boost/nowide/iostream.hpp>

int main() {
  using namespace std::string_view_literals;
  auto u8str = u8R"(日本語出力テスト 🤔 😢 🙇<200d>♂️ 🎉 😰 😊 😭 😥 終端)"sv;

  boost::nowide::cout << reinterpret_cast<const char*>(u8str.data()) << std::endl;
}

試していないので出力がどうなるのかは分かりませんが、実装を見るにおそらくWriteConsoleW()を使用したときと同様になるかと思われます。

UTF-8の直接出力 in Windows

無理です。

絵文字の表示 in Windows

通常のコンソールでは無理ですが、Windows Terminalを使えば表示できます。

上記2の(WriteConsoleW()による)方法での出力 in Windows Terminal

出力結果

上記4の(コードページ変更とバイナリモードによる)方法での出力 in Windows Terminal

出力結果

まだ合字が表示できないみたいですが、今後に期待ですね。

検証環境

参考文献

謝辞

この記事の6割は以下の方々によるご指摘によって成り立っています。

この記事のMarkdownソース

[C++]コンセプトの無言のお願い事

等しさの保持(equality preservation

ある式が等しい入力に対して等しい出力を返すとき、その式は 等しさを保持(equality-preserving しています。式とはわかりやすいところでは関数であり、演算子のことです。ある式をf()とするとa == bならばf(a) == f(b)となり、かつ常にこれが成り立つ時、f()は等しさを保持する式ということです。

この場合の入力とは、その式に直接与えられた引数全てのことであり、1つの引数は1つの式のことです。正確には以下のものだけを含む一番大きな部分式のことです。

  • id-expression(一次式)
  • std::move(), std::forward(), std::declval()の呼び出し

例えば、f(std::move(a), std::declval<T>(), c)みたいなコードでは、まずこの全体が1つの式です。この式の部分式とはstd::move(a), a, std::declval<T>(), cで、上記2つだけを含む最大の部分式=入力は、std::move(a), std::declval<T>(), cの3つです。
別の例では、a = std::move(b)と言う式の入力はa, std::move(b)の2つです。

式は最終的に結果となる1つの値になるので、等しさを保持する式の入力(の式)というのはつまりある1つの引数値と言う事です。

そして、出力とは式の結果(上記のf()ならその戻り値)および、その式の実行によって変更された引数の集合です(変更されなかった引数は含まれない)。

等しさを保持する式の入力と出力はこれら以外にあってはいけません。

安定(stable

あるオブジェクトを入力(引数)にとるある式の2回の評価において、そのオブジェクトの明示的な変更が介在しない限り等しい出力が得られる時、その式は 安定(stable な式です。等しさを保持する式は安定でなければなりません。

つまり、等しさを保持し安定である式は内部や外部の状態に依存してはならず、直接の引数以外に対して副作用を持ってはならないと言う事です。

そして、標準ライブラリにおけるコンセプト定義内の全ての制約式は、特に注釈がない限り等しさを保持し安定でなければなりません。これは、そのコンセプトを満たそうとする場合にユーザーコードに対しても要求されます。

例外的に等しさを保持することを要求されないコンセプトには例えばstd::invocableなどがあります。

定義域(domain

等しさを保持する式はその入力となりうる全ての値について有効である必要はありません。例えば、整数に対するa / bと言う式は等しさを保持する式ですが、b == 0の時この式は有効ではありません。
しかし、この様な入力を取り得たとしても、そのことはその式が等しさを保持することに影響を与えません。

ある等しさを保持する式の入力の全体から、この様な有効ではない入力を除いた集合をその式の 定義域(domain と呼びます。

この用語はコンセプトの意味論的な制約条件に出現することがあります(例えば、std:: equality_comparable_withなど)。

制約式の引数に対しての制約

標準ライブラリのコンセプト定義においては、あるrequires式内の各制約式が引数に対して副作用を及ぼしても良いかどうか(引数を変更することが許されるか)をそのrequires式の引数(ローカルパラメータ)のconst修飾によって表現しています。ローカルパラメータがconst修飾されている場合はそのパラメータを引数に取る制約式は対応する引数を変更してはなりません。逆に、const修飾されていなければ変更しても構いません。

このことも、コンセプトを満たそうとすれば自然にユーザーコードに対して要求されることになります。とはいえ、const修飾されたローカルパラメータが渡ってくるところでその引数を変更しようとするのは、const_castとかmutableとかなんかおかしなことをしない限りそれを破ることは無いでしょう・・・?

template<typename T>
concept C1 = requires(T a, T b) {
  f(a, b);
  a + b;
  // このC1コンセプトを満たす型は、f(T, T)とoperator+(T, T)の呼び出しが可能である必要がある
  // そして、そのような型に対するf()とoperator+の実装は、2つの引数に対して副作用を及ぼしても(引き数を変更しても)良い
};

template<typename T>
concept C2 = requires(const T a, T b) {
  f(a, b);
  a + b;
  // このC2コンセプトを満たす型Tは、f(T, T)とoperator+(T, T)の呼び出しが可能である必要がある
  // そのようなTに対するf()とoperator+の実装は、その第一引数は変更してはならない(`const`修飾されたローカルパラメータ`a`が渡されている)
  // ただし、第二引数は`const`修飾のないローカルパラメータ`b`が渡されているので、変更しても良い
};

この様に決めた上で、コンセプトの型パラメータTがCV修飾されていないオブジェクト型であり完全型と仮定すると、その定義内requires式ではそのローカルパラメータのCV/参照修飾から各ローカルパラメータの値カテゴリとCV修飾を確定することができます。 このようにCV修飾と値カテゴリを指定したローカルパラメータを利用すれば、各制約式が引数としてどのようなCV修飾でどの値カテゴリを受け取るべきなのか?という制約を表現することができます。

// このTがCV修飾されていないオブジェクト型であり完全型と仮定すると
template<typename T>
concept C = requires(T a, T&& b, const T& c) {
  // aの型はCV無しのTであり、左辺値
  // bの型はTの参照型であり、右辺値
  // cの型はconst T&であり、左辺値
  // というように、CV修飾と値カテゴリを指定できる

  f(a);             // 式f()はTのconst無し左辺値を受け取れる必要がある
  g(std::move(b));  // 式g()はTのconst無し右辺値を受け取れる必要がある
  h(c);             // 式h()はTのconst左辺値参照を受け取れる必要がある
};

requires式とrequires

C++20コンセプトではrequiresキーワードは、それを書く場所によってrequires-clauserequires節)とrequires-expressionrequires式)のどちらかとして扱われます。

また、requires節内にrequires式を書くこともできます。

template<typename T>
concept C1 =
  requires { T{}; };  // requires式

template<typename T>
concept C2 = 
  requires(T a) {     // requires式
    ++a; 
  };

template<typename T>
  requires C1<T> // requires節
void f(T t)
  requires C2<T> // requires節
{
  /*関数本体*/
}

template<typename T>
  requires ( requires { T{}; } )          // requires節とその中のrequires式
void g(T t)
  requires ( requires(T a) { t += a; } )  // requires節とその中のrequires式
{
  /*関数本体*/
}

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

requires式は任意の型に対する制約条件を表現する制約式となり、requires節はテンプレートにおいてそのテンプレートパラメータに対する制約を指定するものです。

ローカルパラメータを取れるのはrequires式だけなので、そのconst修飾による引数への副作用の制約表現のお話はrequires式だけの話です。

暗黙的な式のバリエーション(implicit expression variations

requires式ローカルパラメータのconst修飾によって制約式が引数を変更しないことを表明する場合、その制約式には非constの左辺値、右辺値、およびconst右辺値を取る追加の形式が暗黙に要求されます。これら暗黙の追加形式のことを 暗黙的な式のバリエーション(implicit expression variations と呼びます。

template<typename T>
concept C = requires(const T a, T b) {
  f(a, b);  // このf()は第一引数に対して暗黙的な式のバリエーションが要求される
};

// 明示的に書けば以下の様になる
template<typename T>
concept C = 
  requires(const T a, T b) { 
    f(a, b);
    f(std::move(a), b);
  } &&
  requires(T   a, T b) { f(a, b); } &&
  requires(T&& a, T b) { f(std::move(a), b); };

この様なf()は例えば次の様になります。

// 例えばT = intとすると

// ok
f(int n, int m);         // コピー・ムーブによって上記バリエーションの全てを受けられる
f(const int& n, int m);  // const左辺値参照は上記バリエーションの全てを受けられる

// ng
f(int& n, int m);  // 非const左辺値だけしか受けられない
f(int&& n, int m); // 非const右辺値だけしか受けられない

ただし、これら追加のバリエーションが制約式として明示的に書かれていない場合、それをどこまで構文的にチェックするのかは実装依存となります、

requires式ではありませんがこの様な追加の暗黙のバリエーションを明示的に書いているものには、std::copyablestd::copy_constructibleなどがあります。

コンセプトのモデルとなるために

「等しさの保持(かつ安定)」「引数への副作用の制約」「暗黙的な式のバリエーション」、これらの標準ライブラリのコンセプトが暗黙的に要求する事は構文的な制約ではなく意味論的な制約です。つまり、コンパイル時にチェックされる(あるいはできる)ものではありません。違反していたとしてもコンパイルエラーにはならないでしょう・・・

あるコンセプトCについて、型TCの要求する構文的な制約(制約式)を全て満たしていて、上記3つの暗黙的な制約も全て満たしており、かつCに追加で指定される意味論的な制約を全て満たしている時、型TはコンセプトCモデル(model であると言います。

型がコンセプトのモデルであることは、標準ライブラリのテンプレート(クラス・関数)の事前条件(Post Condition)において要求されます。このとき、そこに指定されているコンセプトのモデルとならない型の入力は診断不用(チェックも警告もされない)の未定義動作になります。
少なくとも標準ライブラリのものを利用するときは、コンセプトのモデルについて意識を向ける必要があるでしょう。

C++20で導入された<span>, <ranges>, <format><algorithm>, <iterator>の一部等は既にコンセプトを使用するように定義されているため、その事前条件においてはコンセプトのモデルであることを要求するようになっています。

参考文献

謝辞

この記事の6割は以下の方々によるご指摘によって成り立っています。

この記事のMarkdownソース

[C#/.Net Core] .Net Core(3.1)でSystem.Drawingをクロスプラットフォームに利用する

System.Drawing.Commonパッケージ

System.Drawing名前空間には画像を扱う上で便利なものが含まれていますが、そのままだとドキュメントに見えるものの半分も利用できません。Windowsで開発していてもです・・・

これは.Net Frameworkから引き継いだもので、System.Drawing名前空間のものはWindowsのGDI+ APIに依存しているためWindows以外の環境で利用できません。.Net Coreはクロスプラットフォームがデフォルトなので、このような環境依存のものは利用できないのでしょう。でもドキュメントを眺めていると.Net Core 3.0から対応という記述が見られます。いや使えないじゃん・・・

これの正式な解決策かは知りませんが、NugetからSystem.Drawing.Commonパッケージを追加することでSystem.Drawing名前空間内のものを.Net Coreなプロジェクトにおいても利用可能になります(MS公式のものなので多分正解なはず・・・)。

www.nuget.org

これをインストールしておくことでWindowsではそのまま意図通りの動作を得られますし、シングルバイナリ出力しても問題ありません。しかし、Windows以外のプラットフォームへ持っていくとDllNotFoundExceptionが投げられることでしょう。結局Windows依存なのか・・・

libgdiplus

依存関係が無いなら依存関係を整えてやれば良いだけのこと。非Windowsにおける.Net実装であるMonoがSystem.Drawingの非Windows実装であるlibgdiplusというライブラリをリリースしています。

www.mono-project.com

これがあればWindows以外でもそのままのコードでSystem.Drawing名前空間のものをほぼ利用できます(100%実装してないと書かれているので、利用できないものもあるでしょう・・・)。

MacOS

$ brew install mono-libgdiplus

Linux

$ sudo apt-get update
$ sudo apt-get install libgdiplus

.NET COREでSYSTEM.DRAWINGを使う UBUNTU編より。

※私自身は、Linuxでの動作は未確認です。

参考文献

この記事のMarkdownソース

[C++] メンバ関数のCV/参照修飾

メンバ関数の修飾

メンバ関数にはCV修飾と参照修飾を行えます。CV修飾は呼び出すオブジェクトがconst/volatileであるときに優先して選択されるようになり、参照修飾は呼び出すオブジェクトの状態が左辺値/右辺値であるときに優先して選択されるようになります。そして、この二つは重複して指定することができます。つまり、以下のような組み合わせが可能です。

struct X {
  int f() &       // *thisが非constな左辺値である場合に呼び出される
  { return 1; }

  int f() const & // *thisがconstな左辺値である場合に呼び出される
  { return 2; }

  int f() &&      // *thisが右辺値である場合に呼び出される
  { return 3; }

  int f() const &&      // *thisがconstな右辺値である場合に呼び出される
  { return 4; }

  int f() volatile &    // *thisがvolatileな左辺値である場合に呼び出される
  { return 5; }

  int f() const volatile &  // *thisがconst volatileな左辺値である場合に呼び出される
  { return 6; }

  int f() volatile &&       // *thisがvolatileな右辺値である場合に呼び出される
  { return 7; }

  int f() const volatile && // *thisがconst volatileな右辺値である場合に呼び出される
  { return 8; }
};

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

まあこんだけ全部書いてあれば想定通りに呼び出されます。しかし、実際は書いたとしてもconstと参照修飾あるいはその組み合わせくらいでしょう。すると、書かなかった種類のものは書いてあるもののどれかにマッチすればそれによって呼び出されるはずです。

例えば次のように、関数を左辺値からのみ呼び出したいがconstである場合は処理を分けたいこともあるでしょう。この場合は、右辺値オブジェクトからf()を呼び出そうとするとコンパイルエラーになることが期待されるはず・・・

struct X {
  int f() &       // *thisが非constな左辺値である場合に呼び出される
  { return 1; }

  int f() const & // *thisがconstな左辺値である場合に呼び出される
  { return 2; }
};

int main() {
  X x;
  const X cx;

  std::cout << x.f() << std::endl;   // 1
  std::cout << cx.f() << std::endl;  // 2
  std::cout << X().f() << std::endl; // 2 !?
}

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

なんと、右辺値からの呼び出しがコンパイルエラーになりません。どうやらconst &な関数に引っかかっているようです。

const auto&が右辺値を束縛できるのは理解できますが、この挙動は一見非自明でイミフです・・・

暗黙の引数this

C++のクラスメンバ関数はユーザーが指定した引数リストの一番先頭で、暗黙の引数としてthisポインタを受け取っています。CV修飾はこの暗黙の引数の型に対して適用されます。

struct X {
  int f() const
  { return 0; }

  int f(int arg1, int arg2) volatile
  { return -1; }
};

X x;
x.f();

//↑この様なクラスXとメンバ関数呼び出しは、実質的に次↓の様なコードの様に扱われている

struct X {};

int f(const X* this)
{ return 0; }

int f(volatile X* this, int arg1, int arg2)
{ return -1; }

X x;
f(&x);

この辺りのことはコンパイラがよしなにしていることなので、C++ヲタク以外は気にしなくても良いことです。

これを知ると多分もうわかると思いますが、参照修飾もこれと同じことが起こっています。そして上記のことはより正確にはポインタではなく参照によって行われます。

struct X {
  int f() const
  { return 0; }

  int f(int arg1, int arg2) volatile
  { return -1; }

  int f() &
  { return 1; }

  int f() const &
  { return 2; }
};

X x;
x.f();

//↑この様なクラスXとメンバ関数呼び出しは、実質的に次↓の様なコードの様に扱われている

struct X {};

int f(const X& this)
{ return 0; }

int f(volatile X& this, int arg1, int arg2)
{ return -1; }

int f(X& this)
{ return 1; }

int f(const X& this) // 実はconst修飾とconst&修飾は同じ意味を持つので両方書くとコンパイルエラー
{ return 2; }

X x;
f(x);

なお、これらの事はオーバーロード解決時に行われる事なので、ユーザーが触れる部分ではthisはポインタです。

詳細には、関数呼び出しに伴うオーバーロード解決時にメンバ関数の暗黙の第一引数の型は、その関数の参照修飾とCV修飾によって次の様に決められます(ここでの対象となるクラス型をTとします)。

ここで先ほどの不可解なコードを振り返ってみると、もう自明になっている事でしょう。

struct X {
  int f() &       // *thisが非constな左辺値である場合に呼び出される
  { return 1; }

  int f() const & // *thisがconstな左辺値である場合に呼び出される
  { return 2; }
};

//f()の宣言だけ書いてみると
int f(X& this);
int f(const X& this);

f(X{}); // const &なメンバ関数が呼ばれる

const auto&が右辺値を束縛できる様に、あるいはムーブコンストラクタがない場合は右辺値に対してコピーコンストラクタが呼ばれる様に、thisが右辺値であり&&修飾されたメンバ関数がない場合にはconst &(あるいはconst)修飾のメンバ関数がベストマッチしてしまうわけです。

C++は奥が深いですね・・・

防止策?

とはいえ、メンバ関数const修飾の意図からするとこの事はやはり不自然であり、constオブジェクトに対して呼んで良くても右辺値に対しては呼んでほしくない関数はあるでしょう。そんな場合には、&&修飾された関数に対してdelete指定をしてやれば意図通りになります。

struct X {
  int f() &       // *thisが非constな左辺値である場合に呼び出される
  { return 1; }

  int f() const & // *thisがconstな左辺値である場合に呼び出される
  { return 2; }

  int f() && = delete;  // *thisが右辺値なら呼び出し禁止!
};

int main() {
  X x;
  const X cx;

  std::cout << x.f() << std::endl;   // 1
  std::cout << cx.f() << std::endl;  // 2
  std::cout << X().f() << std::endl; // コンパイルエラー
}

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

正直冗長だと思いますが多分こうするのがベストでしょう・・・・

CV修飾と参照修飾と暗黙の引数の型

オブジェクト型をTとするとメンバ関数の修飾によって決まる、オーバーロード解決時に考慮される暗黙の第一引数(this)の型は以下のようになります。

CV修飾\参照修飾 なし & &&
なし T& T& T&&
const const T& const T& const T&&
volatile volatile T& volatile T& volatile T&&
const volatile const volatile T& const volatile T& const volatile T&&

メンバ関数の第一引数にはその修飾によって上記のいずれかと同じシグネチャで隠れた引数があり、メンバ関数呼び出しの際にはそのTのオブジェクトがそのままそこに渡されます。

その際のTのオブジェクトのCV修飾と値カテゴリによって、上記のいずれかから最もマッチする関数が選択され、呼び出されることになります。

CV修飾に関してはほぼ想定通りになると思われますが、参照修飾をするときは少し気にする必要があるかもしれません。

参考文献

謝辞

この記事の8割は以下の方々によるご指摘によって成り立っています。

この記事のMarkdownソース

[C++]std::common_referenceの概念

C++20より追加されたstd::common_reference<T, U>は型T, U両方から変換可能な共通の参照型を求めるメタ関数です。ただしその結果型は必ずしも参照型ではなかったりします。std::common_typeとの差など、存在理由がよく分からない物でもあります・・・

同じ事を思う人は他にも居たようでstackoverflowで質問している人がいて、そこにcommon_referenceを考案したEric Nieblerさん御本人が解説を書いていました。本記事はそれを日本語で説明し直したものです。

イテレータ::reference::value_typeの関連性

標準ライブラリのイテレータ::reference::value_typeという二つの入れ子型を持っています。referenceは通常*itの戻り値型であり、value_typeはイテレートしているシーケンスの要素型(constや参照型ではない)を表します。そして、referencevalue_type&もしくはそのconst付きの型です。これによって次のような操作が可能です。

//要素型を明示的にコピーしたい場合など?
value_type t = *it;

このように、これら2つの型の間には通常自明な関係性が存在しています。そして、それら2つの型はconst value_type&という共通の型に変換可能であるはずで、実際ほとんどのイテレータはそうなっています。

より高機能なイテレータ

rangeライブラリのようなシーケンスを抽象化して扱うライブラリ(例えばC#LINQなど)によくある操作に、2つのシーケンスのzipという操作があります。zipは2つのシーケンスをまとめて1つのシーケンスとして扱う操作です(2つのシーケンスを1つのシーケンスに圧縮するからzip?)。2つのシーケンスの同じ位置の要素をペアにして、そのペアのシーケンスを生成する操作とも言えます。その場合、わざわざ元のシーケンスをコピーしてから新しいシーケンスを作成なんてことをするはずもなく、イテレータを介してそのようなシーケンスを仮想的に作成します。

その時、そのイテレータzip_iteratorと呼ぶことにします)の::reference::value_typeはどうなっているでしょう?例えば、std::vector<int>std::vector<double>をzipしたとすれば、その場合のzip_iteratorの2つの入れ子型は次のようになるでしょう。

  • reference : std::pair<int&, double&>
  • value_type : std::pair<int, double>

見て分かるように、これらの型はもはやconst value_type&という型で受けることはできず、value_type t = *itのような操作もできません。2つの型の間の関連性は失われてしまっています。

しかし、この場合でもこの2つの型の間に何の関連も無いという事はなく、なにかしら関連性があるように思えます。しかし、どのような関連性を仮定し利用できるのでしょうか・・・?

std::common_reference

std::common_referenceはそれに対する1つの解(あるいは要求)です。つまり、イテレータ種別に関わらずそのreferencevalue_typeの間には共通の参照型(従来のイテレータにおけるconst value_type&)に相当するものがあるはずであり、あるものとして、ジェネリックな操作において(すなわち任意のイテレータについて)その仮定を安全なものであると定めます。

そして、そのような共通の参照型に相当するものを統一的に求め、表現するために用意されたのがstd::common_referenceメタ関数です。

これは次のような操作が常に可能であることを保証します。

template<typename Iterator>
void algo(Iterator it, Iterator::value_type val) {
  using CR = std::common_reference<Iterator::reference, Iterator::value_type>::type;

  //共に束縛可能
  CR r1 = *it;
  CR r2 = val;
}

このように、common_referenceは従来のイテレータ型のreferencevalue_typeの間の関連性をより一般化したものであり、ジェネリックな処理においてzip_iteratorのようなより一般的なイテレータを区別なく扱うために必要なものであることが分かります。

なお、名前にreferencceとあるのは従来のイテレータにおけるcommon_referenceconst value_type&という参照型であったことから来ていると思われ、より一般的なcommon_referenceは必ずしも参照型ではなく、そうである必要もありません。

std::common_reference_withコンセプト

ある型のペアがcommon_referenceを有しているかをstd::common_reference<T, U>::typeが有効かを調べて判定するなどということは前時代的です。C++20にはコンセプトがあり、それで判定すればいいはず。

ということで?C++20ではそのような用途のためにstd::common_reference_withコンセプトが<concepts>に用意されています。これは、std::common_reference_with<T, U>のように2つの型の間にcommon_referenceがあることを表明(要求)するそのままのものです。

C++20にzip_viewが無いのは・・・

std::common_referenceの動機付けでもあったzip_iteratorを返すzip_viewのようなrange操作はC++20には導入されていません(同じ事情を持つものにはstd::vector<bool>が既にあります)。なぜかといえば、zip_iteratorについてのcommon_referenceを単純には求められなかったためです。再掲になりますが、zip_iteratorreferencevalue_typeは一般的には次のようになります。

  • reference : std::pair<T&, U&>
  • value_type : std::pair<T, U>

この場合のcommon_referenceは単純にはstd::pair<T&, U&>になるでしょうが、現在のstd::pairstd::pair<T, U> -> std::pair<T&, U&>のような変換はできません。C++20でもそれは可能になってはいません。かといって、zip_viewのためだけにpair-likeな型を用意するのも当然好まれません。

この問題をどうするのかの議論はされていますが、結論がC++20には間に合わなかったためC++20にはzip_viewはありません。

そして、この影響を受けてしまったのがstd::flat_mapです。C++20入りを目指していましたが、パフォーマンスのためにそのKeyのシーケンスとvalueのシーケンスをそれぞれ別で持つという設計を選択していたため、要素のイテレートのためにzip_viewが必要となりました。しかし、zip_viewは延期されたのとその設計からくる問題があったのとでstd::flat_mapも延期され、それに引きずられてstd::flat_setC++20には入りませんでした。

どれも将来的には入るとは思いますが、C++20で使いたかった・・・

コンセプト定義に現れるcommon_reference_with

std::common_referenceイテレータ型を受け取るのではなく2つの型を受け取ってその間のcommon_referenceを求めます。common_referenceの動機付けはイテレータ型からのものでしたが、そこから脱却すれば、common_referenceはより一般化した2つの型の間の関連性ととらえることができます。

そのようなcommon_referenceとは、2つの型に共通している部分を表す型だと見ることができます。std::common_type集合論的な意味での共通部分に相当していますが、std::common_referenceはそのような共通部分を参照、あるいは束縛できる型を表します。つまり、std::common_referencestd::common_typeを包含しています。
このことは逆に言えば、common_referenceを持つ2つの型は何かしら共通した部分を持つ、という事です。それはおそらく基底クラスもしくは同じ型のメンバであるでしょう。

そして、この性質は標準ライブラリにおけるコンセプト定義において利用されます。例えば、std::totally_ordered_with<T, U>std::swappable_with<T, U>のように2つの型の間で可能な性質を表明するコンセプトの定義においてほぼ必ずstd::common_reference_with<T, U>が現れています(そのようなコンセプトは多くが~_withという命名になっています)。
これらのようなコンセプトが表明する関係は明らかに2つの型の間に共通した部分があることを前提にしています。std::common_reference_withを定義中に使用しているのは、そのような関連性があることを表明し、要請するためです。

例えば、std::totally_ordered_with, std::equality_comparable_with, std::three_way_comparable_withなど比較に関するコンセプトなら、比較可能であるという事は2つの型の間に比較可能な部分がある筈ですし、std::swappable_withならば2つの型の間に交換可能な共通の部分がある筈です。

std::common_referencestd::common_reference_withは一見すると意味不明で何に使うのか良く分かりませんが、ここまでくるとようやく意味合いが見えてくるでしょうか。特に、コンセプト定義におけるstd::common_reference_with意味のあるコンセプトを定義するのに役立つかもしれません。

参考文献

この記事のMarkdownソース