2017 年 - C++ でプリプロセッサはまだ必要ですか?

C++、ええと C のプリプロセッサは素晴らしいです。

いいえ、それは素晴らしいことではありません。

これは、C++ を使用するために使用しなければならない原始的なテキスト置換ツールです。しかし、「しなければならない」というのは本当でしょうか?新しいより優れた C++ 言語機能のおかげで、ほとんどの使用法は時代遅れになりました。モジュールなどの多くの機能が間もなく登場します™ .では、プリプロセッサを取り除くことはできますか?もしそうなら、どうすればそれを行うことができますか?

プリプロセッサの使用の多くは、すでに悪い習慣です:記号定数に使用しないでください、インライン関数などに使用しないでください.

しかし、慣用的な C++ で使用される方法がまだいくつかあります。それらを見て、どのような代替手段があるか見てみましょう.

ヘッダー ファイル インクルージョン

最も一般的な使用法から始めましょう:03 ヘッダー ファイル。

なぜプリプロセッサが必要なのですか?

ソースファイルをコンパイルするために、コンパイラは呼び出されているすべての関数の宣言を確認する必要があります。そのため、あるファイルで関数を定義し、別のファイルでそれを呼び出したい場合は、そのファイルで次のように宣言する必要があります。そうして初めて、コンパイラは関数を呼び出すための適切なコードを生成できます。

もちろん、宣言を手動でコピーするとエラーが発生する可能性があります。署名を変更すると、すべての宣言も変更する必要があります。そのため、宣言を手動でコピーする代わりに、それらを特別なファイル (ヘッダー ファイル) に書き込み、プリプロセッサは 19 でコピーします .ここで、すべての宣言を更新する必要がありますが、1 か所だけです。

しかし、プレーンテキストのインクルードはばかげています.同じファイルが2回インクルードされ、そのファイルの2つのコピーが作成されることがあります.これは関数宣言には問題ありませんが、ヘッダーファイルにクラス定義がある場合はエラーになります. .

それを防ぐには、インクルードガードまたは非標準の 23 を使用する必要があります .

どうすれば交換できますか?

現在の C++ の機能では、(パスタをコピーする手段を使わずに) できません。

しかし、モジュール TS を使用すると、ヘッダー ファイルとソース ファイルを提供する代わりに、モジュールと 30 を記述できます。

モジュールについて詳しく知りたい場合は、最新の CppChat を強くお勧めします。

条件付きコンパイル

プリプロセッサの 2 番目に一般的なジョブは、条件付きコンパイルです。マクロを定義するか定義しないかによって、定義/宣言を変更します。

プリプロセッサが必要な理由

関数 41 を提供するライブラリを作成している状況を考えてみましょう 画面上に 1 つの三角形を描画します。

宣言は簡単です:

// draws a single triangle
void draw_triangle();

ただし、機能の実装は、オペレーティング システム、ウィンドウ マネージャー、ディスプレイ マネージャー、ムーンフェイズ (エキゾチック ウィンドウ マネージャーの場合) によって異なります。

したがって、次のようなものが必要です:

// use this one for Windows
void draw_triangle()
{
 // create window using the WinAPI 
 // draw triangle using DirectX
}

// use this one for Linux
void draw_triangle()
{
 // create window using X11
 // draw triangle using OpenGL
}

プリプロセッサはそこで役立ちます:

#if _WIN32
 // Windows triangle drawing code here 
#else
 // Linux triangle drawing code here
#endif

取られていないブランチのコードはコンパイル前に削除されるため、API の欠落などのエラーは発生しません。

どうすれば交換できますか?

C++17 は 56 を追加します 、これは単純な 63 を置き換えるために使用できます :

これの代わりに:

void do_sth()
{
 #if DEBUG_MODE
 log();
 #endif
 …
}

これを書くことができます:

void do_sth()
{
 if constexpr (DEBUG_MODE)
 {
 log();
 }

 …
}

73 の場合 82 です の場合、ブランチは適切にコンパイルされず、まだインスタンス化されていないテンプレートに対して行われるチェックと同様に、構文エラーのみがチェックされます。

これは 95 よりも優れています すべてのマクロの組み合わせをチェックしなくても、コード内の明らかなエラーを検出できるためです。105 のもう 1 つの利点 それは 119 です 通常の 123 にできるようになりました マクロ展開から得られる定数ではなく、変数。

もちろん、138 には欠点もあります。 :プリプロセッサ ディレクティブ、つまり 149 を制約するために使用することはできません .158 の場合 たとえば、コードには適切なシステム ヘッダーを含める必要があります。161 そのため、真の条件付きコンパイルが必要になるか、宣言を手動でコピーする必要があります。

また、システム ヘッダーにはインポートできるモジュールが定義されていないため、モジュールも役に立ちません。さらに、モジュールを条件付きでインポートすることはできません (私の知る限り)。

設定オプションを渡す

関連して、いくつかの構成オプションをライブラリに渡したい場合があります。アサーション、前提条件チェックを有効または無効にしたり、デフォルトの動作を変更したりしたい場合があります…

たとえば、次のようなヘッダーが含まれる場合があります:

#ifndef USE_ASSERTIONS
 // default to enable
 #define USE_ASSERTIONS 1
#endif

#ifndef DEFAULT_FOO_IMPLEMENTATION
 // use the general implementation
 #define DEFAULT_FOO_IMPLEMENTATION general_foo
#endif

…

ライブラリをビルドするときに、コンパイラを呼び出すとき、または CMake などを介してマクロをオーバーライドできます。

どうすれば交換できますか?

ここではマクロが当然の選択ですが、別の方法があります:

選択した動作を定義するクラス テンプレートにポリシーを渡すポリシー ベースの設計など、別の戦略を使用してオプションを渡すこともできます。これには、すべてのユーザーに単一の実装を強制するのではなく、コースには独自の欠点があります。

しかし、私が本当に見たいのは、171 のときにこれらの構成オプションを渡す機能です。 モジュール:

import my.module(use_assertions = false);
…

これは次のものの理想的な代替品です:

#define USE_ASSERTIONS 0
#include "my_library.hpp"

しかし、モジュールが提供する利点を犠牲にすることなく、技術的に実現可能だとは思いません。モジュールをプリコンパイルします。

アサーション マクロ

最も一般的に使用するマクロは、おそらく何らかのアサーションを行います。ここではマクロを選択するのが当然です:

  • アサーションを条件付きで無効にして削除し、リリース時のオーバーヘッドがゼロになるようにする必要があります。
  • マクロがある場合は、定義済みの 184 を使用できます 、 198208 アサーションの場所を取得し、それを診断に使用します。
  • マクロがある場合は、チェックされている式を文字列化して、診断にも使用できます。

そのため、ほとんどすべてのアサーションがマクロです。

どうすれば交換できますか?

条件付きコンパイルを置き換える方法と、条件付きコンパイルを有効にするかどうかを指定する方法については既に調べたので、問題ありません。

210 を追加すると、Library Fundamentals TS v2 でもファイル情報を取得できます。 :

void my_assertion(bool expr, std::experimental::source_location loc = std::experimental::source_location::current())
{
 if (!expr)
 report_error(loc.file_name, loc.line, loc.function_name);
}

関数 222 書いた時点でのソースファイルの情報に展開します。さらに、デフォルトの引数として使用すると、呼び出し元の場所に展開されます。したがって、2 番目のポイントも問題ありません。

3 番目のポイントは重要です。マクロを使用しないと、式を文字列化して診断に出力することはできません。問題がなければ、今日アサーション関数を実装できます。

しかし、それ以外の場合は、そのためにマクロが必要です。231 でレベルを制御できる (ほとんど) マクロのないアサーション関数を実装する方法については、このブログ投稿を確認してください。 マクロの代わりに変数を使用します。完全な実装はここにあります。

互換性マクロ

すべてのコンパイラがすべての C++ 機能をサポートしているわけではないため、特に、テストのためにコンパイラにアクセスできず、「行を変更し、CI にプッシュし、CI ビルドを待ち、別の一部のコンパイラが重要な C++ 機能を本当に好まないという理由だけで!

とにかく、通常の互換性の問題はマクロで解決できます。機能を実装すると、実装によって特定のマクロが定義されるため、チェックが簡単になります。

#if __cpp_noexcept
 #define NOEXCEPT noexcept
 #define NOEXCEPT_COND(Cond) noexcept(Cond)
 #define NOEXCEPT_OP(Expr) noexcept(Expr)
#else
 #define NOEXCEPT
 #define NOEXCEPT_COND(Cond)
 #define NOEXCEPT_OP(Expr) false
#endif

…

void func() NOEXCEPT
{
 …
}

これにより、すべてのコンパイラに機能が備わっているわけではありませんが、移植可能な機能を使用できます。

どうすれば交換できますか?

他の方法ではこれを行うことはできません.不足している機能を回避するには、サポートされていない機能を取り除くために何らかの前処理ツールが必要です.ここではマクロを使用する必要があります.

ボイラープレート マクロ

C++ のテンプレートと TMP は、他の方法で作成する必要のある定型コードの多くを排除するのに大いに役立ちます。ただし、同じでも完全ではない多くのコードを作成する必要がある場合もあります。 同じ:

struct less
{
 bool operator()(const foo& a, const foo& b)
 {
 return a.bar < b.bar;
 }
};

struct greater
{
 bool operator()(const foo& a, const foo& b)
 {
 return a.bar > b.bar;
 }
};

…

マクロはそのボイラープレートを生成できます:

#define MAKE_COMP(Name, Op) \
struct Name \
{ \
 bool operator()(const foo& a, const foo& b) \
 { \
 return a.bar Op b.bar; \
 } \
};

MAKE_COMP(less, <)
MAKE_COMP(greater, >)
MAKE_COMP(less_equal, <=)
MAKE_COMP(greater_equal, >=)

#undef MAKE_COMP

これにより、繰り返しコードを大幅に節約できます。

または、醜い SFINAE コードを回避する必要がある場合を考えてみましょう:

#define REQUIRES(Trait) \
 typename std::enable_if<Trait::value, int>::type = 0

template <typename T, REQUIRES(std::is_integral<T>)>
void foo() {}

または、 247 を生成する必要があります 250 の実装 、X マクロを使用した簡単なタスクです:

// in enum_members.hpp
X(foo)
X(bar)
X(baz)

// in header.hpp
enum class my_enum
{
 // expand enum names as-is
 #define X(x) x,
 #include "enum_members.hpp"
 #undef X
};

const char* to_string(my_enum e)
{
 switch (e)
 {
 // generate case
 #define X(x) \
 case my_enum::x: \
 return #x;
 #include "enum_members.hpp"
 #undef X
 };
};

多くのコードを読みやすく、操作しやすくするだけです。コピペは必要なく、凝ったツールも必要なく、ユーザーにとって本当の「危険」はありません。

どうすれば交換できますか?

これらすべてを 1 つの言語機能で置き換えることはできません。最初の機能については、オーバーロードされた関数 (演算子など) をテンプレートに渡す方法が必要です。次に、それをテンプレート パラメーターとして渡し、単純にエイリアスすることができます。 2 つ目については概念が必要です。3 つ目については考察が必要です。

そのため、ボイラープレート コードを手動で記述する以外に、このようなボイラープレート マクロを取り除く方法はありません。

結論

現在の C++(17) では、プリプロセッサの使用のほとんどを簡単に置き換えることはできません。

モジュール TS では、最も一般的な使用法 - 263 を置き換えることができます。 、しかし、特にプラットフォームとコンパイラの互換性を確保するために、プリプロセッサが必要になる場合があります。

そしてそれでも:適切だと思います コンパイラの一部であり、AST 生成用の非常に強力なツールであるマクロは、あると便利です。たとえば、Herb Sutter のメタクラスのようなものです。 /コード> .