We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
使用 CRTP 实现编译期接口定义 https://ift.tt/mJcnloY
有一种类型叫 pure virtual,不仅多态,而且强制要求 Base 类不做实现(子类必须 override),只声明为接口,实现了接口语义
pure virtual
Base
但有时候并不需要多余的多态,我就需要一个类做接口声明
C++ 作为追求 zero overhead 的语言,能不能做到完全不必承担 virtual 开销的接口语义?
C++
virtual
那当然可以,只要你能折腾
PS. 全文废话非常多(如果不愿意看这个东西是怎么来的),解决方案只在正文最后一段
期望自然是做到接口语义,伪代码如下
class Interface { DEFINE int func(); DEFINE void func2(int val); }; class Impl: public Interface { // 不实现 func 和 func2,你就得死 };
class Interface { DEFINE int func(); DEFINE void func2(int val); };
class Impl: public Interface { // 不实现 func 和 func2,你就得死 };
既然是编译时,又是继承,还是接口,很容易想到用 CRTP 惯用法
CRTP
为了简单起见,目前接口只有 void func()
void func()
template <typename Derived> class Interface { private: void __require() { auto that = static_cast<Derived*>(this); that->func(); } };
然后随便写一个测试
// Impl "is-an" Interface class Impl: public Interface<Impl> {}; // 不写实现,直观上应该编译不了 int main() { Impl test; return 0; }
// Impl "is-an" Interface class Impl: public Interface<Impl> {}; // 不写实现,直观上应该编译不了
int main() { Impl test; return 0; }
直观上来说,由于 Impl 继承了奇异递归的 Interface,
Impl
Interface
而 Interface 声明的 __require 中调用了子类 Impl 的 func,
__require
func
目前显然是没有 func 符号,编译期就会无法通过,起到接口约束的作用
要不编译试试?
很不幸,这段代码是完全可以编译的,这个所谓的 CRTP 完全没有实现接口语义的作用
那是因为踩了__模板实例化__的坑:你不用,编译期间就不会实例化,哪怕你调用了 Interface<Impl>,由于没有任何一处调用到 __require(),那么 __require() 部分就不会实例化
Interface<Impl>
__require()
也就是说,你需要在每一个子类中显式地调用到 __require() 才能起到约束作用
template <typename Derived> class Interface { protected: // 需要开放权限为 protected void __require() { auto that = static_cast<Derived*>(this); that->func(); } }; class Impl: public Interface<Impl> { public: void func() {} private: void checkInterface() { // 需要添加一个丑陋的函数 Interface<Impl>::__require(); } }; int main() { Impl test; return 0; }
template <typename Derived> class Interface { protected: // 需要开放权限为 protected void __require() { auto that = static_cast<Derived*>(this); that->func(); } }; class Impl: public Interface<Impl> { public: void func() {} private: void checkInterface() { // 需要添加一个丑陋的函数 Interface<Impl>::__require(); } };
class Impl: public Interface<Impl> { public: void func() {} private: void checkInterface() { // 需要添加一个丑陋的函数 Interface<Impl>::__require(); } };
开放权限不是大事,但是每一个子类都要加一个极具侵入性的函数是非常丑陋的行为
能不能再改进一点?我不希望子类有任何额外的添加
其实思路很简单,把检查的流程写到接口类的构造函数即可
template <typename Derived> class Interface { public: Interface() { __require(); } private: // 回归到 private 了 void __require() { auto that = static_cast<Derived*>(this); that->func(); } }; class Impl: public Interface<Impl> { public: void func() {} };
class Impl: public Interface<Impl> { public: void func() {} };
实现类一下子清爽多了!而且做到了接口约束的效果
上面这段代码忽略的一个很大问题是调用 that->func() 是有__副作用__的
that->func()
怎么解决?
最简单的方法是效仿 linux kernel 里的小技巧:sizeof 是编译阶段的求值,并不会真的执行
linux kernel
sizeof
template <typename Derived> class Interface { public: Interface() { __require(); } private: void __require() { auto that = static_cast<Derived*>(this); sizeof (that->func()); // 严格确保是编译时的行为 } };
但是仍有不足之处:
sizeof(void)
既然如此那就用稍微 modern C++ 的做法:decltype
modern C++
decltype
template <typename Derived> class Interface { public: Interface() { __require(); } private: void __require() { auto that = static_cast<Derived*>(this); decltype(that->func()) * _ { }; // 一段只有谜语人才敢写的玩意 } };
这里的意思是说
nullptr
虽然代码有点令人不适,但它确实是类型安全的
但这代码确实是令人不适,那我们还能换种形式
template <typename Derived> class Interface { public: Interface() { __require(); } private: void __require() { auto that = static_cast<Derived*>(this); using RequireFunc = decltype(that->func()); // 稍微体面一点 } };
那么,这样就能结束了?一个完美的接口类型?
并没有
假设现在要求实现另一个接口
int func2(int, double);
它现在有两个问题
int
double
先来解决 2:使用 std::is_same<> 再一次判断
std::is_same<>
再来解决 1:反正是编译时推导的东西,可以通过默认构造函数解决,能塞进去就算成功
总结如下
template <typename Derived> class Interface { public: Interface() { __require(); } private: void __require() { auto that = static_cast<Derived*>(this); using RequireFunc = decltype(that->func()); static_assert(std::is_same<RequireFunc, void>::value, "please check func"); // 使用默认构造函数,这种统一的方式可以把整个过程封装为 template // 使用 STL 提供的类型萃取轻松完成返回值类型判断 using RequireFunc2 = decltype(that->func2(int{}, double{})); static_assert(std::is_same<RequireFunc2, int>::value, "please check func2"); } }; class Impl: public Interface<Impl> { public: void func() {} int func2(int, double) { return 1; } };
template <typename Derived> class Interface { public: Interface() { __require(); } private: void __require() { auto that = static_cast<Derived*>(this); using RequireFunc = decltype(that->func()); static_assert(std::is_same<RequireFunc, void>::value, "please check func"); // 使用默认构造函数,这种统一的方式可以把整个过程封装为 template // 使用 STL 提供的类型萃取轻松完成返回值类型判断 using RequireFunc2 = decltype(that->func2(int{}, double{})); static_assert(std::is_same<RequireFunc2, int>::value, "please check func2"); } };
using RequireFunc = decltype(that->func()); static_assert(std::is_same<RequireFunc, void>::value, "please check func"); // 使用默认构造函数,这种统一的方式可以把整个过程封装为 template // 使用 STL 提供的类型萃取轻松完成返回值类型判断 using RequireFunc2 = decltype(that->func2(int{}, double{})); static_assert(std::is_same<RequireFunc2, int>::value, "please check func2"); }
};
class Impl: public Interface<Impl> { public: void func() {} int func2(int, double) { return 1; } };
似乎又解决了一个难题?还没完! 假设现在要求实现另外一个接口
void func3(NoDefaultCtor); // NoDefaultCtor 的定义 struct NoDefaultCtor { NoDefaultCtor() = delete; NoDefaultCtor(int) {} };
void func3(NoDefaultCtor);
// NoDefaultCtor 的定义 struct NoDefaultCtor { NoDefaultCtor() = delete; NoDefaultCtor(int) {} };
前面那种基于默认构造函数的做法是无效的
这不是只要传入一个 NoDefaultCtor(1) 就能解决的问题
NoDefaultCtor(1)
它导致的更严重的后果是:无法使用 template 来封装整个静态检查过程
template
一个类被限制了只能用某一种特定的构造函数的情况,是非常难通过模板来推导的
既然类型是个坎,那就得正面从类型去考虑,不要停留在值或者对象的角度
对类型最有表达能力的方式自然是模板的推导
已知:给出函数,通过推导可以得出它的返回类型和参数
怎么做的:见 sRpc/FunctionTraits.h at master · Caturra000/sRpc (github.com)
我在上述链接实现了一个简单的 FunctionTraits,可以推导出各种不同的函数(普通函数、成员函数、lambda、std::function)的元信息
FunctionTraits
lambda
std::function
另一方面:希望通过注册的形式来完成接口检查,由使用方主动告知函数需要的信息,然后对比一下是否匹配(IsSameInterface)即可
IsSameInterface
template <typename T> struct FunctionTraits; // 用于推导函数的 meta-data,包括返回值和函数参数 template <typename Ret, typename ...Args> struct FunctionTraits<Ret(Args...)> { constexpr static size_t ArgsSize = sizeof...(Args); using ReturnType = Ret; using ArgsTuple = std::tuple<Args...>; }; // 针对成员函数的偏特化 template <typename Ret, typename C, typename ...Args> struct FunctionTraits<Ret(C::*)(Args...)>: public FunctionTraits<Ret(Args...)> {}; // 这里的意思是说,如果返回值和函数参数完全一致,那它是 meta-data 匹配了 // 这里 FT 是 FunctionTraits 的实例化 // 此时我们不知道双方的 function 的名字是什么,需要在 Interface 类中主动提供 template <typename FT1, typename FT2> struct IsSameInterface { constexpr static bool value = std::is_same<typename FT1::ReturnType, typename FT2::ReturnType>::value && std::is_same<typename FT1::ArgsTuple, typename FT2::ArgsTuple>::value; }; struct NoDefaultCtor { NoDefaultCtor() = delete; NoDefaultCtor(int) {} }; template <typename Derived> class Interface { public: Interface() { __require(); } private: void __require() { constexpr bool check = IsSameInterface< FunctionTraits<decltype(&Interface::_func3)>, FunctionTraits<decltype(&Derived::func3)>> ::value; static_assert(check, "check func3"); } // 注册一个主动提供的信息,告知 func3 需要什么样的返回值和参数 void _func3(NoDefaultCtor); }; class Impl: public Interface<Impl> { public: void func3(NoDefaultCtor) {}; };
template <typename T> struct FunctionTraits; // 用于推导函数的 meta-data,包括返回值和函数参数 template <typename Ret, typename ...Args> struct FunctionTraits<Ret(Args...)> { constexpr static size_t ArgsSize = sizeof...(Args); using ReturnType = Ret; using ArgsTuple = std::tuple<Args...>; }; // 针对成员函数的偏特化 template <typename Ret, typename C, typename ...Args> struct FunctionTraits<Ret(C::*)(Args...)>: public FunctionTraits<Ret(Args...)> {}; // 这里的意思是说,如果返回值和函数参数完全一致,那它是 meta-data 匹配了 // 这里 FT 是 FunctionTraits 的实例化 // 此时我们不知道双方的 function 的名字是什么,需要在 Interface 类中主动提供 template <typename FT1, typename FT2> struct IsSameInterface { constexpr static bool value = std::is_same<typename FT1::ReturnType, typename FT2::ReturnType>::value && std::is_same<typename FT1::ArgsTuple, typename FT2::ArgsTuple>::value; }; struct NoDefaultCtor { NoDefaultCtor() = delete; NoDefaultCtor(int) {} }; template <typename Derived> class Interface { public: Interface() { __require(); } private: void __require() { constexpr bool check = IsSameInterface< FunctionTraits<decltype(&Interface::_func3)>, FunctionTraits<decltype(&Derived::func3)>> ::value; static_assert(check, "check func3"); } // 注册一个主动提供的信息,告知 func3 需要什么样的返回值和参数 void _func3(NoDefaultCtor); };
// 用于推导函数的 meta-data,包括返回值和函数参数 template <typename Ret, typename ...Args> struct FunctionTraits<Ret(Args...)> { constexpr static size_t ArgsSize = sizeof...(Args); using ReturnType = Ret; using ArgsTuple = std::tuple<Args...>; };
// 针对成员函数的偏特化 template <typename Ret, typename C, typename ...Args> struct FunctionTraits<Ret(C::*)(Args...)>: public FunctionTraits<Ret(Args...)> {};
// 这里的意思是说,如果返回值和函数参数完全一致,那它是 meta-data 匹配了 // 这里 FT 是 FunctionTraits 的实例化 // 此时我们不知道双方的 function 的名字是什么,需要在 Interface 类中主动提供 template <typename FT1, typename FT2> struct IsSameInterface { constexpr static bool value = std::is_same<typename FT1::ReturnType, typename FT2::ReturnType>::value && std::is_same<typename FT1::ArgsTuple, typename FT2::ArgsTuple>::value; };
struct NoDefaultCtor { NoDefaultCtor() = delete; NoDefaultCtor(int) {} };
template <typename Derived> class Interface { public: Interface() { __require(); } private: void __require() { constexpr bool check = IsSameInterface< FunctionTraits<decltype(&Interface::_func3)>, FunctionTraits<decltype(&Derived::func3)>> ::value; static_assert(check, "check func3"); }
// 注册一个主动提供的信息,告知 func3 需要什么样的返回值和参数 void _func3(NoDefaultCtor);
class Impl: public Interface<Impl> { public: void func3(NoDefaultCtor) {}; };
从这里我们已经到了使用模板的方案,除了显得繁琐
但这个可以用宏来缓解一下 __require 流程,毕竟过程是很单调重复的(详略)
现在,前面的问题全部都解决了,通过当前的模板甚至能扩展到处理左右值、const 语义等细节,已经很能打了
const
在上述模板方案中,有一点是模板无法解决的(或者很难处理)
对于 FunctionTraits<decltype(&Interface::_func3)> 这种形式的调用,模板无法区分 _func3 到底是什么
FunctionTraits<decltype(&Interface::_func3)>
_func3
比如要求实现含有重载的接口
void func3(NoDefaultCtor); void func3(int);
这时候就无法解析了,很显然,因为存在二义性
这里提供一种新的处理思路:
如果给出成员函数指针 ReturnType Class::*fptr
ReturnType Class::*fptr
只要有对应的函数参数列表,那么 fptr 都是能匹配上的
fptr
相比上述的解决方案,这里能匹配的原因是多提供了参数的信息,这是基本语法就提供的特性,不需要额外的模板推导
因此换一种方式:不是从函数推导出参数,而是提供参数和利用上述重载特性找出匹配函数
这里重新定义一套 require-define 的步骤,如下所示
require-define
template <typename Ret, typename ...Args> struct Require; template <typename Ret, typename ...Args> struct Require<Ret(Args...)> { template <typename C> constexpr static void define(Ret (C::*fp)(Args...)) {} }; struct NoDefaultCtor { NoDefaultCtor() = delete; NoDefaultCtor(int) {} }; template <typename Derived> class Interface { public: constexpr Interface() { __require(this); } private: // 加上 Interface 参数防止潜在签名冲突(虽然没多大必要) constexpr void __require(Interface*) { Require<void(int, double&)>::define(&Derived::func); Require<void(int, double)>::define(&Derived::func); Require<long()>::define(&Derived::func); Require<int()>::define(&Derived::func2); Require<int(const NoDefaultCtor&)>::define(&Derived::func2); } }; // Impl "is-an" Interface class Impl: public Interface<Impl> { public: void func(int, double&) {} void func(int, double) {} long func() { return 1; } int func2() { return 1; } int func2(const NoDefaultCtor&) { return 1; } };
template <typename Ret, typename ...Args> struct Require; template <typename Ret, typename ...Args> struct Require<Ret(Args...)> { template <typename C> constexpr static void define(Ret (C::*fp)(Args...)) {} }; struct NoDefaultCtor { NoDefaultCtor() = delete; NoDefaultCtor(int) {} }; template <typename Derived> class Interface { public: constexpr Interface() { __require(this); } private: // 加上 Interface 参数防止潜在签名冲突(虽然没多大必要) constexpr void __require(Interface*) { Require<void(int, double&)>::define(&Derived::func); Require<void(int, double)>::define(&Derived::func); Require<long()>::define(&Derived::func); Require<int()>::define(&Derived::func2); Require<int(const NoDefaultCtor&)>::define(&Derived::func2); } };
template <typename Ret, typename ...Args> struct Require<Ret(Args...)> { template <typename C> constexpr static void define(Ret (C::*fp)(Args...)) {} };
template <typename Derived> class Interface { public: constexpr Interface() { __require(this); }
private: // 加上 Interface 参数防止潜在签名冲突(虽然没多大必要) constexpr void __require(Interface*) { Require<void(int, double&)>::define(&Derived::func); Require<void(int, double)>::define(&Derived::func); Require<long()>::define(&Derived::func); Require<int()>::define(&Derived::func2); Require<int(const NoDefaultCtor&)>::define(&Derived::func2); } };
// Impl "is-an" Interface class Impl: public Interface<Impl> { public: void func(int, double&) {} void func(int, double) {} long func() { return 1; } int func2() { return 1; } int func2(const NoDefaultCtor&) { return 1; } };
是不是非常简洁明了?只要 define 匹配上的,那么接口就是合法
define
同样的,它处理了前面所有的问题
至此,总算是折腾出了一套 CRTP 下的静态接口,总结有以下优点:
The text was updated successfully, but these errors were encountered:
No branches or pull requests
使用 CRTP 实现编译期接口定义
https://ift.tt/mJcnloY
前言
有一种类型叫
pure virtual
,不仅多态,而且强制要求Base
类不做实现(子类必须 override),只声明为接口,实现了接口语义但有时候并不需要多余的多态,我就需要一个类做接口声明
C++
作为追求 zero overhead 的语言,能不能做到完全不必承担virtual
开销的接口语义?那当然可以,只要你能折腾
PS. 全文废话非常多(如果不愿意看这个东西是怎么来的),解决方案只在正文最后一段
期望
期望自然是做到接口语义,伪代码如下
初探
既然是编译时,又是继承,还是接口,很容易想到用
CRTP
惯用法为了简单起见,目前接口只有
void func()
然后随便写一个测试
直观上来说,由于
Impl
继承了奇异递归的Interface
,而
Interface
声明的__require
中调用了子类Impl
的func
,目前显然是没有
func
符号,编译期就会无法通过,起到接口约束的作用要不编译试试?
坑 1:模板
很不幸,这段代码是完全可以编译的,这个所谓的
CRTP
完全没有实现接口语义的作用那是因为踩了__模板实例化__的坑:你不用,编译期间就不会实例化,哪怕你调用了
Interface<Impl>
,由于没有任何一处调用到__require()
,那么__require()
部分就不会实例化也就是说,你需要在每一个子类中显式地调用到
__require()
才能起到约束作用开放权限不是大事,但是每一个子类都要加一个极具侵入性的函数是非常丑陋的行为
能不能再改进一点?我不希望子类有任何额外的添加
构造即检查
其实思路很简单,把检查的流程写到接口类的构造函数即可
实现类一下子清爽多了!而且做到了接口约束的效果
坑 2:副作用
上面这段代码忽略的一个很大问题是调用
that->func()
是有__副作用__的怎么解决?
最简单的方法是效仿
linux kernel
里的小技巧:sizeof
是编译阶段的求值,并不会真的执行但是仍有不足之处:
sizeof(void)
并不是特别合法的行为既然如此那就用稍微
modern C++
的做法:decltype
这里的意思是说
nullptr
decltype
同样只是编译时推导类型虽然代码有点令人不适,但它确实是类型安全的
但这代码确实是令人不适,那我们还能换种形式
那么,这样就能结束了?一个完美的接口类型?
并没有
坑 3:参数类型
假设现在要求实现另一个接口
它现在有两个问题
decltype
中表达出int
和double
参数int
先来解决 2:使用
std::is_same<>
再一次判断再来解决 1:反正是编译时推导的东西,可以通过默认构造函数解决,能塞进去就算成功
总结如下
似乎又解决了一个难题?还没完! 假设现在要求实现另外一个接口
前面那种基于默认构造函数的做法是无效的
这不是只要传入一个
NoDefaultCtor(1)
就能解决的问题它导致的更严重的后果是:无法使用
template
来封装整个静态检查过程一个类被限制了只能用某一种特定的构造函数的情况,是非常难通过模板来推导的
模板参数推导
既然类型是个坎,那就得正面从类型去考虑,不要停留在值或者对象的角度
对类型最有表达能力的方式自然是模板的推导
已知:给出函数,通过推导可以得出它的返回类型和参数
怎么做的:见 sRpc/FunctionTraits.h at master · Caturra000/sRpc (github.com)
我在上述链接实现了一个简单的
FunctionTraits
,可以推导出各种不同的函数(普通函数、成员函数、lambda
、std::function
)的元信息另一方面:希望通过注册的形式来完成接口检查,由使用方主动告知函数需要的信息,然后对比一下是否匹配(
IsSameInterface
)即可从这里我们已经到了使用模板的方案,除了显得繁琐
但这个可以用宏来缓解一下
__require
流程,毕竟过程是很单调重复的(详略)现在,前面的问题全部都解决了,通过当前的模板甚至能扩展到处理左右值、
const
语义等细节,已经很能打了坑 4:死于重载
在上述模板方案中,有一点是模板无法解决的(或者很难处理)
对于
FunctionTraits<decltype(&Interface::_func3)>
这种形式的调用,模板无法区分_func3
到底是什么比如要求实现含有重载的接口
这时候就无法解析了,很显然,因为存在二义性
用重载解决重载
这里提供一种新的处理思路:
如果给出成员函数指针
ReturnType Class::*fptr
只要有对应的函数参数列表,那么
fptr
都是能匹配上的相比上述的解决方案,这里能匹配的原因是多提供了参数的信息,这是基本语法就提供的特性,不需要额外的模板推导
因此换一种方式:不是从函数推导出参数,而是提供参数和利用上述重载特性找出匹配函数
这里重新定义一套
require-define
的步骤,如下所示是不是非常简洁明了?只要
define
匹配上的,那么接口就是合法同样的,它处理了前面所有的问题
THE END
至此,总算是折腾出了一套 CRTP 下的静态接口,总结有以下优点:
virtual
开销,完全编译时处理via Caturra’s Blog
September 10, 2024 at 11:29AM
The text was updated successfully, but these errors were encountered: