ダーティC ++ Macのトリック

この記事では、2つのことを行います。マクロが悪である理由とその対処方法を説明するとともに、コードの操作を簡素化して読みやすくするために使用するC ++マクロをいくつか示します。 実際、トリックはそれほど汚くない:



事前に警告します。カットの下で何かクールで不可解で見事なものを見たいと思っているなら、記事にはそのようなものはありません。 マクロの明るい面に関する記事。



便利なリンク



手始めに: Anders Lindgrenによる記事(英語) -プリプロセッサを使用したヒントとコツ(パート1)は 、マクロの基本をカバーしています。

上級者向け: Anders Lindgrenの記事「プリプロセッサを使用したヒントとコツ(パート2)」では、より深刻なトピックを取り上げています。 この記事には何かがありますが、すべてではなく、説明も少なくなります。

専門家向け: Aditya Kumar、Andrew Sutton、Bjarne Stroustrup-DemacroficationによるC ++プログラムの若返りによる記事(英語)で、マクロをC ++ 11機能に置き換える可能性について説明しています。



わずかな文化の違い



ウィキペディアと私自身の感情によると、ロシア語では通常「マクロ」という言葉でこれを意味します。
#define FUNC(x, y) ((x)^(y))
      
      



そして、以下:

 #define VALUE 1
      
      



これを「プリプロセッサ定数」(または単に「定義」オーム)と呼びます。 英語では、少し間違っています。1つ目は関数のようなマクロと呼ばれ、2つ目はオブジェクトのようなマクロです(再度、 Wikipediaへのリンクを提供します)。 つまり、マクロについて話すときは、一方と他方の両方を意味し、すべてを一緒に意味します。 英語のテキストを読むときは注意してください。



良い点と悪い点



最近、 マクロは悪であると一般に信じられてきました。 この意見は根拠がないわけではありませんが、私の意見では、説明が必要です。 質問に対する1つの答えで、 なぜプリプロセッサマクロは悪であり、代替手段は何ですか? マクロを悪とみなす理由と、それらを取り除くいくつかの方法のかなり完全なリストを見つけました。 以下にロシア語で同じリストを示しますが、問題の例と解決策は、指定されたリンクと同じではありません。

  1. マクロはデバッグできません
    まず、実際には、次のことができます。
    プロジェクトまたはソースファイルのプロパティを右クリックして[プロパティ]に移動します。 [構成プロパティ]-> [C / C ++]-> [プリプロセッサ]で、[前処理済みファイルの生成]を行番号の有無にかかわらず、どちらか好きな方に設定します。 これは、マクロがコンテキスト内で展開するものを示します。 コンパイル済みのライブコードでデバッグする必要がある場合は、それをカットアンドペーストし、デバッグ中にマクロの代わりに配置します。
    したがって、「マクロのデバッグは難しい」と言う方が正しいでしょう。 しかし、それでも、マクロのデバッグには問題があります。



    使用しているマクロにデバッグが必要かどうかを判断するには、ブレークポイントをそこにプッシュする価値があるかどうかを考えます。 これは、パラメータ、変数の宣言、外部からのオブジェクトまたはデータの変更などを通じて取得した値の変更です。
    問題の解決策:

    • マクロを関数で置き換えることにより、マクロを完全に取り除きます(重要な場合はインライン化できます)。
    • マクロのロジックを関数に移動し、マクロ自体がこれらの関数にデータを転送することのみを担当するようにします。
    • デバッグを必要としないマクロのみを使用してください。
  2. マクロを展開すると、奇妙な副作用が表示される場合があります。
    関係する副作用を示すために、通常、算術演算の例を示します。 私もこの伝統から離れません:

     #include <iostream> #define SUM(a, b) a + b int main() { //    x? int x = SUM(2, 2); std::cout << x << std::endl; x = 3 * SUM(2, 2); std::cout << x << std::endl; return 0; }
          
          



    出力では、4と12が期待されますが、4と8が得られます。実際には、マクロは指定された場所のコードを単純に置き換えます。 この場合、コードは次のようになります。

     int x = 3 * 2 + 2;
          
          



    これは副作用です。 期待どおりに機能させるには、マクロを変更する必要があります。

     #include <iostream> #define SUM(a, b) (a + b) int main() { //    x? int x = SUM(2, 2); std::cout << x << std::endl; x = 3 * SUM(2, 2); std::cout << x << std::endl; return 0; }
          
          



    今。 しかし、それだけではありません。 乗算に移りましょう:

     #define MULT(a, b) a * b
          
          



    すぐに「正しく」書きますが、少し違った使い方をします。

     #include <iostream> #define MULT(a, b) (a * b) int main() { //    x? int x = MULT(2, 2); std::cout << x << std::endl; x = MULT(3, 2 + 2); std::cout << x << std::endl; return 0; }
          
          



    Deja vu:再び4と8を取得します。この場合、展開されたマクロは次のようになります。

     int x = (3 * 2 + 2);
          
          



    つまり、次のように書く必要があります。

     #define MULT(a, b) ((a) * (b))
          
          



    このバージョンのマクロと出来上がりを使用します。

     #include <iostream> #define MULT(a, b) ((a) * (b)) int main() { //    x? int x = MULT(2, 2); std::cout << x << std::endl; x = MULT(3, 2 + 2); std::cout << x << std::endl; return 0; }
          
          



    これですべてが正しくなりました。



    算術演算を無視する場合、一般的な場合、マクロを書くとき、

    • 式全体を囲む括弧
    • 各マクロパラメータを囲む括弧
    つまり、代わりに

     #define CHOOSE(ifC, chooseA, otherwiseB) ifC ? chooseA : otherwiseB
          
          



    あるべき

     #define CHOOSE(ifC, chooseA, otherwiseB) ((ifC) ? (chooseA) : (otherwiseB))
          
          





    この問題は、すべてのタイプのパラメーターを括弧で囲むことができないという事実によってさらに悪化します(実際の例については、この記事でさらに詳しく説明します)。 このため、高品質のマクロを作成することは非常に困難です。



    さらに、 百科事典がコメントで思い出したように、括弧が保存されない場合があります。

    副作用に関するセクションでは、まだ一般的な問題について言及するのを忘れていました-マクロは引数を数回計算できます。 最悪の場合、これは奇妙な副作用を引き起こしますが、穏やかなものではパフォーマンスの問題を引き起こします。





     #define SQR(x) ((x) * (x)) y = SQR(x++);
          
          



    問題の解決策:

    • 関数を優先してマクロを放棄し、
    • このようなマクロを使用するプログラマーがマクロを正しく使用する方法を簡単に理解できるように、わかりやすい名前、簡単な実装、適切に配置されたブラケットを使用してマクロを使用します。
  3. マクロには名前空間がありません
    マクロが宣言されている場合、それはグローバルであるだけでなく、単に同じ名前の何かを使用することを許可しません(マクロの実装は常に置換されます)。 最も有名な例は、おそらくWindowsでのminとmaxの問題です。
    解決策は、たとえば次のものと交差する可能性が低いマクロの名前を選択することです。

    • 大文字の名前。通常、他のマクロ名としか交差できません。
    • 接頭辞(プロジェクトの名前、名前空間、他の一意の名前)を持つ名前、他の名前との交差は非常に少ない確率で可能になりますが、プロジェクト外でこのようなマクロを使用するのは少し難しくなります。
  4. マクロはあなたが疑わないことをすることができます
    実際、これはマクロの名前を選択する際の問題です。 リンクで回答に示されているのと同じ例を取り上げるとしましょう。

     #define begin() x = 0 #define end() x = 17 ... a few thousand lines of stuff here ... void dostuff() { int x = 7; begin(); ... more code using x ... printf("x=%d\n", x); end(); }
          
          



    明確に選択された名前があり、誤解を招く可能性があります。 マクロがset0toX()およびset17toX()またはそのようなものと呼ばれた場合、問題を回避できます。
    問題の解決策:

    • 有能なマクロの名前付け、
    • マクロを関数に置き換え、
    • 暗黙的に何かを変更するマクロを使用しないでください。


上記のすべての後、「良い」マクロを定義できます。 良いマクロとは、



安全なメソッド呼び出し



Habrのテストに合格しなかった古いバージョン
 #define prefix_safeCall(value, object, method) ((object) ? ((object)->method) : (value)) #define prefix_safeCallVoid(object, method) ((object) ? ((void)((object)->method)) : ((void)(0)))
      
      





実際、このバージョンを使用しました
 #define prefix_safeCall(defaultValue, objectPointer, methodWithArguments) ((objectPointer) ? ((objectPointer)->methodWithArguments) : (defaultValue)) #define prefix_safeCallVoid(objectPointer, methodWithArguments) ((objectPointer) ? static_cast<void>((objectPointer)->methodWithArguments) : static_cast<void>(0))
      
      



しかし、HabrはIDEではないため、このような長い行は(少なくとも私のモニターでは)lookいように見えます。


コメント内のtenzinkは、 これらのマクロの問題を指摘しましたが 、記事を書くときに安全に考慮していませんでした。

 prefix_safeCallVoid(getObject(), method());
      
      



この呼び出しでは、getObjectが2回呼び出されます。



残念ながら、この記事が示したように、すべてのプログラマーがそれを推測するわけではないため、これらのマクロが優れているとはもはや考えられません。 :-(



それにもかかわらず、私は実際の製品コードで似たようなマクロ(多少異なる実装)に出会い、私を含むプログラマーのチームによって使用されました。 私の記憶には問題がなかった



レメリスクとC ++ 14のおかげで登場した新しいバージョン:

 #define prefix_safeCall(defaultValue, objectPointer, methodWithArguments)\ [&](auto&& ptr) -> decltype(auto)\ {\ return ptr ? (ptr->methodWithArguments) : (defaultValue);\ }\ (objectPointer) #define prefix_safeCallVoid(objectPointer, methodWithArguments)\ [&](auto&& ptr)\ {\ if(ptr)\ (ptr->methodWithArguments); \ }\ (objectPointer)
      
      





C ++ 11のバージョン
 #define prefix_safeCallBaseExpression(defaultValue, objectPointer, methodWithArguments)\ ((ptr) ? ((ptr)->methodWithArguments) : (defaultValue)) #define prefix_safeCall(defaultValue, objectPointer, methodWithArguments)\ [&](decltype((objectPointer))&& ptr)\ -> decltype(prefix_safeCallBaseExpression(defaultValue, ptr, methodWithArguments))\ {\ return prefix_safeCallBaseExpression(defaultValue, ptr, methodWithArguments);\ }\ (objectPointer) #define prefix_safeCallVoid(objectPointer, methodWithArguments)\ [&](decltype((objectPointer))&& ptr)\ {\ if (ptr)\ (ptr->methodWithArguments);\ }\ (objectPointer)
      
      





methodWithArgumentsパラメーターに注意してください。 これは、括弧で囲むことができないパラメーターの同じ例です。 つまり、メソッドの呼び出しに加えて、他の何かをパラメーターにプッシュすることもできます。 しかし、これを誤って配置することは非常に問題があるため、これらのマクロは「悪い」とは考えていません。



さらに、ラムダ呼び出しにオーバーヘッドを追加しました。 理論的には、定義されている場所で呼び出されるラムダはインラインであると想定できます。 しかし、私はネットワーク上でこれの確認を見つけられなかったので、あなたのコンパイラのために「手動で」それをチェックすることが最善でしょう。



これら2つのマクロの使用方法は理解できると思います。 コードがある場合:

 auto somePointer = ...; if(somePointer) somePoiter->callSomeMethod();
      
      



safeCallVoidマクロを使用すると、次のようになります。

 auto somePointer = ...; prefix_safeCallVoid(somePointer, callSomeMethod());
      
      



そして、同様に、戻り値がある場合:

 auto somePointer = ...; auto x = prefix_safeCall(0, somePointer, callSomeMethod());
      
      





何のために? まず、これらのマクロを使用すると、コードの可読性を高め、ネストを削減できます。 最大のプラスの効果は、小さな方法と組み合わせて提供されます(つまり、リファクタリングの原則に従う場合)。



未使用の変数



 #define prefix_unused(variable) ((void)variable)
      
      





実際、私が使用するオプションも異なります
 #define prefix_unused1(variable1) static_cast<void>(variable1) #define prefix_unused2(variable1, variable2) static_cast<void>(variable1), static_cast<void>(variable2) #define prefix_unused3(variable1, variable2, variable3) static_cast<void>(variable1), static_cast<void>(variable2), static_cast<void>(variable3) #define prefix_unused4(variable1, variable2, variable3, variable4) static_cast<void>(variable1), static_cast<void>(variable2), static_cast<void>(variable3), static_cast<void>(variable4) #define prefix_unused5(variable1, variable2, variable3, variable4, variable5) static_cast<void>(variable1), static_cast<void>(variable2), static_cast<void>(variable3), static_cast<void>(variable4), static_cast<void>(variable5)
      
      



2つのパラメーターで始まるこのマクロは、理論的には副作用があることに注意してください。 信頼性を高めるために、クラシックを使用できます。

 #define unused2(variable1, variable2) do {static_cast<void>(variable1); static_cast<void>(variable2);} while(false)
      
      



しかし、この形式では読みにくいため、「安全性の低い」オプションを使用しています。



同様のマクロは、たとえばcocos2d-xにあり、CC_UNUSED_PARAMと呼ばれます。 欠点のうち、理論的には、すべてのコンパイラーで機能するとは限りません。 ただし、cocos2d-xでは、すべてのプラットフォームでまったく同じように定義されています。



使用法:

 int main() { int a = 0; //  . prefix_unused(a); return 0; }
      
      





何のために? このマクロは、未使用の変数に関する警告を回避し、「これを書いた人は変数が使用されていないことを知っていたので、すべてが順調だった」と言ってコードを読みます。



文字列への変換



 #define prefix_stringify(something) std::string(#something)
      
      





はい、すぐにstd ::文字列で非常に厳しいです。 文字列クラスを使用することの長所と短所は会話から除外され、マクロについてのみ説明します。



次のように使用できます。

 std::cout << prefix_stringify("string\n") << std::endl;
      
      



など:

 std::cout << prefix_stringify(std::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
      
      



それにしても:

 std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something) std::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
      
      



ただし、最後の例では、改行はスペースに置き換えられます。 実際の転送には、「\ n」を使用します。

 std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something)\nstd::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
      
      



また、「\」などの他の文字を使用して、文字列や「\ t」などを連結することもできます。



何のために? デバッグ情報の出力を単純化するため、またはテキストIDを持つオブジェクトのファクトリを作成するために使用できます(この場合、このようなマクロは、クラスをファクトリに登録してクラス名を文字列に変換するときに使用できます)。



マクロパラメーターのコンマ



 #define prefix_singleArgument(...) __VA_ARGS__
      
      





ここでアイデアが見張られています



そこからの例:

 #define FOO(type, name) type name FOO(prefix_singleArgument(std::map<int, int>), map_var);
      
      





何のために? 必要に応じて、コンマを含む引数を1つの引数として別のマクロに転送し、これに括弧を使用できないようにします。



無限ループ



 #define forever() for(;;)
      
      





ジョエル・スポルスキー版
 #define ever (;;) for ever { ... }
      
      



PSリンクをたどった人が質問の名前を読もうと考えなかった場合、「マクロの実際の最悪の悪用は何でしたか?」


使用法:

 int main() { bool keyPressed = false; forever() { ... if(keyPressed) break; } return 0; }
      
      





何のために? while(true)、while(1)、for(;;)、およびループを作成する他の標準的な方法があまり有益ではないと思われる場合は、同様のマクロを使用できます。 彼が与える唯一のプラスは、コードの可読性がわずかに優れていることです。



おわりに



適切に使用すると、マクロはまったく悪くありません。 主なことは、それらを乱用せず、「良い」マクロを作成するための簡単なルールに従うことではありません。 そして、彼らはあなたの最高のヘルパーになります。



更新しました 。 記事「Safe Method Call」に戻りlemeliskはラムダに関するヘルプをありがとう。



PS

また、プロジェクトで使用する興味深いマクロは何ですか? コメントで自由に共有してください。



All Articles