プリプロセッサマクロが悪である理由と代替手段は何ですか?


92

私はいつもこれを尋ねてきましたが、本当に良い答えを受け取ったことはありません。最初の「Hello World」を書く前のほとんどのプログラマーは、「マクロは使用してはならない」、「マクロは悪だ」などのフレーズに遭遇したと思います。私の質問は、なぜですか?新しいC ++ 11では、何年も経たないうちに本当の選択肢はありますか?

簡単な部分は、のようなマクロです#pragma。これは、プラットフォーム固有およびコンパイラ固有であり、ほとんどの場合、#pragma once少なくとも2つの重要な状況でエラーが発生しやすいという重大な欠陥があります。異なるパスに同じ名前があり、一部のネットワークセットアップとファイルシステムにエラーがあります。

しかし、一般的に、マクロとその使用法の代替についてはどうでしょうか?


19
#pragmaマクロではありません。
FooF

1
@foofプリプロセッサディレクティブ?
user1849534

6
@ user1849534:はい、それがそうです...そしてマクロに関するアドバイスは話していません#pragma
Ben Voigt

1
あなたが多くを行うことができconstexprinline機能を、そしてtemplates、しかし、boost.preprocessorおよびchaosマクロがその場所を持っていることを示しています。異なるコンパイラ、プラットフォームなどの構成マクロは言うまでもなく
Brandon

回答:


161

マクロは他のツールとまったく同じです-殺人で使用されるハンマーはハンマーなので悪ではありません。そのように人がそれを使う方法は悪いです。釘で打ちたい場合は、ハンマーが最適です。

マクロを「悪い」ものにするいくつかの側面があります(後で詳しく説明し、代替案を提案します)。

  1. マクロはデバッグできません。
  2. マクロ展開は、奇妙な副作用を引き起こす可能性があります。
  3. マクロには「名前空間」がないため、他の場所で使用されている名前と競合するマクロがある場合、不要な場所でマクロが置き換えられ、通常は奇妙なエラーメッセージが表示されます。
  4. マクロは、気づかないことに影響を与える可能性があります。

ここで少し拡張してみましょう:

1)マクロはデバッグできません。 数値または文字列に変換されるマクロがある場合、ソースコードにはマクロ名が含まれ、多くのデバッガーでは、マクロの変換先を「見る」ことができません。だから、実際に何が起こっているのかわかりません。

交換enumまたはを使用const T

「関数のような」マクロの場合、デバッガーは「現在のソース行ごと」レベルで機能するため、マクロは1つのステートメントであるか100であるかに関係なく、1つのステートメントのように機能します。何が起こっているのか理解するのを難しくします。

置換:関数を使用します-「高速」にする必要がある場合はインラインで使用します(ただし、インラインを多すぎるのは良くないことに注意してください)

2)マクロ展開は奇妙な副作用をもたらす可能性があります。

有名なもの#define SQUARE(x) ((x) * (x))とその使い方x2 = SQUARE(x++)。これはにつながりますx2 = (x++) * (x++);。これは、たとえそれが有効なコード[1]であったとしても、ほぼ間違いなくプログラマーが望んでいたものではありません。関数の場合、x ++を実行しても問題はなく、xは1度だけインクリメントします。

もう1つの例は、マクロの「if else」です。これがあるとしましょう。

#define safe_divide(res, x, y)   if (y != 0) res = x/y;

その後

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

それは実際には完全に間違ったものになります...

交換:実際の機能。

3)マクロには名前空間がありません

マクロがある場合:

#define begin() x = 0

そしてbeginを使用するC ++のコードがあります。

std::vector<int> v;

... stuff is loaded into v ... 

for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
   std::cout << ' ' << *it;

ここで、どのエラーメッセージが表示されると思いますか。どこでエラーを探しますか(他の誰かが書いたヘッダーファイルにあるbeginマクロを完全に忘れてしまった、または知らなかった場合)。[そして、インクルードの前にそのマクロをインクルードした場合はさらにおもしろい-コード自体を見てもまったく意味のない奇妙なエラーに溺れてしまうでしょう。

置換:まあ、「ルール」ほど置換はありません-マクロには大文字の名前のみを使用し、他のものにはすべて大文字の名前を使用しないでください。

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();

}

さて、マクロを見ないで、beginはxに影響を与えてはならない関数であると考えるでしょう。

この種のこと、そしてもっと複雑な例を見てきましたが、本当にあなたの1日を台無しにする可能性があります!

置換:マクロを使用してxを設定しないか、xを引数として渡します。

マクロの使用が明らかに有益な場合があります。1つの例は、ファイル/行情報を渡すためにマクロで関数をラップすることです:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

my_debug_mallocコードで通常のmallocとして使用できるようになりましたが、追加の引数があるため、最後に「解放されていないメモリ要素」をスキャンすると、割り当てが行われた場所を印刷できるので、プログラマはリークを追跡できます。

[1]「シーケンスポイント内」で1つの変数を複数回更新することは未定義の動作です。シーケンスポイントはステートメントと正確に同じではありませんが、ほとんどの目的と目的のために、それを検討する必要があります。したがって、これを行うx++ * x++x2回更新さxれます。これは未定義であり、おそらく異なるシステムでは異なる値になり、同様に異なる結果値になります。


6
if else問題は、マクロ本体内をラップすることによって解決することができますdo { ... } while(0)。一つとしてこの振る舞いはに対する期待ifforその他の潜在的に危険な制御フローの問題や。しかし、はい、通常、実際の関数がより良いソリューションです。 #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
Aaron McDaid 2013年

11
@AaronMcDaid:はい、これらのマクロで公開されている問題のいくつかを解決するいくつかの回避策があります。私の投稿の全体の要点は、マクロをうまく実行する方法を示すことではなく、優れた代替手段がある「マクロを間違えるのはどれほど簡単か」です。とはいえ、マクロは非常に簡単に解決できるものもあり、マクロも適切な場合があります。
Mats Petersson 2013年

1
ポイント3では、エラーは実際にはもう問題ではありません。Clangなどの最新のコンパイラーは、次のように言ってnote: expanded from macro 'begin'、どこbeginが定義されているかを示します。
kirbyfan64sos

5
マクロを他の言語に翻訳することは困難です。
Marco van de Voort 2016年

1
@FrancescoDondi:stackoverflow.com/questions/4176328/...。その答えで(かなりダウンし、それは私++ * I ++などについて語る
マットピーターソン

21

「マクロは悪だ」という言葉は通常、#pragmaではなく#defineの使用を指します。

具体的には、式は次の2つのケースを参照します。

  • マジックナンバーをマクロとして定義する

  • マクロを使用して式を置き換える

新しいC ++ 11では、何年も経たないうちに本当の選択肢がありますか?

はい、上のリストの項目について(マジックナンバーはconst / constexprで定義し、式は[normal / inline / template / inline template]関数で定義する必要があります。

マジックナンバーをマクロとして定義し、式をマクロで置き換える(これらの式を評価するための関数を定義するのではなく)ことによって生じる問題の一部を次に示します。

  • マジックナンバーのマクロを定義する場合、コンパイラは定義された値の型情報を保持しません。これにより、コンパイル警告(およびエラー)が発生し、コードのデバッグを混乱させる可能性があります。

  • 関数の代わりにマクロを定義するとき、そのコードを使用するプログラマは、それらが関数のように機能することを期待し、そうではないことを期待します。

このコードを考えてみましょう:

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )

int a = 5;
int b = 4;

int c = max(++a, b);

(マクロの代わりにstd :: maxを使用した場合と同様に)cへの代入後は、aとcが6になると予想します。代わりに、コードは以下を実行します。

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

さらに、マクロは名前空間をサポートしていません。つまり、コードでマクロを定義すると、クライアントコードで使用できる名前が制限されます。

これは、上記のマクロを(maxに対して)定義した場合#include <algorithm>、明示的に記述しない限り、以下のどのコードでも使用できなくなることを意味します。

#ifdef max
#undef max
#endif
#include <algorithm>

変数/関数の代わりにマクロを持つことは、それらのアドレスを取得できないことも意味します:

  • 定数としてのマクロがマジックナンバーに評価される場合、アドレスで渡すことはできません

  • 関数としてのマクロの場合、それを述語として使用したり、関数のアドレスを取得したり、関数として扱うことはできません。

編集:例として、上記の正しい代替#define max

template<typename T>
inline T max(const T& a, const T& b)
{
    return a > b ? a : b;
}

これは、マクロが行うすべてのことを行いますが、1つの制限があります。引数のタイプが異なる場合、テンプレートバージョンは明示的にすることを強制します(実際には、より安全でより明示的なコードにつながります)。

int a = 0;
double b = 1.;
max(a, b);

この最大値がマクロとして定義されている場合、コードはコンパイルされます(警告付き)。

この最大値がテンプレート関数として定義されている場合、コンパイラーはあいまいさを指摘し、max<int>(a, b)またはmax<double>(a, b)(または意図を明示的に述べる)のいずれかを言わなければなりません。


1
C ++ 11固有である必要はありません。関数を使用して、マクロとしての式の使用法を置き換え、[静的] const / constexprを使用して、マクロとしての定数の使用法を置き換えることができます。
utnapistim 2012

1
C99でもを使用const int someconstant = 437;でき、マクロが使用されるほとんどすべての方法で使用できます。同様に、小さな関数の場合。Cの正規表現では機能しないマクロとして何かを記述できることがいくつかあります(Cが実行できない、任意のタイプの数値の配列を平均化するものを作成できますが、C ++にはテンプレートがあります)そのため)。C ++ 11は「これにはマクロが必要ない」ということをいくつか追加していますが、以前のC / C ++ではほとんど解決済みです。
Mats Petersson

引数を渡しながらプリインクリメントを行うことは、ひどいコーディングの習慣です。そして、C / C ++でコーディング誰もがすべきではない関数のような呼び出しはマクロではないと推測します。
StephenG 2018年

多くの実装は、自発的に識別子maxを括弧で囲み、minその後に左括弧が続く場合。しかし、そのようなマクロを定義するべきではありません...
LF

14

一般的な問題はこれです:

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

プリプロセッサが次のように展開するため、5ではなく10が出力されます。

printf("25 / (3+2) = %d", 25 / 3 + 2);

このバージョンはより安全です:

#define DIV(a,b) (a) / (b)

2
興味深い例、基本的にはセマンティクスのない単なるトークンです
user1849534

はい。それらはマクロに与えられる方法で拡張されます。DIVマクロは、()の周りのペアで書き換えることができますb
phaazon

2
あなたは意味し#define DIV(a,b)ない、#define DIV (a,b)非常に異なっています、。
rici

6
#define DIV(a,b) (a) / (b)十分ではありません。一般診療の問題として、常にこのよう、ブラケットを最も外側の追加:#define DIV(a,b) ( (a) / (b) )
PJTraill

3

マクロは、一般的なコード(マクロのパラメーターは何でもかまいません)を作成する場合、特にパラメーターを使用する場合に特に役立ちます。

さらに、このコードはマクロが使用される場所に配置(挿入)されます。

OTOH、同様の結果は以下で達成される可能性があります:

  • オーバーロードされた関数(異なるパラメータータイプ)

  • テンプレート、C ++(ジェネリックパラメーターの型と値)

  • インライン関数(単一ポイントの定義にジャンプするのではなく、コードが呼び出される場所にコードを配置します。ただし、これはコンパイラにとってはむしろ推奨事項です)。

編集:なぜマクロが悪いのか:

1)引数の型チェックがない(型がない)ため、簡単に誤用される可能性があります2)非常に複雑なコードに展開されることがあり、前処理されたファイルで識別して理解するのが難しい場合があります3)エラーが発生しやすい-次のようなマクロの傾向のあるコード:

#define MULTIPLY(a,b) a*b

そして次に電話する

MULTIPLY(2+3,4+5)

拡大する

2 + 3 * 4 + 5(および(2 + 3)*(4 + 5)ではない)。

後者を使用するには、以下を定義する必要があります。

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

3

プリプロセッサの定義やマクロを呼び出すときに使用することに問題はないと思います。

これらはc / c ++に見られる(メタ)言語の概念であり、他のツールと同様に、自分が何をしているのかを理解していれば、生活を楽にすることができます。マクロの問題点は、マクロがc / c ++コードの前に処理され、新しいコードを生成することです。これにより、エラーが発生し、コンパイラエラーが発生します。明るい面では、コードをクリーンに保ち、適切に使用すれば入力の手間を省くことができるため、個人の好みに合わせて調整できます。


また、他の回答で指摘されているように、不適切に設計されたプリプロセッサー定義は、有効な構文で異なる意味論的意味を持つコードを生成する可能性があります。
SandiHrvić12年

3

C / C ++のマクロは、バージョン管理の重要なツールとして機能します。マクロのマイナー構成を使用して、同じコードを2つのクライアントに配信できます。私は次のものを使用します

#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT 
  #define SOME_VALUE1 X
  #define SOME_VALUE2 Y
#else
  #define SOME_VALUE1 P
  #define SOME_VALUE2 Q
#endif

この種の機能は、マクロなしでは簡単には実現できません。マクロは実際には優れたソフトウェア構成管理ツールであり、コードを再利用するためのショートカットを作成するだけの方法ではありません。マクロでの再利用を目的として関数を定義すると、間違いなく問題が発生する可能性があります。


コンパイル中にコマンドラインでマクロ値を設定して、1つのコードベースから2つのバリアントを構築するのは、本当に素晴らしいことです。適度に。
kevinf 2017

1
いくつかの観点からは、この使用法は最も危険なものです。ツール(IDE、静的アナライザー、リファクタリング)は、可能なコードパスを理解するのに苦労します。
エレノン

1

問題は、マクロがコンパイラーによって適切に最適化されておらず、読み取りとデバッグが「醜い」ことです。

多くの場合、適切な代替手段は、ジェネリック関数またはインライン関数です。


2
マクロが十分に最適化されていないと思われる理由は何ですか?それらは単純なテキスト置換であり、結果はマクロなしで書かれたコードと同じくらい最適化されます。
Ben Voigt

@BenVoigt彼らはセマンティクスを考慮していないと、これは「ない最適」として考えることができる何かにつながることができます...少なくとも、これはそのことについて私の最初の取り払わですstackoverflow.com/a/14041502/1849534
user1849534

1
@ user1849534:コンパイルのコンテキストで「最適化」という言葉が意味するものではありません。
Ben Voigt

1
@BenVoigtまさに、マクロは単なるテキスト置換です。コンパイラーはコードを複製するだけで、パフォーマンスの問題ではありませんが、プログラムのサイズが大きくなる可能性があります。特に、プログラムのサイズに制限がある場合に当てはまります。一部のコードはマクロが多すぎてプログラムのサイズが2倍になります。
Davide Icardi

1

プリプロセッサマクロは、次のような意図された目的に使用される場合は問題ありません。

  • #ifdefタイプのコンストラクトを使用して、同じソフトウェアの異なるリリースを作成します(たとえば、異なる地域のウィンドウのリリース)。
  • 関連する値をテストするコードを定義します。

代替案- 同様の目的で、ini、xml、json形式のある種の構成ファイルを使用できます。しかし、それらを使用すると、プリプロセッサマクロが回避できるコードに実行時の影響があります。


1
C ++ 17のconstexpr if以降では、「config」constexpr変数を含むヘッダーファイルで#ifdefを置き換えることができます。
Enhex
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.