C++03 用模板元编程替换预处理器指令

C++03 Replace Preprocessor Directives with Template Metaprogramming

提问人:willdo 提问时间:8/17/2022 最后编辑:willdo 更新时间:8/18/2022 访问量:176

问:

我有一个嵌入式 C++03 代码库,它需要支持不同的小工具供应商,但一次只能支持一个。大多数功能在几个小工具之间重叠,但有一些独家功能,这些独家功能正在产生我需要解决的问题。

下面是一个使用预处理器条件的笨拙代码示例:

#define HW_TYPE1    0
#define HW_TYPE2    1

#define HW_TYPE HW_TYPE1

struct  GadgetBase  {
    void    FncA();
    // Many common methods and functions
    void    FncZ();
};
#if HW_TYPE==HW_TYPE2
struct  Gadget  :   public  GadgetBase  {
    bool    Bar()       {return(true);}
};
#else
struct  Gadget  :   public  GadgetBase  {
    bool    Foo()       {return(false);}
};
#endif

Gadget  A;

#if HW_TYPE==HW_TYPE2
bool    Test()  {return(A.Bar());}
#else
bool    Test()  {return(A.Foo());}

这是我尝试在没有预处理器指令的情况下将上述代码转换为 C++ 模板的尝试。 由于在我的特定平台上定义错误,以下代码无法编译,因为 either 或 未定义取决于 的值。Test()Foo()Bar()Type

enum    TypeE   {
    eType1,
    eType2
};

const TypeE Type= eType1; // Set Global Type

// Common functions for both Gadgets
struct  GadgetBase  {
    void    FncA();
    // Many common methods and functions
    void    FncZ();
};

// Unique functions for each gadget
template<TypeE  E=  eType1>
struct  Gadget  :   public  GadgetBase          {
    bool    Foo()       {return(false);}
};
template<>
struct  Gadget<eType2>  :   public  GadgetBase  {
    bool    Bar()       {return(true);}
};

Gadget<Type>    A;
template<TypeE  E=  eType1>
bool    Test()  {return(A.Foo());}
template<>
bool    Test()  {return(A.Bar());}

我想使用模板来执行此操作,以便在添加新类型或其他函数时减少代码更改的数量。目前有五种类型,预计很快至少还会有两种。预处理器实现代码散发着恶臭,我想在它变得笨拙之前清理它。

小工具代码只占总代码库的一小部分,因此将每个小工具分解整个项目也可能不理想。

即使每个项目只使用一种类型,未使用的类型仍然需要编译,我如何使用 C++03 最好地设计它(没有 constexpr、const if 等)?我是完全错误地处理这个问题吗?我愿意做一次彻底的检修。

编辑:Tomek 下面的解决方案让我怀疑它是否违反了 LSP。实际上,另一种看待这个问题的方法是成为需要实现的接口的一部分。因此,可以像下面这样重新考虑该示例:Test()

struct  GadgetI    {
    virtual bool Test()=0;
};
template<TypeE  E=  eType1>
struct  Gadget  :   public  GadgetBase, public GadgetI  {
    bool    Foo()       {return(false);}
    bool    Test()      {return Foo();}
};
template<>
struct  Gadget<eType2>  :   public  GadgetBase, public GadgetI  {
    bool    Bar()       {return(true);}
    bool    Test()      {return Bar();}
};
template<>
struct  Gadget<eType3>  :   public  GadgetBase, public GadgetI  {
    bool    Test()      {} // Violation of LSP?
};

或者与编辑后的示例类似:

template<typename T>
bool Test(T& o)              {}  // Violation?
template<>
bool Test(Gadget<eType1> &o) {return(o.Foo());}
template<>
bool Test(Gadget<eType2> &o) {return(o.Bar());}

Test(A);

我可能想多了,我只是不想让现在糟糕的设计以后咬我。

C++ 预处理器 模板专用化 C++03

评论

1赞 Tomek 8/19/2022
很难说......每当需要 GadgetBase 时>您都可以替换 Gadget<whatever),并期望它的行为与您提供的代码相同。但我不能 100% 确定,因为我不知道课程的目的以及为什么您决定对不同的孩子采用不同的方法。无论如何,如果 GadgetBase 打算作为公共父级,它应该至少有一个虚拟方法和公共虚拟或受保护的非虚拟析构函数。
0赞 willdo 8/20/2022
LSP 违规的测试之一是:“子类中一个或多个方法的空、无所事事的实现”,这是我 agita 的来源,但我正在克服它。

答:

1赞 Henrique Bucher 8/17/2022 #1

我同意代码看起来很复杂,我和你在一起。但我相信你走错了方向。模板看起来很酷,但在这种情况下,它们不是正确的工具。使用模板,您每次都会编译所有选项,即使它们没有被使用。

你想要的恰恰相反。您一次只想编译一个源代码。两全其美的正确方法是将每个实现分离到不同的文件中,然后使用外部方法选择要包含/编译的文件。

构建系统通常有很多工具可以用于这方面。例如,对于本地编译,我们可以依靠 CMAKE 自己的CMAKE_SYSTEM_PROCESSOR来识别哪个是当前的处理器。

如果要交叉编译,则需要指定要编译到的平台。

举个例子,我有一个软件需要在许多操作系统中编译,如 Redhat、CentOS、Ubuntu、Suse 和 Windows/Mingw。我有一个 bash 脚本文件,用于检查环境并加载特定于该操作系统的工具链 cmake 文件

你的情况似乎更简单。您可以指定要使用的平台,并指示构建系统仅编译特定于该平台的文件。

评论

0赞 willdo 8/18/2022
MadFred,谢谢你的回复。我相信您建议我为每个独特的小工具功能创建一个新的实现文件,并为每个构建使用不同的工具链命令。您能帮助我了解这与我上面的预处理器 (PP) 解决方案有何不同吗?但它不是条件指令,而是将代码放在不同的翻译单元中。实际上,我们只是以不同的方式向编译器隐藏代码,对吧?与PP解决方案相比,这有什么可取之处?
1赞 Henrique Bucher 8/18/2022
@willdo 当然,这与预处理器非常相似,因为只有所需的代码才会提交给编译器。但是,这是一个更清洁的解决方案。您可以在 C 库和 Linux 内核中看到这一点,它们在每个平台都有文件夹。但这完全是规模问题。如果在几个变体中只有几个类,请使用预处理器。但是,如果您想支持少数或更多代码,则代码会变得令人眼花缭乱而无法处理,最好拆分为文件。
1赞 Tomek 8/17/2022 #2

你:)到达那里。

重写函数,使其不依赖于全局 Gadget 对象,而是将一个对象作为模板化参数:Test

template<class T>
bool Test(T &o, std::integral_constant<bool (T::*)(), &T::Foo> * = 0)
{
  return(o.Foo());
}

template<class T>
bool Test(T &o, std::integral_constant<bool (T::*)(), &T::Bar> * = 0)
{
  return(o.Bar());
}

并称其为:

Test(A);

这依赖于 SFINAE(替换失败不是错误)成语。根据定义,编译器将推断出 的类型为 .现在,根据 和 函数的可用性,它将选择其中一个重载。TGadgetFooBar

请注意,如果两者都被定义为两个重载将匹配,则此代码将中断。FooBarGadget

如果你不能在类中包装对 Foo 和 Bar 的调用,这就会带来一个问题:Gadget

template<TypeE  E=  eType1>
struct  Gadget  :   public  GadgetBase          {
    bool    Foo()       {return(false);}
    bool    Test()      {return Foo();}
};
template<>
struct  Gadget<eType2>  :   public  GadgetBase  {
    bool    Bar()       {return(true);}
    bool    Test()      {return Bar();}
};

并始终如一地打电话?A.Test()

编辑:

我可能把它弄得太复杂了。以下重载可能是一种更简单的方法:

bool Test(Gadget<eType1> &o)
{
  return(o.Foo());
}

bool Test(Gadget<eType2> &o)
{
  return(o.Bar());
}

Test(A);

评论

0赞 willdo 8/17/2022
Tomek,感谢您的回复。两条评论。1. 我相信integral_constant是在 C++11 年引入的。2.您的解决方案似乎有点像访客模式。很酷。我在 EDITED 解决方案中遇到的问题是:如果我有第三个不需要 'Test() 的小工具怎么办?如果我为第三个小工具做一个空的测试,代码不会开始闻起来吗?
1赞 Tomek 8/18/2022
添加一个空的、捕获所有内容的模板,该模板不执行任何操作。即使在 C++03 上,integral_constant实施也是微不足道的。
0赞 willdo 8/18/2022
谢谢。我编辑了上面的 OP 以反映这一贡献,但我对可能违反 LSP 有疑问。如果我遇到 C++03 的限制,那很好。它就是这样。