使用CRTP做更好的封装

使用CRTP做更好的封装

原文链接:http://madmanahong.blog.163.com/blog/static/48850962008994165152/

翻译:疯子阿虹

Better Encapsulation for the Curiously Recurring Template Pattern
使用CRTP做更好的封装

长久以来,C++一直突出于优秀的技巧和典范。老有名气的一个就是James Coplien在1995年提出的奇异递归模板模式(CRTP)。自那以后,CRTP便开始流行并在多个库中使用,尤其是Boost。例如,你可以在Boost.Iterator,Boost.Python或者Boost.Serialization库中看到他们。

在这篇文章中,我假设读者已经熟悉了CRTP。如果你想温习一下的话,我推荐你去阅读《C++模板编程》的第17章。在www.informit.com上,你可以找到该章节的免费版本。

如果你抱着OO的观点去看CRTP的话,你会发现,他和OO框架的有着共同的特点,都是基类调用虚函数,
真正的实现在派生类中。下面是一个最简单的OO框架实现代码:

// Library code
class Base
{
public:
virtual ~Base();
int foo() { return this->do_foo(); }

protected:
virtual int do_foo() = 0;
};


这里,Base::foo调用了一个虚函数do_foo,他是声明在Base类中的一个纯虚函数,而且他必须在基类中实现。也就是说,do_foo的实体出现在Derived类中。

// User code
class Derived : public Base
{
private:
virtual int do_foo() { return 0; }
};

这里有个有意思的地方是do_foo函数必须将访问符从保护修改成私有。这在C++中是比较好的访问控制,同时实现它只需要键入几个简单的字符。为什么要在这里有意强调do_foo不是共有使用呢?理由是一个用户应该尽力隐藏类的实现细节从而使类更加简单。(用户如果觉得这个类没有对外暴露的价值,甚至应该隐藏整个Derived类)。

现在让我们假设,有一些限制性的因素导致virtual函数不能胜任,同时框架的作者决定使用CRTP。


// Library code
template<class DerivedT>
class Base
{
public:
DerivedT& derived() {
return static_cast<DerivedT&>(*this); }
int foo() {
return this->derived().do_foo(); }
};

// User code
class Derived : public Base<Derived>
{
public:
int do_foo() { return 0; }
};

尽管do_foo是同一个实现,但是它可以被任意访问。为什么不将它设置为私有或者保护?答案是在foo函数中调用了Derived::do_foo,或者说,基类直接调用了一个在派生类中的函数。

现在让我们找一个最简单方法,对于Derived的用户隐藏其实现细节。他应该足够简单,否则,用户将不会使用它。对于Base类的作者,这个稍微有些麻烦,但也应该是不难解决的。

最显而易见的方法是在Base类和Derived类之间建立一个友谊关系。

// User code
class Derived : public Base<Derived>
{
private:
friend class Base<Derived>;
int do_foo() { return 0; }
};


这个解决方案并不是很完美,只因为一个简单的理由:每一个Base的模板参数类,都要定义一个friend声明。如果模板参数较多,那么这个声明列表将会很长。

为了解决这个问题,同时将友元列表的长度固定,我们引入一个非模板类Accessor来做一次前向调用。

// Library code
class Accessor
{
private:
template<class> friend class Base;
template<class DerivedT>
static int foo(DerivedT& derived)
{
return derived.do_foo();
}
};


函数Base::foo应该称为Accessor::foo,他用来转发调用至Derived::do_foo。

首先是这个调用链永远会成功,因为Base类是Accessor类的友元。

// Library code
template<class DerivedT>
class Base
{
public:
DerivedT& derived() {
return static_cast<DerivedT&>(*this); }
int foo() {
return Accessor::foo(this->derived()); }
};

其次是当do_foo为公有或者当do_foo是保护同时Accessor类是Derived类的一个友元时才会成功。我们只感兴趣第二种情况。

// User code
class Derived : public Base<Derived>
{
private:
friend class Accessor;
int do_foo() { return 0; }
};

这种方法被boost的多个库使用,譬如:Boost.Python中的def_visitor_access和Boost.Iterator的iterator_core_access都应该被声明为友元,以此来访问用户从def_visitor或者iterator_facade定义的私有函数。


尽管这个解决方案很简单。但是我们还是会有一种方法可以省略友元声明这个列表。在这种情况下,do_foo不能是私有,你必须要把它修改成保护。这其实没什么,因为这两者之间的访问控制差别对于CRTP的用户来说不重要。为什么呢?让我们看一下用户将如何派生于CRTP基类。

class Derived : public Base<Derived> { /* ... */ };

这里,你将把最终类给模板参数列表。任何试图派生于Derived的类都没有太大意义,因为基类Base<Derived>仅仅知道Derived类。

由于我们不用考虑派生问题了,那么我们现在的目标就是如何实现在Base类中访问声明为protected的函数Derived::do_foo。

// User code
class Derived : public Base<Derived>
{
protected:
// No friend declaration here!
int do_foo() { return 0; }
};

通常,你可以在子类中访问基类中一个保护函数。现在的挑战是如何反过来访问。

第一步是显而易见的。因为我们唯一的切入点是一个保护函数可以被Derived的后代访问。

struct BreakProtection : Derived
{
static int foo(Derived& derived) {
/* call do_foo here */ }
};

当我们试图去完成他的实体:
return derived.do_foo();

然而,BreakProtection::foo将会失败,因为根据C++标准,这个是被禁止的。

paragraph 11.5:

When a friend or a member function of a derived class references a protected nonstatic
member of a base class, an access check applies in addition to those described earlier
in clause 11. Except when forming a pointer to member (5.3.1), the access must be through
a pointer to, reference to, or object of the derived class itself
(or any class derived from that class) (5.2.5).


所以这个函数仅能被类型为BreakProtection的物体访问。


好吧,如果一个函数不能直接访问,那我们就间接访问。在BreakProtection类中得到do_foo的地址。

&BreakProtection::do_foo;

BreakProtection中并没有do_foo函数,因此,表达式将会被解析成&Derived:do_foo。

既然公有访问一个指向保护成员函数的指针是允许的,那我们就赶紧调用吧。


struct BreakProtection : Derived
{
static int foo(Derived& derived)
{
int (Derived::*fn)() = &BreakProtection::do_foo;
return (derived.*fn)();
}
};


为了更好的封装,BreakProtection可以被移动到Base模板类的私有节中。最终的解决方案是:


// Library code
template<class DerivedT>
class Base
{
private:
struct accessor : DerivedT
{
static int foo(DerivedT& derived)
{
int (DerivedT::*fn)() = &accessor::do_foo;
return (derived.*fn)();
}
};
public:
DerivedT& derived() {
return static_cast<DerivedT&>(*this); }
int foo() { return accessor::foo(
this->derived()); }
};

// User code
struct Derived : Base<Derived>
protected:
int do_foo() { return 1; }
};

这回用户的代码比第一种解决方案苗条和清晰了,而库代码则没有太大的变动。

尽管如此,但还是有一个瑕疵的地方,大多数编译器不能优化间接的函数指针,即使他就是指向在原本对象的地方。

return (derived.*(&accessor::do_foo))();


无论如何,CRTP的也会比虚函数优化的更好。因为CRTP没有虚函数调用的负担,所以他的效率是很快的。
同时在编译的时候,代码类型是可以被编译器导出的,所以生成的代码也会更小。(当然,上面提到的第二种方法还是有了类型信息。我们希望未来的主流编译器都可以将其优化。)还有就是,使用成员函数指针不是很便利,尤其是重载函数。


参考文档

[Coplien] James O. Coplien. "Curiously Recurring Template Patterns", C++ Report, February 1995.
[Vandevoorde-] David Vandevoorde, Nicolai M. Josuttis. "C++ Templates: The Complete Guide".
http://www.informit.com/articles/article.asp?p=31473
[Boost] Boost libraries. http://www.boost.org.
[standard] ISO-IEC 14882:1998(E),Programming languages - C++.

原文链接:http://accu.org/index.php/journals/296


译者评注:C++中的静多态和CRTP(奇异递归模板模式)已经不像5年前那样令人陌生。记得当时在国内论坛上讨论静多态的时候,很多人都是不约而同的反应出?号。本人第一次接触这些也是在《C++ Template中文版》。还记得那个时候通宵达旦的看完《C++ Template中文版》之后,兴奋的想把所有类都用模板重构。然而做技术和做产品始终不同,做技术可以走在行业的前沿,不断的创新和改造,然而做产品则需要中规中矩,基础稳定,便于维护的简单代码将永远放在首位。所以这几年来对于C++ 0X 的关注越来越少。有时看到一些同事写的代码,我总是特别强调代码的语义。极限编程中说过:“源代码就是注释”。其实这句话并不是强调你不用去写帮助文档,就是强调代码的语义。举一个例子吧:一个类的析构函数到底是否为virutal,并不是取决你当时的心情。STL中的容器类的析构函数都不会定义virtual,是因为没有这个关键字,就明确告诉你了这个类不希望被继承。这其实就是代码的语义。之所以翻译这篇文章,并不是想向大家讲述CRTP,而是翻译了作者的态度。实现静多态很简单,但是实现好的CRTP却需要注意太多细节。本文从开始提出了一个想法,到最后通过技巧来写出了一个方法,直至最后完善成几乎完美,这无处不体现着国外技术高手的对于技术一丝不苟的精神。想想为什么我们永远不可能写出来《Modern C++ Design》那样惊世骇俗的宝典?也许当自己有一天也习惯了利用随想设计,利用口语编程,同时对手下指指点点的时候,道理就会不言而喻了。


关键词:CRTP 奇异递归模板模式 封装 静多态

Oct 09th, 2008 - Gnoha

<think>嗯,用户问的是在给STM32H743ZIT6编写多个电机的C++驱动时,使用继承好不好,需要分析原因。首先,我需要回忆一下C++中继承的概念和适用场景。继承通常用于实现“是一个”的关系,也就是子类是父类的一种特殊化。比如,如果不同的电机类型有共同的接口和部分实现,那么用继承可能合适。 不过,用户提到的多个电机驱动,可能这些电机有不同的类型,比如步进电机、直流电机、伺服电机等。这时候,它们可能有共同的函数,比如启动、停止、设置速度等,但具体实现不同。所以,用继承的话可以定义一个基类Motor,然后各个子类继承并实现虚函数。这样可以利用多态性,统一管理不同的电机对象,放在一个数组或列表中,方便调用共同的方法。 不过,继承也有一些缺点。比如,如果不同电机之间的差异很大,或者有些电机需要额外的功能,可能会导致基类变得臃肿,或者子类需要重写很多方法,甚至可能破坏里氏替换原则。另外,C++中的虚函数会带来一定的运行时开销,虽然对于电机控制来说可能影响不大,但在实时性要求高的嵌入式系统中,可能需要考虑性能问题。 另外,还有组合的方式,也就是使用策略模式,将不同的功能模块作为成员对象组合到主类中。这样可能更灵活,避免继承层次过深的问题。比如,每个电机驱动类可以包含一个PWM控制器、一个GPIO配置等,而不是通过继承来获得这些功能。 再考虑STM32H743ZIT6的具体情况,它是高性能的MCU,资源相对充足,但嵌入式环境下依然需要注意代码效率和资源使用。继承带来的虚函数表可能会增加内存占用,不过对于少量电机对象来说应该可以接受。另外,C++在嵌入式系统中的使用需要注意异常处理和RTTI(运行时类型信息)是否启用,通常这些会被禁用,但虚函数本身不依赖RTTI。 可能用户希望代码结构清晰,易于扩展和维护。使用继承可以提供一个清晰的层次结构,方便后续添加新的电机类型。但若电机之间的共同点不多,或者需要频繁变更接口,组合可能更合适。 总结的话,使用继承的好处是代码复用、结构清晰、利用多态统一管理;潜在的缺点是虚函数开销、设计不当可能导致层次复杂。需要根据具体电机类型的相似度、系统对性能的要求等因素来决定。如果电机类型有高度一致的接口,继承是好的选择;如果差异较大,组合可能更好。</think>在STM32H743ZIT6中为多个电机编写C++驱动时,**使用继承是合理的选择,但需结合具体场景权衡利弊**。以下是详细分析: --- ### **适合使用继承的场景及优势** 1. **共性抽象与代码复用** 如果多个电机驱动(如步进电机、直流电机、BLDC电机)有**共同的操作接口**(如 `启动`、`设置速度`、`停止`),可通过基类(如 `MotorBase`)抽象公共逻辑,子类继承后仅实现差异部分。例如: ```cpp class MotorBase { public: virtual void setSpeed(uint32_t rpm) = 0; virtual void enable() = 0; virtual void disable() = 0; // 公共工具函数(如CRC校验) }; class StepperMotor : public MotorBase { // 实现步进电机特有逻辑 }; ``` 2. **多态统一管理** 通过基类指针或引用,可将不同电机对象存入同一容器统一操作,简化代码结构: ```cpp std::vector<MotorBase*> motors; motors.push_back(new StepperMotor()); motors.push_back(new DCMotor()); for (auto motor : motors) { motor->enable(); } ``` 3. **扩展性与维护性** 新增电机类型时只需继承基类,符合开闭原则(OCP),降低耦合度。 --- ### **不适合使用继承的场景及替代方案** 1. **电机驱动差异过大** 若不同电机硬件接口(如PWM、CAN、SPI)或控制算法(FOC、步进脉冲)差异显著,强行继承会导致基类冗余或虚函数过多。此时可用**组合模式**: ```cpp class DCMotor { public: DCMotor(PWMDriver& pwm, EncoderInterface& encoder) : pwm_(pwm), encoder_(encoder) {} // 实现直流电机逻辑 }; ``` 2. **实时性敏感操作** 虚函数调用存在**运行时开销**(查虚函数表),对实时性要求极高的控制环(如电流环)可能不适用。可改用**静态多态(CRTP)** 或 **策略模式**: ```cpp template <typename ConcreteMotor> class MotorController { void controlLoop() { static_cast<ConcreteMotor*>(this)->applyControl(); } }; class BLDCmotor : public MotorController<BLDCmotor> { void applyControl() { /* FOC算法实现 */ } }; ``` 3. **硬件资源限制** 若继承层次过深,可能增加代码体积(虚表占用Flash)。STM32H743虽资源充足,但需注意代码优化(如 `-fno-rtti` 禁用RTTI)。 --- ### **推荐实践** 1. **分层设计** - **硬件抽象层(HAL)**:封装STM32外设(如TIM、GPIO)。 - **驱动层**:通过继承或组合实现电机控制逻辑。 - **应用层**:调用统一接口管理多电机。 2. **混合模式** ```cpp class MotorInterface { public: virtual void update() = 0; // 通用接口 }; class StepperMotor : public MotorInterface { private: StepPulseGenerator pulseGen; // 组合步进脉冲策略 public: void update() override { pulseGen.generate(); } }; ``` --- ### **结论** - **优先使用继承**:若电机类型共享明确的逻辑层次,且需多态管理。 - **改用组合/模板**:若硬件接口或算法差异大,或对性能敏感。 - **结合STM32H743特性**:利用其高性能内核(Cortex-M7)和硬件FPU,平衡设计灵活性与实时性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值