Reflection(反射 · 進階)
ll/api/reflection/ · 通用 · 進階
這篇文檔給誰看
如果你只是想把普通結構體存成 JSON、或者給一個類型寫個"字符串形式"的特化,先看 Reflection(反射)。這一篇面向需要:
- 精確控制字段名、必選規則、輸出形態的自定義特化;
- 弄清框架到底按什麼規則挑特化、為什麼"明明寫了卻沒生效";
- 邊寫邊查:完整簽名表、返回值約束、編譯期報錯對照表。
文中所有複雜示例都來自測試 src-test/server/ReflectionTest.cpp,每個示例後面都標了對應的 gtest 用例名,可以直接搜來跑。
調度模型與術語
優先級標籤
框架不靠嵌套 if constexpr 排查,而是用「標籤繼承 + 重載決議」做編譯期優先級派發:所有候選函數都叫 serialize_impl / deserialize_impl,區別只在最後一個參數 meta::PriorityTag<N>(PriorityTag<N> 繼承自 PriorityTag<N-1>,編號越大越"特化")。傳入 PriorityTag<11> 時,重載決議會挑當前類型能滿足的最高優先級分支。
| 優先級 | 命中條件(以反序列化方向為例) |
|---|---|
| 11 | 算術類型 |
| 10 | 有 Serializer<T> 自定義特化(值式或原地式) |
| 9 | std::optional、Dispatcher |
| 8 | VectorBase(向量類型) |
| 7 | 枚舉 |
| 6 | std::variant |
| 5 | 字符串類型、std::string_view |
| 4 | tuple-like(std::pair / std::tuple / std::array) |
| 3 | array-like(std::vector / std::set …) |
| 2 | associative(std::map …) |
| 1 | 可反射聚合體 |
| 0 | 兜底:能轉換就轉換,否則給出 cannot deserialize this type 斷言 |
這一切都是編譯期決定的,和運行時值無關;同一處換個類型,命中的分支就可能完全不同。
探測 trait
框架用一組變量模板判斷"某種形態的特化到底存不存在",它們都在 ll::reflection::detail:
| trait | 判斷的內容 |
|---|---|
has_value_serializer_v<T, J, F> | Serializer<T>(或 Serializer<T, J>)有沒有值式 serialize |
has_inplace_serializer_v<T, J, F> | 有沒有原地式 serialize(T const&, J&[, F const&]),返回值必須是 ll::Expected<> |
has_custom_serializer_v | 上面兩者之一 |
has_value_deserializer_v<T, J, F> | 有沒有值式 deserialize |
has_inplace_deserializer_v<T, J, F> | 有沒有原地式 deserialize(T&, J const&[, F const&]) |
has_custom_key_serializer_v<T, F> | 有沒有 Serializer<T>::to_string(t[, f]) |
has_custom_key_deserializer_v<T, F> | 有沒有 Serializer<T>::from_string(sv[, f]) |
has_typed_key_formatter_v<T, F> | formatter 有沒有 operator()<T>(value) 這種按類型定製的重載 |
is_key_stringifiable_v<T> | 能不能當關聯容器的鍵(字符串 / 枚舉 / 有 to_string + from_string) |
一個容易忽略的細節:探測值式特化時用的是不帶返回類型約束的複合要求 { expr };——這樣你就能直接返回 J 而不必包 Expected<J>,但副作用是 void 也算"滿足"。所以返回 void 的值式特化會被選中,然後在框架內部編譯失敗。void 從來不是合法返回值。
三種特化形態
| 形態 | 位置 | 語義 |
|---|---|---|
| 字符串轉換 | to_string / from_string | 隻影響關聯容器鍵與 type_to_string / string_to_type,不改變對象序列化 |
| 值式 | serialize / deserialize 返回值 | 整個類型當成一個值 |
| 原地式 | serialize / deserialize 收 J& / T& | 逐字段填充,可複用 field / member 系列 |
命中順序
- 當前值類型 + 當前目標類型能命中的自定義特化(優先級 10);
- 內建邏輯:optional / dispatcher / enum / variant / 容器(優先級 9 ~ 2);
- 默認聚合反射(優先級 1);
- 兜底轉換(優先級 0)。
兩個方向的內部次序略有不同,同時實現兩種形態時會各自命中:
- 序列化:優先值式特化,沒有值式才看原地式;
- 反序列化:優先原地式特化,沒有才走值式 / 內建邏輯。
特化簽名與返回值
框架按簽名識別特化,簽名不匹配等於特化不存在。各形態允許的寫法:
| 形態 | 簽名 | 允許的返回類型 |
|---|---|---|
| 字符串轉換 | static std::string to_string(T const&) / static ll::Expected<T> from_string(std::string_view) | std::string(或可轉 std::string_view);ll::Expected<T> |
| 值式(帶 formatter) | template <typename J, typename F> static ll::Expected<J> serialize(T const&, F const&) | J、可隱式轉成 J 的類型、ll::Expected<J> |
| 值式(不帶 formatter) | template <typename J> static ll::Expected<J> serialize(T const&) | 同上,但框架不會把 key formatter 傳進來 |
| 原地式 | template <typename J, typename F> static ll::Expected<> serialize(T const&, J& out, F const&) | 必須是 ll::Expected<> |
deserialize 與之對稱:值式收輸入、返回 T 或 ll::Expected<T>;原地式收 T&、返回 ll::Expected<>。
簽名自查清單
- 和上表逐字符對齊:多一個
const、少一個typename F、參數順序寫反,都會被靜默忽略。 - 聚合體下沒有報錯:特化被忽略時會直接走默認聚合反射——編譯通過,行為卻不是你以為的。
- 非聚合體才會報錯:這時才會落到
cannot serialize this type/cannot deserialize this type。 - 不要返回
void:探測會通過,實現會編譯失敗或被忽略。 - 需要"只對某種目標生效"時用
Serializer<T, J>(見下文 NBT / JSON 雙形態示例)。
鍵的字符串化
關聯容器、枚舉、以及顯式調用都會用到這一層:
template <typename T, typename F>
std::string type_to_string(T const& value, F const& keyFormatter);
template <typename T, typename F>
std::optional<T> string_to_type(std::string_view text, F const& keyFormatter);對應的概念是 IsLeastStringifiableType:字符串類、枚舉、以及提供 to_string + from_string 的類型。
按類型定製的 key formatter
formatter 除了處理鍵名(operator()(std::string_view)),還可以提供按類型定製的重載 operator()<T>(value);它隻影響寫出,不影響讀入:
struct UuidKeyFormatter {
std::string operator()(std::string_view key) const { return std::string{key}; }
template <typename T>
requires std::same_as<T, mce::UUID>
std::string operator()(T const& value) const {
auto text = value.asString();
std::ranges::transform(text, text.begin(), [](unsigned char ch) {
return static_cast<char>(std::toupper(ch));
});
return text;
}
};這樣 std::map<mce::UUID, int> 會寫出大寫 UUID 鍵,而讀入仍按普通 UUID 字符串解析。測試:AssociativeKeysCanCustomizeSerializationStyleWithoutAffectingParsing。
formatter 的傳播邊界
formatter 會傳給成員鍵名和關聯鍵,但不會漏進值式特化內部:值式特化不帶 formatter 形參時,框架不會把 formatter 傳進去,特化裡的 to_string 也就不受外部風格影響。測試:MemberHelperDoesNotPropagateOuterKeyFormatterIntoValueSerializer。
成員與字段輔助函數
這類 API 是給 Serializer<T> 特化內部用的簡寫。member / field 的方向由 json 實參的 const 性決定——可變左值 = 寫出,const 左值 / 右值 = 讀入;在特化之外請直接調用 serialize / deserialize,或使用方向明確的名字。
| 語義 | 成員指針版 | 固定鍵名版 |
|---|---|---|
缺字段時重置為 nullopt(成員是 optional) | member<&T::m>(t, j, f) | field<"m">(t, j, f) |
| 字段必須存在,即使成員是 optional | required_member<&T::m>(t, j, f) | required_field<"m">(t, j, f) |
| 缺字段時保留目標對象當前值 | default_member<&T::m>(t, j, f) | default_field<"m">(t, j, f) |
| 缺字段時寫入提供的默認值 | default_member<&T::m>(t, j, d, f) | default_field<"m">(t, j, d, f) |
| 序列化單個成員 / 字段 | member_serialize<&T::m>(j, t, f) | field_serialize<"m">(j, t, f) |
| 反序列化單個成員 / 字段 | member_deserialize<&T::m>(t, j, f) | field_deserialize<"m">(t, j, f) |
補充語義:
- 寫出方向:成員是空的
std::optional<T>時不會寫出這個鍵,而不是寫null; - 參數順序:序列化是"先 json 後值",反序列化是"先值後 json",容易寫反;
- 成員指針版自動取反射成員名,字段名與成員名不一致時用固定鍵名版。
複雜示例(摘自測試)
1. 判別字段決定必選字段
ItemInfo 用 type 字段決定 name / snbt 誰必選。只有原地式才能"先讀判別字段、再決定後續字段":
template <>
struct ll::reflection::Serializer<ItemInfo> {
template <typename J, typename F>
static ll::Expected<> deserialize(ItemInfo& out, J const& j, F const& keyFormatter) {
using ll::reflection::field;
using ll::reflection::required_field;
if (!j.is_object()) return ll::reflection::makeDeserObjectTypeError();
if (auto res = field<"type">(out.type, j, keyFormatter); !res) return ll::forwardError(res.error());
using enum ItemInfoType;
switch (out.type) {
case Name:
if (auto res = required_field<"name">(out.name, j, keyFormatter); !res) return ll::forwardError(res.error());
if (auto res = field<"aux">(out.aux, j, keyFormatter); !res) return ll::forwardError(res.error());
out.snbt.reset();
return {};
case Snbt:
if (auto res = required_field<"snbt">(out.snbt, j, keyFormatter); !res) return ll::forwardError(res.error());
out.name.reset();
out.aux.reset();
return {};
}
return {};
}
};測試:ItemInfoUsesTypeFieldToSelectRequiredFields、ItemInfoReportsMissingFieldsRequiredByType。
2. 把多個子對象壓平成同一個對象
類型繼承 std::tuple 並特化 std::tuple_size / std::tuple_element / std::get,就能走 tuple-like 分支;再寫值式序列化器把各段的鍵合併進同一個對象:
template <typename... Types>
struct FlatReflection : std::tuple<Types...> {
using Base = std::tuple<Types...>;
using Base::Base;
constexpr FlatReflection() = default;
constexpr FlatReflection(Types... values) : Base(std::move(values)...) {}
};
template <typename... Types>
struct ll::reflection::Serializer<FlatReflection<Types...>> {
template <typename J, typename F, size_t... I>
static ll::Expected<J> serializeElements(FlatReflection<Types...> const& input, F const& keyFormatter,
std::index_sequence<I...>) {
ll::Expected<J> output{J::object()};
auto append = [&]<size_t Index>() {
if (!output) return;
auto value = ll::reflection::serialize<J>(std::get<Index>(input), keyFormatter);
if (!value) {
output = ll::forwardError(value.error());
return;
}
for (auto const& [key, child] : value->items()) (*output)[key] = child;
};
(append.template operator()<I>(), ...);
return output;
}
};測試:FlatReflectionMergesObjectSerializersAndRoundTrips。
3. 不可默認構造的類型
只能寫值式:deserialize 返回 T,由特化自己決定怎麼構造。放進容器、std::optional、std::variant 裡同樣生效(框架內部走 deserialize_construct):
template <>
struct ll::reflection::Serializer<NonDefaultConstructibleItem> {
template <typename J, typename F>
static ll::Expected<NonDefaultConstructibleItem> deserialize(J const& j, F const& keyFormatter) {
using ll::reflection::required_field;
if (!j.is_object()) return ll::reflection::makeDeserObjectTypeError();
int id = 0;
std::string name;
if (auto res = required_field<"id">(id, j, keyFormatter); !res) return ll::forwardError(res.error());
if (auto res = required_field<"name">(name, j, keyFormatter); !res) return ll::forwardError(res.error());
return NonDefaultConstructibleItem{id, std::move(name)};
}
};測試:DeserializeToSupportsNonDefaultConstructibleTypesWithValueDeserializer、ContainersSupportNonDefaultConstructibleValuesWithValueDeserializer。
4. 帶判別字段的變體
普通 std::variant 只能按值形狀猜分支。要顯式 discriminator,就包一層類型並給它寫特化:
template <typename Enum, typename Variant>
struct VariantWithEnumIndex : Variant {
static constexpr std::string_view typeKey = "type";
};
template <typename Enum, typename Variant>
struct ll::reflection::Serializer<VariantWithEnumIndex<Enum, Variant>> {
// serialize: 写 {"type": <枚举名>, "value": <当前分支>}
// deserialize: 先读 type,再按枚举索引派发到对应分支
};測試:VariantWithEnumIndexSerializesAndDeserializesUsingEnumDiscriminator。
5. ABI 包裝類型
ll::TypedStorage<Align, Size, T> 這類包裝不會被默認反射看穿,需要顯式放行:
template <size_t Align, size_t Size, typename T>
struct ll::reflection::Serializer<ll::TypedStorageImpl<Align, Size, T>> {
using storage_type = ll::TypedStorageImpl<Align, Size, T>;
template <typename J, typename F>
static ll::Expected<J> serialize(storage_type const& value, F const& keyFormatter) {
return ll::reflection::serialize<J>(value.get(), keyFormatter);
}
template <typename J, typename F>
static ll::Expected<storage_type> deserialize(J&& j, F const& keyFormatter) {
auto value = ll::reflection::deserialize_to<T>(std::forward<J>(j), keyFormatter);
if (!value) return ll::forwardError(value.error());
return storage_type{std::move(*value)};
}
};測試:SerializedSkinImplSpecializationSupportsTypedStorageFields。
6. 同一類型對 JSON / NBT 輸出不同形態
對 J 做 if constexpr 判斷即可。測試裡給 CompoundTagVariant 寫特化:JSON 目標輸出 SNBT 字符串,NBT 目標原樣返回節點。
template <>
struct ll::reflection::Serializer<CompoundTagVariant> {
template <typename J, typename F>
static ll::Expected<J> serialize(CompoundTagVariant const& value, F const&) {
if constexpr (std::same_as<std::remove_cvref_t<J>, CompoundTagVariant>) {
return value;
} else {
return value.toSnbt(SnbtFormat::Minimize, 0);
}
}
};測試:NbtValueSpecializationCanSerializeDifferentlyForJsonAndNbt。
7. 只反序列化、不序列化(向後兼容)
繼承包一層,讓序列化輸出空對象、反序列化照常工作:
template <typename T>
struct OnlyDeserialize : T {
using Base = T;
using Base::Base;
OnlyDeserialize() = default;
explicit OnlyDeserialize(T value) : T(std::move(value)) {}
};測試:CustomDeserializerCanHideATypeDuringSerialization。
8. 其它值得一看的用例
| 場景 | 測試名 |
|---|---|
| 同一類型同時支持字符串形式與值式 | ValueSerializerAndStringSerializerCanCoexistOnSameType |
路徑 / 時長 / ordered_json 的值式特化 | OrderedJsonAndFilesystemPathUseValueSpecializations、DurationSpecializationSerializesTheUnderlyingCount |
string_view 只接受可借用的左值源 | StringViewDeserializationRequiresBorrowableLvalueSource |
| 普通 variant 的精確匹配與失敗路徑 | PlainVariantPrefersExactArithmeticAlternativeMatches、PlainVariantReportsCastFailureWhenNoAlternativeMatches |
| 數值越界 / 非整數的報錯 | NumericDeserializationRejectsOutOfRangeAndFractionalValues |
| 原地特化被所有入口使用 | InPlaceDeserializerIsUsedByDeserializeAndDeserializeTo |
自定義目標類型(把新的數據結構接到反射上)
框架對目標類型 J 是完全泛型的:沒有基類、沒有 trait、也沒有適配接口,唯一契約就是——你調 serialize<J> / deserialize_to<T>(J) 時,J 恰好有那些成員函數。所以"支持一種新的數據結構"(腳本引擎對象、Json::Value、自己的 Value 類型)就等於讓那個類型滿足下面這份契約。
契約
| 分類 | 需要的成員 | 什麼時候需要 |
|---|---|---|
| 構造 | static J object()、static J array() | 只要序列化對象 / 數組 |
| 構造 | 可由 nullptr_t 構造 | 序列化空 std::optional(框架會 return t ? … : nullptr;) |
| 構造 | 標量 / 字符串可直接構造(見下文"兩步轉換") | 序列化算術類型、字符串、枚舉 |
| 構造 | 可默認構造、可賦值 | 只在原地式特化分支和 serialize_to 用 |
| 查詢 | is_object / is_array / is_string / is_null / is_boolean / is_number / is_number_float / is_number_integer / is_number_unsigned、type_name()、size() | 按實際用到的分支裁剪即可 |
| 對象 | contains(key)、operator[](key)、items()(能 for (auto&& [k, v] : …)) | 讀 / 寫對象 |
| 數組 | operator[](index)、push_back(value) | 讀 / 寫數組與 tuple |
| 標量 | 能 static_cast 成算術類型、能隱式轉成 std::string 與 std::string_view | 讀標量;後者是 LeviLamina 內置的字符串型特化(mce::UUID、ll::data::Version 等)會用的寫法 |
| variant 專用 | get<bool>() / get<double>() / get<int64>() / get<uint64>()、dump() | 只有當類型裡有 std::variant |
| string_view 專用 | 成員別名 using string_t = ...; + template <class T> T const& get_ref() const(即 BorrowableStringSource) | 只有當類型裡有 std::string_view;見下文"借用字符串" |
三條硬約束
operator[]必須交出"同一個 J"。框架對普通J直接return static_cast<J&&>(j[key]);(見object_child_at/array_child_at),只有CompoundTagVariant/UniqueTagPtr有專門分支把子節點包一層。所以"返回裸句柄"的類型(JSValue、lua_State*之類)不能直接當J,要包成"子節點也是自己"的節點類型。- 左值來源要求引用,右值來源走拷貝。同一個函數里,
J是左值引用時要求j[key]給出真實引用;是右值時會std::remove_cvref_t<J>(j[key])拷貝一份。所以operator[]返回臨時對象的適配類型,只能以右值形式傳入(deserialize_to<T>(makeNode())),否則要麼編譯不過、要麼懸垂。 - 寫入口的名字是固定的:寫對象是
(*res)[key] = value,寫數組是res->push_back(value),因此operator[]要返回可賦值引用,並且要有push_back。
三個實測會踩到的坑
- 不要只提供"從中間類型構造"的構造函數。框架會寫
return t;這種隱式轉換(例如序列化一個int得到Expected<J>),而int → Json::Value → J屬於兩步用戶定義轉換,在隱式語境裡不成立。要給標量和字符串提供直達構造函數(is_arithmetic_v<T>的模板 +std::string+char const*)。 - 算術轉換運算符會和內置下標運算符打架。適配類型為了讀數值通常帶一個
template <class T> operator T(),於是node["a"]會被解釋成"把 node 轉成__ptrdiff_t再用內置operator[](__ptrdiff_t, const char*)",和你的operator[](std::string_view)形成歧義。補一個operator[](char const* key)讓字符串字面量精確匹配即可消歧義。 - 別給字符類型提供轉換。
std::string{j}是花括號初始化:只要適配類型存在到char的隱式轉換,就會走initializer_list<char>那條構造函數,把static_cast<char>(j)變成一個 1 字節的字符串(讀出來就是\0,字符串成員會靜默變成垃圾,枚舉成員才會報invalid enum value)。轉換運算符的約束要寫成std::is_arithmetic_v<T> && !ll::traits::is_char_v<T>——CompoundTagVariant一直就是這麼寫的。
借用字符串(string_t + get_ref)
目標類型裡有 std::string_view 成員時,框架需要兩樣東西:成員別名 string_t,以及
// 返回指向自己存储里那个字符串的引用;引用有效期由实现者负责。
// 框架只会用 T = string_t const& 这一个类型调用它。
template <class T>
[[nodiscard]] T const& get_ref() const;框架拿到 string_t const& 之後自己構造成需要的類型(這裡是 std::string_view)。CompoundTagVariant(string_t = std::string)和 nlohmann::json 都滿足。
底層不是 std::string 的適配類型也能滿足它:Json::Value 只能按值交出字符串(asString()),於是 JsonValueNode 用 using string_t = std::string;,把這份文本緩存進一個 mutable 成員,再由 get_ref 返回它的引用。
兩點語義要記住:
- 借出的引用 / 視圖指向源自己擁有的那份字符串(
CompoundTagVariant、nlohmann::json是它們的內部存儲;Json::Value適配類型是節點緩存的副本),所以那份字符串必須活得比視圖久; - 框架只在來源是左值時借用。傳臨時節點(右值)依然會報
field cannot be safely deserialized into string_view from a temporary or non-borrowing source——命名一個 node 再傳進去就能用。
get_ref 返回的引用必須指向存儲,不能返回指向臨時對象的引用:T const& 的返回類型下,return somePointerValue; 這種寫法綁的是棧上的臨時對象,函數一返回就懸空。
完整實現與測試
src-test/server/ReflectionTest.cpp 裡的 JsonValueNode 就是照這份契約給 Bedrock 的 Json::Value 寫的適配類型。它同時是個可寫視圖:拷貝節點是淺拷貝(共享同一份 Json::Value),給節點賦值會寫穿到底層 Json::Value,元素訪問返回子節點緩存裡的引用——所以框架的左值路徑也能走通。配套測試:
| 測試 | 覆蓋 |
|---|---|
JsonValueNodeSerializesAggregatesIntoJsonValue | 聚合體 → Json::Value:算術 / 字符串 / 布爾 / 空 optional 省略鍵 / 枚舉 / vector / map / 嵌套結構 / variant / 自定義 Serializer<T>,並做往返比較 |
JsonValueNodeDeserializesFromAConstJsonValue | 從只讀 Json::Value 反序列化(含缺 optional 鍵 → nullopt) |
JsonValueNodeReportsPathsRangesAndUnsupportedTargets | 嵌套路徑錯誤、缺必選字段、帶小數 / 越界 / 負數進無符號、布爾轉數字、variant 無法匹配(錯誤裡帶 dump())、string_view 的右值來源報生命週期錯誤 |
JsonValueNodeBorrowsStringViewsFromAnLvalueSource | 左值來源下 std::string_view 成員成功借用,並斷言同一個子節點重複借出拿到的是同一份緩存文本 |
JsonValueNodeSupportsTheBuiltInStringifiedTypes | mce::UUID 與 ll::data::Version 經適配類型往返(它們的特化用 std::string_view{j}) |
JsonValueNodeFeedsEveryScalarAccessorOfAVariant | variant 的四個標量訪問器(bool / int64 / uint64 / double)都經適配類型取值 |
PlainVariantDeserializesFromNbt | std::variant 從 NBT 反序列化 |
JsonValueNodeWritesThroughAndSharesItsValue | 寫穿語義、淺拷貝共享、push_back、空 optional 走的 nullptr 構造 |
自己實現時有三個細節值得照抄:子節點緩存要用指針間接(std::map<Json::Value*, std::unique_ptr<Node>>)而不是直接放 std::deque<Node>,否則類自引用會因為類型不完整而編譯失敗,並且要按槽位地址去重(Json::Value 的子節點是堆分配的,地址穩定),不然每訪問一次就多存一份視圖;拷貝構造顯式寫成淺拷貝、賦值運算符顯式寫成寫穿(兩者語義刻意不同)。
什麼時候別這麼做
- 只想在兩個 JSON 表示之間轉一次(例如 Bedrock 的
Json::Value↔nlohmann::json):寫值式互轉特化更省事,測試裡的Serializer<Json::Value, nlohmann::json>就是現成例子。 - 數據源是腳本引擎對象:
JSON.stringify/JSON.parse配serialize<nlohmann::json>/deserialize_to<T>通常比包一層適配便宜得多。 - 只想讓某個 C++ 類型按特定形狀寫進某個目標:那是
Serializer<T, J>的活(它只改變T自己怎麼變成J,成員仍然走內建分支,所以底層協議依舊要滿足)。
變體的匹配規則
std::variant 的反序列化分兩遍:
- 精確匹配:布爾只接受布爾,浮點只接受浮點,有符號只接受有符號整數,無符號只接受無符號整數,且值必須能放進目標類型;多個候選都精確匹配時取聲明順序靠前的。
- 回退匹配:按聲明順序逐個嘗試各分支的類型,此時算術分支走嚴格的數值轉換(越界、帶小數同樣報錯)。
兩遍都失敗時,錯誤裡會帶上輸入類型名和文本,例如 could not cast target object ({"unexpected":true}) to any type in the variant。需要顯式判別字段請按示例 4 自己寫特化。
std::string_view 生命週期
反序列化到 std::string_view 只在輸入可安全借用時成立:
- 從穩定的左值 JSON / NBT 對象讀取,視圖指向其內部存儲;
- 從臨時對象或非借用型後端讀取,返回
field cannot be safely deserialized into string_view from a temporary or non-borrowing source。
這是為了避免懸垂視圖;需要長期持有就改用 std::string。
Dispatcher<T, Listener>
Dispatcher 把一份數據和它的監聽者綁在一起。反射對它的約定是:序列化只寫 storage,反序列化讀回 storage 之後再調用一次 call()。
struct LevelNotifier {
void call(int const& storage) { /* 更新界面、重算缓存、发事件…… */ }
};
using LevelDispatcher = ll::reflection::Dispatcher<int, LevelNotifier>;
struct GameOptions {
LevelDispatcher level{0}; // 构造时不会调用 call()
std::string name{};
};要點:
Listener必須可默認構造、可移動,並提供call(Storage const&);- 只有通過
Dispatcher賦值(disp = value)或反序列化才會調用call(),直接改disp.storage不會觸發; - 第三個模板參數
CallInit(默認false)為true時,構造也會調用一次; - 可隱式轉換回
Storage,能直接當底層值使用; - 監聽者可能持有狀態,因此只可移動、不可拷貝,移動時連同監聽者一起搬走。
測試:SerializeAndDeserializeRespectKeyFormatterAndDispatcher。
診斷與自查
編譯期報錯對照表
| 報錯信息(片段) | 含義 | 怎麼辦 |
|---|---|---|
cannot serialize this type: ... | 沒有任何可用的序列化路徑 | 寫 Serializer<T> 特化;或把類型改成聚合體、改用受支持的成員類型 |
cannot deserialize this type: ... | 同上,反序列化方向 | 同上 |
an associative container key has to be a string, ... | 關聯容器鍵類型不能轉成字符串 | 給鍵類型補 to_string / from_string,或用按類型定製的 formatter |
the key type has to be a string, ... | type_to_string / string_to_type 拿到的類型不支持 | 同上 |
Boost.PFR: ... Please report this issue ... | Boost.PFR 數不清該聚合體的字段(帶基類、成員的轉換構造函數被刪除等) | 給該類型寫 Serializer<T>,不要指望默認反射 |
定位手法
- 先確認是不是"被靜默忽略":臨時在特化里加一句
static_assert(ll::traits::always_false<std::remove_cvref_t<T>>, "");,如果編譯仍然通過,說明框架根本沒選中它。 - 縮小到單個成員:直接用
deserialize_to<MemberType>(json)/serialize<J>(member)單獨驗證,比在大結構體裡試錯快得多。 - 看報的是哪一層:報枚舉值非法、缺字段之類,說明命中的是內建分支而不是你的特化;報
cannot serialize說明所有分支都沒命中。 - 聚合體卻報 PFR 斷言:那是 PFR 自己的字段計數失敗,只能顯式寫特化繞開。
實現註記
- 數值轉換是嚴格的:越界、帶小數、負值寫入無符號類型都會報錯,JSON 與 NBT 行為一致;範圍檢查在
ll::reflection內部實現(MSVC STL 的std::in_range不接受char系列類型)。 - 失敗原子性:值式路徑先在臨時對象裡構造、成功後才寫回目標,失敗時目標保持不變;原地式路徑逐個字段寫入,中途失敗會留下"半寫"狀態。需要強一致就選值式。
- 變體錯誤的構造是懶的:只有所有分支都失敗時才生成帶完整 dump 的錯誤,命中分支不必為此序列化整棵子樹。
- 入口一致性:
deserialize_to與deserialize都會優先使用原地式特化(deserialize_to會先把目標默認構造出來),所以"只寫原地式"的類型必須可默認構造;不可默認構造就寫值式。
相關文檔
- Reflection(反射):第一次使用的用法與最簡單的特化
- Config:配置系統,反射最主要的消費者
- Base:反射依賴的基礎類型工具(概念、容器、
Expected)