C++ TMP (テンプレートメタプログラミング) シリーズ
| Part1 constexpr | Part2 Concepts | Part3 Variadic | Part4 型リスト | Part5 正規表現 |
|---|---|---|---|---|
| ✅ | ✅ | 👈 Now |
はじめに
C++11で導入されたバリアディックテンプレート(可変長テンプレート)。
これのおかげで:
std::tuplestd::variantstd::make_unique- 型安全な
printf
などが実現できるようになった。
今回は、この強力な機能を深掘りする。
基本構文
// ... が「パラメータパック」を表す
template<typename... Types> // 型パラメータパック
void func(Types... args); // 関数パラメータパック
// 展開例
func(1, 2.0, "hello");
// コンパイラが展開:
// func<int, double, const char*>(int, double, const char*)
パラメータパックの展開
基本パターン
template<typename... Args>
void print(Args... args) {
// パックを展開
((std::cout << args << " "), ...); // 折りたたみ式(C++17)
std::cout << "\n";
}
print(1, 2.0, "hello"); // 1 2 hello
展開の場所
template<typename... Args>
void examples(Args... args) {
// 1. 関数呼び出し
other_func(args...);
// 2. 初期化リスト
int arr[] = {args...};
// 3. テンプレート引数
std::tuple<Args...> t;
// 4. sizeof...
constexpr size_t n = sizeof...(Args); // パックの要素数
}
C++17 折りたたみ式
パックの全要素に演算子を適用する。
4つの形式
| 形式 | 展開結果 |
|---|---|
(E op ...) |
E1 op (E2 op (E3 op ...)) |
(... op E) |
((E1 op E2) op E3) op ... |
(E op ... op init) |
E1 op (E2 op (... op init)) |
(init op ... op E) |
((init op E1) op E2) op ... |
// 合計
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 右折りたたみ
}
// 初期値付き
template<typename... Args>
auto sum_init(Args... args) {
return (args + ... + 0); // 初期値0
}
// 論理AND
template<typename... Args>
bool all_true(Args... args) {
return (args && ...);
}
// 論理OR
template<typename... Args>
bool any_true(Args... args) {
return (args || ...);
}
カンマ演算子による副作用
template<typename... Args>
void print_all(Args... args) {
((std::cout << args << "\n"), ...);
}
// 展開:
// (std::cout << arg1 << "\n"),
// ((std::cout << arg2 << "\n"),
// ((std::cout << arg3 << "\n"), ...))
再帰的パック展開
C++11/14スタイル(折りたたみ式がない場合)。
// ベースケース
void print() {
std::cout << "\n";
}
// 再帰ケース
template<typename First, typename... Rest>
void print(First first, Rest... rest) {
std::cout << first << " ";
print(rest...); // 残りで再帰
}
print(1, 2.0, "hello");
// print(1, 2.0, "hello")
// -> cout << 1; print(2.0, "hello")
// -> cout << 2.0; print("hello")
// -> cout << "hello"; print()
// -> "\n"
インデックスシーケンス
コンパイル時のインデックス生成。
// std::index_sequence の使い方
template<typename Tuple, size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << std::get<Is>(t) << " "), ...);
}
template<typename... Args>
void print_tuple(const std::tuple<Args...>& t) {
print_tuple_impl(t, std::index_sequence_for<Args...>{});
}
auto t = std::make_tuple(1, 2.0, "hello");
print_tuple(t); // 1 2 hello
make_index_sequence
// 0, 1, 2, ..., N-1 のシーケンスを生成
std::make_index_sequence<5> // -> index_sequence<0, 1, 2, 3, 4>
// 型パラメータパックに対応するインデックス
template<typename... Args>
using indices_for = std::index_sequence_for<Args...>;
完全転送
template<typename... Args>
auto make_unique_wrapper(Args&&... args) {
// 完全転送: 左辺値/右辺値を維持
return std::make_unique<Widget>(std::forward<Args>(args)...);
}
// 各引数が個別にforward される
// make_unique<Widget>(forward<Args1>(arg1), forward<Args2>(arg2), ...)
実践例1: 型安全printf
template<typename T>
void format_arg(std::ostream& os, const char* fmt, size_t& pos, T&& arg) {
while (fmt[pos] && fmt[pos] != '%') {
os << fmt[pos++];
}
if (fmt[pos] == '%') {
pos++; // '%' をスキップ
// フォーマット指定子を処理(簡略化)
os << std::forward<T>(arg);
}
}
template<typename... Args>
std::string safe_printf(const char* fmt, Args&&... args) {
std::ostringstream os;
size_t pos = 0;
(format_arg(os, fmt, pos, std::forward<Args>(args)), ...);
// 残りの文字を出力
while (fmt[pos]) {
os << fmt[pos++];
}
return os.str();
}
auto s = safe_printf("Hello %, you have % messages!", "Alice", 5);
// "Hello Alice, you have 5 messages!"
実践例2: 関数合成
template<typename... Funcs>
auto compose(Funcs... funcs) {
return [=](auto x) {
// 右から左に適用
return (funcs(...), x); // これでは動かない
};
}
// 正しい実装
template<typename F>
auto compose(F f) {
return f;
}
template<typename F, typename... Rest>
auto compose(F f, Rest... rest) {
return [=](auto x) {
return f(compose(rest...)(x));
};
}
auto add1 = [](int x) { return x + 1; };
auto mul2 = [](int x) { return x * 2; };
auto sub3 = [](int x) { return x - 3; };
auto combined = compose(add1, mul2, sub3);
combined(10); // add1(mul2(sub3(10))) = add1(mul2(7)) = add1(14) = 15
実践例3: ビジター
// 複数のラムダを継承してオーバーロードセットを作る
template<typename... Funcs>
struct overloaded : Funcs... {
using Funcs::operator()...; // usingでパック展開
};
// 推論ガイド
template<typename... Funcs>
overloaded(Funcs...) -> overloaded<Funcs...>;
// 使用例
std::variant<int, double, std::string> v = 42;
std::visit(overloaded{
[](int i) { std::cout << "int: " << i << "\n"; },
[](double d) { std::cout << "double: " << d << "\n"; },
[](const std::string& s) { std::cout << "string: " << s << "\n"; }
}, v);
実践例4: タプル操作
// タプルの各要素に関数を適用
template<typename Tuple, typename F, size_t... Is>
auto tuple_transform_impl(Tuple&& t, F&& f, std::index_sequence<Is...>) {
return std::make_tuple(f(std::get<Is>(std::forward<Tuple>(t)))...);
}
template<typename Tuple, typename F>
auto tuple_transform(Tuple&& t, F&& f) {
constexpr size_t size = std::tuple_size_v<std::remove_reference_t<Tuple>>;
return tuple_transform_impl(
std::forward<Tuple>(t),
std::forward<F>(f),
std::make_index_sequence<size>{}
);
}
auto t = std::make_tuple(1, 2, 3);
auto doubled = tuple_transform(t, [](auto x) { return x * 2; });
// doubled = (2, 4, 6)
実践例5: 型安全なイベントシステム
template<typename... EventTypes>
class EventDispatcher {
std::tuple<std::vector<std::function<void(EventTypes)>>...> handlers_;
public:
template<typename Event>
void subscribe(std::function<void(Event)> handler) {
std::get<std::vector<std::function<void(Event)>>>(handlers_)
.push_back(std::move(handler));
}
template<typename Event>
void dispatch(Event&& event) {
for (auto& handler : std::get<std::vector<std::function<void(Event)>>>(handlers_)) {
handler(std::forward<Event>(event));
}
}
};
// 使用例
struct MouseClick { int x, y; };
struct KeyPress { char key; };
EventDispatcher<MouseClick, KeyPress> dispatcher;
dispatcher.subscribe<MouseClick>([](MouseClick e) {
std::cout << "Click at (" << e.x << ", " << e.y << ")\n";
});
dispatcher.dispatch(MouseClick{100, 200});
コンパイル時文字列処理
template<char... Chars>
struct static_string {
static constexpr char value[] = {Chars..., '\0'};
static constexpr size_t size = sizeof...(Chars);
};
// ユーザー定義リテラル
template<typename Char, Char... Chars>
constexpr auto operator""_s() {
return static_string<Chars...>{};
}
auto str = "hello"_s;
static_assert(str.size == 5);
パック展開のパターン集
template<typename... Args>
void patterns(Args... args) {
// パターン1: 各要素に関数適用
(func(args), ...);
// パターン2: 各要素を式で加工
(process(args * 2), ...);
// パターン3: 条件付き
((condition(args) ? action(args) : void()), ...);
// パターン4: 型変換
(static_cast<int>(args), ...);
}
template<typename... Types>
struct TypePatterns {
// パターン: 各型のvectorを持つタプル
std::tuple<std::vector<Types>...> data;
// パターン: 各型のポインタ
std::tuple<Types*...> ptrs;
// パターン: 各型をoptionalに
std::tuple<std::optional<Types>...> optionals;
};
sizeof...の活用
template<typename... Args>
constexpr size_t count = sizeof...(Args);
template<typename... Args>
void debug_pack(Args... args) {
std::cout << "Pack contains " << sizeof...(Args) << " types\n";
std::cout << "Pack contains " << sizeof...(args) << " values\n";
}
// 空パックのチェック
template<typename... Args>
void maybe_empty(Args... args) {
if constexpr (sizeof...(Args) == 0) {
std::cout << "Empty!\n";
} else {
// ...
}
}
まとめ
| 機能 | 用途 |
|---|---|
typename... Ts |
型パラメータパック |
Ts... args |
関数パラメータパック |
args... |
パック展開 |
sizeof...(Ts) |
パックのサイズ |
(args op ...) |
折りたたみ式 |
index_sequence |
インデックス生成 |
バリアディックテンプレートはジェネリックプログラミングの要。これがないとtupleもvariantも作れない。
次回は型リストを使ったコンパイル時計算を解説する。
続きが気になったら、いいね・ストックしてもらえると嬉しいです!