30
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++コンパイル時コード生成で(Rustみたいに)先を見通す型推論 〜コンパイルは2回〜

30
Last updated at Posted at 2025-12-04

要約

おふざけ記事です。
これができます。

#include <print>
#include <vector>
#include "hyper_auto.hpp"

int main() {
    AUTO x;  // この時点で x の型はわからない
    x = 42;  // ここで int だとわかる
    std::println("{}", x);

    std::vector<AUTO> vec;  // この時点で vec の型はわからない
    vec.push_back(3.14);    // ここで std::vector<double> だとわかる
    std::println("{}", vec);
}
標準出力
42
[3.14]

はじめに

Rustの型推論、便利ですよね?
(これはC++ Advent Calendar 2025の5日目の記事です。)

Rustは、変数宣言で型が確定しなくても、後にどう変数が使われたかまで見て型を推論します。
例えば、次のコードのようなことができます。

fn main() {
    let x;      // この時点で x の型はわからない
    x = 42i32;  // ここで i32 だとわかる
    println!("{}", x);

    let mut vec = vec![];  // この時点で vec の型はわからない
    vec.push(3.14f64);     // ここで Vec<f64> だとわかる
    println!("{:?}", vec);
}

C++ではそうはいきません。
変数宣言の時点で型やテンプレート引数を推論できる必要があります。
例えば、次のコードはエラーになります。

int main() {
    auto x;  // error: x の型がわからない
    x = 42;
    std::vector vec;  // error: vec の型がわからない( std::vector のテンプレート引数がわからない)
    vec.push_back(3.14);
}

困りました。
これでは、C++よりRustのほうが優れていると思われかねません。

ならば、C++の黒魔術で、変数の使われ方まで考慮する型推論を作るまでです!

環境

C++26が必要です。
後述しますが、P2741を使うからです。

コンパイラはgccとclangを考えます。

提案手法

コンパイルは2回!

Rustのような型推論をC++で実現するために、同じソースファイルを2回コンパイルします。

1回目のコンパイルでは変数の型がわからないのでエラーになりますが、その変数が使われた時の型の情報を残しておき、2回目はその型情報を使ってコンパイルする、というような流れです。

何をしてたら、こんなことしようだなんて思うんでしょうか??
正解は「LaTeXを書いてたら」です。コンパイルは2回するものです。

具体的なコードで提案手法による型推論の流れを説明していきます。
まず、型推論したい箇所に AUTO マクロを置きます。

動かしたいコード
int main() {
    AUTO x;
    x = 42;
    std::vector<AUTO> vec;
    vec.push_back(3.14);
}

1回目のコンパイルでは、 AUTO は型推論用の仮の型 unknown<ID> になります。
ID には AUTO が展開される事に一意な整数値が振られます。
unknown<ID> のコンストラクタ、代入演算子は任意の型の引数を取るテンプレートになっていて、それらがインスタンス化される時にテンプレート引数を推論された型として報告します。

コンパイル1回目の AUTO の展開イメージ
int main() {
    unknown<0> x;
    x = 42;  // unknown<0>::operator=(int&&) 「 0 番目の AUTO は int に置換してね」
    std::vector<unknown<1>> vec;
    vec.push_back(3.14);  // unknown<1>::unknown(double&&) 「 1 番目の AUTO は double に置換してね」
}

2回目のコンパイルでは、 AUTO は1回目のコンパイルで推論された型になります。
これで型推論は完了です。

コンパイル2回目の AUTO の展開イメージ
int main() {
    int x;
    x = 42;
    std::vector<double> vec;
    vec.push_back(3.14);
}

実際の実装では、 AUTO は直接推論された型に展開されず、型エイリアスに展開されます。

1回目と2回目で AUTO の指す型を切り替えるために、クラステンプレートの特殊化を利用します。
クラステンプレート hyper_auto<ID> を定義し、 AUTOtypename hyper_auto<ID>::type に展開されるよう定義します。
プライマリテンプレートでは、 hyper_auto<ID>::typeunknown<ID> のエイリアスです。

1回目のコンパイルで hyper_auto<ID> の特殊化が定義されることにより、2回目で AUTO の指す型が切り替わります。
この特殊化をどうやって定義するかは次の節で説明します。

提案手法の実装は、ヘッダファイル hyper_auto.hpp にまとめます。
以下は実際の実装のイメージです。

hyper_auto.hpp
template <std::uintmax_t ID>
struct unknown { /* … */ };

template <std::uintmax_t ID>
struct hyper_auto {
    using type = unknown<ID>;
};

#define AUTO typename hyper_auto<__COUNTER__>::type  // ※ __COUNTER__ についてはコードの下で説明

// ====================
// 1回目のコンパイルで特殊化のコードを生成する
template <> struct hyper_auto<0> { using type = int; };
template <> struct hyper_auto<1> { using type = double; };
// ====================
main.cpp
#include <vector>
#include "hyper_auto.hpp"

int main() {
    AUTO x;
    x = 42;
    std::vector<AUTO> vec;
    vec.push_back(3.14);
}


__COUNTER__ は展開されるたび増加する整数値に展開される、非標準のマクロです。
「お前も標準にならないか?」

参考: https://yohhoy.hatenadiary.jp/entry/20121211/p1

コンパイル時コード生成

1回目のコンパイルでは、推論した型を設定する特殊化のコードを生成します。
このプロセスについて説明します。

まず、コンパイル時処理でコードを文字列として作成します。
C++20以降は std::string がコンパイル時に使えるので、文字列操作は簡単にできます。

作成したコードはエラーメッセージに埋め込みます。
コード以外の部分がコメントアウトされるように、コードを */ … /* で挟んで出力します。

エラーメッセージはリダイレクトでファイルに書き出しておき、2回目のコンパイルでインクルードされるようにします。
typefile.inc というファイル名で書き出すことにします。

以下は実装の断片と書き出されるエラーのイメージです。

hyper_auto.hpp
// …

template <std::uintmax_t ID>
struct unknown {
    template <class T>
    static constexpr void infer() {
        // コード生成+出力
        static_assert(false,
            "*/ template <> struct " + type_name_of<hyper_auto<ID>>() + " { using type = " + type_name_of<T>() + "; }; /*");
        // 出力の仕組みと type_name_of については後述
    }

    // コンストラクタ、代入演算子で infer<T>() がインスタンス化されることでコードが生成される
};

// …

#include "typefile.inc"
// 1回目は空のファイル、
// 2回目は生成されたコードが書き込まれたファイルをインクルード
main.cpp
#include <vector>
#include "hyper_auto.hpp"

int main() {
    AUTO x;
    x = 42;
    std::vector<AUTO> vec;
    vec.push_back(3.14);
}
コンパイルコマンド(コンパイルは2回)
g++ -std=c++26 main.cpp -DHPA_DUMPTYPES (……後述する引数……) 2> typefile.inc  # エラーを書き出し
g++ -std=c++26 main.cpp
typefile.inc
/*
…
In file included from main.cpp:2:
hyper_auto.hpp: In instantiation of 'static constexpr void unknown<ID>::infer() [with T = int; long unsigned int ID = 0]':
required from 'constexpr unknown<ID>& unknown<ID>::operator=(T&&) [with T = int; long unsigned int ID = 0]'
hyper_auto.hpp:52:31:   
   52 |         infer<std::decay_t<T>>();
      |         ~~~~~~~~~~~~~~~~~~~~~~^~
required from here
main.cpp:6:9:   
    6 |     x = 42;
      |         ^~
hyper_auto.hpp:37:23: error: static assertion failed: */ template <> struct hyper_auto<0> { using type = int; }; /*
   37 |         static_assert(false,
      |                       ^~~~~
  • 'false' evaluates to false
hyper_auto.hpp: In instantiation of 'static constexpr void unknown<ID>::infer() [with T = double; long unsigned int ID = 1]':
required from 'constexpr unknown<ID>::unknown(T&&) [with T = double; long unsigned int ID = 1]'
hyper_auto.hpp:46:31:   
   46 |         infer<std::decay_t<T>>();
      |         ~~~~~~~~~~~~~~~~~~~~~~^~
required from here
main.cpp:8:19:   
    8 |     vec.push_back(3.14);
      |                   ^~~~
hyper_auto.hpp:37:23: error: static assertion failed: */ template <> struct hyper_auto<1> { using type = double; }; /*
   37 |         static_assert(false,
      |                       ^~~~~
  • 'false' evaluates to false
…
*/

公式コンパイル時出力

コード生成をするために、コンパイル時に処理した任意の文字列を出力できる仕組みが必要です。
このような仕組みを「コンパイル時出力」といいます(私が勝手に言ってるだけです)。

私はコンパイル時出力を無理やり作って遊んでいました。(過去の記事)
C++26ではコンパイル時出力が公式サポート(?)され、変なことをしなくてもコンパイル時処理した文字列を出力できるようになりました。

static_assert のメッセージには文字列リテラルしか使えませんでした。
C++26で、文字列リテラルに加えて文字列オブジェクトを指定することができるようになります。

static_assert のメッセージに指定する文字列オブジェクト msg は、次の条件を満たす必要があります。

  • msg.size() から std::size_t への暗黙変換がコンパイル時にできて、表示する文字数を示す
  • msg.data() から const char* への暗黙変換がコンパイル時にできて、表示する文字列の先頭を指す

std::string はこれらの条件を満たします。
つまり、 std::string をそのまま出力できます。

これにより、以下のようなコードが動きます。

#include <string>

constexpr std::string hello(int n) {
    std::string msg = "\n---\n";
    for (int i = 0; i < n; ++i) {
        msg += "Hello, Compile Time Output!\n";
    }
    msg += "---";
    return msg;
}

static_assert(false, hello(3));
標準エラー出力
main.cpp:12:15: error: static assertion failed: 
---
Hello, Compile Time Output!
Hello, Compile Time Output!
Hello, Compile Time Output!
---
   12 | static_assert(false, hello(3));
      |               ^~~~~

型を表す文字列の取得

using type = の後に繋げる、型を表す文字列をコンパイル時に取得する必要があります。

今回は、 std::source_location::function_name() で取得できる文字列に含まれるテンプレート引数の情報から、必要な型の文字列を切り取って使います。
いにしえより伝わる __PRETTY_FUNCTION__ を使う方法の、現代版ですね(?)。

function_name() で取得できる文字列は処理系依存なので、切り取る処理は処理系ごとに書く必要があります。

以下のようなコードが動きます。

#include <string>
#include <string_view>
#include <source_location>

template <class T>
constexpr std::string type_name_of() {
    using namespace std::string_view_literals;
    std::string_view s = std::source_location::current().function_name();
#if defined(__clang__)
    s.remove_prefix("std::string type_name_of() [T = "sv.size());
    s.remove_suffix("]"sv.size());
#elif defined(__GNUC__)
    s.remove_prefix("constexpr std::string type_name_of() [with T = "sv.size());
    s.remove_suffix("; std::string = std::__cxx11::basic_string<char>]"sv.size());
#else
    #error This platform is not supported.
#endif
    return std::string{ s };
}


int x;
std::string y;

static_assert(false,
    "\n---"
    "\nx is " + type_name_of<decltype(x)>() +
    "\ny is " + type_name_of<decltype(y)>() +
    "\n---");
標準エラー出力
main.cpp:25:15: error: static assertion failed: 
---
x is int
y is std::__cxx11::basic_string<char>
---
   25 | static_assert(false,
      |               ^~~~~

コード以外をコメントアウト

エラーメッセージをそのままインクルードするために、読み込みたいコードの部分以外をコメントアウトします。
ここは美しい方法が見つからなかったので、少し無理やりなところがあります。

基本は static_assert のメッセージを */ … /* で囲んで出力することで、囲まれた部分以外をコメントアウトします。
この方法の課題は、エラーメッセージ全体の最初に /* 、最後に */ を配置する必要があることです。

キャレットの非表示

エラーメッセージに付属するキャレット(コードの断片)は、最後に表示されるものがどうしてもコメントアウトできないので、コンパイラのコマンドラインオプションで非表示にします。

  • gcc: -fno-diagnostics-show-caret
  • clang: -fno-caret-diagnostics

最初の /*

ライブラリをヘッダファイルで用意してインクルードする場合、エラーメッセージの先頭に In file included from main.cpp:3: のように表示されるので、先頭に /* を置くことはできません。
インクルードせず main.cpp に全部書く場合は #line 1 "/*" とかすればエラーの先頭に /* を置けます。

仕方がないので、この最初に表示されるメッセージの先頭の In をコメント開始のマクロにします。
説明が難しいのでコードで説明します。

① エラーメッセージを書き出すとき、ライブラリのヘッダファイル hyper_auto.hpp で )/* を static_assert します。

hyper_auto.hpp
#ifdef HPA_DUMPTYPES
    static_assert(false, ")/*");
#endif

② エラーメッセージを書き出した typefile.inc の冒頭は次のようになります。

typefile.inc の冒頭
In file included from main.cpp:3:
hyper_auto.hpp:118:19: error: static assertion failed: )/*

③ 引数を単に無視する関数型マクロ HPA_IGNORE を定義し、 InHPA_IGNORE( に置換されるように定義してから、 typefile.inc をインクルードします。

#define HPA_IGNORE(...)
#define In HPA_IGNORE(
#include "typefile.inc"

④ typefile.inc が次のように展開されます。

HPA_IGNORE( file included from main.cpp:3:
hyper_auto.hpp:118:19: error: static assertion failed: )/*

HPA_IGNORE(...) の中身は無視されるので、実質先頭に /* を置くことができました。

最後の */

仕方がないので、 */ を static_assert するだけのファイルを、一緒にコンパイルします。
キャレットを非表示にしたので、この */ が最後になります。

hpa_end.cpp
#ifdef HPA_DUMPTYPES
    static_assert(false, "*/");
#endif
1回目のコンパイルコマンド
g++ -std=c++26 main.cpp -DHPA_DUMPTYPES -fno-diagnostics-show-caret hpa_end.cpp 2> typefile.inc
typefile.inc の末尾

hpa_end.cpp:2:19: error: static assertion failed: */

完成品

Wandbox でお試しください。

GitHubでも公開しています。

main.cpp
#include <print>
#include <vector>
#include "hyper_auto.hpp"

int main() {
    AUTO x;  // この時点で x の型はわからない
    x = 42;  // ここで int だとわかる
    std::println("{}", x);

    std::vector<AUTO> vec;  // この時点で vec の型はわからない
    vec.push_back(3.14);    // ここで std::vector<double> だとわかる
    std::println("{}", vec);
}
シェル
g++ -std=c++26 main.cpp -DHPA_DUMPTYPES -fno-diagnostics-show-caret hpa_end.cpp 2> typefile.inc
g++ -std=c++26 main.cpp
./a.out
標準出力
42
[3.14]

課題

ポインタが絡むと動きません。
ほかにもいろいろ動かないケースがあります。
実用には耐えません。

#include "hyper_auto.hpp"

int main() {
    int x;
    AUTO* p;
    p = &x;
}

おわりに

それっぽいことはできましたが、2回コンパイルするなんて面倒くさくて嫌です。
型推論はC++よりRustの方が便利なのかもしれません。
それでも私はC++が好きです。

30
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?