0%

Refelection in C++

Why Refelection

有些時候,我們需要遍歷struct/class的member,最常見的的用途就是print/serialization/deserialization

1
2
3
4
5
6
7
8
struct obj {
int a;
};

void print(const obj& o)
{
printf("%d\n", o.a);
}

這樣子的做法雖然直接,不過有幾個問題

  • 只要structure改變,你的implementation就要跟著改變
  • 假設要一直支持新的structure,我們需要一個新的overload function

另外有時候我們也需要 struct field name的資訊,例如我們想知道struct file的名稱,而Compiler編譯出來的程式碼沒有struct/class的field資訊,所以我們會這樣手動寫死

1
2
3
4
void print(const obj& o)
{
printf("a: %d\n", o.a);
}

如果我們把a名稱改成a1,也是要手動維護程式碼,那有什麼適合的方案嗎

Compilier dependent solution

clang的__builtin_dump_struct
只支援dump功能,其他沒了,也只有clang能用

1
2
3
4
5
6
7
8
9
10
struct obj1 {
int a;
int b;
};

int main() {
struct obj1 o = { .a=1, .b=2 };
__builtin_dump_struct(&o, &printf);
return 0;
}

Wrong Idea

想到最直覺的方法,當然是這樣寫

1
2
3
4
5
6
template <typename T>
void print(const T& o)
{
for (auto& field : { field of o })
std::cout << field << "\n";
}

不過眾所周知,for loop不能這樣用

Boost pfr for resuce

山不轉路轉,有無數的聰明人想出了方法,其中最有名的就是boost pfr

1
2
3
4
5
6
7
8
#include <boost/pfr/ops.hpp>
template <typename T>
void print(const T& o)
{
boost::pfr::for_each_field(o, [&](const auto& v) {
std::cout << v << "\n";
});
}

不過這方法也是有其侷限性

  • 增加了對 boost pfr的依賴
  • 只能對Aggregate type使用
  • 不能解決field name的問題

    nameof

    一個借鑑於C#的library
    大概的用法是這樣子
    1
    2
    NAMEOF(somevar) -> "somevar"
    NAMEOF(person.address.zip_code) -> "zip_code"
    對單一變數效果還行,不過對struct/class裡面的field name還是無能為力

Macro based Solution

以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
#include <boost/hana.hpp>
struct OrderedItem {
BOOST_HANA_DEFINE_STRUCT(
OrderedItem,
(std::string, item_name),
(int64_t, quantity),
(int64_t, price_cents)
);
};

template<typename T>
boost::json::value FormatStructure(const T &t) {
boost::json::object result;
boost::hana::for_each(t, boost::hana::fuse([&result](auto name, auto member) {
result.emplace(boost::hana::to<const char *>(name), FormatObject(member));
}));
return result;
}
template<typename T>
boost::json::value FormatObject(const T &t) {
if constexpr (boost::hana::Struct<T>::value) {
return internal::FormatStructure(t);
} else {
return internal::FormatValue(t);
}
}

光看程式碼就猜的到,BOOST_HANA_DEFINE_STRUCT做了很多事情,維護每個除了原先的 field declaration之外,還維護了field name的資訊
不過Macro就是黑魔法,維護起來就是麻煩,不過現階段也沒更好的方法

Runtime Refelection

上面說的都是Compile-time Refelection,當然還有一派作法是在Runtime時做Refelection,能無視編譯器的差異,提供比編譯器更多的Metadata,不過這一切都是要手動做

不管Compile-time Refelectionc還是Runtime Refelection,都掙脫不了Macro和Template的禁錮

Future

有個實驗性質的reflection TS

1
2
3
4
5
6
7
8
9
10
11
struct S {
int b;
std::string s;
std::vector<std::string> v;
};
 
// Reflection TS
#include <experimental/reflect>
using meta_S = reflexpr(S);
using mem = std::reflect::get_data_members_t<meta_S>;
using meta = std::reflect::get_data_members_t<mem>;

不過前途未卜啊,搞不好像NetworkTS那樣推倒重來,C++23是無望了

Reference