C++中实现具备编译期灵活性的基于特性的设计
实现支持可选特性编译的位掩码设计
问题描述
我需要实现基于特性的设计,每个特性会被添加到类的位掩码FeatureList中。要求即使类依赖的个别特性未被编译,类仍能正常编译且不执行无效操作。
原有写法(存在编译问题)
如果PositionFeature未被编译,下面的代码会直接编译失败:
// 若PositionFeature未编译则无法通过编译! #include "PositionFeature.h" class Object { Object() { FeatureList.AddFlag(RegisteredTypeIndex<PositionFeature>); } }; ENGINE_REGISTER_TYPE(Object)
期望写法(支持可选编译)
即使PositionFeature未被编译,代码仍能正常编译,且调用AddFlag时不执行任何操作:
// 无PositionFeature时也能编译,且不执行任何操作! class PositionFeature; class Object { Object() { AddFeatureFlag<PositionFeature>(FeatureList); } }; ENGINE_REGISTER_TYPE(Object)
当前的类型注册表无法识别未注册但名称一致的特性(会将未定义的类型视为不同特性导致编译失败),需要修改实现以支持这种编译期灵活性。
当前注册表实现代码
#pragma once #include <cstddef> #include <type_traits> #include <utility> template <typename T> struct tag { using type = T; }; template <typename...> struct type_list {}; namespace List { // 返回元素在type_list中的索引,不存在则报错 template <typename L, typename T> struct find_type {}; template <typename T> struct find_type<type_list<>, T> {}; template <typename F, typename ...P, typename T> struct find_type<type_list<F, P...>, T> : std::integral_constant<std::size_t, 1 + find_type<type_list<P...>, T>::value> {}; template <typename F, typename ...P> struct find_type<type_list<F, P...>, F> : std::integral_constant<std::size_t, 0> {}; } namespace StatefulList { namespace impl { template <typename Name, std::size_t Index> struct ElemReader { // 屏蔽GCC警告 #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnon-template-friend" #endif friend constexpr auto adl_ListElem(ElemReader<Name, Index>); #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic pop #endif }; template <typename Name, std::size_t Index, typename Value> struct ElemWriter { friend constexpr auto adl_ListElem(ElemReader<Name, Index>) { return tag<Value>{}; } }; constexpr void adl_ListElem() {} // 占位ADL目标 template <typename Name, std::size_t Index, typename Unique, typename = void> struct CalcSize : std::integral_constant<std::size_t, Index> {}; template <typename Name, std::size_t Index, typename Unique> struct CalcSize < Name, Index, Unique, decltype(void(adl_ListElem(ElemReader<Name, Index>{}))) > : CalcSize<Name, Index + 1, Unique> {}; template <typename Name, std::size_t Index, typename Unique> using ReadElem = typename decltype(adl_ListElem(ElemReader<Name, Index>{}))::type; template <typename Name, typename I, typename Unique> struct ReadElemList {}; template <typename Name, std::size_t ...I, typename Unique> struct ReadElemList<Name, std::index_sequence<I...>, Unique> { using type = type_list<ReadElem<Name, I, Unique>...>; }; } struct DefaultUnique {}; template <typename T> struct DefaultPushBackUnique {}; // 计算当前列表大小 template <typename Name, typename Unique = DefaultUnique> inline constexpr std::size_t size = impl::CalcSize<Name, 0, Unique>::value; // 向列表追加Value template <typename Name, typename Value, typename Unique = DefaultPushBackUnique<Value>> using PushBack = impl::ElemWriter<Name, size<Name, Unique>, Value>; // 返回指定位置的元素类型,不存在则触发SFINAE template <typename Name, std::size_t I, typename Unique = DefaultUnique> using Elem = impl::ReadElem<Name, I, Unique>; // 返回列表所有元素的type_list template <typename Name, typename Unique = DefaultUnique> using Elems = typename impl::ReadElemList<Name, std::make_index_sequence<size<Name, Unique>>, Unique>::type; } // 通用类型计数器标签 struct CounterCommonTag {}; // 返回T的注册索引,未注册则报错 template <typename T> constexpr std::size_t RegisteredTypeIndex = List::find_type<StatefulList::Elems<CounterCommonTag, T>, T>::value; // 返回已注册类型总数 // 注意:需在所有注册完成后再使用,否则值会固定;可通过传入不同Unique类型强制重新计算 template <typename Unique = StatefulList::DefaultUnique> constexpr std::size_t RegisteredTypeCount = StatefulList::size<CounterCommonTag, Unique>; // 注册类型宏 #define ENGINE_REGISTER_TYPE(type_) static_assert((void(StatefulList::PushBack<CounterCommonTag, type_>{}), true));
解决方案
核心思路是利用**SFINAE(替换失败并非错误)**实现编译期分支:当特性类型未注册或未定义时,跳过AddFlag操作;当类型存在且已注册时,正常添加位掩码。
步骤1:修改List::find_type支持SFINAE
让find_type在类型不存在时不直接报错,而是提供可检测的标记:
namespace List { // 基础模板:默认标记为不存在 template <typename L, typename T, typename = void> struct find_type : std::false_type { static constexpr std::size_t value = static_cast<std::size_t>(-1); // 用无效索引标记不存在 }; // 特化:类型存在于列表头部 template <typename F, typename ...P> struct find_type<type_list<F, P...>, F, void> : std::integral_constant<std::size_t, 0> {}; // 特化:递归查找剩余列表 template <typename F, typename ...P, typename T> struct find_type<type_list<F, P...>, T, std::enable_if_t<!std::is_same_v<F, T>>> : std::integral_constant<std::size_t, 1 + find_type<type_list<P...>, T>::value> {}; // 辅助模板:检测类型是否在列表中 template <typename L, typename T> constexpr bool contains_type = find_type<L, T>::value != static_cast<std::size_t>(-1); }
步骤2:添加安全的特性索引获取函数
创建SafeRegisteredTypeIndex模板,未注册类型返回无效值:
// 安全获取类型索引:未注册则返回-1 template <typename T, typename = void> constexpr std::size_t SafeRegisteredTypeIndex = static_cast<std::size_t>(-1); template <typename T> constexpr std::size_t SafeRegisteredTypeIndex<T, std::enable_if_t<List::contains_type<StatefulList::Elems<CounterCommonTag>, T>>> = List::find_type<StatefulList::Elems<CounterCommonTag>, T>::value;
步骤3:封装条件添加特性的辅助函数
编写模板函数,仅当类型有效且已注册时调用AddFlag:
// 条件添加特性标记:仅当特性已注册时执行 template <typename Feature> constexpr void AddFeatureFlag(FeatureList& list) { constexpr auto idx = SafeRegisteredTypeIndex<Feature>; if constexpr (idx != static_cast<std::size_t>(-1)) { list.AddFlag(idx); } // 未注册时无操作 }
步骤4:兼容前向声明类型
修改StatefulList的ReadElem实现,避免前向声明类型触发编译错误:
namespace StatefulList { namespace impl { // 修改ReadElem,支持前向声明类型的SFINAE检测 template <typename Name, std::size_t Index, typename Unique, typename = void> struct ReadElemHelper { using type = void; // 占位类型 }; template <typename Name, std::size_t Index, typename Unique> struct ReadElemHelper<Name, Index, Unique, decltype(void(adl_ListElem(ElemReader<Name, Index>{})))> { using type = typename decltype(adl_ListElem(ElemReader<Name, Index>{}))::type; }; template <typename Name, std::size_t Index, typename Unique> using ReadElem = typename ReadElemHelper<Name, Index, Unique>::type; // 过滤type_list中的void类型 template <typename L> struct FilterVoid {}; template <> struct FilterVoid<type_list<>> { using type = type_list<>; }; template <typename T, typename... Rest> struct FilterVoid<type_list<T, Rest...>> { using type = std::conditional_t<std::is_same_v<T, void>, typename FilterVoid<type_list<Rest...>>::type, type_list<T, typename FilterVoid<type_list<Rest...>>::type>>; }; } // 修改Elems为过滤无效void类型后的列表 template <typename Name, typename Unique = DefaultUnique> using ElemsFiltered = typename impl::FilterVoid<impl::ReadElemList<Name, std::make_index_sequence<size<Name, Unique>>, Unique>::type>::type; } // 更新SafeRegisteredTypeIndex依赖为过滤后的列表 template <typename T> constexpr std::size_t SafeRegisteredTypeIndex<T, std::enable_if_t<List::contains_type<StatefulList::ElemsFiltered<CounterCommonTag>, T>>> = List::find_type<StatefulList::ElemsFiltered<CounterCommonTag>, T>::value;
最终使用示例
现在可以直接使用前向声明的特性类型,无需包含头文件:
// 仅前向声明,无需包含PositionFeature.h class PositionFeature; class VelocityFeature; // 假设这个特性未被编译 class Object { Object() { // 仅当PositionFeature已注册时添加标记 AddFeatureFlag<PositionFeature>(FeatureList); // VelocityFeature未注册,此调用无操作 AddFeatureFlag<VelocityFeature>(FeatureList); } }; ENGINE_REGISTER_TYPE(Object)
关键改动说明
- SFINAE支持:通过
std::enable_if_t和模板特化,让编译期能够判断类型是否存在于注册表中,避免直接报错。 - 无效索引标记:用
static_cast<std::size_t>(-1)作为未注册类型的索引,方便在constexpr if中判断分支。 - 条件执行:利用C++17的
constexpr if实现编译期分支,未注册的特性不会生成任何代码。 - 前向声明兼容:修改
ReadElem的实现,避免前向声明类型触发编译错误。
内容的提问来源于stack exchange,提问作者ingotangjingle
相关产品推荐
相关产品推荐

