私は今、マクロが実際には指し示すものが多すぎるという考えを持っています。少なくとも、私の頭の中でマクロと呼ばれるものはたくさんあります。まず、私の頭の中にあるものを列挙し、それらの間にどのような関連があるかを見てみます。
- プリプロセッサ:単純なテキスト置換で、構文や意味解析は関与しません。
- 構文マクロ:構文レベルでコードを操作し生成し、言語の構文構造を理解します。
- 部分展開器:コードを展開する際に一定の意味解析を行い、コードの正しさを確保します。
- アドホック展開器:高度に柔軟なコード生成システムで、開発者が必要に応じてコード展開ルールをカスタマイズできます。
あなたが見たことがあるものもあれば、見たことがないものもあるかもしれません。私はそれぞれについて話し、彼らの関連性と違いを説明します。
-
文字列置換マクロ(プリプロセッサ):
- このマクロは通常、C 言語の
#define
マクロのように単純なテキスト置換によって実現されます。このマクロはコンパイル前にテキスト置換を行い、構文解析や意味理解は関与しません。彼らの役割は、確かにコンパイル前にソースコードを前処理することです。
- このマクロは通常、C 言語の
-
Common Lisp のマクロ(構文マクロ):
- Common Lisp のマクロは「構文マクロ」であり、コンパイル時に展開され、Lisp コードを操作し生成できます。これらのマクロは単なるテキスト置換ではなく、Lisp の構文構造を理解し、新しい構文構造を生成できます。
-
Scheme のマクロ(ハイジニックマクロ)/ 部分展開器:
- Scheme のマクロ(特にハイジニックマクロ)は、展開時に変数捕獲などの問題を回避し、マクロ展開後のコードがプログラムの意味を意図せずに変更しないことを保証します。彼らはコードを展開する際に一定の意味解析を行い、展開後のコードが意味的に正しいことを確保します。
-
Racket のマクロ / Lean のテンプレート / Template Haskell のテンプレート(アドホック展開器):
- Racket、Lean、Template Haskell では、マクロやテンプレートシステムがより柔軟で強力であり、開発者が必要に応じて複雑なコード生成ルールを定義できます。これらのシステムは、開発者が具体的なニーズに基づいてカスタマイズされたコード生成と展開を行うことを可能にし、高い柔軟性と表現力を持っています。
文法と意味#
私はプリプロセッサ、構文マクロ、および ** 展開器(elaborator)** の違いを詳しく説明し、** 文法(Syntax)と意味(Semantics)** を組み合わせてさらに説明します。
1. プリプロセッサ#
- キーワード:テキスト置換、文法意識なし、意味意識なし。
- 定義:プリプロセッサは、コンパイル前にソースコードに対して単純なテキスト置換を行うツールです。コードの文法構造や意味には関心を持たず、単に機械的に文字列を置換します。
- 文法:プリプロセッサはコードの文法構造を解析しないため、コード内の構文要素(変数、関数、式など)を区別することができません。
- 意味:プリプロセッサはコードの意味に関心を持たず、単にある文字列を別の文字列に置き換えるだけで、置き換え後のコードが合法であるかどうかはコンパイラやインタプリタが後で処理します。
- 例:
- C 言語の
#define
マクロ:ここで#define MAX(a, b) ((a) > (b) ? (a) : (b))
MAX(a, b)
は単純に((a) > (b) ? (a) : (b))
に置き換えられ、プリプロセッサはa
とb
が合法な式であるかどうかをチェックしません。 - プリプロセッサは条件付きコンパイルにも使用できます:
ここでの
#ifdef DEBUG printf("Debug mode\n"); #endif
#ifdef
と#endif
はプリプロセッサ指令であり、条件に基づいて特定のコードを含めるかどうかを決定します。
- C 言語の
2. 構文マクロ#
- キーワード:文法意識、構文木操作、意味意識なし。
- 定義:構文マクロは、コンパイル時に展開されるメカニズムで、コードの構文構造(通常は抽象構文木、AST)を操作します。構文マクロは新しい構文構造を生成できますが、生成されたコードの意味が正しいかどうかには関心を持ちません。
- 文法:構文マクロはコードの文法構造を理解しているため、構文木(AST)を操作できます。たとえば、関数呼び出し、変数宣言などの構文要素を認識し、新しい構文構造を生成できます。
- 意味:構文マクロはコードの意味に関心を持ちません。構文構造を生成することだけを担当し、生成されたコードが合法であるかどうかは後続のコンパイラやインタプリタがチェックします。
- 例:
- Common Lisp のマクロ:
このマクロは
(defmacro unless (condition &body body) `(if (not ,condition) (progn ,@body)))
unless
構造を定義し、コンパイル時にif
式に展開されます。マクロ展開時には構文木を操作しますが、condition
とbody
の意味が正しいかどうかはチェックしません。 - Scheme の
syntax-rules
マクロ:このマクロも構文木を操作しますが、ハイジニックマクロ(hygenic macro)メカニズムを通じて変数捕獲の問題を回避します。(define-syntax unless (syntax-rules () ((_ condition body ...) (if (not condition) (begin body ...)))))
- Common Lisp のマクロ:
3. 展開器(elaborator)#
- キーワード:文法意識、意味意識、型チェック、コード生成。
- 定義:展開器は、構文木を操作するだけでなく、意味解析(型チェック、スコープ解析など)を行う高度なコード展開メカニズムです。展開器は通常、高度な言語機能(パターンマッチング、型クラス、依存型など)をより低レベルのコア言語に展開するために使用されます。
- 文法:展開器はコードの文法構造を理解しているため、構文木を操作できます。
- 意味:展開器はコードの意味に関心を持ちます。コードを展開する際に意味解析を行い、生成されたコードが意味的に正しいことを確保します(たとえば、型が正しい、変数のスコープが正しいなど)。
- 例:
- Lean の展開器:
Lean 言語の展開器は、高度な構文(依存型、パターンマッチングなど)をコア言語に展開する役割を担います。型チェックと意味解析を行い、生成されたコードが合法であることを確保します。ここでのdef add (x y : Nat) : Nat := x + y
add
関数定義は、展開器によってより低レベルのコア言語表現に展開され、型チェックが行われます。 - Idris の展開器:
Idris の展開器は、高度な構文(型クラス、依存型など)をコア言語に展開する役割を担います。たとえば:ここでのadd : Nat -> Nat -> Nat add x y = x + y
add
関数定義は、展開器によってより低レベルの表現に展開され、型チェックが行われます。
- Lean の展開器:
比較まとめ#
特性 | プリプロセッサ | 構文マクロ | 展開器 |
---|---|---|---|
文法意識 | 無 | 有 | 有 |
意味意識 | 無 | 無 | 有 |
操作対象 | テキスト文字列 | 構文木(AST) | 構文木(AST) |
合法コード生成の有無 | 不一定(依存後続コンパイラ) | 不一定(依存後続コンパイラ) | 是(意味チェックを行う) |
典型例 | C 言語の#define | Common Lisp のマクロ | Lean、Idris の展開器 |
では、あなたは何を言っているのか、アドホック展開器
とは何ですか?#
アドホックとは? 古典的な質問です#
- アドホックはラテン語のフレーズで、「特定の目的のために設計された」または「特設の」を意味します。プログラミング言語において、アドホックは通常、汎用的でない、特定の問題に対する解決策を説明するために使用されます。
- アドホック展開器は、特定のニーズに合わせて設計されたコード展開メカニズムと理解できます。それは、一般的な展開器のように厳格なルールや制約がなく、開発者が必要に応じて柔軟にコード生成ロジックを定義できることを意味します。
なぜ Racket のマクロはアドホック展開器なのか?#
Racket のマクロシステムは非常に強力で柔軟であり、開発者が必要に応じて複雑なコード生成ルールを定義できることを許可します。以下は、Racket のマクロがアドホック展開器と呼ばれる理由です:
柔軟性#
- Racket のマクロシステムは、開発者が任意の複雑なコード変換ルールを定義することを許可します。開発者は具体的なニーズに基づいてマクロを設計でき、言語のコア構文の制約を受けません。
- たとえば、Racket のマクロは構文木(AST)を操作し、新しい構文構造を生成し、さらには新しい言語機能(DSL、ドメイン特化言語)を導入することができます。
フェーズ(Phase)と名前空間(Namespace)#
- Racket のマクロシステムは ** フェーズ(phase)** の概念を導入し、マクロが異なるコンパイル段階で実行されることを許可します。各フェーズには独自の名前空間があり、名前の衝突を回避します。
- たとえば、Racket のマクロはコンパイル時に実行され、コードを生成し、そのコードは実行時に使用されます。
- この階層メカニズムにより、マクロはコード生成と展開をより柔軟に処理できます。
- 他の言語のマクロシステムは通常、このような階層や名前空間の概念を持たないため、マクロ機能は相対的に制限されます。
特定の問題に対する解決策#
- Racket のマクロシステムは、開発者が必要に応じて特設のコード生成ルールを定義することを許可します。たとえば:
- 特定のプログラミングパターンを簡素化するためのマクロを定義できます。
- 特定のドメイン特化言語(DSL)を実現するためのマクロを定義できます。
- この特定の問題に対する設計が、まさにアドホックの核心的な特徴です。
一般的な展開器との違い#
- 一般的な展開器(Lean や Idris の展開器など)は通常、厳格なルールや制約(型チェックや意味解析など)を持っています。
- Racket のマクロシステムはより柔軟であり、開発者が必要に応じてコード生成ルールを定義できるため、厳格な意味の制約を受けません。したがって、よりアドホック展開器のようです。
Racket のマクロのフェーズと名前空間#
Racket のマクロシステムは ** フェーズ(phase)と名前空間(namespace)** の概念を導入しており、これによりマクロシステムはより柔軟で強力になります。
フェーズ(Phase)#
- Racket のマクロシステムは多段階コンパイルをサポートしています。各段階には独自の構文環境と名前空間があります。
- たとえば、マクロはコンパイル時に実行され、コードを生成し、そのコードは実行時に使用されます。
- この階層メカニズムは、マクロがコード生成と展開をより柔軟に処理できるようにします。
- 他の言語のマクロシステムは通常、このような階層の概念を持たないため、マクロ機能は相対的に制限されます。
名前空間(Namespace)#
- Racket のマクロシステムは、各フェーズに独立した名前空間を提供します。これにより:
- マクロはコンパイル時に変数を定義して使用でき、実行時の変数と衝突することはありません。
- この名前空間メカニズムにより、マクロはコードをより柔軟に操作でき、意図しない副作用を引き起こすことはありません。
なぜ他の言語のマクロはアドホック展開器ではないのか?#
- Common Lisp のマクロ:Common Lisp のマクロは非常に強力ですが、階層や名前空間の概念がないため、特定の問題に柔軟にカスタマイズするアドホックの特徴には合致しません。
- Scheme のマクロ:Scheme のマクロ(特にハイジニックマクロ)は変数捕獲の問題を回避しますが、機能が相対的に制限されており、Racket のマクロのように柔軟にコード生成ルールを定義することはできません。
では、展開器
とコンパイル時実行
の違いは何ですか?#
この章の
マクロ
は展開器
を指し、構文マクロ
ではありません。
実際、この方向を考えると、あなたは核心に近づいています。今、私たちはマクロ(elaborator)とコンパイル時実行(compile-time execution)の関係を深く探求し、なぜ展開器がコンパイル時実行の協力を必要とするのか、より強力な機能を実現するために必要なのかを見ていきます。
マクロ(展開器)の役割#
- 展開器の主なタスクは、高度な言語機能(パターンマッチング、型クラス、依存型など)をより低レベルのコア言語表現に展開することです。
- 単なる構文変換ではなく、意味解析(型チェック、スコープ解析など)を行い、生成されたコードが意味的に正しいことを確保します。
コンパイル時実行(Compile-time Execution)の役割#
- コンパイル時実行は、コンパイル段階で特定のコードを実行して最終プログラムを生成または最適化することを指します。
- コンパイル時実行のコードは通常、基本形式評価器であり、コンパイル時に定数式を計算したり、マクロを展開したり、コードを最適化したりします。
展開器とコンパイル時実行の関係#
展開器とコンパイル時実行は相互に補完し合い、より強力なコンパイル時機能を実現します。
展開器はコンパイル時実行のサポートを必要とする#
- 展開器は高度な言語機能を展開する際に、通常、いくつかの計算を実行する必要があります。たとえば:
- 依存型システムでは、型チェックがコンパイル時に特定の式を計算する必要がある場合があります。
- パターンマッチングでは、展開時に特定の条件を計算する必要があります。
- これらの計算はコンパイル時実行のサポートが必要であり、そうでなければ展開器はそのタスクを完了できません。
コンパイル時実行は展開器のサポートを必要とする#
- コンパイル時実行は通常、構文木(AST)を操作する必要があり、展開器は高度な言語機能を構文木に展開する役割を担います。
- たとえば、コンパイル時に特定のコードを実行する場合、まず展開器を通じて高度な言語機能をより低レベルの表現に展開し、その後計算を行う必要があります。
具体例#
依存型システム#
- 依存型システム(Idris や Lean など)では、型チェックがコンパイル時に特定の式を計算する必要がある場合があります。
- たとえば:
ここでの型チェックは、コンパイル時に
add : (n : Nat) -> (m : Nat) -> Nat add n m = n + m
n + m
の型を計算する必要があります。展開器はadd
関数をコア言語に展開し、コンパイル時実行は型を計算する役割を担います。
パターンマッチング#
- パターンマッチングでは、展開時に特定の条件を計算する必要があります。
- たとえば:
ここでのパターンマッチングは、コンパイル時に
factorial : Nat -> Nat factorial 0 = 1 factorial n = n * factorial (n - 1)
if-else
構造に展開する必要があります。展開器はパターンマッチングをコア言語に展開し、コンパイル時実行は条件を計算する役割を担います。
Racket のマクロシステム#
- Racket のマクロシステムは、コンパイル時に任意のコードを実行して新しい構文構造を生成することを許可します。
- たとえば:
ここでの
(define-syntax (unless stx) (syntax-case stx () [(_ condition body ...) #'(if (not condition) (begin body ...))]))
unless
マクロは、コンパイル時にif
式に展開されます。展開器はunless
をif
式に展開し、コンパイル時実行はcondition
とbody
を計算する役割を担います。
なぜ展開器はコンパイル時実行がなければ完璧に機能しないのか?#
- 動的計算:展開器は高度な言語機能を展開する際に、通常、動的に特定の式を計算する必要があります。これらの計算はコンパイル時に完了する必要があり、コンパイル時実行のサポートが必要です。
- 最適化:コンパイル時実行は、コンパイル時にコードを最適化することができます。たとえば、定数折りたたみや死コード削除などです。これらの最適化は展開器のサポートが必要であり、高度な言語機能をより低レベルの表現に展開する必要があります。
- 柔軟性:コンパイル時実行は、コンパイル時に任意のコードを実行することを許可し、これにより展開器はコード生成と展開をより柔軟に処理できます。
ちょっと待って、私はまだ少し混乱しています。Zig がなぜマクロを面倒だと感じているのに、コンパイル時実行を導入した後も柔軟に見えるのか、その原因は何ですか?#
私たちはプログラミング言語設計の根本的なメカニズムから出発し、** コンパイル時実行(compile-time execution)** のみに依存し、** 展開器(elaborator)やマクロ(macro)** システムを欠くことによる根本的な欠陥を分析する必要があります。ここでは、2 つの核心的な問題に焦点を当てます:
- 型システムの剛性の束縛:コンパイル時実行は ** 基本形式(base form)** のみを操作でき、言語設計者が予め設定した抽象の境界を突破できません。
- メタプログラミングの次元の崩壊:コンパイル時実行のメタプログラミング能力は値計算のレベルに留まり、言語の構文構造やコンパイルパイプラインに対する深い制御を欠きます。
型システムの剛性の束縛#
根本的な矛盾:コンパイル時実行が ** 基本形式(base form)** のみを操作できる場合、すべてのメタプログラミング行為は言語の原生の型システムと構文構造内に制限され、言語設計者が予め設定した抽象の境界を突破できません。
型推論の不可拡張性#
- Zig の制限:Zig のコンパイル時関数(comptime)は値を計算できますが、新しい型システムルールを生成することはできません。たとえば:
このようなコード生成は静的であり、コンテキストに基づいて動的に型を推論することはできず、型システムの柔軟性が制限されます。
// コンパイル時に新しい型推論ルールを定義できない const MyType = comptime { // 仮にコンテキストに基づいて特定の型を自動推論したい場合 return if (some_condition) u32 else f64; // 明示的に型を返す必要がある };
- 対比展開器:依存型言語(Lean など)では、展開器が動的に型制約を生成できます:
ここでの型推論は動的であり、Zig のコンパイル時実行では実現できません。
def myFunction (x : Nat) := if x > 0 then x + 1 else "Error" -- 型システムは条件分岐に基づいて自動的に`Nat ⊕ String`を推論
構文と意味の強い結合#
- Zig のジレンマ:コンパイル時実行のコードは Zig の文法と型ルールに厳密に従う必要があり、新しい構文糖や意味ルールを定義することはできません。たとえば:
- 埋め込み開発において「パニックのないコードのサブセット」を定義することはできません。
- パフォーマンスが重要なコードの安全チェックを無効にすることはできません。
- 対比展開器:Racket のマクロは新しい制御フローの意味を定義できます:
このマクロは構文レベルで
(define-syntax-rule (?? expr default) (if (not (null? expr)) expr default))
??
演算子の動作を再定義できますが、Zig のコンパイル時実行では同様の機能を実現できません。
メタプログラミングの次元の崩壊#
核心問題:コンパイル時実行のメタプログラミング能力は値計算のレベルに留まり、言語の構文構造やコンパイルパイプラインに対する深い制御を欠きます。
構文木操作の欠如#
- Zig の欠陥:Zig は抽象構文木(AST)を直接操作できず、すべてのコンパイル時生成コードは文字列の結合やテンプレート化されたコード構造を通じて実現されます。たとえば:
この方法は本質的にテキスト置換であり、安全性の脆弱性(注入攻撃など)を引き起こしやすく、静的解析ができません。
// 文字列結合を通じてコードを生成(Cプリプロセッサに似ている) const code = comptime { var buf: [100]u8 = undefined; _ = std.fmt.bufPrint(&buf, "fn foo() {{}}"); return buf[0..]; };
- 対比展開器:Rust のプロセスマクロ(proc macro)は AST を直接操作できます:
このような AST レベルの操作は型安全であり、解析可能ですが、Zig のテキスト置換方式では同様の機能を実現できません。
#[derive(Debug)] // プロセスマクロが自動的に`Debug`トレイトの実装コードを生成 struct Point { x: i32, y: i32 }
コンパイルパイプラインの干渉不可#
- Zig の閉鎖性:Zig のコンパイルパイプラインは固定されており、開発者はカスタムのコンパイル段階(カスタム最適化、コード変換など)を挿入できません。たとえば:
- LLVM の中間表現(IR)のカスタム最適化を実現できません。
- コンパイル時に動的に生成されたコードセクションを注入することはできません。
- 対比 Lisp ファミリー:Lisp のマクロシステムは任意のコンパイル段階でコード変換を挿入できます:
この能力により、Lisp はコンパイルパイプラインを自由に制御できますが、Zig のコンパイル時実行は言語が予め設定したパイプライン段階を突破できません。
(defmacro at-compile-time (&body body) `(eval-when (:compile-toplevel) ,@body))
意味的一貫性の代償#
深層矛盾:コンパイル時実行のみに依存するメタプログラミングは、言語設計者の心的モデルをすべてのメタプログラミング行為に強制することを余儀なくされ、真のドメイン特化抽象を実現できなくなります。
ドメイン特化言語(DSL)の実現不可#
- Zig の制限:構文マクロが欠如しているため、Zig は特定のドメイン(ハードウェア記述、プロトコル定義など)に対して専用の構文を設計できません。たとえば:
- Verilog のようなハードウェア記述構文を実現できません。
- SQL のようなクエリ構文を定義できません。
- 対比展開器:Idris の展開器は意味マクロを通じて埋め込み DSL を実現できます:
ここでの
query : DSL (List Person) query = select [name, age] from people where (age > 30)
select
とwhere
はマクロ生成の DSL 構造ですが、Zig では同様の機能を実現できません。
意味的一貫性の強制結合#
- Zig の代償:すべてのコンパイル時生成コードは Zig の意味ルール(メモリ安全、エラー処理)に従う必要があり、特定のシナリオに対してルールを緩和または強化することはできません。たとえば:
- 埋め込み開発において「パニックのないコードのサブセット」を定義することはできません。
- パフォーマンスが重要なコードの安全チェックを無効にすることはできません。
- 対比 C++ テンプレートメタプログラミング:C++ のテンプレートは複雑ですが、特化や SFINAE を通じて意味レベルのコード生成を実現できます:
この能力により、C++ は型に基づいて動的にシリアル化戦略を選択できますが、Zig のコンパイル時実行では同様の機能を実現できません。
template<typename T> auto serialize(T t) -> decltype(t.toBytes()) { return t.toBytes(); }
欠陥次元 | Zig(コンパイル時実行のみ) | 展開器 / マクロシステム |
---|---|---|
型システムの拡張性 | 言語が予め設定した型ルールに制限される | 動的に型制約や推論ルールを生成できる |
構文構造の制御 | 原生の文法に合ったコードしか生成できない | 新しい構文構造や意味ルールを定義できる |
コンパイルパイプラインの干渉 | パイプライン段階が固定されており、カスタムロジックを挿入できない | コンパイル段階やコード変換を自由に制御できる |
ドメイン特化抽象 | DSL やドメイン特有の意味を実現できない | 埋め込み DSL やドメイン駆動設計をサポート |
メタプログラミングの安全性 | テキスト置換は安全性の脆弱性を引き起こす | AST 操作により文法と型の安全性を保証 |
究極の矛盾:言語自給能力の欠如#
もしある言語がその自身のメカニズムを通じて完全な自給(つまり、その言語を使って自らのコンパイラやツールチェーンを記述すること)を実現できない場合、そのメタプログラミング能力には根本的な欠陥があります。Zig のコンパイル時実行は部分的なコード生成を実現できますが、以下の重要な能力を実現できません:
- コンパイラの自己修正:Zig を使って自らのコンパイルロジックを動的に修正できるコンパイラを書くことはできません。
- ツールチェーンのメタプログラミング:Zig を使って Lisp のような
meta-object protocol
(メタオブジェクトプロトコル)を実現することはできません。 - 言語進化の自主性:言語機能の拡張はコンパイラ作者の修正に依存し、コミュニティ主導のメタプログラミングによるものではありません。
では、なぜ私が見たものはC++
でも書けるのに、見た目が悪く、使うのが怖いのか、その原因は何ですか?#
C++ のテンプレート(template)
、constexpr
、および概念(concept)
システムには、確かにいくつかの設計上の問題や不明瞭な点があります。私たちはテンプレートが展開器としての役割を果たすこと、constexpr
とテンプレートの関係、および概念システムの導入の 3 つの側面から C++ の設計を批判します。
C++ テンプレートが展開器としての役割を果たすこと#
- C++ テンプレートはコンパイル時にコード生成メカニズムであり、開発者が汎用コード(例えば、ジェネリックプログラミング)を書くことを許可します。コンパイル時に具体的なコードインスタンスを生成できます。
- 展開器としての限界:
- 構文が複雑で理解しにくい:
- C++ テンプレートの構文は非常に複雑で、特にテンプレート特化や SFINAE(Substitution Failure Is Not An Error)などの技術が関与する場合、コードの可読性と保守性が大幅に低下します。
- たとえば、テンプレートメタプログラミング(Template Metaprogramming, TMP)はしばしば難解なコードを書く必要があり、これは展開器が明確で使いやすいべきという原則に反します。
- 意味解析が欠如している:
- C++ テンプレートは本質的に構文置換メカニズムであり、深い意味解析を行いません。たとえば、テンプレートは型が特定の意味的制約を満たしているかどうかを直接チェックできません(C++20 以前)。
- これにより、テンプレートエラーのメッセージは通常非常に難解で、デバッグが困難になります。
- コンパイル時計算能力が限られている:
- C++ テンプレートのコンパイル時計算能力はテンプレートメタプログラミングに依存しており、この方法は使用が難しいだけでなく、パフォーマンスのオーバーヘッドも大きいです。
- たとえば、フィボナッチ数列を計算するテンプレートメタプログラミングコード:
このようなコードは書くのが難しいだけでなく、可読性も極めて低いです。
template<int N> struct Fibonacci { static const int value = Fibonacci<N-1>::value + Fibonacci<N-2>::value; }; template<> struct Fibonacci<0> { static const int value = 0; }; template<> struct Fibonacci<1> { static const int value = 1; };
- 構文が複雑で理解しにくい:
constexpr
とテンプレートの関係が不明確#
- **
constexpr
** は C++11 で導入された機能で、コンパイル時に定数式を計算することを許可します。 - 問題:
- テンプレートとの機能の重複:
constexpr
とテンプレートはどちらもコンパイル時計算に使用できますが、それぞれの設計目的や実装方法はまったく異なります。- テンプレートは主にコード生成に使用され、
constexpr
は主に定数計算に使用されます。この機能の重複は、開発者が使用する際に混乱を招きます。
constexpr
の制限:constexpr
関数や変数には厳しい制限があり、例えば動的メモリ割り当てを使用できず、副作用を持つこともできません。これにより、表現能力が制限されます。- たとえば、
constexpr
関数はテンプレート生成の型を直接操作できず、両者の協力がスムーズではありません。
- 統一されたコンパイル時計算モデルの欠如:
- C++ は統一されたコンパイル時計算モデルを提供しておらず、テンプレートと
constexpr
は独立したメカニズムです。このため、言語の複雑性が増し、開発者の学習コストも増加します。
- C++ は統一されたコンパイル時計算モデルを提供しておらず、テンプレートと
- テンプレートとの機能の重複:
C++ 概念システムの導入#
- ** 概念(Concepts)** は C++20 で導入された機能で、テンプレートパラメータを制約し、テンプレートの可読性とエラーメッセージを向上させます。
- 問題:
- 導入のタイミングが遅すぎる:
- 概念システムは C++20 で導入されましたが、テンプレートシステムは C++98 から存在しています。これは、概念が導入される前に C++ 開発者が数年間難解なテンプレートエラーメッセージに耐えなければならなかったことを意味します。
- テンプレートとの統合が自然でない:
- 概念システムの設計は、テンプレートの複雑性の問題を完全には解決していません。たとえば、概念は依然としてテンプレート構文と組み合わせて使用する必要があり、コードは依然として直感的ではありません。
- たとえば:
概念はコードの可読性を向上させますが、構文は依然として複雑です。
template<typename T> requires Integral<T> T add(T a, T b) { return a + b; }
- 概念の表現能力が限られている:
- 概念システムは主に型制約に使用されますが、テンプレートメタプログラミングの機能を完全に代替することはできません。たとえば、概念はコンパイル時計算やコード生成に直接使用できません。
- 導入のタイミングが遅すぎる:
まとめ#
マクロシステムの設計レベルは、言語の「自己進化能力」を直接決定します。真の言語マスター級のツール(Lisp、Racket など)は、マクロを言語そのものの一級市民と見なし、後からの修正のためのパッチメカニズムとは見なさないのです。