C ++コードのマクロ害

定義する






C ++言語は、マクロなしで実行する可能性を大きく提供します。 それでは、マクロをできる限り使用しないようにしましょう。



すぐに私が狂信者ではなく、理想主義的な理由でマクロを放棄することを勧めないように予約してください。 たとえば、同じ種類のコードを手動で生成することになると、マクロの利点を認識し、それらに同意することができます。 たとえば、MFCを使用して記述された古いプログラムのマクロについては冷静です。 このようなものと戦うことは意味がありません:



BEGIN_MESSAGE_MAP(efcDialog, EFCDIALOG_PARENT ) //{{AFX_MSG_MAP(efcDialog) ON_WM_CREATE() ON_WM_DESTROY() //}}AFX_MSG_MAP END_MESSAGE_MAP()
      
      





そのようなマクロがあり、大丈夫です。 それらはプログラミングを簡素化するために作成されました。



私は、本格的な関数の実装を避けたり、関数のサイズを小さくしようとする他のマクロについて話しています。 そのようなマクロを避けるためのいくつかの動機を検討してください。



ご注意 このテキストは、Simplify C ++ブログのゲスト投稿として書かれました。 ここでロシア語版の記事を公開することにしました。 実際、この記事がなぜ「翻訳」とマークされていないのか、不注意な読者からの質問を避けるために、このメモを書いています。 そして、実際には、英語のゲスト投稿「 C ++コードのマクロ悪 」。



最初:マクロコードはバグを引き付けます



私はこの現象の原因を哲学的な観点から説明する方法を知りませんが、そうです。 さらに、マクロ関連のバグは、コードレビューを実施する際に見つけるのが非常に難しいことがよくあります。



私は記事でそのようなケースを繰り返し説明しました。 たとえば、isspace関数を次のマクロに置き換えます。



 #define isspace(c) ((c)==' ' || (c) == '\t')
      
      





isspaceを使用したプログラマーは、空白やタブだけでなく、LF、CRなども考慮した実際の関数を使用していると考えていました。 その結果、条件の1つが常に真になり、コードが意図したとおりに機能しなくなります。 Midnight Commanderからのこのエラーについては、 こちらで説明しています



または、 std :: printf関数を記述するためのこの短縮形はどうですか?



 #define sprintf std::printf
      
      





読者はそれが非常に失敗したマクロだと推測していると思います。 ところで、それはStarEngineプロジェクトで発見されました。 詳細についてはこちらをご覧ください



プログラマーはマクロではなく、これらのエラーのせいだと主張することができます。 そうです。 当然、プログラマーは常にエラーのせいにします。



マクロがエラーを引き起こすことが重要です。 マクロは、精度を高めて使用するか、まったく使用しないでください。



長い間マクロを使用することに関連する欠陥の例を挙げることができますが、この素敵なメモは重い複数ページのドキュメントに変わります。 もちろん、これは行いませんが、説得力のある他のいくつかの事例を示します。



ATLライブラリ 、文字列を変換するためのA2W、T2Wなどのマクロを提供します。 ただし、これらのマクロがループ内で使用するのに非常に危険であることを知っている人はほとんどいません。 マクロ内では、 alloca関数が呼び出され、ループの各反復でスタック上のメモリが何度も割り当てられます。 プログラムが正しく動作するふりをする場合があります。 プログラムが長い行の処理を開始するか、ループ内の反復回数が増えると、スタックは予想外の瞬間に処理を終了します。 この詳細については、このミニブックで読むことができます(「ループ内でalloca()関数を呼び出さないでください」の章を参照してください)。



A2Wなどのマクロは悪を隠します。 それらは関数のように見えますが、実際には、気づきにくい副作用があります。



マクロを使用してコードを削減するための過去の同様の試みを得ることができません:



 void initialize_sanitizer_builtins (void) { .... #define DEF_SANITIZER_BUILTIN(ENUM, NAME, TYPE, ATTRS) \ decl = add_builtin_function ("__builtin_" NAME, TYPE, ENUM, \ BUILT_IN_NORMAL, NAME, NULL_TREE); \ set_call_expr_flags (decl, ATTRS); \ set_builtin_decl (ENUM, decl, true); #include "sanitizer.def" if ((flag_sanitize & SANITIZE_OBJECT_SIZE) && !builtin_decl_implicit_p (BUILT_IN_OBJECT_SIZE)) DEF_SANITIZER_BUILTIN (BUILT_IN_OBJECT_SIZE, "object_size", BT_FN_SIZE_CONST_PTR_INT, ATTR_PURE_NOTHROW_LEAF_LIST) .... }
      
      





マクロの最初の行のみがifステートメントを参照します。 残りの行は、条件に関係なく実行されます。 GCCコンパイラ内でV640診断を使用して発見したため、このエラーはCの世界からのものであると言えます。 GCCコードは主にCで書かれており、この言語ではマクロを実行するのは困難です。 ただし、そうではないことを認めなければなりません。 ここでは、実際の機能を作成することができました。



2番目:コードの読み取りがより複雑になる



他のマクロで構成されるマクロでいっぱいのプロジェクトに出くわした場合、そのようなプロジェクトを理解することは一体何であるかを理解できます。 あなたが遭遇していないなら、一言言ってください、これは悲しいです。 わかりにくいコードの例として、上記のGCCコンパイラーを引用できます。



伝説によると、AppleはこれらのまさにマクロによるGCCコードの複雑さのために、GCCの代替としてLLVMプロジェクトの開発に投資しました。 これについて読んだところ、覚えていないので、証拠はありません。



第三:マクロを書くのは難しい



悪いマクロを書くのは簡単です。 対応する結果でどこでも彼らに会います。 しかし、よく信頼できるマクロを書くことは、多くの場合、同様の関数を書くよりも困難です。



優れたマクロを書くことは、関数とは異なり、独立したエンティティと見なすことができないという理由で困難です。 使用可能なすべてのオプションのコンテキストでマクロをすぐに検討する必要があります。そうしないと、フォームの問題を非常に簡単に解決できます。



 #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) m = MIN(ArrayA[i++], ArrayB[j++]);
      
      





もちろん、そのような場合、回避策が長い間発明されており、マクロを安全に実装できます。



 #define MAX(a,b) \ ({ __typeof__ (a) _a = (a); \ __typeof__ (b) _b = (b); \ _a > _b ? _a : _b; })
      
      





唯一の質問は、これらすべてをC ++で必要とするかどうかです。 いいえ、C ++には効率的なコードを作成するためのテンプレートやその他の方法があります。 それでは、C ++プログラムで同様のマクロに遭遇し続けるのはなぜですか?



4番目:デバッグは複雑です



デバッグは弱虫用であるという意見があります:)。 もちろん、これは議論するのは興味深いですが、実用的な観点からは、デバッグは有用であり、エラーを見つけるのに役立ちます。 マクロはこのプロセスを複雑にし、間違いなくエラーの検索を遅くします。



5番目:静的アナライザーの誤検知



多くのマクロは、デバイスの特性により、静的コードアナライザーから複数の誤検知を生成します。 CおよびC ++コードをチェックする際の誤検知のほとんどはマクロに関連していると安全に言えます。



マクロの問題は、アナライザーが正しいトリッキーなコードと誤ったコードを区別できないことです。 Chromiumのチェックに関する記事では、これらのマクロの1つについて説明しています。



どうする



絶対に必要でない限り、C ++プログラムでマクロを使用しないでください!



C ++は、テンプレート関数、自動型推論(auto、decltype)、constexpr関数などの豊富なツールを提供します。



ほとんどの場合、マクロの代わりに、通常の関数を作成できます。 多くの場合、これは通常の怠lazのために行われません。 この怠lazは有害であり、私たちはそれと戦わなければなりません。 本格的な関数を書くのに費やしたわずかな追加時間は、興味を持って報われるでしょう。 コードは読みやすく、保守しやすくなります。 自分の足を撃つ可能性が低くなり、コンパイラーと静的アナライザーの誤検知が少なくなります。



関数を含むコードは効率が悪いと主張する人もいるかもしれません。 これも単なる「言い訳」です。



コンパイラは、 インラインキーワードを記述していなくても、コードを完全にインライン化します。



コンパイル段階で式の計算について話している場合、ここではマクロは不要であり、有害ですらあります。 同じ目的で、 constexprを使用する方がはるかに安全で安全です



例で説明します。 これは、FreeBSDカーネルコードから借用した古典的なマクロエラーです。



 #define ICB2400_VPOPT_WRITE_SIZE 20 #define ICB2400_VPINFO_PORT_OFF(chan) \ (ICB2400_VPINFO_OFF + \ sizeof (isp_icb_2400_vpinfo_t) + \ (chan * ICB2400_VPOPT_WRITE_SIZE)) // <= static void isp_fibre_init_2400(ispsoftc_t *isp) { .... if (ISP_CAP_VP0(isp)) off += ICB2400_VPINFO_PORT_OFF(chan); else off += ICB2400_VPINFO_PORT_OFF(chan - 1); // <= .... }
      
      





chan引数は、括弧で囲まずにマクロで使用されます。 結果として、式ICB2400_VPOPT_WRITE_SIZEは式(chan-1)を乗算しませんが、1つだけです。



マクロの代わりに通常の関数が記述されている場合、エラーは表示されません。



 size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; }
      
      





高い確率で、最新のCおよびC ++コンパイラーはインライン関数を独立して実行し、コードはマクロの場合と同様に効率的です。



同時に、コードは読みやすくなり、エラーもなくなりました。



入力値が常に定数であることがわかっている場合は、 constexprを追加して、コンパイル段階ですべての計算が行われるようにすることができます。 C ++言語であり、 chanは常に定数であると想像してください。 次に、 次のように関数ICB2400_VPINFO_PORT_OFFを宣言すると便利です。



 constexpr size_t ICB2400_VPINFO_PORT_OFF(size_t chan) { return ICB2400_VPINFO_OFF + sizeof(isp_icb_2400_vpinfo_t) + chan * ICB2400_VPOPT_WRITE_SIZE; }
      
      





利益!



私はあなたを説得することができたと思います。 コード内の幸運と少ないマクロ!



All Articles