yohhoyの日記

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

fflush(NULL);

C標準ライブラリ関数fflushにヌルポインタを指定すると、プログラムが現在開いている全てのストリームをフラッシュする。

fflush(NULL);  // OK

C99 7.19.5.2/p1-3より引用(下線部は強調)。

#include <stdio.h>
int fflush(FILE *stream);

2 If stream points to an output stream or an update stream in which the most recent operation was not input, the fflush function causes anyunwritten data for that stream to be delivered to the host environment to be written to the file; otherwise, the behavior is undefined.
3 If stream is a null pointer, the fflush function performs this flushing action on all streams for which the behavior is defined above.

(PDF) C99 Rationale, 7.19.5.2より引用。

The fflush function ensures that output has been forced out of internal I/O buffers for a specified stream. Occasionally, however, it is necessary to ensure that all output is forced out, and the programmer may not conveniently be able to specify all the currently open streams, perhaps because some streams are manipulated within library packages. To provide an implementation-independent method of flushing all output buffers, the Standard specifies that this is the result of calling fflush with a NULL argument.

関連URL

_Exit/_exitはストリームをフラッシュしない

C標準ライブラリ_Exit関数およびPOSIX準拠_exit関数は、プログラムを異常終了させるC標準abort関数と同様に開いているストリーム(FILE型)をフラッシュしない*1。ログファイルや標準出力(stdout)への出力欠落に注意。

一方で、C標準ライブラリexit関数ではストリームフラッシュ動作が保証される。エントリポイントmain関数からのreturnexit関数呼び出しと同義。

C99 5.1.2.2.3, 7.20.4.1/p1-2, 7.20.4.3/p1, p4, 7.20.4.4/p1-2より引用(下線部は強調)。

If the return type of the main function is a type compatible with int, a return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument; (snip)

#include <stdlib.h>
void abort(void);

The abort function causes abnormal program termination to occur, unless the signal SIGABRT is being caught and the signal handler does not return. Whether open streams with unwritten buffered data are flushed, open streams are closed, or temporary files are removed is implementation-defined. (snip)

#include <stdlib.h>
void exit(int status);

Next, all open streams with unwritten buffered data are flushed, all open streams are closed, and all files created by the tmpfile function are removed.

#include <stdlib.h>
void _Exit(int status);

The _Exit function causes normal program termination to occur and control to be returned to the host environment. No functions registered by the atexit function or signal handlers registered by the signal function are called. The status returned to the host environment is determined in the same way as for the exit function. Whether open streams with unwritten buffered data are flushed, open streams are closed, or temporary files are removed is implementation-defined.

IEEE Std 1003.1-2024より一部引用(下線部は強調)。この動作仕様はIEEE Std 1003.1-2017で明確化された。

DESCRIPTION
The _Exit() and _exit() functions shall be functionally equivalent.

The _Exit() and _exit() functions shall not call functions registered with atexit() nor at_quick_exit(), nor any registered signal handlers. Open streams shall not be flushed. Whether open streams are closed (without flushing) is implementation-defined. Finally, the calling process shall be terminated with the consequences described below.

CHANGE HISTORY
Issue 7
Austin Group Interpretation 1003.1-2001 #031 is applied, separating these functions from the exit() function.
Austin Group Interpretation 1003.1-2001 #085 is applied, clarifying the text regarding flushing of streams and closing of temporary files.

関連URL

*1:厳密には、C標準ライブラリ仕様では _Exit がストリームをフラッシュするか否かは処理系定義(implementation-defined)とされる。POSIX仕様ではストリームフラッシュ無しと明確に規定する。

bit_castマクロ for C言語

プログラミング言語Cにおいて、ビット表現を維持したまま型変換を安全に行うマクロ。C++標準ライブラリのstd::bit_cast<To>(from)相当。

本記事の内容はStackOverflowで見つけた質問と回答に基づく。

// C99 : 右辺値に未対応
#define bit_cast(T, ...) \
  (*((T*)memcpy(&(T){0}, &(__VA_ARGS__), sizeof(T))))

// C23
#define bit_cast(T, ...) \
  ((union{typeof(T) a; typeof(__VA_ARGS__) b;}) {.b=(__VA_ARGS__)}.a)
// または
#define bit_cast(T, ...) \
  (*(typeof(T)*)memcpy(&(T){0},
    &(typeof(__VA_ARGS__)) {(__VA_ARGS__)}, sizeof(T)))

式static_assert(→id:yohhoy:20250905)を利用して型サイズ検査を組み込んだバージョン:

// C23
#define bit_cast(T, ...) \
  ((union{typeof(T) a; typeof(__VA_ARGS__) b; \
      static_assert(sizeof(T)==sizeof(__VA_ARGS__));}) \
    {.b=(__VA_ARGS__)}.a)

// C11
#define bit_cast(T, ...) \
  ((void)sizeof(struct{int dummy_member; \
     _Static_assert(sizeof(T) == sizeof(__VA_ARGS__), "");}), \
   *((T*)memcpy(&(T){0}, &(__VA_ARGS__), sizeof(T))))
// #include <assert.h>でstatic_assertと記述可能

関連URL

C++26 std::execution

本文こちら→C++ MIX #16に参加しました - yohhoyの日記(別館)

スライド資料:https://www.docswell.com/s/yohhoy/ZQXJ2E-cpp26-execution

関連URL

C++26 Executionライブラリ:非同期スコープ

C++2c(C++26)実行制御ライブラリ(execution control library)(→id:yohhoy:20250609)における非同期操作の早期開始と非同期スコープについて。

Sender型で表現されたタスクチェインに対して、非同期操作の開始要求と完了待機を一括実施する遅延開始(lazy start)と、非同期スコープ(async scope)との組み合わせによる開始要求と完了待機を分離した早期開始(eager start)を行うSenderアルゴリズム(→id:yohhoy:20250612)が提供される。

まとめ:

  • 遅延開始:Senderアルゴリズムthis_thread::sync_wait(_with_variant)により、非同期操作を開始して完了まで待機する。処理結果は戻り値/例外送出によって取得される。
  • 早期開始:Senderアルゴリズムexecution::spawn, execution::spawn_futureにより、非同期スコープと関連付けて非同期操作を早期開始する。完了待機には非同期スコープから生成した合流(join)Senderを利用し、非同期操作の結果はspawn_futureが返すSender経由で取得する。
    • spawnコンシューマ:空の値完了set_value_t()と停止完了set_stopped_t()のみ対応。エラー完了には対応しない。
    • spawn_futureアダプタ:任意の完了シグネチャ集合に対応。
  • Senderアルゴリズムexecution::associate:Senderを非同期スコープと関連付け、非同期操作の生存期間(lifetime)をトラッキングする。任意の完了シグネチャ集合に対応。

spawn/spawn_futrueアルゴリズム

// C++2c
#include <execution>
namespace exec = std::execution;

void proc(int) noexcept;  // 戻り値なし
int compute(int) noexcept; // 戻り値あり

void start_lazy()
{
  // タスクチェインを構成: システムスレッドプール上でproc(1)を呼び出す
  exec::sender auto work = exec::on(
    exec::get_parallel_scheduler(),
    exec::just(1) | exec::then(proc));

  proc(2);

  // 非同期操作を遅延開始し、処理完了まで待機する
  std::this_thread::sync_wait(std::move(work));

  proc(3);
  // proc(2)→proc(1)→proc(3)順に呼び出される
}

void start_eager()
{
  // タスクチェインを構成: システムスレッドプール上でproc(1)を呼び出す
  exec::sender auto work = exec::on(
    exec::get_parallel_scheduler(),
    exec::just(1) | exec::then(proc));

  // 非同期スコープに関連付けて非同期操作を早期開始する
  exec::counting_scope scope;
  exec::spawn(std::move(work), scope.get_token());

  proc(2);

  // 非同期スコープに関連付けられた全ての非同期操作完了を待機する
  std::this_thread::sync_wait(scope.join());

  proc(3);
  // {proc(1) | proc(2)}→proc(3)順に呼び出される
  // proc(1)とproc(2)は同時並行に実行されうる
}

void start_eager_future()
{
  // タスクチェインを構成: システムスレッドプール上でcompute(1)を呼び出す
  exec::sender auto work = exec::on(
    exec::get_parallel_scheduler(),
    exec::just(1) | exec::then(compute));

  // 非同期スコープに関連付けて非同期操作を早期開始する
  exec::counting_scope scope;
  exec::sender auto future =
    exec::spawn_future(std::move(work), scope.get_token());

  int r2 = compute(2);

  // 非同期スコープに関連付けられた全ての非同期操作完了を待機する
  auto result = std::this_thread::sync_wait(
                  exec::when_all(std::move(future), scope.join()));
  auto [r1] = result.value();

  // compute(1)とcompute(2)は同時並行に実行されうる
}

Senderチェイン内で入れ子の非同期スコープを自動管理するSenderアダプタexecution::let_async_scopeは提案文書P3296にて検討中。*1

// P3296
std::this_thread::sync_wait(
  exec::just(1)
  | exec::let_async_scope(
    [] (auto token, int n) {
      // 非同期スコープに関連付けて非同期操作を早期開始する
      exec::spawn(exec::on(
        exec::get_parallel_scheduler(),
        exec::just(n) | exec::then(proc)
      ), token);

      proc(2);
    }
  ) | then([] {
      proc(3);
    }
  ));
// {proc(1) | proc(2)}→proc(3)順に呼び出される
// proc(1)とproc(2)は同時並行に実行されうる

非同期スコープ: Counting Scope

C++2c標準ライブラリは、カウンタ値に基づく非同期スコープ実装クラスを提供する。これらの非同期スコープ型はコピー/ムーブ不可。

  • execution::simple_counting_scope:非同期操作の早期開始でカウンタをインクリメントし、非同期操作の完了時にカウンタをデクリメントする。
    • get_token操作:非同期スコープトークン(scope_token)を取得する。
    • close操作:以降は非同期操作との関連付けを抑止し、早期開始操作(spawn, spawn_future)では非同期操作をスキップする。spawn_future戻り値Senderでは停止完了(set_stopped)が即時送信される。
    • join操作:カウンタ値が0になるまで合流待機(join)するSenderを生成する。合流Senderの完了シグネチャ集合=空の値完了(set_value_t())+任意のエラー完了(set_error_t(E))+停止完了(set_stopped_t()) *2
  • execution::counting_scopesimple_counting_scope+非同期キャンセル(request_stop)のサポート。

Counting Scopeクラスは内部状態を管理しており、各操作をトリガとして状態遷移が行われる。

  • オブジェクト構築後の初期状態:unused
  • オブジェクト破棄時の事前条件:unused/joined/unused-and-closedのいずれか

操作assocはSenderとの関連付け成功、操作disassocは非同期操作の完了によりカウンタ値が0となる最終関連付けの解除、操作start-joinは合流Senderの開始を表す。

状態\操作 assoc disassoc start-join close
unused open - joined unused-and-closed
open (+1) - open-and-joining closed
open-and-joining (+1) joined (open-and-joining) closed-and-joining
closed - - closed-and-joining -
unused-and-closed - - joined -
closed-and-joining - joined (closed-and-joining) -
joined - - (joined) -

表中の下線付き状態はオブジェクト破棄可能であることを、括弧は内部状態が変化しない自己遷移を表す。"(+1)" はカウンタのインクリメントのみ行われ、状態遷移しない。

associateアルゴリズム

入力Senderに対して非同期スコープとの関連付けを行い、関連付けされた(associated)Senderを返す。同Senderがそのまま破棄されるか、開始(start)された非同期操作の完了までを非同期スコープを介して追跡する。

exec::counting_scope scope;

// タスクチェインを構成し、非同期スコープに関連付ける
exec::sender auto work =
  exec::just(1)
  | exec::then(proc)
  | exec::associate(scope.get_token());

// タスクを別スレッド上で開始 or そのまま破棄する操作へ委譲
async_start_or_discard_task(std::move(work));

// (自スレッドの処理)

// 非同期操作の完了 or タスク破棄を待機
std::this_thread::sync_wait(scope.join());

associateアルゴリズムが関連付けに失敗すると関連付けの無い(unassociated)Senderを返し、同Senderの開始操作は上流側Senderを開始せず即座に停止完了を送信する。Counting Scopeに対してclose操作を行うと以降の関連付けが失敗するようになり、別スレッドからの非同期タスクの開始をの受付停止機構を実現できる。

exec::scheduler auto sch = /*Scheduler*/;
exec::counting_scope scope;

// 非同期要求の受付停止とリソース解放
void finish()
{
  // 非同期スコープをcloseし、以降の非同期タスク開始を抑止する
  scope.close();
  // 関連付けが行われた全タスクの完了(or破棄)を待機する
  std::this_thread::sync_wait(scope.join());

  // (リソース解放処理)
}

// sch上で実行される非同期スコープに関連付けたタスクを返す
//   finish前: 処理Xが実行され、戻り値を値送信する
//   finish後: 停止完了が送信される(処理Xは実行されない)
exec::sender auto create_new_task(int args);
{
  return exec::schedule(sch)
       | exec::then([args](){ return /*処理X*/; })
       | exec::associate(scope.get_token());
}

関連URL

*1:https://github.com/cplusplus/papers/issues/1948

*2:Receiver環境から取得したSchedulerに対するスケジュールSender(schedule sender)の完了シグネチャ集合に相当する。

*3:https://github.com/cplusplus/papers/issues/2315

*4:https://github.com/cplusplus/papers/issues/2335

式static_assert in C言語

プログラミング言語Cにおいて、式(expression)として扱えるコンパイル時アサートの実装例(C11以降)。*1 *2

#include <assert.h>

#define static_assert_expr(cexp_, msg_) \
  ((void)sizeof(struct {        \
    static_assert(cexp_, msg_); \
    int dummy_member;           \
  }))

C言語では空の構造体が禁止される(→id:yohhoy:20230430)ため、ダミーのメンバdummy_memberを追加している。C11 6.2.5/p20, 6.7.2.1/p1, 6.7.10/p1より一部引用(下線部は強調)。

Any number of derived types can be constructed from the object and function types, as follows:

  • (snip)
  • A structure type describes a sequentially allocated nonempty set of member objects (and, in certain circumstances, an incomplete array), each of which has an optionally specified name and possibly distinct type.
  • (snip)

Syntax
struct-or-union-specifier:
  struct-or-union identifieropt { struct-declaration-list }
  struct-or-union identifier
struct-or-union:
  struct
  union
struct-declaration-list:
  struct-declaration
  struct-declaration-list struct-declaration
struct-declaration:
  declaration-specifiers init-declarator-listopt ;
  static_assert-declaration
(snip)

Syntax
static_assert-declaration:
  _Static_assert ( constant-expression, string-literal ) ;

ノート:C++ではsizeof式での新しい型定義は明示的に禁止されている。*3

関連URL

*1:C11で導入された _Static_assert/static_assert は構文要素上は宣言(declaration)に区分されるため、そのままでは式(expression)の一部として利用できない。C17時点では<assert.h>ヘッダによる static_assert マクロ定義を必要とするが、C23現在は static_assert がキーワードに格上げされ、従前の _Static_assert 利用は非推奨となっている。

*2:C99時点ではLinuxの BUILD_BUG_ON マクロのように、負の要素数を持つ配列型によるHACK実装が行われていた。実装例:((void)sizeof(char[(cexpr_)?1:-1]))

*3:C++03 5.3.3/p5: "Types shall not be defined in a sizeof expression."