0%

A Real Usage for C++26 Reflection

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--; // Remove the last comma
}

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的signature
      1
      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> {
      // ignore
      };
      不過要像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> {
      // ignore
      };
      接著修改最後的NetworkAddress
      1
      2
      3
      struct [[=derive<Debug>]] NetworkAddress {
      // ignore
      };
      到此結束,就可以跟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