yohhoyの日記

技術的メモをしていきたい日記

構造化束縛 in 条件式 @ C++26

プログラミング言語C++の次期標準C++2c(C++26)から、if/while/for/switch構文の条件式(condition)部に構造化束縛(structured binding)を記述できる。

// C++2c
if (auto [a, b] = func()) {
  // 関数戻り値からbool型への変換結果がtrueとなるときに限り
  // 戻り値オブジェクトから変数a, bへの分割代入が行われる
}
// 下記コード動作と等価
auto r = func();
if (r) {
  auto [a, b] = r;
  // ...
}

またC++2c採択済み提案文書P2497R0により、std::to_charsstd::from_chars関数の各戻り値型std::to_chars_resultstd::from_chars_resultにbool型への変換(→id:yohhoy:20121110)が追加され、構造化束縛による結果取得と変換成功判定ec == errc{}を簡素に記述できるようになる。*1

#include <charconv>

char buf[8];
char* last = buf + sizeof(buf);

// C++17/20/23
auto [ptr, ec] = std::to_chars(buf, last, 42);
if (ec == std::errc{}) {
  // ...
}

// C++2c
if (auto [ptr, ec] = std::to_chars(buf, last, 42)) {
  // ...
}

Clang 6.0.0以降は独自拡張として本機能をサポート*2しているが、戻り値オブジェクトの条件評価と分割代入の評価順がC++2c言語機能と異なることに注意。

// Clang独自拡張: 下記コード動作と等価
auto r = func();
if (auto [a, b] = r; r) {
  // ...
}

関連URL

*1:本来はC++23時点で採択予定だったようだが、手続き上の問題でC++2cに遅延したとのこと。https://github.com/cplusplus/papers/issues/1454

*2:https://github.com/llvm/llvm-project/blob/llvmorg-6.0.0/clang/include/clang/Basic/DiagnosticSemaKinds.td#L417-L419

R.I.P. <strstream>ヘッダ

C++2c(C++26)標準ライブラリでは、ようやく <strstream> ヘッダが削除される。同ヘッダはC++ ISO標準化されたC++98当初から非推奨(deprecated)とされていた。

代替機能として下記C++標準ヘッダが提供するクラス群を利用する。*1

  • <sstream>
    • stringstream, istringstream, ostringstream, stringbuf
  • <spanstream> [C++23以降]
    • spanstream, ispanstream, ospanstream, spanbuf

関連URL

*1:本文中では <strstream> ヘッダ提供機能に対応する basic_xxxstream<char>, basic_xxxbuf<char> エイリアス名だけを列挙している。

struct.pack/unpack関数フォーマットにはメモリレイアウトを指定する

Python言語のstructモジュールでは、フォーマット指定先頭文字(@/=/<>/!)にてメモリレイアウト指定を明示すべき。

パック(pack)/アンパック(unpack)フォーマット文字列の省略時デフォルトのメモリレイアウトは、Pythonプログラマの期待に反する可能性が高い。

By default, C types are represented in the machine’s native format and byte order, and properly aligned by skipping pad bytes if necessary (according to the rules used by the C compiler). This behavior is chosen so that the bytes of a packed struct correspond exactly to the memory layout of the corresponding C struct. Whether to use native byte ordering and padding or standard formats depends on the application.

Alternatively, the first character of the format string can be used to indicate the byte order, size and alignment of the packed data, according to the following table:

Character Byte order Size Alignment
@ native native native
= native standard none
< little-endian standard none
> big-endian standard none
! network (= big-endian) standard none

If the first character is not one of these, '@' is assumed.

Byte Order, Size, and Alignment

デフォルト動作(@相当)ではC/C++コンパイラの構造体メモリレイアウトに従うため、型指定子の配置順によって意図しないパディング挿入が行われるケースがある。64bitプロセッサ/Native Endian=Little Endian環境での実行結果。

import struct
from binascii import b2a_hex 
printhex = lambda s: print(b2a_hex(s, ' '))

a, b = 0x12345678, 0x123456789abcdef0

printhex(struct.pack('@Iq', a, b))
# b'78 56 34 12 00 00 00 00 f0 de bc 9a 78 56 34 12'
printhex(struct.pack('=Iq', a, b))
# b'78 56 34 12 f0 de bc 9a 78 56 34 12'
printhex(struct.pack('<Iq', a, b))
# b'78 56 34 12 f0 de bc 9a 78 56 34 12'
printhex(struct.pack('>Iq', a, b))
# b'12 34 56 78 12 34 56 78 9a bc de f0'

printhex(struct.pack('@qI', b, a))
# b'f0 de bc 9a 78 56 34 12 78 56 34 12'
printhex(struct.pack('=qI', b, a))
# b'f0 de bc 9a 78 56 34 12 78 56 34 12'
printhex(struct.pack('<qI', b, a))
# b'f0 de bc 9a 78 56 34 12 78 56 34 12'
printhex(struct.pack('>qI', b, a))
# b'12 34 56 78 9a bc de f0 12 34 56 78'

@指定メモリレイアウトの結果は、一般的なC/C++コンパイラの構造体メモリレイアウトと整合する。*1

#include <stdint.h>

struct S1 {
  uint32_t m1;  // +0: 4byte
  // (padded: 4byte)
  uint64_t m2;  // +8: 8byte
};

struct S2 {
  uint64_t m1;  // +0: 8byte
  uint32_t m2;  // +8: 4byte
};

*1:構造体 S2 ではメンバ変数 m2 に後続して4byteパディングが挿入されるが、structモジュールは末尾パディングを自動挿入しない。

MS-ERREF: Windows Error Codes

Windows OSのエラーコードに関するMicrosoft公式仕様書。HRESULT値/Win32エラーコード/NTSTATUS値を広範にカバーしている。

関連URL

Passkey Idiom

他クラスに対して “メンバ関数単位でのアクセス制限” を実現するイディオム。

template <class T>
class Passkey {
  friend T;
  Passkey() {}
};

class A;
class B;

class C {
public:
  // クラスAに対してのみ公開
  void mfA(Passkey<A>, int arg);
  // クラスBに対してのみ公開
  void mfB(Passkey<B>, int arg);
};

class A {
public:
  void f(C& c) {
    c.mfA({}, 1);  // OK
    c.mfB({}, 2);  // NG
  }
};

上記コードでPasskeyコンストラクタを= defaultでユーザ宣言すると、C++17以前ではイディオムの意図通りに動作しない。コンストラクタのprivateアクセス制限を無視してPasskey<T>の集成体初期化(aggregate initialization)に成功してしまう問題があり、C++20で言語仕様が変更された。

関連URL

Read-Copy Update @ C++26

C++2c(C++26)標準ライブラリに追加される<rcu>ヘッダについて。Read copy updateの略。

// C++2c
#include <atomic>
#include <mutex>  // scoped_lock
#include <rcu>

struct Data { int m1; /*...*/ };
std::atomic<Data*> data_;
// new確保された初期値が別途設定される前提

void multiple_reader()
{
  // Readerロック取得(ノンブロッキング操作)
  std::scoped_lock rlk(std::rcu_default_domain());
  // 現在の共有データへのポインタ取得
  Data* p = data_.load(std::memory_order_acquire);
  // アドレスloadにはmemory_order_consume指定が想定されていたが、
  // C++20以降はconsume指定はdiscourage扱いとなっている(P0371R1)。
  // acquire指定セマンティクスはconsumeを完全に包含する。
  int m1 = p->m1; 
} // Readerロック解放

void single_writer()
{
  // 新データを作成
  Data* newdata = new Data();
  newdata->m1 = /*...*/; 

  // ポインタをアトミック更新
  Data* olddata = data_.exchange(newdata, std::memory_order_release);

  // 全Readerロック解放後のデータ回収をスケジュールする
  std::rcu_retire(olddata);
  // 内部実装には何らのスケジューラの存在が仮定されている。
  // ポインタはstd::default_delete<Data>経由でdeleteされる。
  // 実装によっては呼出スレッド上にて待機+deleteされる可能性あり。
}

まとめ:

  • プリミティブなスレッド間同期機構として、ユーザ空間RCU(Userspace Read-Copy Update)を提供する。
    • RCU機構で実現される制御はReader-Writerロック*1と似ているが、Readerロック取得はブロッキングされず高いスケーラビリティを持つ。
    • Writerスレッドは共有データを直接書き換えるのではなく、旧データから新データへとコピー&書き換えたのち、“共有データを指すポインタ” が指す先を旧データから新データへとアトミック更新する。
    • 全体としては新旧データが同時に存在する状態となり、以降のReaderロック取得スレッドは新データを参照する。全Readerスレッドが旧データを読取り終わった後に、旧データのメモリ領域を回収(reclaim)しなければならない。
    • RCU機構が直接提供するのは旧データのメモリ解放タイミング制御のみ。
  • RCUドメインstd::rcu_domain
    • Readerロック管理とメモリ回収機構を提供するクラス。Readerロック取得/解放操作のためにLockable要件を満たす。*2
    • rcu_domain::lockは同一スレッド上での再帰的Readerロックをサポートする。*3
    • C++2c標準ライブラリではシステムグローバルなstd::rcu_default_domain()のみが提供される。
  • 下記3パターンの実装方式に対応したAPIが提供される。
    • 侵襲(intrusive) RCU:データ型Tstd::rcu_obj_base<T>から継承*4し、retire()メンバ関数を利用する。
    • 非侵襲(non-intrusive) RCU:std::rcu_retire(p)関数を利用する。
    • 同期(synchronous) RCU:std::rcu_synchronize()関数+手動メモリ解放を行う。
  • 全retire操作の完了待ち:std::rcu_barrier()関数
  • retire操作のメモリ解放処理はカスタマイズ可能。デフォルトstd::default_delete<T>
  • プロダクション品質の実装例として facebook/folly ライブラリが存在する。機能的にはC++2c標準ライブラリの上位互換相当。*5

関連URL

*1:C++標準ライブラリの std::shared_mutex、POSIXの pthread_rwlock_* など。

*2:データ読取り操作を行うRCU保護区間(region of RCU protection)の開始/終了を、std::scoped_lock や std::unique_lock によるScoped Lockイディオムで実現するのために lock/unlock メンバ関数が提供される。

*3:C++標準 std::shared_mutex では再帰的なロック操作をサポートしない。

*4:いわゆるCRTP(Curiously Recurring Template Pattern)継承関係。

*5:P2545R4 "A near-superset of this proposal is implemented in the Folly RCU library."

std::submdspan関数

C++2c(C++26)標準ライブラリに追加される多次元部分ビューstd::submdspanについて。

// <mdspan>ヘッダ
namespace std {
  template<
    class T, class E, class L, class A,
    class... SliceSpecifiers>
  constexpr auto submdspan(
    const mdspan<T, E, L, A>& src,
    SliceSpecifiers... slices) -> /*see below*/;
}

まとめ:

  • 多次元ビューstd::mdspan(→id:yohhoy:20230303)から部分ビューを取り出す(slice)関数。
  • 第2引数以降のスライス指定子リストにより、各次元のスライス方法(slicing)を指定する。
    • 単一インデクス:単一値。指定次元に対するインデクスを固定し、次元数(rank)を1つ削減する。*1
    • インデクス範囲:開始位置(begin)+終了位置(end)の組。std::pairや2要素std::tuple等により*2、指定次元に対するインデクス範囲を取り出す。
    • ストライド指定範囲:オフセット位置(offset)+要素数(extent)+ストライド(stride)の組。std::strided_sliceにより、指定次元に対してずらし幅指定したインデクス範囲を取り出す。
    • 全選択:タグ値std::full_extent。指定次元をそのまま取り出す。
  • ストライド指定範囲” 用のstrided_slice型は名前付き初期化(→id:yohhoy:20170820)をサポートする。
    • 類似機能を提供するPython/numpyやMatlabFortranと異なり、変換元における終了位置(end)ではなく変換元における要素数(extent)による指定を行う。
    • 例:strided_slice{.offset=1, .extent=10, .stride=3}またはstrided_slice{1, 10, 3}
  • “インデクス範囲” および “ストライド指定範囲” で用いるインデクス値は、通常の値と整数定数の2種類をサポートする。*3
    • 戻り値型mdspan<T,E,L,A>における多次元インデクス型Eの各次元要素数に影響を与える。
    • 通常の値:整数リテラル(literal)*4を含む整数値。指定次元の要素数は実行時(std::dynamic_extent)に決定する。
    • 整数定数:std::integral_constant互換の定数値*5。指定次元の要素数コンパイル時に決定する。
  • レイアウトポリシーLC++標準ライブラリ提供メモリレイアウトのみサポートする。
    • std::layout_right:同ポリシー型を維持できる場合はlayout_rightを利用。それ以外はlayout_strideへ変換。
    • std::layout_left:同ポリシー型を維持できる場合はlayout_leftを利用。それ以外はlayout_strideへ変換。
    • std::layout_stridelayout_strideのまま。
    • ユーザ定義レイアウトポリシーをサポートするには、カスタマイズポイントsubmdspan_mapping関数を実装する。カスタマイズポイント実装は必須要件ではないが、汎用のフォールバック実装は提供されない。
  • 要素型TとアクセスポリシーAは原則維持される。*6

スライス指定の例

int a[15];  // {1, 2, ... 15}
std::ranges::iota(a, 1);

// 3x5要素の2次元ビュー
std::mdspan m0{a, std::extents<size_t, 3, 5>{}};
//  1  2  3  4  5
//  6  7  8  9 10
// 11 12 13 14 15

auto m1 = std::submdspan(m0, 1, std::full_extent);
// [6 7 8 9 10] (5要素1次元)
auto m2 = std::submdspan(m0, std::full_extent, 2);
// [3 8 13] (3要素1次元)
auto m3 = std::submdspan(m0, 1, 2);
// 8 (0次元)

// 2x3要素の2次元部分ビュー
auto m4d = std::submdspan(m0, std::pair{1,2}, std::tuple{1,3});
//  -  -  -  -  -
//  -  7  8  9  -
//  - 12 13 14  -
// Extents = std::dextents<size_t, 2>

// 2x2要素の2次元部分ビュー
auto m5d = std::submdspan(m0,
  std::strided_slice{.offset=0, .extent=3, .stride=2},
  std::strided_slice{.offset=1, .extent=4, .stride=3});
//  -  2  -  -  5
//  -  -  -  -  -
//  - 12  -  - 15
// Extents = std::dextents<size_t, 2>
template <int N>
constexpr auto Int = std::integral_constant<int, N>{};

// 2x3要素(静的要素数)の2次元部分ビュー
auto m4s = std::submdspan(m0,
  std::pair{Int<1>,Int<2>}, std::tuple{Int<1>,Int<3>});
//  -  -  -  -  -
//  -  7  8  9  -
//  - 12 13 14  -
// Extents = std::extents<size_t, 2, 3>

// 2x2要素(静的要素数)の2次元部分ビュー
auto m5s = std::submdspan(m0,
  std::strided_slice{.offset=0, .extent=Int<3>, .stride=Int<2>},
  std::strided_slice{.offset=1, .extent=Int<4>, .stride=Int<3>});
//  -  2  -  -  5
//  -  -  -  -  -
//  - 12  -  - 15
// Extents = std::extents<size_t, 2, 2>
// strided_slice::offset型はsubmdspan適用後の型に影響しない

レイアウトポリシー変換

int a[60] = /*...*/;

// 3x4x5要素の3次元ビュー(LayoutPolicy=layout_right)
std::mdspan m0{a, std::extents<size_t, 3, 4, 5>{}};

auto m1 = std::submdspan(m0, 1, std::full_extent, std::full_extent);
// LayoutPolicy = layout_right (4x5要素2次元)
auto m2 = std::submdspan(m0, 1, std::pair{1,2}, std::full_extent);
// LayoutPolicy = layout_right (2x5要素2次元)
auto m3 = std::submdspan(m0, 1, 0, 2);
// LayoutPolicy = layout_right (0次元)

auto m4 = std::submdspan(m0, std::full_extent, 0, std::full_extent);
// LayoutPolicy = layout_stride (3x5要素2次元)
// 変換後m4の strides[] = {20, 1}
// 3x4x5要素の3次元ビュー, layout_stride = {20, 1, 4}
using Exts3x4x5 = std::extents<size_t, 3, 4, 5>;
std::array strides = {20, 1, 4};
auto mapping = std::layout_stride::mapping{Exts3x4x5{}, strides};
std::mdspan m0s{a, mapping};

auto m5 = std::submdspan(m0s, 0, std::full_extent, 0);
// LayoutPolicy = layout_stride (4要素1次元)
assert(m5.mapping().stride(0) == 1);
// メモリレイアウト的にはlayout_right互換となる
// コンパイル時の型計算ではstridesアクセスできないため
// layout_strideからは常にlayout_strideへと変換される

// 変換コンストラクタによりlayout_rightへ明示変換可能
std::mdspan<int, std::extents<size_t, 4>> m5r{ m5 };
// LayoutPolicy = layout_right (4要素1次元)

関連URL

*1:1次元 mdspan からは 0次元 mdspan(→id:yohhoy:20230309)が生成される。

*2:2要素ペアとしてアクセス可能な型(index-pair-like)を広くサポートする。

*3:“単一インデクス” も整数定数をサポートするが、その効果は通常の値を指定したときと同じ。

*4:戻り値型の決定で利用されるため、std::integral_constant<int,N>::value のように値が型情報にエンコードされていないと、コンパイル時にその値(value)へアクセスできない。

*5:データメンバ value で定数値アクセス可能な型(integral-constant-like)を広くサポートする。

*6:厳密には submdspan 関数適用後 mdspan<T,E,L,A> の要素型 T やアクセスポリシー A は A::offset_policy 型に依存する。通常は A::offset_policy == A として定義されるため、テンプレートパラメータ T, A 型が変化するケースは稀。