C++編【言語解説】 第10章 マクロとその代替

先頭へ戻る

この章の概要

この章の概要です。


マクロ

C++ でも、C言語のマクロ機能を使用できますが、原則として避けるべきです。C++ にはより良い代替策があります。

マクロは、単に名前を置換するだけなので、その名前が変数として使われていても、関数として使われていても、そのほかの何かであってもお構いなしです。スコープの考慮もありませんし、型も関係ありません。言語が持つ安全対策がほとんど機能していません。

マクロが便利に使える場面はありますが、少なくとも、最初に選択すべき手段ではありません。また、マクロに限らず、プリプロセッサに頼らないようにしたいところです。

今のところ、プリプロセッサに頼らざるを得ない代表的な場面は以下のものがあります。

オブジェクト形式マクロの代替

オブジェクト形式マクロは、const や enum に置き換えます。名前空間(第3章)に入れることも考えましょう。

C++11 以降なら、constexpr を使うと良いです。

C++ では、const修飾子が付加された変数を定数として利用できます。例えば、次のコードは、C言語ではコンパイルエラーになりましたが、C++ では問題なく動作します。

int main()
{
    const int N = 10;
    int array[N];  // C言語ではエラー、C++ では OK
}

const であれば、型を指定でき、スコープも限定されますから、オブジェクト形式マクロよりも圧倒的に安全です。このコードの「N」は、main関数の外側には無関係でいられます。

const に関しては、第15章でさらに詳しく取り上げます。

const 以外にも、enum を使う選択肢がありますが、こちらは整数型ですから、使いどころは限定されます。

C++11 (constexpr)

C++11 からは、const の代わりに constexpr を使うと良いです。

int main()
{
    constexpr int N = 10;
    int array[N];
}

const による定数は実行時定数ですが、constexpr はコンパイル時定数です。つまり前者は、実行時に値が確定し、確定したら変化しない。後者は、コンパイル時に値が確定して変化しないという違いがあります。

コンパイル時に値が確定するため、初期化子が複雑な計算を必要とするものであっても、実行時に処理コストを支払う必要がありません。また、いかなる手を使っても値は変化しないと約束されるため、コンパイラは最適化を施しやすくなります。つまり、const による定数よりも、constexpr による定数の方が、効率的になる可能性があります

constexpr による定数は、コンパイル時に値を確定できなければならないので、実行時でないと値が定まらないような初期値は与えられません。例えば、次のように、関数呼び出しの結果を初期値にはできません。

#include <iostream>

int func()
{
    return 10;
}

int main()
{
    constexpr int N = func();  // コンパイルエラー

    std::cout << N << std::endl;
}

ただし、関数の側にも constexpr が付加されていれば受け付けられます。

#include <iostream>

constexpr int func()
{
    return 10;
}

int main()
{
    constexpr int N = func();

    std::cout << N << std::endl;
}

実行結果:

10

この場合、func関数の呼び出しは実行時ではなく、コンパイル時に行われていることが特徴的です。

constexpr の関数は実行時に呼び出すこともできます。定数式の文脈の中で呼び出そうとした場合はコンパイル時に、そうでなければ実行時に呼び出されます。

constexpr については、第15章でも取り上げています。

関数形式マクロの代替

関数形式マクロは、関数テンプレート(第9章)に置き換えます。また、スコープを与えるために、名前空間(第3章)に入れることも考えましょう。

関数形式マクロは、オブジェクト形式マクロ以上に危険です。特に、インクリメントやデクリメントを伴う際の問題を避ける手段がないことが致命的です(C言語編第28章)。

関数テンプレートを使うことによって、関数形式マクロが抱える問題を解決できます。関数テンプレートは実体化されてしまえば普通の関数ですから、マクロ特有な問題とは無縁です。

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

int main()
{
    int a = 10;
    int ans1 = max(a, 5);
    int ans2 = max(++a, 5);

    std::cout << ans1 << "\n"
              << ans2 << std::endl;
}

実行結果:

10
11

気になることがあるとすれば実行効率です。関数形式マクロは、関数呼び出しに関するコストが避けられますが、関数テンプレートではどうでしょうか。

関数呼び出しに関するコストとは、実引数のやり取りのためにスタックを操作する処理や、プログラムカウンタが関数のコードの位置へ移動することで起こる参照の局所性の悪化が代表的です。

現代のコンパイラは優秀なので、どのような手法が取られるかはわかりませんが、可能な限り効率を高めるようにコンパイルされることが期待できます。ですから、それほど気にする必要はありませんが、よく、inline指定子を使って最適化を促しているプログラムを見かけると思います。

インライン関数

関数形式マクロを関数テンプレートに置き換えることに加えて、インライン関数にする場合もあります。インライン関数とは、宣言時に inline指定子を付加された関数のことです。

inline void f();

もちろん、関数の本体も記述して、定義にしても構いません。

inline void f() {}

inline指定子は、関数テンプレートの定義にも付加できます。このような関数テンプレートから実体化された関数は、inline指定子が付加された関数になっています。

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

inline指定子は、インライン展開を行うことを要請します。インライン展開とは、関数の本体のコードを、その関数を呼び出している箇所に展開することです。これは、関数形式マクロがしていることと実質的に同じといえます。

インライン展開は多くの場合、コンパイル時に行われますが、リンク時など、ほかのタイミングで行われる可能性もあります。

インライン展開が行われることで、関数呼び出しのコストを避けられます。一方で、呼び出し箇所ごとにコードが重複することになるので、プログラムのサイズは大きくなるかもしれません

インライン関数の本体が小さければ、かえってプログラムサイズが小さくなる可能性はあります。

inline指定子は、インライン展開して欲しいという要請に過ぎず、強制的な指示ではありません。インライン展開することが難しい場面もあるため、要請は無視される可能性があります。インライン展開が行われなかった場合は、通常の関数呼び出しの仕組みが使われます。

ループを含む関数や、再帰呼び出しを行う関数、仮想関数(第27章)では、inline指定子の要請を無視することがあります。

ある呼び出し箇所ではインライン展開され、ほかの呼び出し箇所ではインライン展開されないということも起こり得ます。例えば、インライン関数のメモリアドレスを取得している箇所があると、関数の実体が必要になるため、インライン展開を行わないはずです。しかし、それとは別の箇所でインライン展開可能なら、効率を優先して、インライン展開を行うかもしれません。

反対に、inline指定子を付加していなくとも、通常の最適化の一環として、関数がインライン展開されることもあります

結局、inline指定子を付加してもしなくても、インライン展開は行われるかもしれないし、行われないかもしれないということです。インライン関数を使うことによる多少の弊害もあるため、inline指定子を付けることには慎重であるべきです。

弊害とは例えば、インライン関数の本体のコードをヘッダファイル側に露出しなければならないため、後から実装を変更すると、呼び出し側の再コンパイルを必要とすることなどがあります。大規模な開発では、プログラムのビルド時間も開発効率に大きく影響します。

インライン関数は、これを呼び出そうとするすべてのソースファイルに、それぞれまったく同一内容の定義がなければなりません。そのため、複数のソースファイルから呼び出したいのであれば、インライン関数の定義をヘッダファイルに記述して、各ソースファイルからインクルードさせるようにします。

以下は、inline指定子を付加した関数テンプレートの使用例です。

// main.cpp

#include <iostream>
#include "sub.h"

int main()
{
    std::cout << max(10, 20) << std::endl;
    std::cout << max(10.5, 10.1) << std::endl;
    std::cout << max('a', 'c') << std::endl;
}
// sub.h

#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

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

#endif

実行結果:

20
10.5
c


__cplusplus

__cplusplus は、コンパイラが対応している C++ の規格バージョンを表す整数定数に置換される事前定義マクロです。

(C++03 までの)置換結果は、199711L です。

C++11 (__cplusplus)

C++11 で、__cplusplus の置換結果が 201103L に変わりました。

C++14 (__cplusplus)

C++14 で、__cplusplus の置換結果が、201402L に変わりました。

C言語との連携

__cplusplusマクロは、C言語のソースファイルと C++ のソースファイルが混ざったプログラムを作るときに利用されることがあります。

C言語のソースファイルと、C++ のソースファイルを混在すると、うまくいかないことがあります。例えば、次のサンプルプログラムはリンクエラーになります。

// main.cpp
#include <iostream>
#include "sub.h"

int main()
{
    std::cout << function(100) << std::endl;
}
/* sub.c */
#include "sub.h"

int function(int num)
{
    return num * 2;
}
/* sub.h */

int function(int num);

main.cpp は C++ で書かれています。一方、sub.c と sub.h はC言語のつもりで書かれています。「つもり」というのは、sub.c も sub.h も、C++ とみなしても何ら問題のないソースコードになっているからです。ここでは、拡張子が .c であることで、コンパイラがC言語であるとみなしているものとします。

VisualStudio が拡張子によってプログラミング言語を判断することについて、「資料集 - VisualStudio - 拡張子について」のページで取り上げています。このページでは、拡張子によらず、強制的に言語を指定する方法も取り上げています。

このプログラムを、VisualStudio でビルドすると、次のようなログが出ます。

1>コンパイルしています...
1>sub.c
1>コンパイルしています...
1>main.cpp
1>リンクしています...
1>main.obj : error LNK2019: 未解決の外部シンボル "int __cdecl function(int)" (?function@@YAHH@Z) が関数 _main で参照されました。
1>C:\test.exe : fatal error LNK1120: 外部参照 1 が未解決です。

sub.c 、main.cpp のコンパイルには成功していますが、リンクの過程で失敗しているようです。

ログを良く見ると、"?function@@YAHH@Z" という謎の文字列が含まれています。この文字列は、C++ としてコンパイルされる main.cpp での、function関数の呼び名です。このように C++ では、関数などの名前が、コンパイラによって別の名前に置き換えられることがあります。これは、名前マングリング(名前修飾)と呼ばれています。

つまり、main.cpp は function関数を "?function@@YAHH@Z" という名前で main.obj に出力しています。一方、C言語としてコンパイルされる sub.c では恐らく、"function" を "function" という名前のままで sub.obj に出力します。そのため、リンクの段階で、"?function@@YAHH@Z" の定義を見つけることができずにエラーになります。

C言語では、名前マングリングに関する仕様は特に決められていません。必要がなければ行わないでしょうし、処理系が独自に行うかもしれません。いずれにしても、C言語と C++ とでルールが異なるのなら、この問題が起こります。

なお、名前マングリングでどのような名前になるかは、処理系依存です。

名前マングリング後の名前を知る方法が用意されていることがあります。

名前マングリングが必要な理由はいくつかありますが、例えば、関数オーバーロードの存在が代表的です。オーバーロードされた関数群はいずれも同じ名前なので、コンパイル後は何らかのルールで名前を変えておかないと、リンク時に区別を付けられません。

関数オーバーロード以外にも名前マングリングの必要性はあるため、関数オーバーロードを行っていないからといって、名前マングリングが行われないとは限りません。

例えば、どの名前空間に含まれているか、どのクラス(第11章)のメンバであるか、などが関係する可能性があります。いずれにしても、ルールは処理系依存です。

名前マングリングによって、C言語と C++ の連携がうまくいかないときには、extern "C" という目印を、関数宣言に付けます。こうすると、C言語のルールに従った名前でコンパイルされます("C" はC言語を意味しています)。

/* sub.h */

extern "C" int function(int num);

あるいは、次のようにも書けます。こちらの表記は、複数の名前にまとめて extern "C" の効果を与えられます。あとで説明しますが、こちらの記法を使うことが多いです。

/* sub.h */

extern "C" {

int function(int num);

}

こうしておけば、C++ のコンパイラが main.cpp をコンパイルするとき、"function" という名前をC言語のルールで出力します。これで、sub.obj に出力される名前と一致するはずなので、正しくリンクできます。

しかしまだ問題があります。extern "C" 自体が C++ 独自の機能であるため、C言語としてコンパイルするソースファイルからこの記述が見えていると、コンパイルエラーになってしまいます。

そこで、__cplusplusマクロを利用して、C++ としてコンパイルされるときにだけ、extern "C" が有効になるようにします。

/* sub.h */

#ifdef __cplusplus
extern "C" {
#endif

int function(int num);

#ifdef __cplusplus
}
#endif

このような記述になるため、extern "C" は { } を使った形式で書いた方が便利です。

プログラムの全体像は、次のようになります。

/* main.c */
#include <stdio.h>
#include "sub.h"

int main(void)
{
    printf("%d\n", function(100));
}
// sub.cpp
#include "sub.h"

int function(int num)
{
    return num * 2;
}
/* sub.h */

#ifdef __cplusplus
extern "C" {
#endif

int function(int num);

#ifdef __cplusplus
}
#endif

実行結果:

200


練習問題

問題① extern "C" { } で囲むコードは、ヘッダファイルの数が多くなると毎回記述しなければならず、わりと不便です。#define を利用して、記述量を減らせないでしょうか?

問題② 次のヘッダファイルを、C言語と C++ で共用できるように修正してください。

/* sub.h */
#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

namespace MyLib {

enum Type {
    TYPE_A,
    TYPE_B,
};

struct Data {
    Type  type;
    char  name[16];
};


void print_data(const Data* data);

}

#endif

問題③ 関数テンプレートをC言語からも利用することは可能でしょうか?

問題④ 次の関数形式マクロを、より安全な方法で置き換えてください。

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


解答ページはこちら

参考リンク



更新履歴

'2018/8/22 章の内容を全面的に変更して新規作成。



前の章へ (第9章 関数テンプレート)

次の章へ (第11章 クラス)

C++編のトップページへ

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー