C++ プリプロセッサの定義解除

言語には 2 種類しかありません:人々が不平を言う言語と、誰も使用しない言語です — Bjarne Stroustrup

私はその引用が好きです。 JavaScript と Haskell の両方について説明しています。その意味で、プリプロセッサは人々がよく使うという点で優れた言語です。 C や C++ と切り離して考えられることは決してありませんが、もしそうであれば、TIOBE でナンバーワンの言語になります。プリプロセッサは非常に便利であり、普及しています。真実は、本当に ある時点でプリプロセッサを使用せずに、あらゆる種類の本格的で移植可能な C++ アプリケーションを作成するのは困難です。

— プリプロセッサは最悪です — わかりますよね?最悪です。ねえ、私のコミットをマージできますか?便利なマクロをたくさん追加しました。

そういう会話は知っている人も多いと思いますが、気をつけないと20年後も残っているかもしれません。残念ながら、存在することは、プリプロセッサの唯一の償還品質であるためです。悲しいかな、私の問題は理論的でも、哲学的でも、理想主義的でもありません.

プリプロセッサが、何のチェックもなしに誰でも識別子やキーワードを置換できるようにしても (それは実際には違法であると言う人もいますが…)、まったく気にしません。また、コンマを適切に処理できないにもかかわらず、プリプロセッサがチューリング完全であることも気にしません。インクルードとインクルード ガードも気にしませんし、#pragma に関する問題は 1 つもありません。時には現実的にならなければなりません。

ただし。

シナリオを提供させてください。不自然だと思うかもしれませんが、ご容赦ください。クロス プラットフォーム アプリケーションをリファクタリングしていて、たとえば関数の名前を変更するなど、通常とは異なることを行うことにしたとします。

それは可能ではありません。行ったことはありません。おそらくこれからもありません。

#ifdef WINDOWS
 foo(43);
#else
 foo(42);
#endif

基本的に、コンパイラもツール (ツールは必要に応じて本格的なコンパイラ フロント エンドです) も、コードを完全に把握することはできません。無効な部分は、コンパイル、解析、字句解析、またはその他の方法で分析されません。

まず、無効なパスは有効な C++ である必要はありません。これは有効です:

#if 0
#!/bin/bash
 g++ "$0" && ./a.out && rm ./a.out
 exit $?;
#else
#include <iostream>
int main() {
 std::cout << "Hello ?\n";
}
#endif

そのため、コンパイラがプリプロセッサの無効なパスを考慮した場合、有効な AST を取得できない可能性があります。さらに悪いことに、前処理はその名前が示すように、別の状態として発生し、任意の式またはステートメントの途中を含む任意の 2 つの C++ トークンの間に前処理ディレクティブが挿入される可能性があります。

#if 0
 void
#else
 bool
#endif

#if 0
 &
#endif
#if 0
 bar(int
#else
 baz(long,
#endif
#if 0
 , std::vector<
# if 0
 double
# else
 int
# endif
 >)
#else
 double)
#endif
;

同様に懸念されるもう 1 つの問題は、コンパイラが #ifdef ステートメントと #defines ステートメントのどの組み合わせが有効なプログラムを形成することになっているかを認識できないことです。

例として、Qt は、コンパイル時に Qt の特定の機能を有効または無効にするように設定できる一連の定義を提供します。カレンダー ウィジェットが必要ないとします。#QT_NO_CALENDAR_WIDGET を定義すると、より小さなバイナリになります。うまくいきません。 決してないと思います 働きました。ある時点で、Qtにはそのようなコンパイル時の構成オプションが約100ありました。可能なビルド構成の数が変数の数で指数関数的に爆発することを考えると。プログラムに 2¹⁰⁰ のバリエーションがある場合、ビッグウェブ、ディープクラウド、ヘキサ スケールであっても、自動化は困難です。

テストされていないコードは壊れたコードです。

あなたはおそらくその有名な格言を知っているでしょう。では、コンパイルされていないコードはどうでしょうか?

プラットフォーム固有のファイルにプラットフォーム固有のメソッドを配置すると、まったく同じ問題が発生することを指摘する必要があります。基本的に、コンパイラが認識するコードは、自己完結型の 1 つの信頼できる情報源であるべきですが、その代わりに、コードは断片化されており、あなたが持っているコードのビジョンは、せいぜい不完全なものです。

プリプロセッサは有害であると見なされています。それについて何ができますか?

ところで、欠陥があるのはプリプロセッサだけではありません。現代のすべてのプロセッサも同様です。なんらかの処理を行うことは避けるべきではないでしょうか?

とにかく、今日はプリプロセッサ ディレクティブについて何ができるか見てみましょう。

1. #define よりも定数を強く好む

これは非常に単純ですが、マクロを使用して定義された多くの定数をまだ目にします。 define ではなく、常に static const または constexpr を使用してください。ビルド プロセスでバージョン番号や git ハッシュなどの一連の変数を設定する必要がある場合は、定義をビルド パラメーターとして使用するのではなく、ソース ファイルを生成することを検討してください。

2.関数は常にマクロよりも優れています

#ifndef max
#define max(a,b) ((a)>(b)?(a):(b))
#endif
#ifndef min
#define min(a,b) ((a)<(b)?(a):(b))
#endif

上記のスニペットは Win32 API からのものです . 「シンプル」で短いワンライナーの場合でも、常に機能を優先する必要があります。

関数の引数の遅延評価が必要な場合は、ラムダを使用します。皮肉なことに、マクロを使用するソリューションを次に示しますが、それはスタートです ![C++ での関数引数の遅延評価] (http://foonathan.net/blog/2017/06/27/lazy-evaluation.html)

3.移植性の問題を抽象化します。

プラットフォーム固有の問題を個別のファイル、個別のライブラリ、およびメソッドに適切に分離することで、#ifdef の発生を減らす必要があります。 コード内のブロック。上記の問題は解決しませんが、そのプラットフォームで作業していないときに、プラットフォーム固有のシンボルの名前を変更したり、別の方法で変換したりする可能性は低くなります。

4.ソフトウェアのバリエーションの数を制限してください。

その依存関係は本当にオプションであるべきですか?

プラグイン システムの使用を検討しているソフトウェアの一部の機能を有効にするオプションの依存関係がある場合、またはプロジェクトをいくつかに分割する場合は、依存関係が欠落しているときに #ifdef を使用して一部のコード パスを無効にするのではなく、コンポーネントとアプリケーションを無条件にビルドします。その依存関係の有無にかかわらず、ビルドを必ずテストしてください。煩わしさを避けるために、依存関係をオプションにしないことを検討してください

このコードは本当にリリース モードでのみ実行する必要がありますか?

多くの異なるデバッグ/リリース コード パスを持つことは避けてください。コンパイルされていないコードは壊れたコードであることを忘れないでください。

その機能は本当に非アクティブ化できるべきですか?

依存関係以上に、機能はコンパイル時にオプションであってはなりません。ランタイム フラグまたはプラグイン システムを提供します。

5.インクルードよりもプラグマを優先

現在、#pragma once をサポートしていないエキゾチックな C++ コンパイラはほとんどありません。 #pragma once を使用すると、エラーが発生しにくくなり、簡単かつ高速になります。インクルード ガードに別れを告げましょう。

6.より多くのマクロよりもより多くのコードを好む

これはそれぞれの状況に適応するものですが、ほとんどの場合、いくつかの C++ トークンをマクロに置き換える価値はありません。言語の規則の範囲内でプレイしてください。過度に巧妙になろうとせず、多少の繰り返しを許容しようとしないでください。おそらく、同じように読みやすく、保守しやすくなり、IDE に感謝されるでしょう。

7.マクロをサニタイズ

マクロは、できるだけ早く #undef で未定義にする必要があります。ドキュメント化されていないマクロをヘッダー ファイルに含めないでください。

マクロにはスコープがありません。プロジェクトの名前を前に付けた長い大文字の名前を使用してください。

短いマクロ名と長いマクロ名 ( signal と QT_SIGNAL ) の両方を持つ Qt などのサードパーティ フレームワークを使用している場合、特に API の一部としてリークする可能性がある場合は、前者を無効にしてください。そのような短い名前を自分で提供しないでください。マクロ名は、コードの残りの部分から独立している必要があり、boost::signal または std::min と競合してはなりません

8. C++ ステートメントの途中に ifdef ブロックを配置しないでください。

foo( 42,
#if 0
 "42",
#endif
 42.0
);

上記のコードにはいくつかの問題があります。読みにくく、保守が難しく、clang-format などのツールで問題が発生します。そして、たまたま壊れています。

代わりに、2 つの異なるステートメントを記述してください:

#if 0
 foo(42, "42", 42.0);
#else
 foo(42, 42.0);
#endif

それが難しい場合もあるかもしれませんが、それはおそらく、コードをより多くの関数に分割するか、条件付きでコンパイルしているものをより適切に抽象化する必要があることを示しています.

9. #error よりも static_assert を優先

ビルドを失敗させるには、単に static_assert(false) を使用してください。

未来、過去のプリプロセッサ

前のアドバイスはどの C++ バージョンにも当てはまりますが、最新のコンパイラにアクセスできれば、毎日のマクロの摂取量を減らすのに役立つ方法が増えています。

1.インクルードよりもモジュールを優先

モジュールはコンパイル時間を改善する必要がありますが、マクロが漏洩できないバリアも提供します。 2018 年の初めには、その機能を備えた製品版のコンパイラはありませんが、GCC、MSVC、および clang はそれを実装しているか、実装中です。

全体的に経験が不足していますが、モジュールによってツールが簡単になり、不足しているシンボルに対応するモジュールを自動的に含める、不要なモジュールをクリーニングするなどの機能が有効になることを期待するのは理にかなっています…

2.可能な限り #ifdef よりも if constexpr を使用してください

無効化されたコード パスが整形式である (不明なシンボルを参照しない) 場合、無効化されたコード パスは依然として AST の一部であり、コンパイラとツール (使用している静的アナライザーとリファクタリング プログラム。

3.ポストモダンの世界でも #ifdef に頼る必要があるかもしれないので、ポストモダンの使用を検討してください。

それらは当面の問題の解決にはまったく役立ちませんが、コンパイラが提供する一連の標準機能を検出するために、一連のマクロが標準化されています。必要に応じて使用してください。私のアドバイスは、ターゲットとするすべてのコンパイラが提供する機能に固執することです。ベースラインとスティックを選択します。 C++98 でアプリケーションを作成するよりも、最新のコンパイラをターゲット システムにバックポートする方が簡単かもしれないと考えてください。

4. LINE ではなく std::source_location を使用してください とファイル

誰もが独自のロガーを作成するのが好きです。そして、std::source_location を使用して、マクロをほとんどまたはまったく使用せずにそれを行うことができます .

マクロのないアプリケーションへの長い道のり

いくつかの機能は、いくつかのマクロの使用法に代わるより良い方法を提供しますが、現実的には、遅かれ早かれプリプロセッサに頼る必要があります。しかし幸いなことに、私たちにできることはまだたくさんあります。

1. -D をコンパイラ定義の変数に置き換えます

define の最も頻繁な使用例の 1 つは、ビルド環境を照会することです。デバッグ/リリース、ターゲット アーキテクチャ、オペレーティング システム、最適化…

これらのビルド環境変数の一部を公開するために、std::compiler を通じて一連の定数を公開することを想像できます。

if constexpr(std::compiler.is_debug_build()) { }

同様に、何らかの extern コンパイラ constexpr 変数がソース コードで宣言されているが、コンパイラによって定義または上書きされることを想像できます。 constexpr x =SOME_DEFINE; よりも実際の利点があるだけです。これらの変数が保持できる値を制限する方法があるかどうか.

多分そのようなもの

enum class OS {
 Linux,
 Windows,
 MacOsX
};

[[compilation_variable(OS::Linux, OS::Windows, OS::MacOsX)]] extern constexpr int os;

私の希望は、さまざまな構成変数が何であるか、さらには変数のどの組み合わせが有効であるかについて、コンパイラーにより多くの情報を提供することで、ソース コードのより良いモデリング (したがって、ツールと静的分析) につながることです。

2.その他の属性

C++ の属性は優れているので、もっと多くの属性を用意する必要があります。 [[可視性]] は、始めるのに最適な場所です。インポートからエクスポートに切り替える引数として constexpr 変数を取ることができます。

3. Rust の本からの引用

Rust コミュニティは、Rust 言語のメリットを熱心に宣伝する機会を逃すことはありません。実際、Rust は多くのことを非常にうまく行っています。コンパイル時の構成もその 1 つです。

// The function is only included in the build when compiling for macOS
#[cfg(target_os = "macos")]
fn macos_only() {
 // ...
}

// This function is only included when either foo or bar is defined
#[cfg(any(foo, bar))]
fn needs_foo_or_bar() {
 // ...
}

属性システムを使用して条件付きでコンパイル単位にシンボルを含めることは、実に興味深いアイデアです。

まず、非常に読みやすく、自己文書化されています。第二に、シンボルがビルドに含まれていない場合でも、解析を試みることができます。さらに重要なことに、唯一の宣言はエンティティに関する十分な情報をコンパイラに提供し、強力なツール、静的分析、およびリファクタリングを可能にします。

次のコードを検討してください:

[[static_if(std::compiler.arch() == "arm")]]
void f() {}


void foo() {
 if constexpr(std::compiler.arch() == "arm") {
 f();
 }
}

それは驚くべき特性を持っています:それはよく形成されています.コンパイラは f が有効なエンティティであり、関数名であることを認識しているため、破棄された if constexpr ステートメントの本体を明確に解析できます。

同じ構文をあらゆる種類の C++ 宣言に適用でき、コンパイラはそれを理解できます。

[[static_if(std::compiler.arch() == "arm")]]
int x = /*...*/

ここで、コンパイラは左側のみを解析できます。残りは静的解析やツールには必要ないためです。

[[static_if(std::compiler.is_debugbuild())]]
class X {
};

静的分析の目的では、クラス名とそのパブリック メンバーのみをインデックス化する必要があります。

もちろん、破棄された宣言をアクティブなコード パスから参照するのは不適切ですが、コンパイラはそれが決してないことを確認できます。 有効な構成で発生します。確かに、計算が無料というわけではありませんが、すべて あなたのコードは整形式です。 Linux マシンでコードを書いたために Windows ビルドを壊すことは、はるかに難しくなります。

しかし、それは簡単なことではありません。破棄されたエンティティの本体に、現在のコンパイラが知らない構文が含まれている場合はどうなりますか?ベンダの拡張機能か、新しい C++ の機能でしょうか。解析がベスト エフォート ベースで行われるのは合理的だと思います。解析に失敗した場合、コンパイラは現在のステートメントをスキップして、ソースの理解できない部分について警告することができます。 「110 行目と 130 行目の間で Foo の名前を変更できませんでした」は、「Foo のいくつかのインスタンスの名前を変更しました。すべてではないかもしれませんが、プロジェクト全体を手作業でざっと目を通してみてください。コンパイラは気にせず、grep を使用してください。

4.すべてのものを constexpr します。

constexpr std::chrono::system_clock::now() が必要かもしれません __TIME__ を置き換える

コンパイル時の乱数ジェネレーターも必要になる場合があります。なぜだめですか ?とにかく、再現可能なビルドを誰が気にしますか?

5.リフレクションでコードとシンボルを生成

メタクラスの提案は、スライスされたパン、モジュール、および概念以来最高のものです。特に P0712 は多くの点で素晴​​らしい論文です。

導入された多くの構造の 1 つは、文字列と数字の任意のシーケンスから識別子を作成する declname キーワードです

int declname("foo", 42) = 0; 変数 foo42 を作成します .新しい識別子を形成するための文字列の連結がマクロの最も頻繁な使用例の 1 つであることを考えると、これは非常に興味深いことです。コンパイラが、この方法で作成された (または参照された) シンボルに関する十分な情報を持っていることを願っています。

悪名高い X マクロも、今後数年間で過去のものになるはずです。

6.マクロをなくすには、新しい種類のマクロが必要です

マクロは単なるテキスト置換であるため、引数は遅延評価されます。ラムダを使用してその動作をエミュレートすることはできますが、かなり面倒です。では、関数内の遅延評価の恩恵を受けることができるでしょうか?

これは私が昨年考えたトピックですC++ でのコード インジェクションとリフレクションに関する研究

私のアイデアは、コード インジェクションによって提供される機能を使用して、新しい種類の「マクロ」を作成することです。これを「構文マクロ」と呼んでいますが、これ以上の名前はありません。基本的に、コード フラグメント (プログラムの特定のポイントに挿入できるコードの一部) に名前を付けて、多数のパラメーターを受け取ることができるようにすると、マクロが作成されます。ただし、(プリプロセッサが提供するトークン ソースではなく) 構文レベルでチェックされるマクロ。

constexpr {
 bool debug = /*...*/;
 log->(std::meta::expression<const char*> c, std::meta::expression<>... args) {
 if(debug) {
 -> {
 printf(->c, ->(args)...);
 };
 }
 }
}

void foo() {
 //expand to printf("Hello World") only and only if debug is true
 log->("Hello %", "World");
}

わかりました、ここで何が起こっているのですか。

最初に constexpr { } で constexpr ブロックを作成します .これは、メタ クラスの提案の一部です。 constexpr ブロックは、すべての変数が constexpr であり、副作用がない複合ステートメントです。そのブロックの唯一の目的は、コンパイル時に注入フラグメントを作成し、ブロックが宣言されているエンティティのプロパティを変更することです。 ( メタクラス constexpr の上にシンタックス シュガーがあります 実際にはメタクラスは必要ないと私は主張します.)

constexpr ブロック内で、マクロ ログを定義します。マクロは関数ではないことに注意してください。それらはコードに展開され、何も返さず、スタックにも存在しません。 log は、修飾できる識別子であり、同じスコープ内の他のエンティティの名前にすることはできません。構文マクロは、他のすべての識別子と同じルックアップ ルールに従います。

彼らは -> を使用します インジェクションオペレーター。 -> 現在の用途と競合することなく、コード インジェクションに関連するすべての操作を説明するために使用できます。この場合 log 以降 は、コード インジェクションの形式である構文マクロです。マクロを log->(){....} で定義します。 .

構文マクロの本体自体は、constexpr コンテキストで評価できる任意の C++ 式を含むことができる constexpr ブロックです。

0、1 つ、または複数の インジェクション ステートメントを含めることができます -> {} で示されます .インジェクション ステートメントはコード フラグメントを作成し、呼び出しポイント (構文マクロの場合はマクロが展開される場所) に直ちに挿入します。

マクロは、式または 0 個以上のステートメントを挿入できます。式を挿入するマクロは、式が期待される場所で相互にのみ展開できます。

型はありませんが、コンパイラによって決定される性質があります。

関数に渡すことができる任意の引数を構文マクロに渡すことができます。引数は展開前に評価され、強く型付けされます。

ただし、式にリフレクションを渡すこともできます。それは、任意の式の反射を取得できることを前提としています。式 e のリフレクションには、decltype(e) に​​対応する型があります。

実装に関しては、上記の例では上記 std::meta::expression<char*> タイプが char* である式のリフレクションに一致する概念です .

マクロを評価する際の最後の魔法は、式が展開前に暗黙的に反映に変換されることです。

基本的なレベルでは、AST ノードを移動しています。これは、リフレクションとコード インジェクションに関する現在のアプローチと一致しています。

最後に、 print(->c, ->(args)...) を注入すると -> に注意してください トークン。これにより、リフレクションが元の式に変換され、評価できるようになります。

呼び出しサイトから、log->("Hello %", "World"); -> 以外は、通常の void 関数呼び出しのように見えます。 マクロ展開の存在を示します。

最後に、評価の前に引数として識別子を渡す機能により、新しいキーワードの必要性が軽減される場合があります:

std::reflexpr->(x) __std_reflexpr_intrasics(x) まで拡張できます x より前 が評価されます。

S-Macro はプリプロセッサ マクロを完全に置き換えますか?

彼らはそうしませんが、そうするつもりはありません。特に、有効な C++ である必要があり、複数のポイント (定義時、展開前、展開中、展開後) でチェックされるため、トークン スープを積極的に禁止します。これらは有効な C++ であり、有効な C++ を挿入し、有効な C++ をパラメーターとして使用します。

つまり、部分的なステートメントを挿入したり、部分的なステートメントを操作したり、任意のステートメントをパラメーターとして受け取ったりすることはできません。

それらは、遅延評価と条件付き実行の問題を解決します。たとえば、for(;;) 以降、foreach を実装することはできません。 は完全なステートメントではありません ( for(;;);for(;;){} ですが、あまり役に立ちません)。

名前の検索に関する多くの質問があります。マクロは、展開されたコンテキストを「見る」必要がありますか?引数はマクロの内部を意識する必要がありますか?それは宣言コンテキストです。

制限は良いことだと思います。本当に新しい構成を発明する必要がある場合は、言語が不足している可能性があります。その場合は、提案を書いてください。または、コードジェネレーターが必要かもしれません。または、より抽象化するか、より実際のコードにします。

これは現実の世界ですか?

それは非常にファンタジーであり、絶対にそうではありません 現在の提案の一部ですが、これはコード インジェクション機能の論理的な進化だと思います。

これはマクロを錆びさせるのと少し似ています — 引数として任意のステートメントを許可しないことを除いて — (願わくば) 別の文法を持つ別の言語ではなく、C++ の一部のように感じます。

プリプロセッサは確かに致命的です。しかし、依存を減らすためにできることはたくさんあります。また、より優れた代替手段を提供することでマクロの有用性をますます低下させるために、C++ コミュニティができることはたくさんあります。

何十年もかかるかもしれませんが、それだけの価値があるでしょう。マクロが根本的に悪いからではなく、ツーリングは言語が判断される言語であり、生死が悪いためです。

そして、より優れたツールが切実に必要とされているため、プリプロセッサへの致命的な依存を減らすためにできる限りのことを行う必要があります.

#undef