0%

Meta Programming in C++26

與其說這是Reflection,這個比較像是General meta programming solution
也就是Boost Hana所推薦的類型運算,將Type當作Value,然後對Type做處理的動作
基本的操作就是這樣

1
2
constexpr std::meta::info value = ^int;
using Int = typename[:value:];

這裡的info是個opaque type,只有Compiler看得懂,任何的操作要透過Meta function來進行

在開始之前,先寫一個Helper function,好幫助做驗證

Helper function

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
// start 'expand' definition
namespace __impl {
template<auto... vals>
struct replicator_type {
template<typename F>
constexpr void operator>>(F body) const {
(body.template operator()<vals>(), ...);
}
};

template<auto... vals>
replicator_type<vals...> replicator = {};
}

template<typename R>
consteval auto expand(R range) {
std::vector<std::meta::info> args;
for (auto r : range) {
args.push_back(std::meta::reflect_value(r));
}
return substitute(^__impl::replicator, args);
}
// end 'expand' definition

template <typename S>
constexpr auto print_layout() -> std::string {
std::string desc = "Print struct layout: " + std::string(identifier_of(^S)) + "\n";
desc += "members: {\n";
[: expand(nonstatic_data_members_of(^S)) :] >> [&]<auto e>() mutable {
desc += " " + std::string(identifier_of(e));
desc += " " + std::string(identifier_of(type_of(e)));
desc += "\n";
};
desc += "}";
return desc;
}

測試範例

1
2
3
4
5
6
7
8
9
10
11
12
13
struct X
{
char a;
int b;
double c;
};

int main()
{
auto desc = print_layout<X>();
std::cout << desc << "\n";
return 0;
}

無中生有,define_class

最簡單的例子

1
2
3
4
5
6
7
8
9
struct storage;
static_assert(is_type(define_class(^storage, {data_member_spec(^int, {.name="value"})})));

int main()
{
auto desc = print_layout<storage>();
std::cout << desc << "\n";
return 0;
}

印出來的結果

1
2
3
4
Print struct layout: storage
members: {
value int
}

從上面的程式碼,可以看出

  • storage 是 forward declaration,沒有任何定義
  • 真正的定義是在define_class(....) 這裡
  • 必須使用static_assert(is_type(....))在編譯期完成
  • 宣告member field必須使用data_member_spec

    Template class

    如果我們想要產生類似
    1
    2
    3
    4
    template <size_t N>
    struct storage {
    int value[N];
    };
    該怎麼做
    Step1
    雖然無關緊要,不過我想把定義class的部分獨立成一個函數
    1
    2
    3
    4
    5
    template <size_t N>
    consteval auto define_storage() -> std::meta::info {
    return define_class(^storage, {data_member_spec(^int, {.name="value"})});
    }
    using T = [:define_storage<10>():];
    這樣省下了static_assert(is_type(...)) 的需求
    Step2
    將storage的forward declaration改成這樣
    1
    2
    template <size_t N>
    struct storage;
    Step3
    修改define_storage的實作
    1
    2
    3
    4
    5
    6
    template <size_t N>
    consteval auto define_storage() -> std::meta::info {
    return define_class(substitute(^storage, {^N}), {
    data_member_spec(^int, {.name="value"})
    });
    }
    這邊有個substitute函數,第一個參數就是tempalte class,之後的參數就是要填入的直了,印出的結果類似這樣
    1
    2
    3
    4
    Print struct layout: storage
    members: {
    value int
    }
    距離我們要的還差一點
    Step4
    故技重施,定義一個新type
    1
    2
    template <typename T, size_t N>
    using c_array_t = T[N];
    然後再度修改define_storage
    1
    2
    3
    4
    5
    6
    template <size_t N>
    consteval auto define_storage() -> std::meta::info {
    return define_class(substitute(^storage, {^N}), {
    data_member_spec(substitute(^c_array_t, {^int, ^N}), {.name="value"})
    });
    }
    這下子看到的是
    1
    2
    3
    4
    Print struct layout: storage
    members: {
    value c_array_t
    }
    有沒有辦法將c_array_t變回int[10]
    Step5
    加上dealias就行了,不過應該有更好的方法,暫時還沒想到
    1
    2
    3
    4
    5
    6
    template <size_t N>
    consteval auto define_storage() -> std::meta::info {
    return define_class(substitute(^storage, {^N}), {
    data_member_spec(dealias(substitute(^c_array_t, {^int, ^N})), {.name="value"})
    });
    }
    不過這方法跟原先的方法比,最大的好處是可程式化,可以創造出更特別的玩法,例如

Full(Partial) Template Specialization

將原先的範例改成這樣

1
2
3
4
5
6
7
8
9
template <size_t N>
struct storage {
int value[N];
};
template <>
struct storage<5> {
char value[5];
int value2;
};

我們一樣可以用define_storage來做

1
2
3
4
5
6
7
8
9
10
11
template <size_t N>
consteval auto define_storage() -> std::meta::info {
std::vector<std::meta::info> members;
if constexpr (N == 5) {
members.push_back(data_member_spec(dealias(substitute(^c_array_t, {^char, ^N})), {.name="value"}));
members.push_back(data_member_spec(^int, {.name="value2"}));
} else {
members.push_back(data_member_spec(dealias(substitute(^c_array_t, {^int, ^N})), {.name="value"}));
}
return define_class(substitute(^storage, {^N}), members);
}

而Partial Template Specialization 可以用同樣的方式處理

1
2
3
4
5
6
7
8
9
10
11
template <size_t N1, size_t N2>
struct storage {
int value[N1];
int value2[N2];
};

template <size_t N1>
struct storage<N1, 5> {
int value1[N1];
char value2[5];
};

之後可以這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
template <size_t N1, size_t N2>
struct storage;
template <size_t N1, size_t N2>
consteval auto define_storage() -> std::meta::info {
std::vector<std::meta::info> members;
members.push_back(data_member_spec(dealias(substitute(^c_array_t, {^int, ^N1})), {.name="value1"}));
if constexpr (N2 == 5) {
members.push_back(data_member_spec(dealias(substitute(^c_array_t, {^char, ^N2})), {.name="value2"}));
} else {
members.push_back(data_member_spec(dealias(substitute(^c_array_t, {^int, ^N2})), {.name="value2"}));
}
return define_class(substitute(^storage, {^N1, ^N2}), members);
}

Implement Pick/Omit in TypeScript

Typescript裡面有一個Pick Utility

1
2
3
4
5
6
type Person = {
firstName: string;
lastName: string;
age: number;
};
type PersonName = Pick<Person, 'firstName' | 'lastName'>;

這裡的PersonName就等同於

1
2
3
4
type PersonName = {
firstName: string;
lastName: string;
};

C++的等價版本應該是這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <typename From, typename To>
consteval auto Pick(std::initializer_list<std::string_view> keeps) -> std::meta::info {
std::vector<std::meta::info> members;
[: expand(nonstatic_data_members_of(^From)) :] >> [&]<auto e>() mutable {
auto it = std::ranges::find(keeps, identifier_of(e));
if (it != keeps.end()) {
members.push_back(data_member_spec(type_of(e), {.name=identifier_of(e)}));
}
};
return define_class(^To, members);
}

struct Person {
std::string firstName;
std::string lastName;
int age;
};

struct PersonName;
static_assert(is_type(Pick<Person, PersonName>({ "firstName", "lastName" })));

應該可以更好,不過目前想不到怎麼做
Omit只是Pick的反操作,這邊就不寫了

Revisit Boost MP11

以下是MP11的一個範例

1
2
3
using L = std::tuple<void, int, float>;
using R = mp_transform<std::add_pointer_t, L>;
static_assert(std::is_same_v<R, std::tuple<void*, int*, float*>>);

可以用Meta Programming重寫

1
2
3
4
5
6
7
8
9
10
11
template < typename T>
consteval auto transform(std::meta::info (&F)(std::meta::info)) -> std::meta::info {
std::vector<std::meta::info> new_members;
for (auto member : template_arguments_of(^T)) {
new_members.push_back(F(member));
}
return substitute(template_of(^T), new_members);
}
using L = std::tuple<void, int, float>;
using R = [:transform<L>(std::meta::type_add_pointer):];
static_assert(std::is_same_v<R, std::tuple<void*, int*, float*>>);

省去了很多以往的Template Magic

Conclusion

Reflection如果如期進入C++26,會是極大的變化
只希望一切順利

Reference