在编译时有效配置类层次结构

时间:2015-04-01 21:25:59

标签: c++ inheritance architecture embedded real-time

这个问题是关于嵌入式硬实时系统的C ++架构。这意味着在编译时给出了大部分数据结构以及确切的程序流,性能很重要,并且可以内联大量代码。解决方案最好只使用C ++ 03,但也欢迎使用C ++ 11输入。

我正在寻找建筑问题的既定设计模式和解决方案,其中相同的代码库应该被重复用于几个密切相关的产品,而某些部分(例如硬件抽象)必然是不同的。

我可能最终得到一个封装在类中的模块的层次结构,这些模块可能看起来像这样,假设有4层:

Product A                       Product B

Toplevel_A                      Toplevel_B                  (different for A and B, but with common parts)
    Middle_generic                  Middle_generic          (same for A and B)
        Sub_generic                     Sub_generic         (same for A and B)
            Hardware_A                      Hardware_B      (different for A and B)

此处,某些类继承自公共基类(例如Toplevel_A中的Toplevel_base),而其他类则根本不需要专门化(例如Middle_generic)。

目前我可以考虑以下方法:

  • (A):如果这是常规桌面应用程序,我会使用虚拟继承并在运行时创建实例,例如使用抽象工厂。

    缺点 :但是*_B类永远不会在产品A中使用,因此所有虚拟函数调用和未链接到的成员的解除引用在运行时的地址将导致相当大的开销。

  • (B)使用模板专精作为继承机制(例如CRTP

    template<class Derived>
    class Toplevel  { /* generic stuff ... */ };
    
    class Toplevel_A : public Toplevel<Toplevel_A> { /* specific stuff ... */ };
    

    缺点 :难以理解。

  • (C):使用不同的匹配文件集,让构建脚本包含正确的文件

    // common/toplevel_base.h
    class Toplevel_base { /* ... */ };
    
    // product_A/toplevel.h
    class Toplevel : Toplevel_base { /* ... */ };
    
    // product_B/toplevel.h
    class Toplevel : Toplevel_base { /* ... */ };
    
    // build_script.A
    compiler -Icommon -Iproduct_A
    

    缺点 :令人困惑,难以维护和测试。

  • (D):一个大型typedef(或#define)文件

    //typedef_A.h
    typedef Toplevel_A Toplevel_to_be_used;
    typedef Hardware_A Hardware_to_be_used;
    // etc.
    
    // sub_generic.h
    class sub_generic {
        Hardware_to_be_used the_hardware;
        // etc.
    };
    

    缺点 :一个文件要包含在任何地方,并且仍然需要另一种机制来实际切换不同的配置。

  • (E):类似的"Policy based"配置,例如

    template <class Policy>
    class Toplevel { 
        Middle_generic<Policy> the_middle;
        // ...
    };
    
    // ...
    
    template <class Policy>
    class Sub_generic {
        class Policy::Hardware_to_be_used the_hardware;
        // ... 
    };
    
    // used as
    class Policy_A {
        typedef Hardware_A Hardware_to_be_used;
    };
    Toplevel<Policy_A> the_toplevel;
    

    缺点 :现在一切都是模板;每次都需要重新编译很多代码。

  • (F):编译器开关和预处理器

    // sub_generic.h
    class Sub_generic {
        #if PRODUCT_IS_A
            Hardware_A _hardware;
        #endif
        #if PRODUCT_IS_B
            Hardware_B _hardware;
        #endif
    };
    

    缺点 :Brrr ...,只有在其他所有方法都失败的情况下才会这样做。

是否存在任何(其他)已建立的设计模式或更好的解决方案,以便编译器可以静态分配尽可能多的对象并内联大部分代码,知道正在构建哪个产品以及哪些类将被使用?

6 个答案:

答案 0 :(得分:7)

我要去A.直到它被证明这不够好,做出与桌面相同的决定(当然,在堆栈上分配几千字节,或者使用全局很多兆字节的变量可能是“显而易见的”,它无法正常工作。是的,在调用虚函数时会有一些开销,但我会先选择最明显和最自然的C ++解决方案,然后重新设计它是否足够好&#34; (显然,尝试尽早确定性能等,并使用像采样分析器这样的工具来确定你花时间在哪里,而不是猜测&#34; - 人类被证明是相当差的猜测者。)

如果证明A不起作用,我会转到选项B.这确实不是很明显,但大致上,LLVM / Clang如何解决硬件和操作系统组合的问题,请参阅: https://github.com/llvm-mirror/clang/blob/master/lib/Basic/Targets.cpp

答案 1 :(得分:3)

我知道您有两个重要的要求:

  1. 数据类型在编译时已知
  2. 程序流在编译时已知
  3. CRTP无法真正解决您尝试解决的问题,因为它允许HardwareLayer调用Sub_genericMiddle_generic或{{1}上的方法我并不相信这就是你要找的东西。

    使用Trait patternanother reference)可以满足您的所有要求。这是一个证明满足这两个要求的例子。首先,我们定义代表您可能想要支持的两个硬件的空壳。

    TopLevel

    然后让我们考虑一个描述与class Hardware_A {}; class Hardware_B {}; 对应的一般情况的类。

    Hardware_A

    现在让我们看看Hardware_B的专业化:

    template <typename Hardware>
    class HardwareLayer
    {
    public:
        typedef long int64_t;
    
        static int64_t getCPUSerialNumber() {return 0;}
    };
    

    现在,这是Sub_generic层中的一个用法示例:

    template <>
    class HardwareLayer<Hardware_B>
    {
    public:
        typedef int int64_t;
    
        static int64_t getCPUSerialNumber() {return 1;}
    };
    

    最后,一个执行两个代码路径并使用这两种数据类型的短主体:

    template <typename Hardware>
    class Sub_generic
    {
    public:
        typedef HardwareLayer<Hardware> HwLayer;
        typedef typename HwLayer::int64_t int64_t;
    
        int64_t doSomething() {return HwLayer::getCPUSerialNumber();}
    };
    

    现在,如果您的HardwareLayer需要维护状态,这是另一种实现HardLayer和Sub_generic图层类的方法。

    int main(int argc, const char * argv[]) {
        std::cout << "Hardware_A : " << Sub_generic<Hardware_A>().doSomething() << std::endl;
        std::cout << "Hardware_B : " << Sub_generic<Hardware_B>().doSomething() << std::endl;
    }
    

    这是最后一个变体,只有Sub_generic实现发生了变化:

    template <typename Hardware>
    class HardwareLayer
    {
    public:
        typedef long hwint64_t;
    
        hwint64_t getCPUSerialNumber() {return mySerial;}
    
    private:
        hwint64_t mySerial = 0;
    };
    
    template <>
    class HardwareLayer<Hardware_B>
    {
    public:
        typedef int hwint64_t;
    
        hwint64_t getCPUSerialNumber() {return mySerial;}
    
    private:
        hwint64_t mySerial = 1;
    };
    
    template <typename Hardware>
    class Sub_generic : public HardwareLayer<Hardware>
    {
    public:
        typedef HardwareLayer<Hardware> HwLayer;
        typedef typename HwLayer::hwint64_t hwint64_t;
    
        hwint64_t doSomething() {return HwLayer::getCPUSerialNumber();}
    };
    

答案 2 :(得分:3)

首先我想指出你在问题中基本回答了你自己的问题: - )

接下来我想指出 C ++

  

确切的程序流是在编译时给出的,性能是   很重要,可以内联很多代码

称为模板。利用语言功能而不是构建系统功能的其他方法仅作为在项目中构建代码的逻辑方式,以使开发人员受益。

此外,正如其他答案所述, C 对于硬实时系统比 C ++ 更常见,而在 C 中它是习惯依赖 MACROS 在编译时进行这种优化。

最后,您已经在上面的 B 解决方案中注意到,模板专业化很难理解。我认为这取决于你是如何做到的,也取决于你的团队在C ++ /模板上的经验。我发现很多&#34;模板骑行&#34;项目非常难以阅读,而且它们产生的错误消息充其量只是邪恶,但我仍然设法在我自己的项目中有效地使用模板,因为我在做这个时尊重KISS原则。

所以我的答案是, B 或者 C C

答案 3 :(得分:2)

由于这是一个硬实时嵌入式系统,通常你会选择C类型的解决方案而不是c ++。

对于现代编译器,我说c ++的开销不是很大,所以它不完全是性能问题,但嵌入式系统往往更喜欢c而不是c ++。 您要构建的内容类似于经典的设备驱动程序库(就像ftdi芯片一样)。

那里的方法(因为它用C编写)类似于你的F,但是没有编译时选项 - 你会在运行时根据像PID,VID,SN这样的东西来专门化代码等等......

现在,如果您要使用c ++,模板可能应该是您的最后一个选项(代码可读性通常高于任何有利模板带来的表格)。所以你可能会选择类似于A的东西:一个基本的类继承方案,但不需要特别花哨的设计模式。

希望这会有所帮助......

答案 4 :(得分:2)

在与F类似的思路上,您可以拥有如下目录布局:

Hardware/
  common/inc/hardware.h
  hardware1/src/hardware.cpp
  hardware2/src/hardware.cpp

简化接口以仅假设存在单个硬件:

// sub_generic.h
class Sub_generic {
        Hardware _hardware;
};

然后只编译包含该平台硬件的.cpp文件的文件夹。

这种方法的好处是:

  • 很容易理解发生的事情并添加硬件3
  • hardware.h仍然作为您的API
  • 它消除了编译器的抽象(为了你的速度问题)
  • 编译器1不需要编译hardware2.cpp或hardware3.cpp,它可能包含编译器1无法执行的操作(如内联汇编或其他一些特定的编译器2)
  • hardware3可能因为某些原因而变得复杂得多,因为你还没有考虑过......所以给它一个完整的目录结构封装它。

答案 5 :(得分:0)

我将假设这些类只需要创建一次,并且它们的实例在整个程序运行时都会持续存在。

在这种情况下,我建议使用Object Factory模式,因为工厂只会运行一次来​​创建类。从那时起,专业类都是已知的类型。