1.はじめに
C ++ テンプレートメタプログラミング (テンプレートメタプログラミング) は強力ですが、制限があります 。
- 新しい識別子(識別子The)を生成してテンプレートを展開することはできません 。例:新しい関数名、クラス名、名前空間名などを生成します。
- 事前定義されたテンプレートパラメータを使用するユーザー識別子のみが、記号/マーク(トークン) 、 リテラル(リテラル)によって取得できません。
- たとえば、リフレクションで実際のパラメーターパラメーター名のリテラル値を取得し、アサーションで式のリテラル値を取得します。
したがって、識別子を直接 操作する 必要がある場合は、マクロを使用して前処理段階でメタプログラミングを実行 する必要もあり ます。
- また 、テンプレートのコンパイル時(コンパイル時)は、 コンパイルフェーズの前に完全に拡張された前処理(前処理)で さまざまなマクロを展開します-狭義には、コンパイラは処理マクロコードを認識しません。
#define
/TOKEN1##TOKEN2
/#TOKEN
定義の オブジェクト(マクロオブジェクトのような) および マクロ機能(マクロ機能のような)、あなたは、置換テキスト、スプライシング識別子、機能獲得リテラルを達成することができます。
1.1 C ++マクロのプログラミングとデバッグについて
多くの人々は、「マクロプログラミング」することはできません、デバッグ、および直接「エントリから放棄する」 -カジュアル サインスペルミス、引数の数が間違って、テキストが原因 適切に交換することができないにつながる、 フルスクリーンのコンパイル・エラー、そして最終的に 困難に 問題を特定する場所-
- 最悪の場合、コンパイラ は 、cppファイルのコンパイル時に構文エラーが発生したことのみを通知します。
- 最良のケースでは、コンパイラが あることを教えてくれて XXXマクロの展開結果が含まれている 構文エラーを
- 私は あなたを伝えることは決してありません 失敗するYYYマクロの展開を引き起こす、XXXマクロの展開が何でありますか
- 最後に、 ZZZマクロ展開エラーのみが表示されます
マクロコードはコンパイル前に完全に展開されるため、次のことができます。
- コンパイラに 前処理された結果のみを出力させる
gcc -E
前処理が終了した後、コンパイルおよびリンクせずにコンパイラーを停止させますgcc -P
コンパイラが出力する前処理結果のラインマーカー(ラインマーカー)をシールドして干渉を低減します。また、__ LINE __ ;;;マクロと組み合わせて、コードの行数と位置の配置を実現します。- また、出力結果はフォーマットされていないため、出力
clang-format
する前にフォーマットして送信することをお勧めし ます。 - 無関係な ヘッダーファイルをブロックする
2つ目は、マクロプログラミングの一般的な使用モードです。
C
(およびC++
)のマクロ()はMacro
、コンパイラー前処理のカテゴリーに属し、コンパイル時の概念(ランタイムの概念ではない)に属します。以下は、頻繁に発生するマクロ使用の問題の簡単な要約です。
2.1#と##について(#シンボルスプライシング)
C言語マクロで#
は、次のマクロパラメータを文字列化する機能があります。簡単に言えば、参照するマクロ変数を置き換えた後、その左側と右側に二重引用符を追加します。たとえば、次のコードのマクロ:
#define WARN_IF(EXP) \
do { \
if (EXP) \
fprintf(stderr, "Warning: " #EXP "\n"); \
}while(0)
次に、実際の使用で次の交換プロセスが表示されます。
WARN_IF (divider == 0);
に置き換えられます:
do {
if (divider == 0)
fprintf(stderr, "Warning" "divider == 0" "\n");
} while(0);
このように、divider
(除数)が0になるたびに、プロンプトメッセージが標準エラーストリームに出力されます。
これ##
はコンカテネーターと呼ばれ、2つToken
を1つに接続するために使用されToken
ます。ここで接続されているオブジェクトToken
は問題なく、必ずしもマクロ変数である必要はないことに注意してください。たとえば、メニュー項目のコマンド名と関数ポインターで構成される構造体の配列を作成し、関数名とメニュー項目のコマンド名の間に直感的な名前の関係があることを期待します。次に、次のコードは非常に実用的です。
struct command
{
char * name;
void (*function) (void);
};
#define COMMAND(NAME) { NAME, NAME ## _command }
次に、いくつかの事前定義されたコマンドを使用して、command
構造化配列を便利に初期化します。
struct command commands[] = {
COMMAND(quit),
COMMAND(help),
...
}
COMMAND
ここでのマクロはコードジェネレーターとして機能し、コード密度をある程度減らすことができます。また、不注意によって引き起こされるエラーを間接的に減らすこともできます。また、できるシンボリックリンクの一つが、この機能もあり利用できない記号。といった:n
##
n+1
Token
#
#define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d
typedef struct _record_type LINK_MULTIPLE(name,company,position,salary);
ここで、このステートメントは次のように展開されます。
typedef struct _record_type name_company_position_salary;
2.2(##可変長パラメータ)の使用について
...
これC
はVariadic Macro
、可変パラメーターマクロであるマクロで呼び出されます。
GNU Cでは、C99以降、可変個引数関数と同様に、マクロは可変数のパラメーターを受け入れることができます。関数と同様に、マクロも3つのドットを使用します...変数パラメーターを表します
__VA_ARGS__マクロは、変数パラメーターの内容を表すために使用されます。簡単に言えば、左側のマクロの...の内容は、右側の__VA_ARGS__の場所にそのままコピーされます。次のサンプルコード:
#include <stdio.h>
#define debug(...) printf(__VA_ARGS__)
int main(void)
{
int year = 2018;
debug("this year is %d\n", year); //效果同printf("this year is %d\n", year);
}
さらに、いくつかの構文では、次の例のargsのように、__ VA_ARGS__を使用する代わりに、変数パラメーターに名前を付けることができます。
#include <stdio.h>
#define debug(format, args...) printf(format, args)
int main(void)
{
int year = 2018;
debug("this year is %d\n", year); //效果同printf("this year is %d\n", year);
}
変数パラメーター関数とは異なり、変数パラメーターマクロの変数パラメーターには少なくとも1つのパラメーターを渡す必要があります。そうしないと、エラーが報告されます。この問題を解決するには、特別な「##」操作が必要です。無視または空の場合、「##」操作により、プリプロセッサはその前のコンマを削除します。次の例に示すように
#include <stdio.h>
#define debug(format, args...) printf(format, ##args)
int main(void)
{
int year = 2018;
debug("hello, world"); //只有format参数,没有args可变参数
}
例:マクロは#define XNAME(n)x ## nとして定義され、コードはXNAME(4)であり、プリコンパイル中に、マクロはXNAME(4)がXNAME(n)と一致することを検出し、n次に、右側のnの内容も4に変更され、XNAME(4)全体がx ## n、つまりx4に置き換えられるため、最終的にXNAME(4)はx4になります。次の例に示すように:
#include <stdio.h>
#define XNAME(n) x##n
#define PRINT_XN(n) printf("x" #n " = %d\n", x##n);
int main(void)
{
int XNAME(1) = 14; // becomes int x1 = 14;
int XNAME(2) = 20; // becomes int x2 = 20;
PRINT_XN(1); // becomes printf("x1 = %d\n", x1);
PRINT_XN(2); // becomes printf("x2 = %d\n", x2);
return 0;
}
2.3特別な記号
テンプレートメタプログラミングとは異なり、マクロプログラミングに は型の 概念がありません。入力と出力は両方ともシンボルです。 コンパイル時のC ++構文は含まれず、コンパイル前のテキスト置換のみが含まれます 。
- マクロパラメータは 任意である シンボル系列(シーケンストークン)、異なるマクロパラメータ間のカンマで区切られました
- 各パラメーターは空のシーケンスにすることができ 、空白文字は無視されます(たとえば
a + 1
、とa+1
同じ) - パラメーターに、コンマ(コンマ) または 一致しない括弧(括弧)を表示することはできません (たとえば、次の 3つのパラメーター
FOO(bool, std::pair<int, int>)
があると見なされFOO()
ます:bool
/std::pair<int
/int>
)
std::pair<int, int>
パラメータとして必要な場合は 、C ++タイプのエイリアス (エイリアスタイプ)を使用する方法 (例:) using IntPair = std::pair<int, int>;
、コンマを回避します(パラメータはFOO(bool, IntPair)
2つのパラメータのみで表示され ます)。
より一般的な方法は、括弧のペアを使用して 各パラメーターをカプセル化し(以下、タプルと呼びます )、最終的な展開で括弧を削除します(タプルのアンパック)。
#define PP_REMOVE_PARENS(T) PP_REMOVE_PARENS_IMPL T
#define PP_REMOVE_PARENS_IMPL(...) __VA_ARGS__
#define FOO(A, B) int foo(A x, B y)
#define BAR(A, B) FOO(PP_REMOVE_PARENS(A), PP_REMOVE_PARENS(B))
FOO(bool, IntPair) // -> int foo(bool x, IntPair y)
BAR((bool), (std::pair<int, int>)) // -> int foo(bool x, std::pair<int, int> y)
PP_REMOVE_PARENS(T)
拡大PP_REMOVE_PARENS_IMPL T
フォーム- パラメータ
T
が括弧のペアである 場合、 展開結果はマクロ関数PP_REMOVE_PARENS_IMPL (...)
を呼び出す形式になり ます - 次に、タプルの コンテンツである
PP_REMOVE_PARENS_IMPL(...)
パラメーター自体__VA_ARGS__
(以下で説明する 可変長パラメーター)に展開します。T
さらに、以下で説明する遅延評価には、特別な記号の代わりに一般的に使用されるマクロ関数 が使用されます 。
#define PP_COMMA() ,
#define PP_LPAREN() (
#define PP_RPAREN() )
#define PP_EMPTY()
2.4その他
マクロプログラミングは、チューリング完全であるかどうかにかかわらず、強力な計算能力も備えています。作者はまだわかりません。ただし、そのような BOOST_PP:インクリメントデクリメント、論理演算、ブール条件付き選択の変換、遅延評価、添え字など、多くのプログラミングステートメントコンピューティングインフラストラクチャでマクロを使用する一般的な 前処理ライブラリ(プリプロセッサライブラリ)、パラメータの長さは次のように判断されます空、長さの計算、トラバーサルアクセス、シンボルマッチング、および数値演算、数値比較などに加えて、一般的なデータ構造(タプル、シーケンス、リスト、配列など)を提供します。あなたは知乎の記事を見ることができます:C / C ++マクロプログラミングの芸術。
3.マクロを使用する際の注意点
3.1間違った入れ子-Misnesting
マクロ定義は完全に一致する括弧を持つ必要はありませんが、エラーを回避し、読みやすさを向上させるために、そのような使用を避けることが最善です。
3.2オペレーターの優先順位によって引き起こされる問題-Operator Precedence Problem
マクロは単純な置換にすぎないため、マクロパラメータが複合構造の場合、置換後、さまざまなパラメータ間の演算子の優先度は、単一のパラメータのさまざまな部分間の相互作用の演算子の優先度よりも高くなる可能性があります。ブラケットは各マクロパラメータを保護しません。これにより、予期しない状況が発生する可能性があります。といった:
#define ceil_div(x, y) (x + y - 1) / y
その後、
a = ceil_div( b & c, sizeof(int) );
に変換されます:
a = ( b & c + sizeof(int) - 1) / sizeof(int);
ので、+/-
優先度がより高い&
優先順位、次いで上記の式は、と等価です。
a = ( b & (c + sizeof(int) - 1)) / sizeof(int);
これは明らかに発信者の本来の意図ではありません。これが発生しないようにするには、さらにいくつかの括弧を記述する必要があります。
#define ceil_div(x, y) (((x) + (y) - 1) / (y))
3.3不要なセミコロンを削除する-Semicolon Swallowing
一般に、関数のようなマクロを表面上で通常のC
言語呼び出しのように見せるために、通常、パラメーターを持つ次のマクロのように、マクロの後にセミコロンを追加します。
MY_MACRO(x);
しかし、それが次の状況である場合:
#define MY_MACRO(x) { \
/* line 1 */ \
/* line 2 */ \
/* line 3 */ }
//...
if (condition())
MY_MACRO(a);
else {
...
}
これにより、余分なセミコロンが原因でコンパイルエラーが発生します。この状況を回避し、MY_MACRO(x);
この種の書き込みを維持するには、マクロを次の形式で定義する必要があります。
#define MY_MACRO(x) do {
/* line 1 */ \
/* line 2 */ \
/* line 3 */ } while(0)
常にセミコロンを使用している限り、問題はありません。
3.4副作用の重複
このSide Effect
マクロが展開されたときに、そのパラメータが複数回行われてもよいことを意味するEvaluation
(すなわち、値)が、マクロパラメータが関数である場合、矛盾した結果を達成するために複数回呼び出す、またはそれ以上の重大なミスすることができます。といった:
#define min(X,Y) ((X) > (Y) ? (Y) : (X))
//...
c = min(a,foo(b));
このとき、foo()
関数は2回呼び出されます。この潜在的な問題を解決するには、次のようにmin(X,Y)
このマクロを作成する必要があります。
#define min(X,Y) ({ \
typeof (X) x_ = (X); \
typeof (Y) y_ = (Y); \
(x_ < y_) ? x_ : y_; })
({...})
の関数は、内部ステートメントの最後の値を返すことです。また、変数を内部で宣言することもできます(中括弧で囲まれているためScope
)。
いくつかの興味深い質問
- 次のコード:
#define display(name) printf(""#name"")
int main() {
display(name);
}
操作の結果はname
、なぜ"#name"
ですか?
#
これが文字列化の意味であり、これは。とprintf(""#name"")
同等printf("" "name" "")
です。
printf("" #name "")
<1>は<2>と
同等であり、printf("" "name" "")
<2>
の2番目と3番目の「中央のスペース」は(「空+名前+空」)と同等です。
##
接続記号は2つのポンド記号で構成され、その機能は、マクロ定義内の2つの部分文字列(token
)をパラメーターと接続して、新しい部分文字列を形成することです。ただし、最初または最後の部分文字列にすることはできません。いわゆる部分文字列(token
)は、コンパイラが認識できる最小の構文単位を指します。具体的な定義はコンパイルの原則で詳しく説明されていますが、わからなくても構いません。同時に、#
シンボルは渡されたパラメータを文字列として置き換えることであることに注意してください。それらがどのように機能するかを見てみましょう。これはMSDN
上からの例です。
パラメータを持つそのようなマクロがプログラムで定義されていると仮定します
#define paster( n ) printf( "token" #n " = %d", token##n )
同時に、整数変数が定義されます。
int token9 = 9;
次に、メインプログラムでこのマクロを次のように呼び出します。
paster(9);
次に、コンパイル時に、上記の文は次のように展開されます。
printf( "token" "9" " = %d", token9 );
この例でpaster(9);
は、「9
」の「」はそのままの文字列として扱われ、「token
」に接続されてtoken9
。になることに注意してください。また#n
、「9
」に置き換えられました。
ご想像のとおり、上記のプログラムを実行した結果が画面に出力されますtoken9=9
。
#define display(name) printf(""#name"")
int main() {
display(name);
}
特徴はそれがマクロであり、マクロの処理#
番号はLSが言ったのと同じです!
処理後、それは追加の文字列です!
しかし、printf(""#name"");
それは機能しません!
#define display(name) printf(""#name"")
定義は文字列化されてname
おり、結果は実際にはprintf("name")
(削除される前後の空の文字列)であるため、出力は当然name
です。
別の見方をすれば、これ#
は接続シンボルであり、計算に参加するときに出力されません。