C++17には機能テストのためのCプリプロセッサー機能が追加された。
機能テストというのは、C++の実装(C++コンパイラー)が特定の機能をサポートしているかどうかをコンパイル時に判断できる機能だ。本来、C++17の規格に準拠したC++実装は、C++17の機能をすべてサポートしているべきだ。しかし、残念ながら現実のC++コンパイラーの開発はそのようには行われていない。C++17に対応途中のC++コンパイラーは将来的にはすべての機能を実装することを目標としつつも、現時点では一部の機能しか実装していないという状態になる。
例えば、C++11で追加されたrvalueリファレンスという機能に現実のC++コンパイラーが対応しているかどうかをコンパイル時に判定するコードは以下のようになる。
#ifndef __USE_RVALUE_REFERENCES
#if (__GNUC__ > 4 || __GNUC__ == 4 && __GNUC_MINOR__ >= 3) || \
_MSC_VER >= 1600
#if __EDG_VERSION__ > 0
#define __USE_RVALUE_REFERENCES (__EDG_VERSION__ >= 410)
#else
#define __USE_RVALUE_REFERENCES 1
#endif
#elif __clang__
#define __USE_RVALUE_REFERENCES __has_feature(cxx_rvalue_references)
#else
#define __USE_RVALUE_REFERENCES 0
#endif
#endif
このそびえ立つクソのようなコードは現実に書かれている。このコードはGCCとMSVCとEDGとClangという現実に使われている主要な4つのC++コンパイラーに対応したrvalueリファレンスが実装されているかどうかを判定する機能テストコードだ。
この複雑なプリプロセッサーを解釈した結果、__USE_RVALUE_REFERENCESというプリプロセッサーマクロの値が、もしC++コンパイラーがrvalueリファレンスをサポートしているならば1、そうでなければ0となる。あとは、このプリプロセッサーマクロで#ifガードしたコードを書く。
// 文字列を処理する関数
void process_string( std::string const & str ) ;
#if __USE_RVALUE_REFERENCES == 1
// 文字列をムーブして処理してよい実装の関数
// C++コンパイラーがrvalueリファレンスを実装していない場合はコンパイルされない
void process_string( std::string && str ) ;
#endif
C++17では、上のようなそびえ立つクソのようなコードを書かなくてもすむように、標準の機能テストマクロが用意された。C++実装が特定の機能をサポートしている場合、対応する機能テストマクロが定義される。機能テストマクロの値は、その機能がC++標準に採択された年と月を合わせた6桁の整数で表現される。
例えばrvalueリファレンスの場合、機能テストマクロの名前は__cpp_rvalue_referencesとなっている。rvalueリファレンスは2006年10月に採択されたので、機能テストマクロの値は200610という値になっている。将来rvalueリファレンスの機能が変更された時は機能テストマクロの値も変更される。この値を調べることによって使っているC++コンパイラーはいつの時代のC++標準の機能をサポートしているか調べることもできる。
この機能テストマクロを使うと、上のコードの判定は以下のように書ける。
// 文字列を処理する関数
void process_string( std::string const & str ) ;
#ifdef __cpp_rvalue_references
// 文字列をムーブして処理してよい実装の関数
// C++コンパイラーがrvalueリファレンスを実装していない場合はコンパイルされない
void process_string( std::string && str ) ;
#endif
機能テストマクロの値は通常は気にする必要がない。機能テストマクロが存在するかどうかで機能の有無を確認できるので、通常は#ifdefを使えばよい。
__has_include式は、ヘッダーファイルが存在するかどうかを調べるための機能だ。
__has_include( ヘッダー名 )
__has_include式はヘッダー名が存在する場合1に、存在しない場合0に置換される。
例えば、C++17の標準ライブラリにはファイルシステムが入る。そのヘッダー名は<filesystem>だ。C++コンパイラーがファイルシステムライブラリをサポートしているかどうかを調べるには、以下のように書く。
#if __has_include(<filesystem>)
// ファイルシステムをサポートしている
#include <filesystem>
namespace fs = std::filesystem
#else
// 実験的な実装を使う
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem ;
#endif
C++実装が__has_includeをサポートしているかどうかは、__has_includeの存在をプリプロセッサーマクロのように#ifdefで調べることによって判定できる。
#ifdef __has_include
// __has_includeをサポートしている
#else
// __has_includeをサポートしていない
#endif
__has_include式は#ifと#elifの中でしか使えない。
int main()
{
// エラー
if ( __has_include(<vector>) )
{ }
}
C++実装が特定の属性トークンをサポートしているかどうかをしらべるには、__has_cpp_attribute式が使える。
__has_cpp_attribute( 属性トークン )
__has_cpp_attribute式は、属性トークンが存在する場合は属性トークンが標準規格に採択された年と月を表す数値に、存在しない場合は0に置換される。
// [[nodiscard]]がサポートされている場合は使う
#if __has_cpp_attribute(nodiscard)
[[nodiscard]]
#endif
void * allocate_memory( std::size_t size ) ;
__has_include式と同じく、__has_cpp_attribute式も#ifか#elifの中でしか使えない。#ifdefで__has_cpp_attribute式の存在の有無を判定できる。