yohhoyの日記

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

std::mdspan AccessorPolicy応用例

C++23標準ライブラリの多次元ビューstd::mdspan(→id:yohhoy:20230303)における、第4テンプレートパラメータAcssesorPolicyを用いた要素アクセスカスタマイズの具体事例。

C++2c(C++26)標準ライブラリ採用が決定している線形代数基本アルゴリズム <linalg> ヘッダでは、mdspan参照先のメモリを書換えずに各要素のスケーリング(std::linalg::scaled)や複素共役(std::linalg::conjugated)変換を行うビューを提供する。適用後の多次元ビュー要素は読み取り専用となる。

#include <mdspan>
#include <linalg>  // C++2c(C++26)

using Vec = std::mdspan<int, std::dextent<size_t, 1>>;
int arr[] = {1, 2, 3};

Vec vec1{ arr };
assert(vec1[0] == 1);
// 要素アクセス vec1[i] はint&型を返す
// &(vec1[i]) == &(arr[i])

// 全要素を2倍した1次元ビュー
auto vec2 = std::linalg::scaled(2, vec1);
assert(vec2[0] == 2 && arr[0] == 1);
// 要素アクセス vec2[i] はint型を返すため
// 要素書き換え vec2[0] = 42; はill-formed

提案文書P1673R13 Wordingより一部引用(クラス宣言は簡略化)。

1 The class template scaled_accessor is an mdspan accessor policy which upon access produces scaled elements. reference. It is part of the implementation of scaled [linalg.scaled.scaled].

template<class ScalingFactor, class NestedAccessor>
class scaled_accessor {
public:
  using element_type = add_const_t<
    decltype(declval<ScalingFactor>() * declval<NestedAccessor::element_type>())>;
  using reference = remove_const_t<element_type>;
  using data_handle_type = NestedAccessor::data_handle_type;
  using offset_policy = /*...*/;

  constexpr scaled_accessor(const ScalingFactor& s, const NestedAccessor& a);
  constexpr reference access(data_handle_type p, size_t i) const;
  // ...
};

1 The scaled function template takes a scaling factor alpha and an mdspan x, and returns a new read-only mdspan with the same domain as x, that represents the elementwise product of alpha with each element of x.

template<class ScalingFactor,
         class ElementType,
         class Extents,
         class Layout,
         class Accessor>
constexpr auto scaled(
  ScalingFactor alpha,
  mdspan<ElementType, Extents, Layout, Accessor> x);

2 Let SA be scaled_accessor<ScalingFactor, Accessor>
3 Returns:

mdspan<typename SA::element_type, Extents, Layout, SA>(
  x.data_handle(), x.mapping(), SA(alpha, x.accessor()))

メモ:<linalg> ヘッダではこのほかに行列転置(std::linalg::transposed)や複素共役転置(std::linalg::conjugate_transposed)変換を行うビューも提供する。行列転置はmdspanの第3テンプレートパラメータLayoutPolicyを利用して実現される。

関連URL

std::views::filter適用後の値書換えには要注意

C++標準ライブラリ提供レンジアダプタstd::views::filter適用後の要素に対する変更操作には十分留意すること。

変更操作により要素がフィルタ条件を満たさなくなる場合、C++ライブラリ仕様上は未定義動作(undefined behavior)を引き起こす。この問題は遅延評価によりフィルタ条件が複数評価されるケースで初めて表面化するため、ライブラリ仕様違反が潜在化するリスクが高い。C++ Ranges難しい(´・ω・)(・ω・`)ネー

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

bool is_odd(int x) { return x % 2 != 0; }

std::vector vec1 = { 1, 2, 3, 4, 5, 6 };
std::vector vec2 = vec1, vec3 = vec1;

for (int& e: vec1 | std::views::filter(is_odd) | std::views::reverse) {
  e += 10;  // OK: 条件is_odd(e)は維持される
  std::cout << e << ' ';
}
// 15 13 11 を出力

for (int& e: vec2 | std::views::filter(is_odd) | std::views::reverse) {
  e += 1;  // NG: 条件is_odd(e)を満たさないためUB
  std::cout << e << ' ';
}
// GCC/Clang: SEGV発生

// 上記例から reverse, filter 適用順を入れ替え
for (int& e: vec3 | std::views::reverse | std::views::filter(is_odd)) {
  e += 1;  // NG: 本来はUBだが...
  std::cout << e << ' ';
}
// GCC/Clang: 6 4 2 を出力

C++20 24.7.4.3/p1より引用。

Modification of the element a filter_view::iterator denotes is permitted, but results in undefined behavior if the resulting value does not satisfy the filter predicate.

関連URL

Living Dead/Zombie in C++ Standard

プログラミング言語C++標準規格の索引(Index)に紛れ込むリビングデッド。🧠👀🧟*1

brains
  names that want to eat your, [zombie.names]

living dead
  name of, [zombie.names]

https://github.com/cplusplus/draft/commit/e844e0f45550eb0bf11ea262e4abd8a5403f47d4

関連URL

厳格な式の評価順序 for C2y

プログラミング言語Cの次期仕様C2yに向けて、式の評価順序を厳格に規定する提案 N3203 Strict order of expression evaluation が提出されている。チャレンジングなお話。

C言語C++も同様)では歴史的経緯から、演算子オペランドの評価順*1や関数実引数リストの評価順(→id:yohhoy:20120304)は規定されておらず、任意の順序で実行される可能性がある。*2

C/C++以降のプログラミング言語では演算子オペランドや関数実引数リストの評価順「左→右」と保証されており、本提案がC言語に採用されれば未規定(unspecified)動作や未定義動作(undefined behavior)の回避に大きく貢献するはず。いや、でも厳しそうだなぁ...

int f() { puts("f"); return 1; }
int g() { puts("g"); return 2; }
void h(int, int) {}

int a = f() + g();  // a==3
// C23: 標準出力は f→g / g→f 順のいずれか
// N3203: 必ず f→g の順を保証する

h( f(), g() );
// C23: 標準出力は f→g / g→f 順のいずれか
// N3203: 必ず f→g の順を保証する

関連URL

*1:C/C++いずれも、論理積演算子(&&), 論理和演算子(||), カンマ演算子(,)についてはオペランド評価順が左→右と規定されている。

*2:C++17以降では、一部の演算子に限定して左→右の評価順が保証される。一方で、関数実引数リストの評価順は規程されない。

飽和演算サポート @ C++26

C++2c(C++26)標準ライブラリに追加される飽和演算(saturation arithmetic)サポートについてメモ。

// C++2c <numeric>ヘッダ
namespace std {
  // T,U = 符号付き整数型 or 符号無し整数型
  template<class T>
    constexpr T add_sat(T x, T y) noexcept;
  template<class T>
    constexpr T sub_sat(T x, T y) noexcept;
  template<class T>
    constexpr T mul_sat(T x, T y) noexcept;
  template<class T>
    constexpr T div_sat(T x, T y) noexcept;
  template<class T, class U>
    constexpr T saturate_cast(U x) noexcept;
}
関数 効果
add_sat(x,y) 飽和加算(x + y)
sub_sat(x,y) 飽和減算(x - y)
mul_sat(x,y) 飽和乗算(x * y)
div_sat(x,y) 飽和除算(x / y)
saturate_cast<T>(x) 型キャスト飽和演算

メモ:信号処理分野では飽和演算が多用されるため、例えばOpenCVライブラリでは型キャスト飽和演算cv::saturate_cast<T>(x)が提供される。C++2c標準ライブラリ版はOpenCV版とは異なり、浮動小数点数からの型変換をサポートしない。*1

関連URL

異種クラス同名メンバ関数の個別オーバーライド

プログラミング言語C++において、異なる基底クラスに属する同名メンバ関数*1を個別にオーバーライドする方法。

// 同名メンバ関数をカスタマイズポイントとして提供する
// 互いに無関係なインタフェースクラス
struct Interface1 {
  virtual void process() = 0;
};
struct Interface2 {
  virtual void process() = 0;
};

// インタフェース実装クラス
struct Derived: Interface1, Interface2 {
  void process() override { /*...*/ }
  // Interface1::process と Interface2::process を
  // 同時にオーバーライドするため実装分離は不可能
};

インタフェース別にprocessメンバ関数オーバーライドする中間クラスを定義し、別名メンバ関数(process1, process2)への処理委譲によって個別オーバーライドを可能とする。

// メンバ関数名を変換するプロキシインタフェース
struct ProxyInterface1 : Interface1 {
  void process() override final { return process1(); }
  virtual void process1() = 0;
};
struct ProxyInterface2 : Interface2 {
  void process() override final { return process2(); }
  virtual void process2() = 0;
};

// インタフェース実装クラス
struct Derived: ProxyInterface1, ProxyInterface2 {
  void process1() override { /*A*/ }
  void process2() override { /*B*/ }
  // 下記はコンパイルエラーとして検出
  // void process() override {}
};

Derived obj;
Interface1& if1 = obj;  if1.process();  // A
Interface2& if2 = obj;  if2.process();  // B

関連URL

*1:厳密には同一シグネチャ(→id:yohhoy:20210927)をもつ仮想関数。

NEO assertマクロ

プログラミング言語C/C++の次期標準規格C2x(C23)およびC++2c(C++26)では、アサーションマクロassertの改善が行われる。

#include <assert.h> // C/C++
#include <cassert>  // C++のみ

int is_valid(int);

assert( "42 shall be vaild", is_valid(42) );
// NG: C17/C++20現在
// OK: C2x/C++2c以降

assert(("42 shall be vaild", is_valid(42)));
// OK: 式全体を括弧で囲う

C言語ではトラブルを引き起こすケースは少ないが*1C++言語では初期化子リストやテンプレートパラメータなどでトラブル発生頻度が高い。はず。

// C++事例
assert( get_vec() == std::vector{1, 2, 3} );
assert( calc_mul<int,2>(3) == 6 );

本件は遡及適用されるバグ修正ではなく、新機能として取り扱われる模様。JTC1/SC22/WG14 2022年1-2月会議録*2より引用:

5.29 Sommerlad, Make assert() macro user friendly for C and C++ v2 [N 2829]
Discussion of how the current wording says "scalar expression" so this could be seen as a bug fix.

Concerns about making this a special macro definition vs all the other macros in the standard. Makes it inconsistent. Counters included incremental improvement or that this was the only macro that is a problem (author mentioned specifically for C++).

関連URL

*1:本文中にあるカンマ演算子を用いた作為的な例も、実用的には論理積演算子(&&)を用いて代替記述できる。

*2:https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2991.pdf