class 层次结构在编译时的高效配置
Efficient configuration of class hierarchy at compile-time
这个问题专门针对嵌入式硬实时系统上的 C++ 体系结构。这意味着大部分数据结构以及确切的程序流在编译时给出,性能很重要并且可以内联大量代码。解决方案最好只使用 C++03,但也欢迎使用 C++11 输入。
我正在寻找已建立的设计模式和架构问题的解决方案,其中相同的代码库应该被重新用于几个密切相关的产品,而某些部分(例如硬件抽象)必须是不同的。
我最终可能会得到封装在 classes 中的模块的层次结构,假设有 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)
在这里,一些 classes 继承自一个公共基础 class(例如 Toplevel_A
来自 Toplevel_base
),而其他一些根本不需要特化(例如Middle_generic
).
目前我能想到的有以下几种做法:
(A):如果这是一个常规的桌面应用程序,我会使用虚拟继承并在 运行 时间创建实例,使用例如一个抽象工厂。
缺点:但是 *_B
classes 永远不会在产品 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 (or #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
};
缺点:糟糕...,只有在所有其他方法都失败的情况下。
是否有任何(其他)已建立的设计模式或更好的解决方案来解决这个问题,以便编译器可以静态分配尽可能多的对象并内联大部分代码,知道正在构建哪个产品并且将使用哪些 classes?
我会选择 A。在证明这还不够好之前,做出与桌面相同的决定(好吧,当然,在堆栈上分配几千字节,或者使用很多全局变量兆字节大可能 "obvious" 它不会工作)。是的,调用虚函数有一些开销,但我会首先选择最明显和自然的 C++ 解决方案,如果不是 "good enough",然后重新设计(显然,尽早确定性能等,并使用工具就像一个抽样分析器来确定你把时间花在哪里,而不是 "guessing" - 人类被证明是非常糟糕的猜测者)。
如果 A 被证明行不通,我会转向选项 B。这确实不是很明显,但粗略地说,LLVM/Clang 是如何针对硬件和 OS 的组合解决这个问题的,请参见:
https://github.com/llvm-mirror/clang/blob/master/lib/Basic/Targets.cpp
我了解到您有两个重要要求:
- 数据类型在编译时已知
- 程序流在编译时已知
CRTP 不会真正解决您试图解决的问题,因为它允许 HardwareLayer
调用 Sub_generic
、Middle_generic
或 [=20= 上的方法] 我不相信这是你要找的。
使用 Trait pattern (another reference) 可以满足您的两个要求。这是一个证明满足这两个要求的示例。首先,我们定义代表您可能想要支持的两种硬件的空壳。
class Hardware_A {};
class Hardware_B {};
然后让我们考虑一个class,它描述了一个一般情况,它对应于Hardware_A
。
template <typename Hardware>
class HardwareLayer
{
public:
typedef long int64_t;
static int64_t getCPUSerialNumber() {return 0;}
};
现在让我们看看 Hardware_B 的专业化:
template <>
class HardwareLayer<Hardware_B>
{
public:
typedef int int64_t;
static int64_t getCPUSerialNumber() {return 1;}
};
现在,这是 Sub_generic 层中的一个用法示例:
template <typename Hardware>
class Sub_generic
{
public:
typedef HardwareLayer<Hardware> HwLayer;
typedef typename HwLayer::int64_t int64_t;
int64_t doSomething() {return HwLayer::getCPUSerialNumber();}
};
最后,执行两个代码路径并使用两种数据类型的简短 main :
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;
}
现在,如果您的 HardwareLayer 需要保持状态,这是实现 HardLayer 和 Sub_generic 层 classes 的另一种方法。
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();}
};
这是最后一个变体,其中只有 Sub_generic 实现发生了变化:
template <typename Hardware>
class Sub_generic
{
public:
typedef HardwareLayer<Hardware> HwLayer;
typedef typename HwLayer::hwint64_t hwint64_t;
hwint64_t doSomething() {return hw.getCPUSerialNumber();}
private:
HwLayer hw;
};
由于这是针对硬实时嵌入式系统的,通常您会选择 C 类型的解决方案而不是 C++。
对于现代编译器,我会说 c++ 的开销不是那么大,所以这不完全是性能问题,但嵌入式系统往往更喜欢 c 而不是 c++。
您尝试构建的内容类似于 classic 设备驱动程序库(例如用于 ftdi 芯片的库)。
那里的方法(因为它是用 C 编写的)类似于您的 F,但没有编译时选项 - 您将在运行时根据诸如 PID、VID、SN 等之类的东西专门化代码...
现在,如果您要为此使用 C++,模板可能应该是您最后的选择(代码可读性通常比模板给 table 带来的任何优势排名更高)。所以你可能会选择类似于 A 的东西:一个基本的 class 继承方案,但不需要特别花哨的设计模式。
希望这对您有所帮助...
按照与 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 可能由于某些您尚未考虑的原因而复杂得多.. 所以给它一个完整的目录结构来封装它。
首先我想指出的是,你基本上在问题中回答了你自己的问题:-)
接下来我想指出的是在C++
the exact program-flow are given at compile-time, performance is
important and a lot of code can be inlined
称为 模板 。其他利用语言特性而不是构建系统特性的方法将仅作为一种逻辑方式来构建项目中的代码以使开发人员受益。
此外,如其他答案所述,C 对于硬实时系统比 C++ 更常见,在 C习惯上依赖MACROS在编译时做这种优化
最后,您在上面的 B 解决方案下注意到模板专业化很难理解。我认为这取决于您的操作方式以及您的团队在 C++/模板方面的经验。我发现许多 "template ridden" 项目非常难以阅读,它们产生的错误消息充其量是邪恶的,但我仍然设法在我自己的项目中有效地使用模板,因为我在这样做时尊重 KISS 原则.
所以我对你的回答是,选择 B 或放弃 C++ 转而使用 C
我假设这些 class 只需要创建一次,并且它们的实例在整个程序中持续 运行 次。
在这种情况下,我建议使用对象工厂模式,因为工厂只会获得 运行 一次来创建 class。从那时起,专门的 classes 都是已知类型。
这个问题专门针对嵌入式硬实时系统上的 C++ 体系结构。这意味着大部分数据结构以及确切的程序流在编译时给出,性能很重要并且可以内联大量代码。解决方案最好只使用 C++03,但也欢迎使用 C++11 输入。
我正在寻找已建立的设计模式和架构问题的解决方案,其中相同的代码库应该被重新用于几个密切相关的产品,而某些部分(例如硬件抽象)必须是不同的。
我最终可能会得到封装在 classes 中的模块的层次结构,假设有 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)
在这里,一些 classes 继承自一个公共基础 class(例如 Toplevel_A
来自 Toplevel_base
),而其他一些根本不需要特化(例如Middle_generic
).
目前我能想到的有以下几种做法:
(A):如果这是一个常规的桌面应用程序,我会使用虚拟继承并在 运行 时间创建实例,使用例如一个抽象工厂。
缺点:但是
*_B
classes 永远不会在产品 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 (or #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 };
缺点:糟糕...,只有在所有其他方法都失败的情况下。
是否有任何(其他)已建立的设计模式或更好的解决方案来解决这个问题,以便编译器可以静态分配尽可能多的对象并内联大部分代码,知道正在构建哪个产品并且将使用哪些 classes?
我会选择 A。在证明这还不够好之前,做出与桌面相同的决定(好吧,当然,在堆栈上分配几千字节,或者使用很多全局变量兆字节大可能 "obvious" 它不会工作)。是的,调用虚函数有一些开销,但我会首先选择最明显和自然的 C++ 解决方案,如果不是 "good enough",然后重新设计(显然,尽早确定性能等,并使用工具就像一个抽样分析器来确定你把时间花在哪里,而不是 "guessing" - 人类被证明是非常糟糕的猜测者)。
如果 A 被证明行不通,我会转向选项 B。这确实不是很明显,但粗略地说,LLVM/Clang 是如何针对硬件和 OS 的组合解决这个问题的,请参见: https://github.com/llvm-mirror/clang/blob/master/lib/Basic/Targets.cpp
我了解到您有两个重要要求:
- 数据类型在编译时已知
- 程序流在编译时已知
CRTP 不会真正解决您试图解决的问题,因为它允许 HardwareLayer
调用 Sub_generic
、Middle_generic
或 [=20= 上的方法] 我不相信这是你要找的。
使用 Trait pattern (another reference) 可以满足您的两个要求。这是一个证明满足这两个要求的示例。首先,我们定义代表您可能想要支持的两种硬件的空壳。
class Hardware_A {};
class Hardware_B {};
然后让我们考虑一个class,它描述了一个一般情况,它对应于Hardware_A
。
template <typename Hardware>
class HardwareLayer
{
public:
typedef long int64_t;
static int64_t getCPUSerialNumber() {return 0;}
};
现在让我们看看 Hardware_B 的专业化:
template <>
class HardwareLayer<Hardware_B>
{
public:
typedef int int64_t;
static int64_t getCPUSerialNumber() {return 1;}
};
现在,这是 Sub_generic 层中的一个用法示例:
template <typename Hardware>
class Sub_generic
{
public:
typedef HardwareLayer<Hardware> HwLayer;
typedef typename HwLayer::int64_t int64_t;
int64_t doSomething() {return HwLayer::getCPUSerialNumber();}
};
最后,执行两个代码路径并使用两种数据类型的简短 main :
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;
}
现在,如果您的 HardwareLayer 需要保持状态,这是实现 HardLayer 和 Sub_generic 层 classes 的另一种方法。
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();}
};
这是最后一个变体,其中只有 Sub_generic 实现发生了变化:
template <typename Hardware>
class Sub_generic
{
public:
typedef HardwareLayer<Hardware> HwLayer;
typedef typename HwLayer::hwint64_t hwint64_t;
hwint64_t doSomething() {return hw.getCPUSerialNumber();}
private:
HwLayer hw;
};
由于这是针对硬实时嵌入式系统的,通常您会选择 C 类型的解决方案而不是 C++。
对于现代编译器,我会说 c++ 的开销不是那么大,所以这不完全是性能问题,但嵌入式系统往往更喜欢 c 而不是 c++。 您尝试构建的内容类似于 classic 设备驱动程序库(例如用于 ftdi 芯片的库)。
那里的方法(因为它是用 C 编写的)类似于您的 F,但没有编译时选项 - 您将在运行时根据诸如 PID、VID、SN 等之类的东西专门化代码...
现在,如果您要为此使用 C++,模板可能应该是您最后的选择(代码可读性通常比模板给 table 带来的任何优势排名更高)。所以你可能会选择类似于 A 的东西:一个基本的 class 继承方案,但不需要特别花哨的设计模式。
希望这对您有所帮助...
按照与 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 可能由于某些您尚未考虑的原因而复杂得多.. 所以给它一个完整的目录结构来封装它。
首先我想指出的是,你基本上在问题中回答了你自己的问题:-)
接下来我想指出的是在C++
the exact program-flow are given at compile-time, performance is important and a lot of code can be inlined
称为 模板 。其他利用语言特性而不是构建系统特性的方法将仅作为一种逻辑方式来构建项目中的代码以使开发人员受益。
此外,如其他答案所述,C 对于硬实时系统比 C++ 更常见,在 C习惯上依赖MACROS在编译时做这种优化
最后,您在上面的 B 解决方案下注意到模板专业化很难理解。我认为这取决于您的操作方式以及您的团队在 C++/模板方面的经验。我发现许多 "template ridden" 项目非常难以阅读,它们产生的错误消息充其量是邪恶的,但我仍然设法在我自己的项目中有效地使用模板,因为我在这样做时尊重 KISS 原则.
所以我对你的回答是,选择 B 或放弃 C++ 转而使用 C
我假设这些 class 只需要创建一次,并且它们的实例在整个程序中持续 运行 次。
在这种情况下,我建议使用对象工厂模式,因为工厂只会获得 运行 一次来创建 class。从那时起,专门的 classes 都是已知类型。