C++模板元编程在项目中的应用
近期同事问我如何统一Get/Set接口,大概如下:
struct Class { void SetTimeStamp(timeval t); timeval GetTimeStamp(); void SetFlag(uint32_t); uint32_t GetFlag(); ... };
这种代码有很多Get/Set全都平铺到一个class了,后续随着业务还会扩展,接口也不统一,如何统一接口呢?
然后他想了一个办法,利用C++多态特性,可以把所有数据类型统一基类,然后基类指针就能存到关联容器里面了,从而统一接口GetData/SetData,代码大概如下:
enum DataInfoType { DATA_GRADE, // <int32_t> DATA_FLAG, // <uint32_t> }; struct Data {}; template <typename T> struct DataImpl: Data { T val; }; template <typename T> void SetData(DataInfoType type, T val) { // map<DataInfoType, std::shared_ptr<Data>> keyMap keyMap[type] = std::make_shared<DataImpl<T>>(val); } template <typename T> T GetData(DataInfoType type) { // 基类到子类转换,用static_cast不安全!项目禁用rtti特性,不能使用安全的dynamic_cast return static_pointer_cast<DataImpl<T>>(keyMap[type])->val; }
这样做虽然统一接口了,但是不完全,因为从用户角度来说,需要同时指定DataInfoType和对应的类型,倘若用户不小心写错类型了,也能顺利编译通过,难以排查,如下:
float x = 3.14; SetData(DATA_GRADE, x); char y = GetData(DATA_GRADE);
在SetData的地方,没法检查DataInfoType和对应的类型是否匹配,当用户无意传入float类型,也能顺利存储值,最致命的是取出来的地方,由于static_cast导致从基类到具体子类的不安全转换,最终行为也是未定义的。
没法保证用户存入的类型和DataInfoType和取出来的类型互相匹配,导致未定义行为。
这时候可以上模板元技术了,来做编译期检查,一旦出现这种场景,导致编译不通过。
首先需要定义DataInfoType对应的类型,通过Traits类来绑定枚举与类型,输入枚举,输出类型:
enum DataInfoType { DATA_GRADE, // <int32_t> DATA_FLAG, // <uint32_t> }; template<typename T> struct Return { using type = T; } template<DataInfoType> struct InfoTypeTraits; template<DataInfoType enumt> using InfoTypeTraitsT = typename InfoTypeTraits<enumt>::type; template<> struct InfoTypeTraits<DATA_GRADE> : Return<int32_t> { }; template<> struct InfoTypeTraits<DATA_FLAG> : Return<uint32_t> { };
然后提供一个Checkers类来提供DataInfoType与对应类型的检查:
template<DataInfoType type, typename T> struct Checkers: std::is_same<T, InfoTypeTraitsT<type>> { };
最后提供用于编译期检查的Get/Set方法:
template<DataInfoType type, typename T> void SetData(T&& val) { // 编译期检查 static_assert(Checkers<type, std::decay_t<T>>::value, "SetData type differ!"); keyMap[type] = std::make_shared<DataImpl<T>>(val); } template<DataInfoType type> InfoTypeTraitsT<type> GetData() { return static_pointer_cast<DataImpl<T>>(keyMap[type])->val; }
这样Set接口用户只需要提供DataInfoType和对应类型的值,由编译期检查来避免这种低级错误导致需要在运行期抓日志定位,从源头避免问题;其次Get接口只需要提供DataInfoType,不需要显式指定对应的类型了:
// float fx = 23.0; // SetData<DATA_GRADE>(fx); // error: checker.cpp:28:5: error: static_assert failed due to requirement 'Checkers<DATA_GRADE, float>::value' "SetData type differ!" // static_assert(Checkers<type, std::decay_t<T>>::value, "SetData type differ!"); // ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // checker.cpp:41:5: note: in instantiation of function template specialization 'SetData<DATA_GRADE, float &>' requested here // SetData<DATA_GRADE>(fx); int32_t x = 0; SetData<DATA_GRADE>(x); auto y = GetData<DATA_GRADE>();
而后续扩展枚举类型,只需要新增对应的Traits类定义即可:
struct Test { int x, y; }; template<> struct InfoTypeTraits<DATA_TEST> { using type = Test; }; // char z = GetData<DATA_TEST>(); // checker.cpp:58:10: error: no viable conversion from 'InfoTypeTraitsT<(DataInfoType)2U>' (aka 'Test') to 'char' // char z = GetData<DATA_TEST>(); auto z = GetData<DATA_TEST>();
关于模板元技术,后续我会继续分享用其解决类型不同导致的代码重复问题。
----------------------
作者:netcan
- 点赞
- 收藏
- 关注作者
评论(0)