Skip to content

Latest commit

 

History

History
878 lines (594 loc) · 23.8 KB

200-cpp.md

File metadata and controls

878 lines (594 loc) · 23.8 KB

Cプリプロセッサー

CプリプロセッサーはC++がC言語から受け継いだ機能だ。CプリプロセッサーはソースコードをC++としてパースする前に、テキストをトークン単位で変形する処理のことだ。この処理はソースファイルをC++としてパースする前処理として行われる。CプリプロセッサーはC++ではなく別言語として認識すべきで、そもそもプログラミング言語ではなくマクロ言語だ。

C++ではCプリプロセッサーが広く使われており、今後もしばらくは使われるだろう。読者がC++で書かれた既存のコードを読むとき、Cプリプロセッサーは避けて通れない。Cプリプロセッサーはいずれ廃止したい機能ではあるが、C++はいまだに廃止できていない。

Cプリプロセッサーはプリプロセッシングディレクティブ(preprocessing directive)を認識し、トークン列を処理する。ディレクティブはソースファイルの文頭に文字#から始まり、改行文字で終わる。#とディレクティブの間に空白文字を入れてもよい。

#define NOSPACE
#    define SPACE

#includeディレクティブ

#includeは指定したファイルの内容をその場に挿入する。本質的にはコピペだ。C++では#includeはライブラリを利用するのに使われる。

#includeは以下のいずれかの文法を持つ。

#include <ヘッダーファイルパス> 改行文字
#include "ヘッダーファイルパス" 改行文字

#includeは指定したファイルパスのファイルの内容をその場所に挿入する。このファイルをヘッダーファイルという。<>によるファイルパスは、標準ライブラリやシステムのヘッダーファイルを格納したディレクトリーからヘッダーファイルを探す。""によるファイルパスは、システム以外のディレクトリーからもヘッダーファイルを探す。例えばカレントディレクトリーなどだ。

例えば、以下のようなヘッダーファイルfoo.hがあり、

// foo.h
foo foo foo

以下のようなソースファイルbar.cppがある場合、

// bar.cpp

#include "foo.h"

// end bar.cpp

bar.cppをCプリプロセッサーにかけると、以下のようなソースファイルが出力される。

// bar.cpp

// foo.h
foo foo foo

// end bar.h

このソースファイルはC++のソースファイルとしてはエラーとなるが、Cプリプロセッサーは単純にトークン列で分割したテキストファイルとしてソースファイルを処理するため、Cプリプロセッサーとしてはエラーにはならない。

冒頭で述べたように、#includeの本質はコンパイラーによるコピペである。あるテキストファイルの内容をその場に挿入するコピペ機能を提供する。

#includeは、ほかの言語でモジュール、importなどと呼ばれている機能を簡易的に提供する。C++の標準ライブラリを使うには、<iostream><string><vector>のようなヘッダーファイルを#includeする必要がある。

// iostreamライブラリを使う
#include <iostream>
// stringライブラリを使う
#include <string>

int main()
{
    // <string>のライブラリ
    std::string s("hello") ;
    // iostreamのライブラリ
    std::cout << s ;
}

すでに述べたように#includeはファイルの内容をその場に挿入するだけであり、ほかの言語にあるモジュールのための高級な機能ではない。本書を執筆時点で規格策定中のC++20では、より高級なモジュール機能を追加する予定がある。

同じヘッダーファイルを複数回#includeすると、当然複数回挿入される。

以下のようなval.hを、

// val.h
inline int val ;

以下のように複数回#includeすると、

#include "val.h"
#include "val.h"

以下のように置換される。

// val.h
inline int val ;
// val.h
inline int val ;

これはvalの定義が重複しているためエラーとなる。

しかし、ヘッダーファイルを一度しか#includeしないようにするのは困難だ。なぜならば、ヘッダーファイルはほかのヘッダーファイルから間接的に#includeされることもあるからだ。

// lib_f.h

#include "val.h"

int f() ;
// lib_g.h

#include "val.h"

int g() ;
// main.cpp

#include "lib_f.h"
#include "lib_g.h"

int main()
{
    int result = f() + g() ;
}

このmain.cppをCプリプロセッサーにかけると以下のように置換される。

// main.cpp

// lib_f.h

// val.h
inline int val ;

int f() ;

// lib_g.h

// val.h
inline int val ;

int g() ;


int main()
{
    int result = f() + g() ;
}

これはvalの定義が重複しているためエラーとなる。

この問題に対処するためには、複数回#includeされると困るヘッダーファイルでは、インクルードガード(include guard)と呼ばれている方法を使う。

// val.h

#ifndef INCLUDE_GUARD_HEADER_VAL_H
#define INCLUDE_GUARD_HEADER_VAL_H

inline int val ;

#endif

このように記述したval.hを複数回#includeしても、最初のifndefのみがコンパイル対象になるため、問題は起こらない。

インクルードガードは以下の様式を持つ。

#ifndef 十分にユニークなマクロ名
#define 十分にユニークなマクロ名 

// 重複してコンパイルされたくないコードをここに書く

#endif

十分にユニークなマクロ名は全ソースファイル中で衝突しないそのヘッダーに固有のマクロ名を使う。慣習的に推奨される方法としてはすべて大文字を使い、十分に長いマクロ名にすることだ。

#define

#defineはマクロ置換を行う。マクロにはオブジェクト風マクロ(object-like macro)と関数風マクロ(function-like macro)がある。風というのは、マクロはオブジェクトでも関数でもないからだ。ただ、文法上オブジェクトや関数の似ているだけで、実態はトークン列の愚直な置換だ。

オブジェクト風マクロ

オブジェクト風マクロの文法は以下のとおり。

#define マクロ名 置換リスト 改行文字

#define以降の行では、マクロ名が置換リストに置き換わる。

#define ONE             1
#define ONE_PLUS_ONE    ONE + ONE
#define GNU GNU's is NOT UNIX

ONE
ONE_PLUS_ONE

これをプリプロセスすると以下のソースコードになる。

1
1 + 1

マクロ名ONE1に置換される。

マクロ名ONE_PLUS_ONEONE + ONEに置換される。置換された結果に別のマクロ名があれば、そのマクロ名も置換される。

あるマクロ名を置換した結果、そのマクロ名が現れても再帰的に置換されることはない。

#define GNU GNU's NOT UNIX!

GNU

これは以下のように置換される。

GNU's NOT UNIX!

マクロ名GNUを展開するとトークン`GNU'が現れるが、これは置換されたマクロ名と同じなので、再帰的に置換されることはない。

関数風マクロ

関数風マクロの文法は以下のとおり。

#define マクロ名( 識別子リスト ) 置換リスト 改行文字

関数風マクロはあたかも関数のように記述できる。関数風マクロに実引数として渡したトークン列は、置換リスト内で仮引数としての識別子で参照できる。

#define NO_ARGUMENT()           No argument
#define ONE_ARGUMENT( ARG )     begin ARG end
#define MAKE_IT_DOUBLE( ARG )   ONE_ARGUMENT( ARG ARG )

NO_ARGUMENT()
ONE_ARGUMENT( foo bar )
MAKE_IT_DOUBLE( foo bar )

これは以下のように置換される。

No argument
begin foo bar end
begin foo bar foo bar end

複数の引数を取るマクロへの実引数は、カンマで区切られたトークン列を渡す。

#define TWO( A, B ) A B
#define THREE( A, B, C ) C B A

TWO( 1 2, 3 4 )
THREE( 1, 2, 3 )

これは以下のように置換される。

1 2 3 4
3 2 1

ただし、括弧で囲まれたトークン列の中にあるカンマは、マクロの実引数の区切りとはみなされない。

#define MACRO( A ) A

MACRO( (a,b) )

これは以下のように置換される。

(a,b)

__VA_ARGS__(可変長引数マクロ)

#defineの識別子リストを...だけにしたマクロは、可変長引数マクロになる。このときマクロの実引数のトークン列は、置換リストの中で__VA_ARGS__として参照できる。

#define MACRO(...) __VA_ARGS__

MACRO( You can write , and ,, or even ,,,, )

これは以下のように置換される。

You can write , and ,, or even ,,,,

カンマも含めてすべてのトークン列がそのまま__VA_ARGS__で参照できる。

可変長引数マクロの識別子リストに仮引数と...を書いたマクロの置換リストでは、仮引数の数だけの実引数は仮引数で参照され、残りが__VA_ARGS__で参照される。

#define MACRO( X, Y, Z, ... ) X Y Z and __VA_ARGS__

MACRO( 1,2,3,4,5,6 )

これは以下のように置換される

1 2 3 and 4,5,6

X, Y, Zにそれぞれ1, 2, 3が入り、__VA_ARGS__には4, 5, 6が入る。

__VA_OPT__

__VA_OPT__は可変長引数マクロで__VA_ARGS__にトークン列が渡されたかどうかで置換結果を変えることができる。

__VA_OPT__は可変引数マクロの置換リストでのみ使える。__VA_OPT__(content)__VA_ARGS__にトークンがない場合はトークンなしに置換され、トークンがある場合はトークン列contentに置換される。

#define MACRO( X, ... ) f( X __VA_OPT__(,) __VA_ARGS__ )

MACRO(1)
MACRO(1,2)

これは以下のように置換される。

f( 1 )
f( 1, 2 )

MACRO(1)X1になり、__VA_ARGS__にはトークンがないので、__VA_OPT__(,)は空に置換される。結果としてf(1)となる。

MACRO(1,2)は、X1になり、__VA_ARGS__にはトークン2が入るので、__VA_OPT__(,),に置換される。結果としてf(1,2)となる。

__VA_OPT____VA_ARGS__に実引数となるトークン列がなければ空に置換されるので、このようにトークン列の有無によってカンマなどの文法上必須のトークン列の有無を切り替えたい場合に使うことができる。

#演算子

#はマクロ実引数を文字列リテラルにする。

#は関数風マクロの置換リストの中のみで使うことができる。#は関数風マクロの仮引数の識別子の直前に書くことができる。#が直前に書かれた識別子は、マクロ実引数のトークン列の文字列リテラルになる。

#define STRING( X ) # X

STRING( hello )
STRING( hello world )

これは以下のように置換される。

"hello"
"hello world"

また、可変長マクロと組み合わせた場合、

#define STRING( ... ) # __VA_ARGS__

STRING()
STRING( hello,world )

以下のように置換される。

""
"hello,world"

##演算子

##はマクロ実引数の結合を行う。

##は関数風マクロの置換リストの中にしか書けない。##は両端にマクロの仮引数の識別子を書かなければならない。##は両端の識別子の参照するマクロ実引数のトークン列を結合した置換を行う。

#define CONCAT( A, B ) A ## B

CONCAT( foo, bar )
CONCAT( aaa bbb, ccc ddd)

これは以下のように置換される。

foobar
aaa bbbccc ddd

結合した結果のトークンはさらにマクロ置換の対象となる。

#define CONCAT( A, B ) A ## B
#define FOOBAR hello

CONCAT( FOO, BAR )

これは以下のように置換される。

hello

CONCAT(FOO,BAR)FOOBARに置換され、FOOBARという名前のマクロ名があるためにさらにhelloに置換される。

複数行の置換リスト

#defineディレクティブの置換リストは複数行に渡って書くことができない。これは文法上の制約によるものだ。#defineディレクティブは改行文字で終端される。

しかし、関数やクラスを生成するような複雑なマクロは、複数行に分けて書きたい。

#define LIST_NAME2( PREFIX, TYPE ) PREFIX ## TYPE
#define LIST_NAME( TYPE ) LIST_NAME2( list_, TYPE )

#define DEFINE_LIST( TYPE ) struct LIST_NAME(TYPE){TYPE value ;LIST_NAME(TYPE) * prev ;LIST_NAME(TYPE) * next ;} ; 

DEFINE_LIST(int)
DEFINE_LIST(double)

この場合、行末にバックスラッシュに続けて改行を書くと、バックスラッシュと改行がプリプロセッサーによって削除される。

上の例は以下のように、プリプロセッサーとしては比較的わかりやすく書くことができる。

#define LIST_NAME2( PREFIX, TYPE ) PREFIX ## TYPE
#define LIST_NAME( TYPE ) LIST_NAME2( list_, TYPE )

#define DEFINE_LIST( TYPE )\
struct LIST_NAME(TYPE)\
{\
    TYPE value ;\
    LIST_NAME(TYPE) * prev ;\
    LIST_NAME(TYPE) * next ;\
} ; 

DEFINE_LIST(int)
DEFINE_LIST(double)

C++ではテンプレートがあるために、このようなマクロを書く必要はない。

#undefディレクティブ

#undefはそれ以前に定義されたマクロを削除する。

#define FOO BAR
FOO
#undef FOO
FOO

これは以下のように置換される。

BAR
FOO

条件付きソースファイル選択

#if, #elif, #else, #endif, #ifdef, #ifndefは条件付きのソースファイルの選択(conditional inclusion)を行う。これは条件付きコンパイルに近い機能を提供する。

プリプロセッサーの定数式

プリプロセッサーで使える条件式は、C++の条件式と比べてだいぶ制限がある。基本的には整数定数式で、true, falseが使えるほか、123, 1+1, 1 == 1, 1 < 1のような式も使える。ただし、識別子はすべてマクロ名として置換できるものは置換され、置換できない識別子は、true, false以外はキーワードも含めてすべて0に置換される。

したがって、プリプロセッサーで以下のように書くと、

#if UNDEFINED
#endif

以下のように書いたものと同じになる。

#if 0
#endif

プリプロセッサーであるので、C++としてのconstexpr変数やconstexpr関数も使えない。

constexpr int x = 1 ;

#if x
hello
#endif

これは以下のように置換される。

constexpr int x = 1 ;

プリプロセッサーはC++の文法と意味を理解しない。単にトークン列として処理する。

以下の例はエラーになる。

constexpr int f() { return 1 ; }

#if f()
#endif

なぜならば、0()は整数定数式として合法なコードではないからだ。何度も言うように、プリプロセッサーはC++の文法と意味を理解しない。

プリプロセッサーの定数式では、特殊なマクロ風の式を使うことができる。defined__has_includeだ。

definedは以下の文法を持つ。

defined 識別子
defined ( 識別子 )

definedは識別子がそれ以前の行で#defineでマクロとして定義されていて#undefで取り消されていない場合1になり、それ以外の場合0になる。

// #if 0
#if defined MACRO
#endif

#define MACRO

// #if 1
#if defined MACRO
#endif

#undef MACRO

// #if 0
#if defined MACRO
#endif

__has_includeは以下の文法を持つ。

__has_include ( < ヘッダーファイル名 > )
__has_include ( " ヘッダーファイル名 " )
__has_include ( 文字列リテラル )
__has_include ( < マクロ > )

1番目と2番目は、指定されたヘッダーファイル名がシステムに存在する場合1に、そうでない場合0になる。

// <filesystem>の存在を確認してから#includeする
#if __has_include(<filesystem>)
#   include <filesystem>
#endif

// "mylibrary.h"の存在を確認してから#includeする
#if __has_include("mylibrary.h")
#   include "mylibrary.h"
#endif

3番目と4番目は、1番目と2番目が適用できない場合に初めて考慮される。その場合、まず通常通りにプリプロセッサーのマクロ置換が行われる。

#define STDIO "stdio.h"

#if __has_include( STDIO )
#endif

#define STDLIB stdlib.h

#if __has_include( <STDLIB> )
#endif

#ifディレクティブ

#ifディレクティブは以下の文法を持つ。

#if 定数式 改行文字

#endif

もし定数式がゼロの場合、#if#endifで囲まれたトークン列は処理されない。定数式が非ゼロの場合、処理される。

#if 0
This line will be skipped.
#endif

#if 1
This line will be processed.
#endif

これをプリプロセスすると以下のようになる。

This line will be processed.

#if 0は処理されないので、#endifまでのトークン列は消える。

#elifディレクティブ

#elifディレクティブは、C++でいうelse ifに相当する。

#elif 定数式 改行文字

#elifディレクティブは#ifディレクティブと#endifディレクティブの間に複数書くことができる。#elifのある#ifが処理される場合、#ifから#elifの間のトークン列が処理される、#ifが処理されない場合、#elif#ifと同じように定数式を評価して処理されるかどうかが判断される。#elifが処理される場合、処理されるトークン列は次の#elifもしくは#endifまでの間のトークン列になる。

以下の例は、すべてYESのトークンがある行のみ処理される。

#if 1
YES
#elif 1
NO
#endif

#if 0
NO
#elif 1
YES
#endif

#if 0
NO
#elif 1
YES
#elif 1
NO
#endif

#if 0
NO
#elif 0
NO
#elif 1
YES
#endif

プリプロセスした結果は以下のとおり。

YES
YES
YES
YES

#elseディレクティブ

#elseディレクティブはC++でいうelseに相当する。

#elseディレクティブは#ifディレクティブと#endifディレクティブの間に書くことができる。もし#if#elifディレクティブが処理されない場合で#elseディレクティブがある場合、#elseから#endifまでのトークン列が処理される。

以下の例は、YESのトークンがある行のみ処理される。

#if 1
YES
#else
NO
#endif

#if 0
NO
#else
YES
#endif

#if 0
NO
#elif 1
YES
#else
NO
#endif

#ifdef, #ifndefディレクティブ

#ifdef 識別子
#ifndef 識別子

は、それぞれ以下と同じ意味になる。

#if defined 識別子
#if !defined 識別子

例、

#ifdef MACRO
#endif

// 上と同じ
#if defined MACRO
#endif


#ifndef MACRO
#endif

// 上と同じ
#if !defined MACRO
#endif

#lineディレクティブ

#lineディレクティブはディレクティブの次の行の行番号と、ソースファイル名を変更する。これは人間が使うのではなく、ツールによって生成されることを想定した機能だ。

以下の文法の#lineディレクティブは、#lineディレクティブの次の行の行番号をあたかも数値で指定した行番号であるかのように振る舞わせる。

#line 数値 改行文字

数値として0もしくは2147483647より大きい数を指定した場合の挙動は未定義となる。

以下の例はコンパイルエラーになるが、コンパイルエラーメッセージはあたかも102行目に問題があるかのように表示される。

// 1行目
// 2行目
#line 100 // 3行目
// 100行目
// 101行目
ill-formed line // 102行目

以下の例は999を出力するコードだ。

#include <iostream>
int main()
{
#line 999
    std::cout << __LINE__ ;
}

以下の文法の#lineディレクティブは、次の行の行番号を数値にした上で、ソースファイル名をソースファイル名にする。

#line 数値 "ソースファイル名" 改行文字

例、

#line 42 "answer.cpp"

以下の文法の#lineディレクティブは、プリプロセッサートークン列をプリプロセスし、上の2つの文法のいずれかに合致させる。

#line プリプロセッサートークン列 改行文字

例、

#define LINE_NUMBER 123
#line LINE_NUMBER

#errorディレクティブ

#errorディレクティブはコンパイルエラーを引き起こす。

#error 改行文字
#error トークン列 改行文字

#errorによるコンパイラーのエラーメッセージには#errorのトークン列を含む。

#errorの利用例としては、#ifと組み合わせるものがある。以下の例はCHAR_BITが8でなければコンパイルエラーになるソースファイルだ。

#include <climits>

#if CHAR_BIT != 8
#error CHAR_BIT != 8 implementation is not supported.
#endif

#ifが処理されなければ、その中にある#errorも処理されないので、コンパイルエラーにはならない。

#pragma

#pragmaディレクティブは実装依存の処理を行う。#pragmaはコンパイラー独自の拡張機能を追加する文法として使われている。

文法は以下のとおり。

#pragma プリプロセッサートークン列 改行文字

C++では属性が追加されたために、#pragmaを使う必要はほとんどなくなっている。

Nullディレクティブ

Nullディレクティブとは何もしないプリプロセッサーディレクティブだ。

# 改行文字

つまり、単に#とだけ書いた行はエラーにはならない。

定義済みマクロ名

いくつかのマクロ名がプリプロセッサーによってあらかじめ定義されている。


マクロ名 値 意味


__cplusplus 201703L C++17時点での値
将来の規格で増やされる

__DATE__ "Mmm dd yyyy" ソースファイルがプリプロセスされた日付 Mmmは月、ddは日、yyyyは年
月の文字列はasctimeが生成するものと同じ
日が1桁の場合、ddの最初の文字は空白文字

__FILE__ 文字列リテラル ソースファイルの名前の文字列リテラル

__LINE__ 整数リテラル ソースファイルの現在の行番号

__STDC_HOSTED__ 整数リテラル ホスト実装の場合1
フリースタンディング実装の場合0

__STDCPP_DEFAULT_NEW_ALIGNMENT__ 整数リテラル アライメント