Open8

C 言語なんもわからん

nukopynukopy

define マクロには第二引数はなくても良い

第二引数がある使い方

競プロでよく出てくるマクロの使い方しか知らなかった。こういうの↓(これは C++ だけど)

#include <bits/stdc++.h>
using namespace std;

#define rep(i, n) for (int i = 0; (i) < (n); ++(i))

int main() {
    rep(i, 10) {
        cout << i << '\n';
    }
}

第二引数を渡さない使い方

Bash のソースコードのヘッダファイル include/stdc.h にて、第二引数を渡さない使い方をしている部分があり、読み方がよくわからなかった。

https://github.com/bminor/bash/blob/f3b6bd19457e260b65d11f2712ec3da56cef463f/include/stdc.h

GitHub Copilot Chat に聞いたところ以下のような回答が返ってきて理解できた。

Q. define マクロって第二引数に値がなくても良いの?

回答

はい、その通りです。C言語のプリプロセッサディレクティブ#defineは、マクロ名だけを指定し、値を指定しないことも可能です。このようなマクロは、コード内で特定の条件が満たされているかどうかをチェックするためによく使用されます。

例えば、以下のようなコードがあります:

#define ENABLE_FEATURE

#ifdef ENABLE_FEATURE
   // ENABLE_FEATUREが定義されている場合にのみ実行されるコード
#endif

この例では、ENABLE_FEATUREマクロは値を持っていませんが、#ifdefディレクティブを使用して、そのマクロが定義されているかどうかをチェックしています。このように、値を持たないマクロは、コードの特定の部分を有効化または無効化するためのフラグとして機能します。

なるほど、第二引数がなくてもマクロは定義でき、その機能を使ってコンパイル時のフラグとして使えるのか。

nukopynukopy

typedef の文法

Bash の command.htypedef command {...} COMMAND; を見た時、なぜ構造体の名前の command があるのに型定義の末尾の COMMAND があるのかわからなかった(昔調べた気がする、、、)。

https://github.com/bminor/bash/blob/f3b6bd19457e260b65d11f2712ec3da56cef463f/command.h#L197-L226

Q. typedef の記法がよくわからないんだけど、なぜ command と末尾の COMMAND が存在するの?

なるほど、typedef 型 型名; で型のエイリアスを定義するための文法か。そしてここでの書き方は構造体の定義と型定義を同時に行うための書き方なのか。

これわすれてたのやばみ。変な思い込みしてたな。

typedefはC言語のキーワードで、新しい型名を定義するために使用されます。typedefを使用すると、既存の型に対して新しい名前を付けることができます。これはコードの可読性を向上させたり、特定の型を短い名前で参照できるようにするために役立ちます。

以下のコードを見てみましょう:

typedef struct command {
 // ...
} COMMAND;

このコードは、struct commandという構造体にCOMMANDという新しい名前を付けています。これにより、struct commandCOMMANDとして参照できるようになります。

例えば、struct commandの変数を宣言するには通常以下のようにします:

struct command my_command;

しかし、typedefを使用してCOMMANDという別名を定義した場合、以下のように短く書くことができます:

COMMAND my_command;

このように、typedefはコードを短くし、可読性を向上させるために使用されます。

nukopynukopy

なんかで見たと思ったらこれだ。Token Token の部分、同じ識別子をしてて気持ち悪いけど、これで struct Token *token;Token *token; と書けるようにしてるんだね。

https://www.sigbus.info/compilerbook#ステップ3トークナイザを導入

// トークンの種類
typedef enum {
  TK_RESERVED, // 記号
  TK_NUM,      // 整数トークン
  TK_EOF,      // 入力の終わりを表すトークン
} TokenKind;

// トークン型
struct Token {
  TokenKind kind; // トークンの型
  Token *next;    // 次の入力トークン
  int val;        // kindがTK_NUMの場合、その数値
  char *str;      // トークン文字列
};

typedef struct Token Token;

// 現在着目しているトークン
Token *token;
nukopynukopy

volatile ってなに?

Bash の eval.creader_loop() 関数内にて、変数を宣言するときに volatile というキーワードがあった。

https://github.com/bminor/bash/blob/f3b6bd19457e260b65d11f2712ec3da56cef463f/eval.c#L56-L64

Q. これってどういう意味の書き方なの? COMMAND * volatile current_command;

回答 by GitHub Copilot Chat

COMMAND * volatile current_command; というコードは、C言語でのポインタ変数の宣言です。ここでは current_command という名前のポインタ変数を宣言していますが、このポインタが指す先の内容(つまり、COMMAND 型のオブジェクト)が volatile として宣言されています。

volatile キーワードは、その変数がコンパイラによる最適化から除外されることを示します。つまり、コンパイラは volatile が付けられた変数がプログラムの実行中に予期せぬ方法で変更される可能性があると認識します。そのため、コンパイラは volatile 変数に対する読み取りや書き込みを最適化や再配置の対象から除外します。

この volatile が付けられたポインタ変数 current_command は、マルチスレッド環境や割り込みハンドラなど、複数のコンテキストからアクセスされる可能性がある場合に有用です。これにより、current_command が指す COMMAND オブジェクトが予期せぬタイミングで変更されることを防ぎ、データの一貫性を保つことができます。

nukopynukopy

Q. volatile はどういう役割を持つの?一般的な説明を丁寧にお願い。そのあと、bash の文脈でなぜこの current_command に volatile をつける必要があるのか教えて。

回答

volatileはC言語のキーワードで、変数がコンパイラによる最適化から除外されることを示します。具体的には、volatileが付けられた変数は、プログラムの実行中に予期せぬ方法で変更される可能性があるとコンパイラに伝えます。そのため、コンパイラはvolatile変数に対する読み取りや書き込みを最適化や再配置の対象から除外します。

volatileキーワードは、以下のような状況で使用されます:

  1. ハードウェアレジスタへのアクセス:ハードウェアレジスタは、プログラムの実行中にハードウェアによって予期せぬタイミングで変更される可能性があります。そのため、これらのレジスタへのアクセスを表す変数にはvolatileを使用します。
  2. 割り込みサービスルーチン:割り込みサービスルーチンは、非同期的に実行され、共有変数を変更する可能性があります。これらの変数はvolatileで宣言する必要があります。
  3. マルチスレッドアプリケーション:マルチスレッドアプリケーションでは、複数のスレッドが共有変数にアクセスします。これらの変数は、予期せぬタイミングで変更される可能性があるため、volatileで宣言する必要があります。

bashのコードにおけるvolatileの使用について説明します。volatileキーワードがcurrent_command変数に適用されています。これは、current_commandがシグナルハンドラや他のスレッドなど、プログラムの主要な制御フローの外部から変更される可能性があることを示しています。

具体的には、bashはシグナルを使用してタイムアウトを実装しています。タイムアウトが発生すると、シグナルハンドラが呼び出され、制御が現在のコマンドからシグナルハンドラに移ります。このとき、current_commandの値が変更される可能性があります。そのため、current_commandvolatileとして宣言されています。これにより、コンパイラはcurrent_commandの値がいつでも変更される可能性があると認識し、その最適化を防ぎます。

nukopynukopy

Q. なぜ予期せぬ方法で変更される可能性がある変数は最適化をしてはいけないのですか?最適化について詳しく説明してください。

コンパイラの最適化とは、プログラムの実行速度を向上させたり、メモリ使用量を減らしたりするために、コンパイラが生成する機械語コードを改良するプロセスのことを指します。例えば、不要な計算を削除したり、ループの展開を行ったり、定数の畳み込みを行ったりします。

しかし、volatileキーワードが付けられた変数に対しては、これらの最適化を行うことは適切ではありません。なぜなら、volatile変数はプログラムの実行中に予期せぬ方法で変更される可能性があるからです。これは、例えばハードウェアレジスタや割り込みサービスルーチン、マルチスレッド環境などからのアクセスによって変更される可能性があります。

コンパイラの最適化が行われると、コードの一部が削除されたり、順序が変更されたりすることがあります。これは通常、プログラムの動作に影響を与えません。しかし、volatile変数に対してこれらの最適化を行うと、プログラムの動作が予期せぬ方法で変更される可能性があります。例えば、コンパイラがvolatile変数の読み取りや書き込みを削除したり、順序を変更したりすると、プログラムは正しく動作しなくなる可能性があります。

したがって、volatileキーワードは、コンパイラに対して特定の変数が予期せぬ方法で変更される可能性があることを示し、その変数に対する最適化を抑制する役割を果たします。これにより、volatile変数に対する読み取りや書き込みは、プログラマが指定した順序で、かつ必ず実行されることが保証されます。

nukopynukopy

Q. 例えば、Rust では volatile に相当する機能はある?

はい、Rustにはvolatileに相当する機能が存在します。それはstd::ptr::read_volatilestd::ptr::write_volatileという2つの関数です。これらの関数は、メモリの特定の位置からの読み取りや書き込みを行い、その操作が最適化により削除されないことを保証します。

以下に、これらの関数の使用例を示します:

use std::ptr;

let mut data = 42;
let ptr = &mut data as *mut i32;

unsafe {
    let value = ptr.read_volatile(); // volatile read
    ptr.write_volatile(0); // volatile write
}

このコードでは、dataという変数のポインタを取得し、そのポインタを通じてread_volatilewrite_volatileを使用しています。これにより、dataへの読み取りと書き込みが最適化により削除されないことが保証されます。

ただし、Rustのvolatile操作はC言語のそれとは異なり、スレッド間のデータ競合を防ぐためのものではありません。Rustでは、スレッド間のデータ競合を防ぐためには、std::syncモジュールの機能(例えば、MutexRwLockなど)を使用する必要があります。

nukopynukopy

Q. コンパイラの最適化とメモリ管理とハードウェアレジスタの知識がないとちょっと理解が及ばないかもと思った。むずかしい。

確かに、コンパイラの最適化、メモリ管理、ハードウェアレジスタなどは深い知識を必要とするテーマであり、初めて学ぶときは難しく感じることが多いです。

しかし、基本的な概念を理解することで、これらのテーマがどのようにプログラムの動作に影響を与えるのかを把握することができます。

  • コンパイラの最適化:コンパイラは、人間が書いたソースコードをコンピュータが理解できる機械語に変換します。このとき、コンパイラは単に変換するだけでなく、生成される機械語が効率的に動作するように「最適化」を行います。例えば、不要な計算を省いたり、ループの回数を減らしたりすることがあります。

  • メモリ管理:プログラムが動作するためには、変数の値を保存したり、関数の呼び出し履歴を保持したりするためにメモリが必要です。このメモリの割り当てや解放を適切に行うのがメモリ管理です。不適切なメモリ管理はプログラムの動作を不安定にする原因となります。

  • ハードウェアレジスタ:レジスタはCPU内部に存在する、非常に高速にアクセスできる小さなメモリ領域です。レジスタは計算の結果を一時的に保存したり、特定の情報(例えば、割り込みの発生源)を保持するために使用されます。

これらのテーマは、プログラムがどのように動作するのか、なぜ特定のコードが必要なのかを理解するための基礎となります。それぞれのテーマについて詳しく学ぶことで、より深い理解を得ることができます。