1. ホーム
  2. c++

[解決済み] プリプロセッサー・マクロはなぜ悪なのか、そしてその代替手段は?

2022-10-17 19:02:45

質問

最初の "Hello World" を書く前に、ほとんどすべてのプログラマーは "macro should never be used" や "macro are evil" などのフレーズに遭遇したと思いますが、私の質問は「なぜですか」というものです。新しい C++11 では、これほど長い年月を経て、真の代替手段があるのでしょうか?

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

しかし、一般的に、マクロとその使用に対する代替案はどうでしょうか?

どのように解決するのですか?

マクロは他の道具と同じです。殺人に使われるハンマーは、ハンマーだから悪いというわけではありません。それは、その人がそのように使用することにおいて悪なのです。釘を打ち込みたいなら、ハンマーは完璧なツールです。

マクロには、それらを "悪い" にするいくつかの側面があります (後でそれぞれについて展開し、代替手段を提案します)。

  1. マクロをデバッグすることはできません。
  2. マクロの展開は、奇妙な副作用をもたらすことがあります。
  3. マクロには "namespace" がないため、他の場所で使用されている名前と衝突するマクロがある場合、望んでいない場所でマクロ置換が行われ、これは通常、奇妙なエラー メッセージにつながります。
  4. マクロは、あなたが気づいていないことに影響を与えることがあります。

では、ここで少し展開してみましょう。

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

置き換え : 使用方法 enum または const T

関数型マクロの場合、デバッガはソース行単位で動作するため、マクロが1文であろうが100文であろうが、1文のように動作します。そのため、何が起こっているのかを把握することが困難です。

置き換え : 関数を使う - 速くする必要があるならインラインにする (ただしインラインにしすぎるのは良くない)

2) マクロ拡張は奇妙な副作用があります。

有名なのは #define SQUARE(x) ((x) * (x)) であり、使用されている x2 = SQUARE(x++) . それが導くのは x2 = (x++) * (x++); これは、たとえ有効なコード[1]であったとしても、プログラマが望んだものではないことはほぼ間違いないでしょう。もしそれが関数であれば、x++ を実行するのは問題ないでしょうし、x は 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

で、C++でbeginを使ったコードがあります。

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に影響を与えないはずだと思うでしょう。

このようなことは、もっと複雑な例も見てきましたが、本当に一日を台無しにしてしまいます。

置き換え : xを設定するマクロを使わないか、xを引数として渡すか。

マクロを使用することが間違いなく有益である場合があります。例えば、関数をマクロでラップして、ファイルや行の情報を渡すことが挙げられます。

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

ここで my_debug_malloc を通常の malloc と同様に使用することができますが、これには追加の引数があり、最後に来て "which memory elements hasn't been freed" をスキャンすると、割り当てが行われた場所を表示して、プログラマがリークを突き止めることができます。

[1] シーケンスポイントにおいて、1つの変数を複数回更新することは、未定義の動作です。シーケンス ポイントはステートメントと完全に同じではありませんが、ほとんどの意図と目的では、それを考慮する必要があります。つまり x++ * x++ を実行すると x を2回更新することになりますが、これは未定義であり、おそらくシステムによって異なる値になり、また x の結果値も違ってきます。