yohhoyの日記

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

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

signal関数プロトタイプ宣言

C++17以降のC++標準ライブラリ仕様では、signal関数のプロトタイプ宣言が読みやすく書き直されている。

C++17仕様

C++標準ライブラリ仕様としてプロトタイプ宣言が行われている。C++17 21.10.3より宣言を引用:

Header <csignal> synopsis

namespace std {
  // 21.10.4, signal handlers
  extern "C" using signal-handler = void(int); // exposition only
  signal-handler* signal(int sig, signal-handler* func);
}

The contents of the header <csignal> are the same as the C standard library header <signal.h>.

またC++標準において、シグナルハンドラ内で行っても良い Signal-safe 操作の定義がなされている。C++17 21.10.4/p3より引用:

An evaluation is signal-safe unless it includes one of the following:

  • a call to any standard library function, except for plain lock-free atomic operations and functions explicitly identified as signal-safe. [Note: This implicitly excludes the use of new and delete expressions that rely on a library-provided memory allocator. -- end note]
  • an access to an object with thread storage duration;
  • a dynamic_cast expression;
  • throwing of an exception;
  • control entering a try-block or function-try-block;
  • initialization of a variable with static storage duration requiring dynamic initialization (6.6.3, 9.7); or
  • waiting for the completion of the initialization of a variable with static storage duration (9.7).

A signal handler invocation has undefined behavior if it includes an evaluation that is not signal-safe.

C++03/11/14仕様

C++14以前の仕様では独自のプロトタイプ宣言を行わず、標準Cヘッダ<signal.h>を参照している。

シグナルハンドラ内からの(一部例外を除く)C++関数呼び出しは処理系定義(implementation-defined)とされ、Signal-safeに関する言及は存在しない。C++14 18.10/p10より一部引用:

The common subset of the C and C++ languages consists of all declarations, definitions, and expressions that may appear in a well formed C++ program and also in a conforming C program. A POF ("plain old function") is a function that uses only features from this common subset, and that does not directly or indirectly use any function that is not a POF, except that it may use plain lock-free atomic operations. (snip) All signal handlers shall have C linkage. The behavior of any function other than a POF used as a signal handler in a C++ program is implementation-defined.228
脚注228) In particular, a signal handler using exception handling is very likely to have problems. Also, invoking std::exit may cause destruction of objects, including those of the standard library implementation, which, in general, yields undefined behavior in a signal handler (see 1.9).

C仕様

C17 7.14.1.1/p1より宣言を引用:

Synopsis

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);

シグナルハンドラ内で許可される操作は、条件付きで明示的に列挙されている。C17 7.14.1.1/p4-5より一部引用:

4 If the signal occurs as the result of calling the abort or raise function, the signal handler shall not call the raise function.
5 If the signal occurs other than as the result of calling the abort or raise function, the behavior is undefined if the signal handler refers to any object with static or thread storage duration that is not a lock-free atomic object other than by assigning a value to an object declared as volatile sig_atomic_t, or the signal handler calls any function in the standard library other than (snip)

関連URL

タグ型の実装イディオム

C++標準ライブラリで使われるタグ型(tag type)とタグ値の実装イディオム。

デフォルトコンストラクタへのexplicit指定は、{}によるタグ型(mytag_t)デフォルト構築を禁止するため。

struct mytag_t {
  explicit mytag_t() = default;
};

inline constexpr mytag_t mytag{};  // C++17以降
struct S {};
void f(S);  // #1
void f(mytag_t);  // #2

f({});  // #1を呼び出す

関連URL