C++ 反射入門
反射(reflection)就是程式在執行或編譯期「檢視自己」的能力——問一個值是什麼型別、 一個 struct 有哪些欄位、一個 enum 有哪些名字。序列化、ORM、debug 印整包物件這些事, 背後都需要它。C++ 在這塊一直做得比較克難,這篇用幾個簡單例子把現況講一遍, 最後看一下 C++26 帶來的改變。
為什麼 C++ 的反射一直很弱
像 Java、C#、Python 這些語言,物件在執行期都還帶著完整的型別資訊, 所以「列出一個類別的所有欄位」是內建功能。C++ 走的是另一條路: 編譯完之後,大部分型別資訊就被丟掉了,換來零額外開銷。
代價就是 C++ 長年沒有完整反射,只有兩塊零碎的工具—— 執行期的 RTTI、編譯期的 type_traits。 先看這兩個,再看其他語言的對照,最後是 C++26。
執行期:RTTI
RTTI(Run-Time Type Information)讓你在執行期問「這個東西實際上是什麼型別」。 入口是 typeid 跟 dynamic_cast:
#include <typeinfo>
struct Animal { virtual ~Animal() = default; };struct Dog : Animal {};struct Cat : Animal {};
Animal* a = new Dog;
// typeid 拿到動態(真正)型別std::cout << typeid(*a).name(); // 例如 "3Dog"(mangled,因實作而異)
// dynamic_cast:執行期檢查能不能安全向下轉型if (Dog* d = dynamic_cast<Dog*>(a)) { // 成功,a 真的是 Dog} else { // 失敗會回 nullptr(指標版)}兩個坑先講:typeid(*a) 要拿到動態型別,class 必須有 virtual function(這裡的虛擬解構子就夠了),不然 typeid 只會回靜態型別。另外 name() 回的字串是 「mangled」過的、不保證好讀,要還原成 Dog 得另外做 demangle。
編譯期:type_traits
RTTI 是執行期的事,會有一點點成本。更常用的其實是編譯期的 <type_traits>——它在編譯期回答型別問題,搭配 if constexpr 可以針對不同型別走不同分支,而且沒被選到的分支不會被編譯:
#include <type_traits>
template <typename T>void describe(T value) { if constexpr (std::is_integral_v<T>) std::println("整數:{}", value); else if constexpr (std::is_floating_point_v<T>) std::println("浮點數:{}", value); else if constexpr (std::is_pointer_v<T>) std::println("指標"); else std::println("其他型別");}
describe(42); // 整數:42describe(3.14); // 浮點數:3.14describe("hi"); // 其他型別is_integral、is_same、is_pointer 這些都是純編譯期判斷,常用在泛型程式裡決定「這個型別該怎麼處理」。 這算是 C++ 最實用的一塊反射,雖然它只能問型別、問不到「欄位名」。
想列出 struct 的欄位?
這才是 C++ 反射最尷尬的地方。給一個 struct,C++ 到 C++23 為止都沒辦法用標準語法自動列出它的欄位名稱,只能手寫:
struct Point { int x, y; };
Point p{1, 2};std::println("x={}, y={}", p.x, p.y); // 欄位名只能自己打想自動化(例如自動序列化成 JSON),傳統做法是靠巨集或第三方庫。 像 Boost.PFR 就能在不改 struct 的前提下走訪欄位:
#include <boost/pfr.hpp>
struct Point { int x, y; };
boost::pfr::for_each_field(Point{1, 2}, [](auto& f) { std::print("{} ", f); // 1 2});能動,但這是庫用模板硬湊出來的,拿不到欄位的「名字」(只有值), 而且對型別有限制。這個缺口,就是 C++26 要補的。
其他語言怎麼做
對照一下就知道 C++ 為什麼克難。Python 的物件自己帶著屬性表, 一行就列得出來:
class Point: def __init__(self): self.x, self.y = 1, 2
p = Point()print(vars(p)) # {'x': 1, 'y': 2}print(getattr(p, "x")) # 1,連欄位名都能用字串動態存取Java 也有內建的 Reflection API:
for (Field f : Point.class.getDeclaredFields()) { System.out.println(f.getName()); // x, y}差別在於這些語言把型別資訊留到執行期,所以反射是現成的; C++ 為了效能把資訊丟在編譯期,才一直得繞路。
C++26:static reflection
C++26 終於把反射做進語言本身,而且是編譯期的—— 用 ^^ 取得某個型別/實體的「反射值」,再用 std::meta 裡的函式去查它的成員、名字等等。 最經典的痛點「enum 轉字串」終於不用再靠巨集:
// C++26(語法仍在收斂,示意用)#include <meta>
template <typename E>std::string enum_to_string(E value) { template for (constexpr auto e : std::meta::enumerators_of(^^E)) if (value == [:e:]) return std::string(std::meta::identifier_of(e)); return "?";}
enum class Color { Red, Green, Blue };enum_to_string(Color::Green); // "Green"重點不是記這個語法(它還在變),而是方向:以前要靠巨集、 Boost.PFR 硬湊的事,之後會變成標準、編譯期、零執行期成本。 目前主流編譯器還在陸續實作,先當作「快來了」看待。
小結
一句話收尾:C++ 的反射一直是「做得到,但很克難」。日常需求大多用 type_traits 配 if constexpr 就能解, 執行期要辨型別才動 RTTI;要走訪欄位先靠 Boost.PFR 之類的庫頂著。 等 C++26 的 static reflection 普及,這些繞路大半都能收掉。