yohhoyの日記

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

配列添字演算子の小さな改善案 for C2y

プログラミング言語Cにおける配列添字演算子(array subscript operator)[]の伝統的かつ奇妙な言語仕様に関して、次期C2yをターゲットとした仕様修正が提案されている。小ネタ以外の使い道もなく修正されて良い頃合いかもね?*1

const char msg[] = "Hello";
const char *p = msg + 1;
assert( 1[msg] == 'e' );  // OK: msg[1]と等価
assert( msg[-1] != 0 );   // NG: 未定義動作
assert( p[-1] == 'H' );   // OK: *(p-1)と等価

// N3352 2.2.1 Option1適用後
// 式 E[N] の E は配列型/ポインタ型のいずれか
assert( 1[msg] == 'e' );  // NG: ill-formed
// 配列型Eと整数定数Nの場合は0≦N
assert( msg[-1] != 0 );   // NG: ill-formed
assert( p[-1] == 'H' );   // OK: *(p-1)と等価

C23(N3220) 6.5.3.2/p2より引用(下線部は強調)。C90/C99/C11/C17でも同様。

A postfix expression followed by an expression in square brackets [] is a subscripted designation of an element of an array object. The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). Because of the conversion rules that apply to the binary + operator, if E1 is an array object (equivalently, a pointer to the initial element of an array object) and E2 is an integer, E1[E2] designates the E2-th element of E1 (counting from zero).

C Rationale 6.5.2.1より一部引用。

The C89 Committee found no reason to disallow the symmetry that permits a[i] to be written as i[a].

関連URL

*1:C言語 FAQ 日本語訳, 6章 配列とポインター, 6.11: 「このとんでもない交換可能性は、よくC言語について扱う文章の中で、誇らしく思うかのように記述されているが国際難解Cプログラムコンテスト以外では役に立たない。」

C++ std::strong_orderと浮動小数点数型totalOrder

C++20標準ライブラリのstd::strong_order関数オブジェクト*1は、IEEE 754準拠の浮動小数点数型に対する全順序比較(totalOrder predicate)を実装する。

#include <compare>
#include <concepts>
#include <iostream>
#include <limits>
#include <map>
#include <string>

template<std::floating_point T>
struct totalOrder {
  static_assert(std::numeric_limits<T>::is_iec559);
  bool operator()(T x, T y) const noexcept
    { return std::strong_order(x, y) < 0; };
};

using MapType = std::map<double, std::string, totalOrder<double>>;
// using MapType = std::map<double, std::string>; とすると
// キー比較にはstd::less<double>経由で言語組込の比較演算子<が利用され、
// 負のゼロ(-0.)や各種NaNをキーに用いると予期しない結果をもたらす。

const double qnan = std::numeric_limits<double>::quiet_NaN();
const double snan = std::numeric_limits<double>::signaling_NaN();

MapType m;
m[42] = "42";
m[-0.] = "-0";
m[+0.] = "+0";
m[-qnan] = "-qNaN";
m[+qnan] = "+qNaN";
m[-snan] = "-sNaN";
m[+snan] = "+sNaN";

std::cout
  << m[42] << "\n"     // 42
  << m[-0.] << "\n"    // -0
  << m[+0.] << "\n"    // +0
  << m[-qnan] << "\n"  // -qNaN
  << m[+qnan] << "\n"  // +qNaN
  << m[-snan] << "\n"  // -sNaN
  << m[+snan] << "\n"; // +sNaN

ノート:GCC/libstdc++とLLVM/libcxxの浮動小数点数型totalOrder実装は、NaNペイロード部の比較に関してIEEE 754仕様通りではない気がする(§5.10 d-3-iii)。特殊事情でもない限り浮動小数点数型を辞書型キーに使うべきではないし、totalOrder仕様に依存する処理は好ましくないと思う。

C++20 17.11.6/p1-2より一部引用。

1 The name strong_order denotes a customization point object (16.4.2.2.6). Given subexpressions E and F, the expression strong_order(E, F) is expression-equivalent (16.3.11) to the following:

  • (snip)
  • Otherwise, if the decayed type T of E is a floating-point type, yields a value of type strong_ordering that is consistent with the ordering observed by T's comparison operators, and if numeric_limits<T>::is_iec559 is true, is additionally consistent with the totalOrder operation as specified in ISO/IEC/IEEE 60559.
  • (snip)

2 The name weak_order denotes a customization point object (16.4.2.2.6). Given subexpressions E and F, the expression weak_order(E, F) is expression-equivalent (16.3.11) to the following:

  • (snip)
  • Otherwise, if the decayed type T of E is a floating-point type, yields a value of type weak_ordering that is consistent with the ordering observed by T’s comparison operators and strong_order, and if numeric_limits<T>::is_iec559 is true, is additionally consistent with the following equivalence classes, ordered from lesser to greater:
    • together, all negative NaN values;
    • negative infinity;
    • each normal negative value;
    • each subnormal negative value;
    • together, both zero values;
    • each subnormal positive value;
    • each normal positive value;
    • positive infinity;
    • together, all positive NaN values.
  • (snip)

IEEE 754-2019, 3.4, 5.10より一部引用。

3.4 Binary interchange format encodings
(snip)
In binary interchange formats, all number and NaN encodings are canonical.
(snip)

5.10 Details of totalOrder predicate
For each supported arithmetic format, an implementation shall provide the following predicate that defines an ordering among all operands in a particular format:

  • boolean totalOrder(source, source)

totalOrder(x, y) imposes a total ordering on canonical members of the format of x and y:

  • a) If x < y, totalOrder(x, y) is true.
  • b) If x > y, totalOrder(x, y) is false.
  • c) If x = y:
    • 1) totalOrder(-0, +0) is true.
    • 2) totalOrder(+0, -0) is false.
    • 3) If x and y represent the same floating-point datum:
      • i) If x and y have negative sign, totalOrder(x, y) is true if and only if the exponent of x ≥ the exponent of y
      • ii) otherwise totalOrder(x, y) is true if and only if the exponent of x ≤ the exponent of y.
  • d) If x and y are unordered numerically because x or y is NaN:
    • 1) totalOrder(-NaN, y) is true where -NaN represents a NaN with negative sign bit and y is a floating-point number.
    • 2) totalOrder(x, +NaN) is true where +NaN represents a NaN with positive sign bit and x is a floating-point number.
    • 3) If x and y are both NaNs, then totalOrder reflects a total ordering based on:
      • i) negative sign orders below positive sign
      • ii) signaling orders below quiet for +NaN, reverse for -NaN
      • iii) lesser payload, when regarded as an integer, orders below greater payload for +NaN, reverse for -NaN.

Neither signaling NaNs nor quiet NaNs signal an exception. For canonical x and y, totalOrder(x, y) and totalOrder(y, x) are both true if x and y are bitwise identical.

NOTE -- totalOrder does not impose a total ordering on all encodings in a format. In particular, it does not distinguish among different encodings of the same floating-point representation, as when one or both encodings are non-canonical

関連URL

*1:関数オブジェクトはCPO(Customization Point Object)として定義される。詳細は id:yohhoy:20190403 を参照。

CUDAのメモリアライメント

CUDAアーキテクチャにおける自然なメモリアライメントは 256 バイト。

CUDA提供のメモリ確保関数(cudaMalloc等)は、少なくとも 256 バイト・アライメントされたアドレスを返す。

Size and Alignment Requirement
(snip)
Any address of a variable residing in global memory or returned by one of the memory allocation routines from the driver or runtime API is always aligned to at least 256 bytes.

CUDA C++ Programming Guide, 5.3.2. Device Memory Accesses

関連URL

インクリメント on 複素数

プログラミング言語Cの次期仕様C2yでは、複素数型(_Complex float/double/long double)に対するインクリメント/デクリメントが正式サポートされる。gcc/Clangでは独自拡張としてサポート済み。

_Complex double c = 41.;
++c;  // OK: C2y
assert(c == 42.);

ノート:複素数のうち実部(real part)が+1.0/-1.0される。C++標準ライブラリの複素数std::complex<T>は、C言語とは異なりインクリメント/デクリメントをサポートしない。

gcc 14.2/-pedantic指定時の警告メッセージ:

warning: ISO C does not support '++' and '--' on complex types [-Wpedantic]

Clang 18.1.0/-pedantic指定時の警告メッセージ:

warning: ISO C does not support '++'/'--' on complex integer type '_Complex double' [-Wpedantic]

関連URL

std::submdspanとメモリレイアウト変換

C++2c(C++26)多次元部分ビューstd::submdspan(→id:yohhoy:20240201)によるメモリレイアウト変換のチートシート

変換結果std::mdspan<T,E,L,A>(→id:yohhoy:20230303)のレイアウトポリシーLは、変換元レイアウトマッピングsubmdspan_mappingカスタマイズポイントにより下記ルールにて導出される:

  • layout_left, layout_rightのとき
    1. レイアウト互換ならば、layout_{left,right}を維持
    2. レイアウト互換ならば、layout_{left,right}_paddedに変換(降格)
    3. layout_strideに変換(降格)
  • layout_left_padded, layout_right_paddedのとき
    1. 変換元が0次元(→id:yohhoy:20230309)のとき、layout_{left,right}_paddedを維持*1
    2. レイアウト互換ならば、layout_{left,right}に変換(昇格)*2
    3. レイアウト互換ならば、layout_{left,right}_paddedを維持
    4. layout_strideに変換(降格)
  • layout_strideのとき
    1. layout_strideを維持

ノート:C++2c標準ライブラリ提供メモリレイアウト型は、その型情報として layout_LRlayout_LR_paddedlayout_stride の順で強い制約条件を表す(LR := leftright)。

関連URL

*1:レイアウトとしては layout_{left,right} 型に昇格可能だが、他メモリレイアウト型における変換元=0次元の挙動に揃えたと考えられる。

*2:この変換条件を満たすのは、変換先=1次元以下かつ要素連続配置が保証されるケースに限られる。

構造化束縛 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
if (auto [ptr, ec] = std::to_chars(buf, last, 42); 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> エイリアス名だけを列挙している。