wordpress 禁止評論關(guān)鍵詞優(yōu)化顧問
原文
C++17
基于結(jié)構(gòu)綁定
的編譯期反射
事實上不需要宏的編譯期反射
在C++17
中已用得很多了,比如struct_pack
的編譯期反射
就不需要宏,因為C++17結(jié)構(gòu)綁定
可直接得到一個聚集
類的成員的引用.
struct person {int id;std::string name;int age;
};
int main() {person p{1, "tom", 20};auto &[id, name, age] = p;std::cout << name << "\n";
}
沒有宏也沒有侵入式
,一切都很完美,但是有兩個比較大的問題:
問題一
結(jié)構(gòu)綁定
方式無法取字段名
,這是一個主要問題,如果想序化對象到json,xml
時,需要字段名
時就無法滿足需求了.
問題二
除此外還有另外一個問題,如果一個對象有構(gòu)造器
或私有成員
時,則它就不是一個聚集
類型了,無法再用結(jié)構(gòu)綁定
去反射
內(nèi)部的字段了.
基于結(jié)構(gòu)綁定
的編譯期反射
無法解決這兩個主要問題,導(dǎo)致用途不大.
yalantinglibs.reflection
反射庫
yalantinglibs.reflection
反射庫直面并解決了這兩個問題,提供了統(tǒng)一的編譯期反射方法
,無論對象是否是聚集
類型,無論對象是否有私有字段
都可用統(tǒng)一的api
在編譯期反射
得到其元信息
.
來看看yalantinglibs.reflection
如何反射一個聚集
類型的:
struct simple {int color;int id;std::string str;int age;
};
using namespace ylt::reflection;
int main() {simple p{.color = 2, .id = 10, .str = "hello reflection", .age = 6};//取對象字段個數(shù)static_assert(members_count_v<simple> == 4);//取對象所有字段名 constexpr auto arr = member_names<simple>; //std::array<std::string_view, N> //根據(jù)字段索引取字段值 CHECK(std::get<3> == 6); //get age CHECK(std::get<2> == "hello reflection"); //get str //根據(jù)字段名取字段值auto& age2 = get<"age"_ylts>(p);CHECK(age2 == 6);//遍歷對象,得到字段值和字段名for_each(p, [](auto& field_value, auto field_name) {std::cout << field_value << ", " << field_name << "\n";});
}
yalantinglibs
的編譯期反射相比前結(jié)構(gòu)綁定
方式的反射
更進(jìn)一步了,不僅是無宏非侵入式
,還能得到字段名
,這樣就把第一個問題解決掉了,可用在數(shù)格
和xml
等需要字段名
的場景下了.
yalantinglibs.reflection
是如何完成非侵入式
取得聚集
對象的字段名
的呢?因為reflectcpp
這道光!
該庫的作者發(fā)現(xiàn)了一個新方法可在C++20
高版本的編譯器中非侵入式的取得聚集
對象的字段名.能發(fā)現(xiàn)該方法我只能說你真是個天才
!
該方法說起來也不算復(fù)雜,分兩步實現(xiàn):
第一步:在編譯期取得對象字段值的指針
;
第二步:在編譯期第一步得到的指針解析出字段名
;
是不是很簡單,接著看看具體是如何實現(xiàn)的吧.
在編譯期取得對象字段值的指針
reflectcpp
在實現(xiàn)這一步時做得比較復(fù)雜,yalantinglibs
大幅簡化了這一步的實現(xiàn).
template <class T, std::size_t n>
struct object_tuple_view_helper {static constexpr auto tuple_view(){}
};
template <class T>
struct object_tuple_view_helper<T, 0> {static constexpr auto tuple_view() { return std::tie(); }
};
template <class T>
struct object_tuple_view_helper<T, 4> {static constexpr auto tuple_view() {auto& [a, b, c, d] = get_fake_object<remove_cvref_t<T>>();auto ref_tup = std::tie(a, b, c, d);auto get_ptrs = [](auto&... _refs) {return std::make_tuple(&_refs...);};return std::apply(get_ptrs, ref_tup);}
};
偏特化
模板類object_tuple_view_helper
,在tuple_view
函數(shù)中,先結(jié)構(gòu)綁定
得到字段值的引用
,然后按指針轉(zhuǎn)換它
,并放到元組
中,來返回給用戶.
這里偏特化
的關(guān)鍵在于n
,它表示聚集
對象字段的個數(shù),該字段個數(shù)是可在編譯期
取的.為了避免針對不同字段個數(shù)
的聚集
類型寫重復(fù)的偏特化的代碼
,可用腳本生成這些代碼
.
#define RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( \n, ...) \template <class T> \struct object_tuple_view_helper<T, n> { \static constexpr auto tuple_view() { \auto& [__VA_ARGS__] = get_fake_object<remove_cvref_t<T>>(); \auto ref_tup = std::tie(__VA_ARGS__); \auto get_ptrs = [](auto&... _refs) { \return std::make_tuple(&_refs...); \}; \return std::apply(get_ptrs, ref_tup); \} \}
/*The following boilerplate code was generated using a Python script:
macro =
"RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS"
with open("generated_code4.cpp", "w", encoding="utf-8") as codefile:codefile.write("\n".join([f"{macro}({i}, {', '.join([f'f{j}' for j in range(i)])});"for i in range(1, 256)]))
*/
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS(1, f0);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS(2, f0, f1);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS(3, f0, f1, f2);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( 4, f0, f1, f2, f3);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( 5, f0, f1, f2, f3, f4);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( 6, f0, f1, f2, f3, f4, f5);
...
生成偏特化
代碼后,就可簡單取得聚集
對象字段的指針
.
template <class T>
inline constexpr auto struct_to_tuple() {return object_tuple_view_helper<T, members_count_v<T>>::tuple_view();
}
調(diào)用struct_to_tuple
就能在編譯期取得T所有字段的指針了,其中編譯期取T的字段個數(shù)的方法members_count_v
就是來自于struct_pack
中的方法,前面在介紹struct_pack
的文章里已詳細(xì)講過,就不再贅述了.
根據(jù)字段指針取字段名
有了編譯期得到的字段指針
后就很容易取其字段名
了:
template <auto ptr>
inline constexpr std::string_view get_member_name() {
#if defined(_MSC_VER)constexpr std::string_view func_name = __FUNCSIG__;
#elseconstexpr std::string_view func_name = __PRETTY_FUNCTION__;
#endif
#if defined(__clang__)auto split = func_name.substr(0, func_name.size() - 2);return split.substr(split.find_last_of(":.") + 1);
#elif defined(__GNUC__)auto split = func_name.substr(0, func_name.rfind(")}"));return split.substr(split.find_last_of(":") + 1);
#elif defined(_MSC_VER)auto split = func_name.substr(0, func_name.rfind("}>"));return split.substr(split.rfind("->") + 2);
#elsestatic_assert(false,"You are using an unsupported compiler. Please use GCC, Clang ""or MSVC or switch to the rfl::Fieldsyntax.");
#endif
}
template<auto ptr>
是C++17
的特性,可用動
來聲明一個非類型模板參數(shù)
,來避免寫具體類型
.
有了該編譯期的針
后,剩下的就是根據(jù)編譯產(chǎn)生的符號
去截取需要的部分串
了,注意每個平臺生成的符號有差異,需要宏來區(qū)分各個平臺的截取方式
.
為什么用指針
可以取字段名
?C++17
或以下的編譯器是不是也可這樣來取呢?
第一個問題的答案是:reflectcpp
作者發(fā)現(xiàn)的該方法,很黑客
,但是工作
!
第二個問題的答案是:不可以
,該方法只在支持C++20
的gcc11,clang13,msvc2022
以上編譯器中才有效!
所以該非侵入式取字段名
的方法也是有約束的,不適合低版本
的編譯器.
完整的取字段列表的實現(xiàn):
template <class T>
struct Wrapper {using Type = T;T v;
};
template <class T>
Wrapper(T) -> Wrapper<T>;
//針對clang.
template <class T>
inline constexpr auto wrap(const T& arg) noexcept {return Wrapper{arg};
}
template <typename T>
inline constexpr std::array<std::string_view, members_count_v<T>>
get_member_names() {constexpr auto tp = struct_to_tuple<T>();std::array<std::string_view, Count> arr;[&]<size_t... Is>(std::index_sequence<Is...>) mutable {((arr[Is] = get_member_name<wrap(std::get<Is>(tp))>()), ...);}(std::make_index_sequence<Count>{});return arr;
}
至此,兩步完成,可用get_member_names
函數(shù)非侵入式
的取得聚集
對象的字段名列表
了.
如何處理非聚集
類型?
在高版本的編譯器中無宏非侵入式
得到聚集
對象字段名列表固然很好,但是非聚集
類型要如何處理呢?如果編譯器版本不夠,只有C++17
又該怎么辦?
yalantinglibs.reflection
的未來
遠(yuǎn)不止你想象的,想能統(tǒng)一整個編譯期反射
的內(nèi)容,無論對象是聚集
還是非聚集
,無論對象
是否含有私有字段
都提供統(tǒng)一的反射接口
!
比如像這樣一個對象:
class private_struct {int a;int b;public:private_struct(int x, int y) : a(x), b(y) {}
};private_struct st(2, 4);ylt::reflection::refl_visit_members(st, [](auto&... args) {((std::cout << args << " "), ...);std::cout << "\n";});
private_struct
是一個含私有字段
的非聚集
類型,但是ylt::reflection
也能反射
它.但是這里還漏掉了一個宏,是,還是需要宏,在編譯期反射進(jìn)入到標(biāo)準(zhǔn)庫前,對非聚集
類型仍需要的.
class private_struct {int a;int b;public:private_struct(int x, int y) : a(x), b(y) {}
};
YLT_REFL_PRIVATE(private_struct, a, b);
對沒有私
字段的非聚集
類型來說,也適合C++17
,可這樣定義宏:
struct dummy_t {int id;std::string name;int age;YLT_REFL(dummy_t, id, name, age);
};
struct dummy_t2 {int id;std::string name;int age;
};
YLT_REFL(dummy_t2, id, name, age);
總結(jié)
高版本的編譯器中可完全不使用宏
,低版本或非聚集
類型宏來實現(xiàn)編譯器反射,無論是聚集
還是非聚集
都是使用同一套反射接口
,這樣可覆蓋所有
要用編譯期反射的場景,這就是yalantinglibs.reflection
提供的能力!
后面struct_pack,struct_pb,struct_json,struct_xml,struct_yaml
都會使用yalantinglibs.reflection
提供的統(tǒng)一的編譯期反射接口
來實現(xiàn)序化和反序化
.