X-Macros を知ったばかりです。 X-Macrosの実際の使用例は何ですか?彼らはいつ仕事に適したツールですか?
数年前、コードで関数ポインターを使用し始めたときに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 */
#define 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 {...}command1_resp_t;
typedef struct {...}command2_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が最適なサイズになり、この通信ハンドラーにコマンドを追加すると、バッファーは常に最適なサイズになります。涼しい!
もう1つの用途は、オフセットテーブルを作成することです:メモリは組み込みシステムの制約になることが多いため、スパース配列の場合、ジャンプテーブルに512バイト(ポインターあたり2バイトX 256の可能なコマンド)を使用したくありません。代わりに、可能なコマンドごとに8ビットオフセットのテーブルがあります。次に、このオフセットを使用して、NUM_COMMANDS * sizeof(pointer)のみが必要な実際のジャンプテーブルにインデックスを付けます。私の場合、10個のコマンドが定義されています。私のジャンプテーブルの長さは20バイトで、256バイトのオフセットテーブルがあります。これは、512バイトではなく、合計276バイトです。次に、次のように関数を呼び出します。
jump_table[offset_table[command]]();
の代わりに
jump_table[command]();
次のようにオフセットテーブルを作成できます。
/* initialize every offset to 0 */
static uint8_t offset_table[256] = {0};
/* for each valid command, initialize the corresponding offset */
#define ENTRY(a,b,c) offset_table[c] = offsetof(offset_struct_t, b);
COMMAND_TABLE
#undef ENTRY
offsetofは、「stddef.h」で定義された標準ライブラリマクロです
副次的な利点として、コマンドコードがサポートされているかどうかを判断する非常に簡単な方法があります。
bool command_is_valid(uint8_t command)
{
/* return false if not valid, or true (non 0) if valid */
return offset_table[command];
}
これが、COMMAND_TABLEでコマンドバイト0を予約した理由でもあります。オフセットテーブルへのインデックス付けに無効なコマンドバイトが使用された場合に呼び出される「process_reserved()」という関数を1つ作成できます。
X-Macrosは、本質的にパラメーター化されたテンプレートです。したがって、複数の装いでいくつかの類似したものが必要な場合、これらは仕事に適したツールです。これらを使用すると、抽象フォームを作成し、さまざまなルールに従ってインスタンス化できます。
Xマクロを使用して、列挙値を文字列として出力します。それに遭遇して以来、私は「ユーザー」マクロを各要素に適用するこの形式を強く好みます。複数のファイルをインクルードすると、作業がはるかに難しくなります。
/* x-macro constructors for error and type
enums and string tables */
#define AS_BARE(a) a ,
#define AS_STR(a) #a ,
#define ERRORS(_) \
_(noerror) \
_(dictfull) _(dictstackoverflow) _(dictstackunderflow) \
_(execstackoverflow) _(execstackunderflow) _(limitcheck) \
_(VMerror)
enum err { ERRORS(AS_BARE) };
char *errorname[] = { ERRORS(AS_STR) };
/* puts(errorname[(enum err)limitcheck]); */
また、オブジェクトタイプに基づいた関数ディスパッチにも使用しています。繰り返しますが、列挙値の作成に使用したのと同じマクロをハイジャックします。
#define TYPES(_) \
_(invalid) \
_(null) \
_(mark) \
_(integer) \
_(real) \
_(array) \
_(dict) \
_(save) \
_(name) \
_(string) \
/*enddef TYPES */
#define AS_TYPE(_) _ ## type ,
enum { TYPES(AS_TYPE) };
マクロを使用すると、マクロ定義(TYPESマクロ)からのベアトークンを使用してさまざまなフォームを構築するため、すべての配列インデックスが関連する列挙値と一致することが保証されます。
typedef void evalfunc(context *ctx);
void evalquit(context *ctx) { ++ctx->quit; }
void evalpop(context *ctx) { (void)pop(ctx->lo, adrent(ctx->lo, OS)); }
void evalpush(context *ctx) {
Push(ctx->lo, adrent(ctx->lo, OS),
pop(ctx->lo, adrent(ctx->lo, ES)));
}
evalfunc *evalinvalid = evalquit;
evalfunc *evalmark = evalpop;
evalfunc *evalnull = evalpop;
evalfunc *evalinteger = evalpush;
evalfunc *evalreal = evalpush;
evalfunc *evalsave = evalpush;
evalfunc *evaldict = evalpush;
evalfunc *evalstring = evalpush;
evalfunc *evalname = evalpush;
evalfunc *evaltype[stringtype/*last type in enum*/+1];
#define AS_EVALINIT(_) evaltype[_ ## type] = eval ## _ ;
void initevaltype(void) {
TYPES(AS_EVALINIT)
}
void eval(context *ctx) {
unsigned ades = adrent(ctx->lo, ES);
object t = top(ctx->lo, ades, 0);
if ( isx(t) ) /* if executable */
evaltype[type(t)](ctx); /* <--- the payoff is this line here! */
else
evalpush(ctx);
}
このようにXマクロを使用すると、コンパイラが実際に役立つエラーメッセージを提供するのに役立ちます。上記のevalarray関数を省略したのは、それが私のポイントをそらすためです。ただし、上記のコードをコンパイルしようとすると(もちろん、他の関数呼び出しをコメントアウトし、コンテキストにダミーのtypedefを指定すると)、コンパイラーは関数の欠落について文句を言います。追加する新しいタイプごとに、このモジュールを再コンパイルするときにハンドラーを追加することを思い出します。そのため、X-マクロは、プロジェクトが成長しても並列構造が損なわれないようにするのに役立ちます。
編集:
この答えは私の評判を50%上げました。だからもう少しです。以下は否定的な例で、質問に答えています:when notを使用してX-Macros?
この例は、X-「レコード」への任意のコードフラグメントのパッキングを示しています。最終的に私はプロジェクトのこのブランチを放棄し、後の設計ではこの戦略を使用しませんでした(試してみたくありませんでした)。どういうわけか、おかしくなった。実際、マクロにはX6という名前が付けられました。これは、ある時点で6つの引数があったためですが、マクロ名を変更することにうんざりしていました。
/* Object types */
/* "'X'" macros for Object type definitions, declarations and initializers */
// a b c d
// enum, string, union member, printf d
#define OBJECT_TYPES \
X6( nulltype, "null", int dummy , ("<null>")) \
X6( marktype, "mark", int dummy2 , ("<mark>")) \
X6( integertype, "integer", int i, ("%d",o.i)) \
X6( booleantype, "boolean", bool b, (o.b?"true":"false")) \
X6( realtype, "real", float f, ("%f",o.f)) \
X6( nametype, "name", int n, ("%s%s", \
(o.flags & Fxflag)?"":"/", names[o.n])) \
X6( stringtype, "string", char *s, ("%s",o.s)) \
X6( filetype, "file", FILE *file, ("<file %p>",(void *)o.file)) \
X6( arraytype, "array", Object *a, ("<array %u>",o.length)) \
X6( dicttype, "dict", struct s_pair *d, ("<dict %u>",o.length)) \
X6(operatortype, "operator", void (*o)(), ("<op>")) \
#define X6(a, b, c, d) #a,
char *typestring[] = { OBJECT_TYPES };
#undef X6
// the Object type
//forward reference so s_object can contain s_objects
typedef struct s_object Object;
// the s_object structure:
// a bit convoluted, but it boils down to four members:
// type, flags, length, and payload (union of type-specific data)
// the first named union member is integer, so a simple literal object
// can be created on the fly:
// Object o = {integertype,0,0,4028}; //create an int object, value: 4028
// Object nl = {nulltype,0,0,0};
struct s_object {
#define X6(a, b, c, d) a,
enum e_type { OBJECT_TYPES } type;
#undef X6
unsigned int flags;
#define Fread 1
#define Fwrite 2
#define Fexec 4
#define Fxflag 8
size_t length; //for lint, was: unsigned int
#define X6(a, b, c, d) c;
union { OBJECT_TYPES };
#undef X6
};
大きな問題の1つは、printf形式の文字列でした。見た目はかっこいいですが、それはただの趣味です。 1つの関数でのみ使用されるため、マクロの過剰使用は、一緒にあるべき情報を実際に分離しました。そして、それはそれ自身で関数を読めなくします。このようなデバッグ機能では、難読化は二重に残念です。
//print the object using the type's format specifier from the macro
//used by O_equal (ps: =) and O_equalequal (ps: ==)
void printobject(Object o) {
switch (o.type) {
#define X6(a, b, c, d) \
case a: printf d; break;
OBJECT_TYPES
#undef X6
}
}
だから夢中にならないでください。私がしたように。
Xマクロを使用して「豊富な列挙」を作成し、列挙値の反復と各列挙値の文字列表現の取得をサポートします。
#define MOUSE_BUTTONS \
X(LeftButton, 1) \
X(MiddleButton, 2) \
X(RightButton, 4)
struct MouseButton {
enum Value {
None = 0
#define X(name, value) ,name = value
MOUSE_BUTTONS
#undef X
};
static const int *values() {
static const int a[] = {
None,
#define X(name, value) name,
MOUSE_BUTTONS
#undef X
-1
};
return a;
}
static const char *valueAsString( Value v ) {
#define X(name, value) static const char str_##name[] = #name;
MOUSE_BUTTONS
#undef X
switch ( v ) {
case None: return "None";
#define X(name, value) case name: return str_##name;
MOUSE_BUTTONS
#undef X
}
return 0;
}
};
これは、MouseButton::Value
enum、それはまた私がのようなことをすることができます
// Print names of all supported mouse buttons
for ( const int *mb = MouseButton::values(); *mb != -1; ++mb ) {
std::cout << MouseButton::valueAsString( (MouseButton::Value)*mb ) << "\n";
}
私は、非常に大規模なXマクロを使用して、INIファイルの内容を構成構造体に読み込みますが、その構造体を中心に回転します。
これは私の「configuration.def」ファイルがどのように見えるかです:
#define NMB_DUMMY(...) X(__VA_ARGS__)
#define NMB_INT_DEFS \
TEXT("long int") , long , , , GetLongValue , _ttol , NMB_SECT , SetLongValue ,
#define NMB_STR_DEFS NMB_STR_DEFS__(TEXT("string"))
#define NMB_PATH_DEFS NMB_STR_DEFS__(TEXT("path"))
#define NMB_STR_DEFS__(ATYPE) \
ATYPE , basic_string<TCHAR>* , new basic_string<TCHAR>\
, delete , GetValue , , NMB_SECT , SetValue , *
/* X-macro starts here */
#define NMB_SECT "server"
NMB_DUMMY(ip,TEXT("Slave IP."),TEXT("10.11.180.102"),NMB_STR_DEFS)
NMB_DUMMY(port,TEXT("Slave portti."),TEXT("502"),NMB_STR_DEFS)
NMB_DUMMY(slaveid,TEXT("Slave protocol ID."),0xff,NMB_INT_DEFS)
.
. /* And so on for about 40 items. */
少しわかりにくいと思います。実際に、すべてのフィールドマクロの後にこれらのすべての型宣言を記述したくないことはすぐに明らかになります。 (心配しないで、簡潔にするために省略したすべてを説明する大きなコメントがあります。)
そして、これは私が構成構造体を宣言する方法です:
typedef struct {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) TYPE ID;
#include "configuration.def"
#undef X
basic_string<TCHAR>* ini_path; //Where all the other stuff gets read.
long verbosity; //Used only by console writing functions.
} Config;
次に、コードでは、最初にデフォルト値が構成構造体に読み込まれます。
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,...) \
conf->ID = CONSTRUCTOR(DEFVAL);
#include "configuration.def"
#undef X
次に、INIは、ライブラリSimpleIniを使用して、次のように構成構造体に読み込まれます。
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,DEREF...)\
DESTRUCTOR (conf->ID);\
conf->ID = CONSTRUCTOR( ini.GETTER(TEXT(SECT),TEXT(#ID),DEFVAL,FALSE) );\
LOG3A(<< left << setw(13) << TEXT(#ID) << TEXT(": ") << left << setw(30)\
<< DEREF conf->ID << TEXT(" (") << DEFVAL << TEXT(").") );
#include "configuration.def"
#undef X
また、コマンドラインフラグからのオーバーライドは、同じ名前(GNU long形式)でフォーマットされています)は、ライブラリSimpleOptを使用して、次のように適用されます。
enum optflags {
#define X(ID,...) ID,
#include "configuration.def"
#undef X
};
CSimpleOpt::SOption sopt[] = {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) {ID,TEXT("--") #ID TEXT("="), SO_REQ_CMB},
#include "configuration.def"
#undef X
SO_END_OF_OPTIONS
};
CSimpleOpt ops(argc,argv,sopt,SO_O_NOERR);
while(ops.Next()){
switch(ops.OptionId()){
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,...) \
case ID:\
DESTRUCTOR (conf->ID);\
conf->ID = STRCONV( CONSTRUCTOR ( ops.OptionArg() ) );\
LOG3A(<< TEXT("Omitted ")<<left<<setw(13)<<TEXT(#ID)<<TEXT(" : ")<<conf->ID<<TEXT(" ."));\
break;
#include "configuration.def"
#undef X
}
}
など、同じマクロを使用して--help -flag出力を出力し、デフォルトのiniファイルをサンプルします。configuration.defはプログラムに8回含まれています。 「丸い穴への角釘」、おそらく。実際に有能なプログラマーはこれをどのように進めますか?たくさんのループと文字列処理?
https://github.com/whunmr/DataEx
次のxmacrosを使用して、シリアル化機能と非シリアル化機能が組み込まれたC++クラスを生成しています。
#define __FIELDS_OF_DataWithNested(_) \
_(1, a, int ) \
_(2, x, DataX) \
_(3, b, int ) \
_(4, c, char ) \
_(5, d, __array(char, 3)) \
_(6, e, string) \
_(7, f, bool)
DEF_DATA(DataWithNested);
使用法:
TEST_F(t, DataWithNested_should_able_to_encode_struct_with_nested_struct) {
DataWithNested xn;
xn.a = 0xCAFEBABE;
xn.x.a = 0x12345678;
xn.x.b = 0x11223344;
xn.b = 0xDEADBEEF;
xn.c = 0x45;
memcpy(&xn.d, "XYZ", strlen("XYZ"));
char buf_with_zero[] = {0x11, 0x22, 0x00, 0x00, 0x33};
xn.e = string(buf_with_zero, sizeof(buf_with_zero));
xn.f = true;
__encode(DataWithNested, xn, buf_);
char expected[] = { 0x01, 0x04, 0x00, 0xBE, 0xBA, 0xFE, 0xCA,
0x02, 0x0E, 0x00 /*T and L of nested X*/,
0x01, 0x04, 0x00, 0x78, 0x56, 0x34, 0x12,
0x02, 0x04, 0x00, 0x44, 0x33, 0x22, 0x11,
0x03, 0x04, 0x00, 0xEF, 0xBE, 0xAD, 0xDE,
0x04, 0x01, 0x00, 0x45,
0x05, 0x03, 0x00, 'X', 'Y', 'Z',
0x06, 0x05, 0x00, 0x11, 0x22, 0x00, 0x00, 0x33,
0x07, 0x01, 0x00, 0x01};
EXPECT_TRUE(ArraysMatch(expected, buf_));
}
また、別の例は https://github.com/whunmr/msgrpc にあります。