【C++】#defineとconstexprの違いとは?|マクロの基本と「使わない方がいい」と言われる理由

サムネイル画像 C++

今回は、C++でよく出てくる#defineconstexpr の違いについて解説していきます。

#defineconstexpr って何が違うの?」
「定数を書くならどっちを使えばいいの?」
「マクロは使わない方がいいって聞くけど、なぜ?」

こんな疑問はありませんか?

C++を学び始めると、定数を書く場面で #define を見かけることがあります。ただ、最近のC++では constexpr の方が推奨される場面がかなり多いです。ここを曖昧なまま進めると、あとでバグの原因読みにくいコードにつながりやすくなります。

この記事を読み終えると、あなたは#defineconstexpr の違い・それぞれの役割・マクロが危ないと言われる理由をしっかり理解できるようになると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

#defineとは?

#define とは、プリプロセッサがコンパイル前に文字列を置き換える仕組みです。

たとえば、#define MAX_HP 100 と書いておくと、コード中の MAX_HP がコンパイル前に 100 に置き換えられます。

ここで大事なのは、#define変数を作っているわけではなく、ただ文字列を差し替えているだけということです。#include などと同じくプリプロセッサの仕組みなので、このあたりが気になる方は #includeの””と<>の違いの記事 もあわせて読むと整理しやすいです。

constexprとは?

constexpr は、コンパイル時に値が確定する定数や関数を書くための仕組みです。

こちらは単なる文字列置換ではなく、C++の型を持った正式な定数として扱われます。つまり、コンパイラが型チェックもしてくれますし、スコープの考え方も普通の変数に近いです。

ざっくり言うと、#define は「置き換え」、constexpr は「型付きの定数」というイメージです。

まずはコードで違いを見てみよう

まずは、同じように見える定数を #defineconstexpr で書いた例を見てみましょう。

▼main.cpp


#include <iostream>

#define MAX_HP 100
constexpr int kStartHp = 100;

int main() {
    std::cout << "MAX_HP: " << MAX_HP << "\n";
    std::cout << "kStartHp: " << kStartHp << "\n";
    return 0;
}

実行結果

MAX_HP: 100
kStartHp: 100

見た目の結果は同じですが、中身はかなり違います。MAX_HP は置換、kStartHp は型付き定数です。この違いが、あとで安全性やデバッグのしやすさに効いてきます。

なぜ#defineは「使わない方がいい」と言われるの?

これは、#define が危険だから完全禁止という意味ではありません。

正確には、「定数や簡単な関数の代わりに使うなら、constexprやconstの方が安全で分かりやすい」という意味です。

  • 型がないので、コンパイラのチェックが弱い
  • スコープを持たないので、思わぬ場所に影響しやすい
  • ただの文字列置換なので、式の優先順位でバグを生みやすい
  • デバッグ時に追いにくいことがある

つまり、今のC++では「安全に書ける仕組みがあるのに、あえて危ない書き方を選ぶ必要があまりない」ということです。

マクロでよくある危険な例

特に有名なのが、関数っぽく見えるマクロです。

▼main.cpp


#include <iostream>

#define SQUARE(x) x * x

int main() {
    std::cout << "SQUARE(5): " << SQUARE(5) << "\n";
    std::cout << "SQUARE(2 + 3): " << SQUARE(2 + 3) << "\n";
    return 0;
}

実行結果

SQUARE(5): 25
SQUARE(2 + 3): 11

SQUARE(2 + 3) は本来 25 になってほしいのに、実際には 11 になっています。これは、マクロ展開後に 2 + 3 * 2 + 3 という式になってしまうからです。

このように、マクロは見た目が関数っぽくても、中ではただ置換しているだけなので、かなり思わぬバグを生みやすいです。

constexprならどう書ける?

同じことを constexpr 関数で書くと、かなり安全になります。

▼main.cpp


#include <iostream>

constexpr int Square(int x) {
    return x * x;
}

int main() {
    std::cout << "Square(5): " << Square(5) << "\n";
    std::cout << "Square(2 + 3): " << Square(2 + 3) << "\n";
    return 0;
}

実行結果

Square(5): 25
Square(2 + 3): 25

こちらはちゃんと関数として扱われるので、式の優先順位で壊れにくく、型チェックも効きます。こういう理由で、定数や簡単な計算には constexpr の方がかなり安心です。

【重要】私が実際に#defineで困った体験談

私も自主制作でC++を書いていた時、最初の頃は定数も簡単な処理も、とりあえず #define で書いていたことがありました。

その中で特に困ったのが、ダメージ計算っぽい処理をマクロで書いていた時です。見た目は関数のように使えて便利そうに見えたのですが、引数に式を入れた瞬間に結果が崩れて、思ったより大きなダメージが出たり、小さくなったりしました。

最初は計算式そのものが間違っていると思ってかなり悩んだのですが、原因はマクロ展開後の式の形が崩れていたことでした。そこで、定数は constexpr、簡単な処理は constexpr 関数か普通の関数に書き直したところ、かなり見通しが良くなりました。

この時に実感したのは、「一見ラクに見えるマクロほど、後で読む時と直す時にかなりつらい」ということでした。こういう不具合は値を追うと見つけやすいので、原因特定には C++でのデバッグのやり方の記事 もかなり相性がいいです。

#define使用時のよくある失敗例と対処法

  • 関数の代わりにマクロを書く
    式の優先順位や副作用で壊れやすいです。まずは constexpr 関数や普通の関数で置き換えられないか考えましょう。
  • 定数を全部#defineで書く
    今のC++では、整数や浮動小数点の定数は constexpr の方がかなり自然です。
  • マクロ名が広い範囲に影響する
    #define はスコープを持たないので、名前の衝突や予期しない置換が起きることがあります。名前を雑に付けるのは危険です。

それでも#defineが使われる場面はある?

あります。#define 自体が完全に不要になったわけではありません

たとえば、条件付きコンパイルやヘッダーファイルの制御など、プリプロセッサとしての役割が必要な場面では今でも使われます。つまり、「定数や簡単な処理にはあまり向かないけど、プリプロセッサの仕事には必要」ということです。

クラスや設計と組み合わせて定数を整理する考え方は、クラスの記事 ともつながってきます。

注意点

  • #define はただの文字列置換で、型付き定数ではありません
  • constexpr はコンパイル時定数として扱えるので、安全性と可読性が高いです
  • 初心者のうちは、定数はまず constexpr を第一候補にするとかなり整理しやすいです

また、ポインタやクラスなど他の概念と同じで、「とりあえず動く」書き方よりも「あとで読み返しやすい」書き方を選ぶのがかなり大切です。

まとめ

  • #define は、コンパイル前の文字列置換
  • constexpr は、型付きのコンパイル時定数
  • 定数や簡単な計算には、#define より constexpr の方が安全で分かりやすい
  • マクロは式の優先順位やスコープの問題でバグを生みやすい
  • #define は不要ではなく、条件付きコンパイルなどプリプロセッサ用途で今でも使われる

#defineconstexpr の違いは、最初は小さく見えるかもしれませんが、コードの安全性や読みやすさにかなり影響します。

「定数なら constexpr、プリプロセッサの仕事なら #define」 と考えると、かなり判断しやすくなります。

ここまで読んでくださり、ありがとうございました。

関連記事