「 X-Macros 」の基本的な定義と例、およびいくつかのリファレンスは、C プリプロセッサに関するこのウィキペディアのエントリに記載されています。
X-Macro は、同様のマクロ呼び出し (「コンポーネント マクロ」と呼ぶことができる) のリストを含むヘッダー ファイル (通常、従来の ".h" の代わりに ".def" 拡張子を使用) です。
この強力な手法の使用方法に関する優れた情報源は何ですか? この方法を使用している有名なオープン ソース ライブラリはありますか?
「 X-Macros 」の基本的な定義と例、およびいくつかのリファレンスは、C プリプロセッサに関するこのウィキペディアのエントリに記載されています。
X-Macro は、同様のマクロ呼び出し (「コンポーネント マクロ」と呼ぶことができる) のリストを含むヘッダー ファイル (通常、従来の ".h" の代わりに ".def" 拡張子を使用) です。
この強力な手法の使用方法に関する優れた情報源は何ですか? この方法を使用している有名なオープン ソース ライブラリはありますか?
I use X Macros() in code a lot. The value comes from only adding new data only to the "X list" and not modifying any other code.
The most common use of X Macros() is for associating error text with error codes. When new error codes are added, programmers must remember to add the code and the text, typically in separate places. The X Macro allows the new error data to be added in a single place and get automatically populated anywhere it is needed.
Unfortunately, the mechanisms use a lot of pre-compiler magic that can make the code somewhat hard to read (e.g. string joining with token1##token2
, string creation with #token
). Because of this I typically explain what the X Macro is doing in the comments.
Here is an example using the error/return values. All new data gets added to the "X_ERROR
" list. None of the other code hast to be modified.
/*
* X Macro() data list
* Format: Enum, Value, Text
*/
#define X_ERROR \
X(ERROR_NONE, 1, "Success") \
X(ERROR_SYNTAX, 5, "Invalid syntax") \
X(ERROR_RANGE, 8, "Out of range")
/*
* Build an array of error return values
* e.g. {0,5,8}
*/
static int ErrorVal[] =
{
#define X(Enum,Val,Text) Val,
X_ERROR
#undef X
};
/*
* Build an array of error enum names
* e.g. {"ERROR_NONE","ERROR_SYNTAX","ERROR_RANGE"}
*/
static char * ErrorEnum[] = {
#define X(Enum,Val,Text) #Enum,
X_ERROR
#undef X
};
/*
* Build an array of error strings
* e.g. {"Success","Invalid syntax","Out of range"}
*/
static char * ErrorText[] = {
#define X(Enum,Val,Text) Text,
X_ERROR
#undef X
};
/*
* Create an enumerated list of error indexes
* e.g. 0,1,2
*/
enum {
#define X(Enum,Val,Text) IDX_##Enum,
X_ERROR
#undef X
IDX_MAX /* Array size */
};
void showErrorInfo(void)
{
int i;
/*
* Access the values
*/
for (i=0; i<IDX_MAX; i++)
printf(" %s == %d [%s]\n", ErrorEnum[i], ErrorVal[i], ErrorText[i]);
}
You can also use X Macros() to generate code. For example to test if an error value is "known", the X Macro can generate cases in a switch statement:
/*
* Test validity of an error value
* case ERROR_SUCCESS:
* case ERROR_SYNTAX:
* case ERROR_RANGE:
*/
switch(value)
{
#define X(Enum,Val,Text) case Val:
X_ERROR
#undef X
printf("Error %d is ok\n",value);
break;
default:
printf("Invalid error: %d\n",value);
break;
}
数年前、コードで関数ポインターを使い始めたときに X マクロを発見しました。私は組み込みプログラマであり、ステート マシンを頻繁に使用します。多くの場合、次のようなコードを書きます。
/* declare an enumeration of state codes */
enum{ STATE0, STATE1, STATE2, ... , STATEX, NUM_STATES};
/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {func0, func1, func2, ... , funcX};
問題は、状態の列挙の順序と一致するように関数ポインタ テーブルの順序を維持する必要があると非常にエラーが発生しやすいと考えたことです。
友人が X マクロを紹介してくれましたが、頭の中で電球が切れたようでした。真剣に、あなたは私の人生のx-マクロでどこにいましたか!
そこで、次の表を定義します。
#define STATE_TABLE \
ENTRY(STATE0, func0) \
ENTRY(STATE1, func1) \
ENTRY(STATE2, func2) \
...
ENTRY(STATEX, funcX) \
そして、次のように使用できます。
enum
{
#define ENTRY(a,b) a,
STATE_TABLE
#undef ENTRY
NUM_STATES
};
と
p_func_t jumptable[NUM_STATES] =
{
#define ENTRY(a,b) b,
STATE_TABLE
#undef ENTRY
};
おまけとして、次のようにプリプロセッサに関数プロトタイプを作成させることもできます。
#define ENTRY(a,b) static void b(void);
STATE_TABLE
#undef ENTRY
別の使用法は、レジスタを宣言して初期化することです
#define IO_ADDRESS_OFFSET (0x8000)
#define REGISTER_TABLE\
ENTRY(reg0, IO_ADDRESS_OFFSET + 0, 0x11)\
ENTRY(reg1, IO_ADDRESS_OFFSET + 1, 0x55)\
ENTRY(reg2, IO_ADDRESS_OFFSET + 2, 0x1b)\
...
ENTRY(regX, IO_ADDRESS_OFFSET + X, 0x33)\
/* declare the registers (where _at_ is a compiler specific directive) */
#define ENTRY(a, b, c) volatile uint8_t a _at_ b:
REGISTER_TABLE
#undef ENTRY
/* initialize registers */
#def ENTRY(a, b, c) a = c;
REGISTER_TABLE
#undef ENTRY
ただし、私のお気に入りの使用法は、通信ハンドラーに関してです。
最初に、各コマンド名とコードを含む通信テーブルを作成します。
#define COMMAND_TABLE \
ENTRY(RESERVED, reserved, 0x00) \
ENTRY(COMMAND1, command1, 0x01) \
ENTRY(COMMAND2, command2, 0x02) \
...
ENTRY(COMMANDX, commandX, 0x0X) \
大文字は列挙型に使用され、小文字は関数名に使用されるため、テーブルには大文字と小文字の両方の名前があります。
次に、各コマンドの構造体を定義して、各コマンドがどのように見えるかを定義します。
typedef struct {...}command1_cmd_t;
typedef struct {...}command2_cmd_t;
etc.
同様に、コマンド応答ごとに構造体を定義します。
typedef struct {...}response1_resp_t;
typedef struct {...}response2_resp_t;
etc.
次に、コマンド コードの列挙を定義します。
enum
{
#define ENTRY(a,b,c) a##_CMD = c,
COMMAND_TABLE
#undef ENTRY
};
コマンドの長さの列挙を定義できます。
enum
{
#define ENTRY(a,b,c) a##_CMD_LENGTH = sizeof(b##_cmd_t);
COMMAND_TABLE
#undef ENTRY
};
応答の長さの列挙を定義できます。
enum
{
#define ENTRY(a,b,c) a##_RESP_LENGTH = sizeof(b##_resp_t);
COMMAND_TABLE
#undef ENTRY
};
次のように、コマンドがいくつあるかを判断できます。
typedef struct
{
#define ENTRY(a,b,c) uint8_t b;
COMMAND_TABLE
#undef ENTRY
} offset_struct_t;
#define NUMBER_OF_COMMANDS sizeof(offset_struct_t)
注:offset_struct_tを実際にインスタンス化することはありません。コンパイラがコマンドの数を生成する方法として使用するだけです。
次に、次のように関数ポインターのテーブルを生成できることに注意してください。
p_func_t jump_table[NUMBER_OF_COMMANDS] =
{
#define ENTRY(a,b,c) process_##b,
COMMAND_TABLE
#undef ENTRY
}
そして私の関数プロトタイプ:
#define ENTRY(a,b,c) void process_##b(void);
COMMAND_TABLE
#undef ENTRY
最後に、これまでで最もクールな使い方として、送信バッファーの大きさをコンパイラーに計算させることができます。
/* reminder the sizeof a union is the size of its largest member */
typedef union
{
#define ENTRY(a,b,c) uint8_t b##_buf[sizeof(b##_cmd_t)];
COMMAND_TABLE
#undef ENTRY
}tx_buf_t
繰り返しになりますが、この共用体はオフセット構造体に似ており、インスタンス化されていません。代わりに、sizeof 演算子を使用して送信バッファー サイズを宣言できます。
uint8_t tx_buf[sizeof(tx_buf_t)];
これで、送信バッファー tx_buf が最適なサイズになりました。この通信ハンドラーにコマンドを追加すると、バッファーは常に最適なサイズになります。涼しい!
Dr. Dobb's には、これに関する記事があります。