C++26 Reflection進入標準了,用一個實際的例子來證明這東西有什麼用
當我們有這樣一個struct時
1 2 3 4 struct NetworkAddress { std ::string ip; uint16_t port; };
如果希望使用std::format
系列的函數搭配使用 需要自行定義formatter
1 2 3 4 5 6 template <>struct std : :formatter<NetworkAddress> : std ::formatter<std ::string_view> { auto format (const NetworkAddress& addr, std ::format_context& ctx) const { return std ::format_to(ctx.out(), "{{ip={},port={}}}" , addr.ip, addr.port); } };
直接使用就可以了
1 2 NetworkAddress addr { "127.0.0.1" , 80 }; std ::println("{}" , addr);
不過當你自定義的structure多的話,手寫和維護formatter變成一個工程上的問題 因此我們需要一個自動化的方法
他山之石 Rust是個不錯的參考方案,多虧了Proc Macro這種黑魔法,可以寫出類似這樣的程式碼,也是Rust的殺手鐧之一
1 2 3 4 5 6 7 8 9 10 11 12 13 #[derive(Debug)] struct NetworkAddress { ip: String , port: u16 } fn main () { let addr = NetworkAddress { ip: "127.0.0.1" .to_string(), port: 80 }; println! ("{:#?}" , addr); }
希望之後C++的版本也能這麼乾淨
用魔法打敗魔法 在C++26 Reflection之前,已經有一個解決方案了,完整程式碼可以參考Reference 借助Boost Hana
之便,可以寫出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 namespace hana = boost::hana;template <typename T>constexpr auto CalculateFormatStringLength () { auto keys = hana::accessors<T>(); auto length = hana::fold(keys, size_t {0 }, [](auto sum, auto pair) { return sum + hana::length(hana::first(pair)); }); length += 4 + 4 * (hana::length(keys).value); return length; } template <typename T, std ::size_t ... Is>constexpr auto GenerateFormatStringImpl (std ::index_sequence<Is...>) { auto keys = hana::accessors<T>(); std ::array <std ::string_view, sizeof ...(Is)> key_strings = { hana::to<char const *>(hana::first(keys[hana::size_c<Is>]))... }; std ::array <char , CalculateFormatStringLength<T>()> result{}; std ::size_t pos = 0 ; result[pos++] = '{' ; result[pos++] = '{' ; auto append = [&](std ::string_view str) { for (char c : str) { result[pos++] = c; } }; auto append_key_value = [&](std ::string_view key) { append(key); result[pos++] = '=' ; result[pos++] = '{' ; result[pos++] = '}' ; result[pos++] = ',' ; }; (append_key_value(key_strings[Is]), ...); if (pos > 2 ) { pos--; } result[pos++] = '}' ; result[pos++] = '}' ; result[pos++] = '\0' ; return result; } template <typename T>constexpr auto GenerateFormatString () { return GenerateFormatStringImpl<T>(std ::make_index_sequence<hana::length(hana::accessors<T>()).value>{}); } template <typename T>class FormatStringImpl {public : constexpr FormatStringImpl () : str (GenerateFormatString<T>()) {} std ::array <char , CalculateFormatStringLength<T>()> str; }; template <typename T>struct FormatString { static constexpr FormatStringImpl<T> data{}; static constexpr const char * value () { return data.str.data(); } }; template <typename T>constexpr FormatStringImpl<T> FormatString<T>::data;template <typename T>struct std : :formatter<T, std ::enable_if_t <hana::Struct<T>::value, char >> : std ::formatter<std ::string > { auto format (const T& t, std ::format_context& ctx) const { auto members = hana::members(t); return hana::unpack(members, [&ctx](auto &&... args) { return std ::format_to(ctx.out(), FormatString<T>::value(), args...); }); } }; BOOST_HANA_ADAPT_STRUCT(NetworkAddress, ip, port);
雖然能用,不過C++26都要出了,需要嘗試更好的方法了
Generate string literal in compile time 先跳過反射的部分,解決比較小的問題 如何在compile time對字串做處理 我想寫一個Compile time function,Pseudo Code大概長這樣
1 2 3 4 5 6 consteval const char * make_greeting (std ::string_view name) { std ::string str = "Hello " + std ::string (name); return ????; } constexpr const char * greeting = make_greeting("ChatGPT" );
上面的????
就是難點所在 在C++26之前,看得到的解法大概長這樣
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template <size_t N>consteval auto make_greeting (const char (&name)[N]) { constexpr const char prefix[] = "Hello " ; constexpr size_t prefix_len = sizeof (prefix) - 1 ; std ::array <char , prefix_len + N> result{}; for (size_t i = 0 ; i < prefix_len; ++i) { result[i] = prefix[i]; } for (size_t i = 0 ; i < N; ++i) { result[prefix_len + i] = name[i]; } return result; } static constexpr auto greeting_ = make_greeting("world!" );static constexpr const char * greeting = greeting_.data();
原理跟上面的GenerateFormatStringImpl
差不多 不過這也有它的問題 - 不能直接套用std::string的方式,導致於更複雜的字串處理很難過 - The constexpr 2-Step,上面的greeting_
和greeting
都是必須存在的 Reference中有對上面更進一步的最佳化,不過非本文重點,有興趣自行研究
在C++26 Reflection通過之後,有一個小功能也順便進入標準了,因此我們可以這樣寫了
1 2 3 4 5 6 consteval const char * make_greeting (std ::string_view name) { std ::string str = "Hello " + std ::string (name); return std ::define_static_string(str); } constexpr const char * greeting = make_greeting("world!" );
這就是我們之後產生struct layout description的基礎
C++26 Reflection Revisited 因為引進了反射,我們可以直接得到struct中每個field的名稱了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct NetworkAddress { std ::string ip; uint16_t port; }; template <typename T>consteval const char * FormatString () { std ::string result = "{{" ; auto no_check = std ::meta::access_context::unchecked(); bool first = true ; for (auto info : std ::meta::nonstatic_data_members_of(^^T, no_check)) { if (!first) result += "," ; result += std ::meta::identifier_of(info); result += "={}" ; first = false ; } result += "}}" ; return std ::define_static_string(result); }
現在解決第一部份了,看看剩下的部分
Revisited Hana’s implementation 1 2 3 4 5 6 7 8 9 template <typename T>struct std : :formatter<T, std ::enable_if_t <hana::Struct<T>::value, char >> : std ::formatter<std ::string > { auto format (const T& t, std ::format_context& ctx) const { auto members = hana::members(t); return hana::unpack(members, [&ctx](auto &&... args) { return std ::format_to(ctx.out(), FormatString<T>::value(), args...); }); } };
原來是這樣
將t的members打包成tuple
透過haha::unpack
展開tuple中的所有元素,將其餵入std::format_to當參數
既然我們已經有std::apply
了,就不需要hana::unpack
了,剩下的就是將struct打包成tuple這個問題了struct_to_tuple 在C++ Reflection論文中就有一個現成的實作,直接套來用1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 consteval auto type_struct_to_tuple(std::meta::info type) -> std::meta::info { constexpr auto ctx = std ::meta::access_context::current(); return substitute(^^std ::tuple, nonstatic_data_members_of(type, ctx) | std ::views::transform(std ::meta::type_of) | std ::views::transform(std ::meta::remove_cvref) | std ::ranges::to<std ::vector >()); } template <typename To, typename From, std ::meta::info ... members>constexpr auto struct_to_tuple_helper(From const& from) -> To { return To(from.[:members:]...); } template <typename From>consteval auto get_struct_to_tuple_helper () { using To = [: type_struct_to_tuple(^^From) :]; auto ctx = std ::meta::access_context::current(); std ::vector args = {^^To, ^^From}; for (auto mem : nonstatic_data_members_of(^^From, ctx)) { args.push_back(reflect_constant(mem)); } return extract<To(*)(From const &)>( substitute(^^struct_to_tuple_helper, args)); } template <typename From>constexpr auto struct_to_tuple (From const & from) { return get_struct_to_tuple_helper<From>()(from); }
這時候以下的程式碼就能正常運作了1 2 3 4 5 6 7 8 9 template <>struct std : :formatter<NetworkAddress> : std ::formatter<std ::string _view> { auto format (const NetworkAddress& t, std ::format_context& ctx) const { auto tuple = struct_to_tuple(t); return std ::apply([&](auto &&... args) { return std ::format_to(ctx.out(), FormatString<NetworkAddress>(), args...); }, tuple); } };
Little issue 上面的程式碼雖然可以運作,不過距離一般化差很遠,這樣的程式碼是不行的1 2 3 4 5 6 7 8 9 template <typename T>struct std : :formatter<T> : std ::formatter<std ::string_view> { auto format (const T& t, std ::format_context& ctx) const { auto tuple = struct_to_tuple(t); return std ::apply([&](auto &&... args) { return std ::format_to(ctx.out(), FormatString<T>(), args...); }, tuple); } };
看看Hana的signature1 2 template <typename T>struct std : :formatter<T, std ::enable_if_t <hana::Struct<T>::value, char >>
依樣畫葫蘆,我們使用variable template和concept就能達成這目標了1 2 3 4 5 6 7 8 9 10 template <typename T>constexpr bool can_be_formatter = false ;template <>constexpr bool can_be_formatter<NetworkAddress> = true ;template <typename T> requires (can_be_formatter<T>)struct std : :formatter<T> : std ::formatter<std ::string_view> { };
不過要像Rust這樣標記1 2 3 4 5 #[derive(Debug)] struct NetworkAddress { ip: String , port: u16 }
我們需要另一個C++26特性Annotations
Annotations Annotation的定義和API就不說了,直接擷取跟我們需要的功能1 2 3 4 5 6 7 8 9 10 11 12 13 template <auto V> struct Derive { };template <auto V> inline constexpr Derive<V> derive;inline constexpr struct { } Debug;template <typename T>consteval auto has_annotation(std::meta::info r, T const& value) -> bool { auto expected = std ::meta::reflect_constant(value); for (std ::meta::info a : annotations_of(r)) if (std ::meta::constant_of(a) == expected) return true ; return false ; }
接著修改我們的 std::formatter
1 2 3 4 template <typename T> requires (has_annotation(^^T, derive<Debug>))struct std : :formatter<T> : std ::formatter<std ::string_view> { };
接著修改最後的NetworkAddress
1 2 3 struct [[=derive <Debug>]] NetworkAddress { };
到此結束,就可以跟Boost Hana說再見了
Reference # C++ Reflection in under 100 lines of code # c++ 模板元编程简化format # C++26 反射元编程:Spec API 注入模型 # 如何保存constexpr string的值在运行期使用? Reflection for C++26 Annotations for Reflection Reflection for C++26!!! Code Generation in Rust vs C++26