创建 C++ 类的存根版本

Posted

技术标签:

【中文标题】创建 C++ 类的存根版本【英文标题】:Create a stub version of a C++ class 【发布时间】:2017-05-04 02:37:02 【问题描述】:

我想创建一个基于预处理器定义的类的存根或模拟版本。当预处理器宏设置为“启用”时,该类只是一个普通类。当设置为“禁用”时,该类是一个空的存根,编译器可以完全优化它。然而,使用真实类编译或编译不干净的代码也应该与存根类具有相应的行为。

下面是一个示例:

class _foo 
 public:
    foo(int x) : x_(x) 
    void add(int x)  x_ += x; 
    void add(const char *str)  x_ += atoi(str); 
    bool isset(void)  return x_ > 0; 
 private:
    int x_;
;

#if ENABLE_FOO
using foo = _foo;
#else
class foo 
 public:
    foo(int x)    
    void add(int x)  return; 
    void add(const char *str)  return; 
    bool isset(void)  return false; 
;
#endif

isset() 的定义旨在允许将if (a_foo.isset()) code(); 之类的代码优化为一无所有。显然,这不适用于任何方法以及该方法的任何使用。需要对类进行设计,使 0、false、NULL 等在禁用情况下成为合理的返回值。

这很好用,但必须保持foo 的存根版本与真实版本完美同步。对任何方法的每次更改都必须在存根中复制。这很烦人。如何使存根更自动化?理想情况下,可以写成class foo_stub : public stub<foo> ;STUB(foo),而存根类就是由它们单独创建的。

为此,到目前为止,我已经能够想出这个:

class foo 
 public:
    CTOR_STUB(_foo, foo);
    METHOD_STUB(_foo, add);
    METHOD_STUB(_foo, isset);
;

这将创建_foo 的存根版本。确实需要列出每个方法名称,但既不需要提供返回类型,也不需要提供参数,也不需要提供参数的数量。所有重载(即add() 方法)都被一个METHOD_STUB 覆盖。重载可以有不同的返回类型。如果存根方法是方法模板,它甚至可以工作。

以下是执行此操作的宏:

#define METHOD_STUB(base, func) \
    template <typename... Args> \
    auto func(Args... args)  \
        using RetType = decltype(std::declval<base>().func(std::forward<Args>(args)...)); \
        return (RetType)0; 

#define CTOR_STUB(base, name) \
    template <typename... Args> \
    name(Args... args)  return; base _dummystd::forward<Args>(args)...; 

这个想法是定义一个模板,该模板要求存根类中存在具有适当参数和返回类型的方法才能正确编译,但编译器将优化为无。

有没有办法避免使用宏,并且只使用模板来做到这一点?似乎有人希望方法的名称成为模板参数,但我不知道该怎么做。

有没有办法避免在CTOR_STUB() 中提供当前类的名称?编译器确实知道名称,但我看不到将名称作为可用于定义构造函数模板的符号的方法,而不是将类名称作为文本字符串或类型获取。

当类的真实版本不能做同样的事情时,是否存在使存根正确编译或编译失败的缺陷?

【问题讨论】:

传播签名中的更改与传播具有单独定义和声明的典型类(即非模板)中的更改没有任何不同。如果你忘记这样做,你会得到编译时错误。我认为这里的好处并不比使用宏更重要。 (RetType)0 可能很危险。你的意思可能是static_cast&lt;RetType&gt;(0) @NirFriedman,除了在签名更改时还有一个要更新的地方之外,不同之处在于条件编译。如果忘记更新存根并在“启用”情况下构建,则不会出现错误,因为未使用存根。如果未能更新该类的用户,则在“禁用”情况下没有错误。自动链接它们可确保在启用和禁用情况下获得相同的编译时间检查。 @TrentP 好吧,可以采取多种方法,可能会有少量重复,恕我直言,仍然比宏更好。关键点是存根和真正的类都必须在两个构建中都定义。像这样使用宏进行条件编译是个坏主意。该宏可用于定义一个 constexpr bool,可以将其输入std::conditional 以选择正在使用的类。 【参考方案1】:

不是一个完美的解决方案,但你可以做到

#ifdef ENABLE
#define IF_ENABLED(x) x
#define IF_DISABLED(x)
#else
#define IF_ENABLED(x)
#define IF_DISABLED(x) x
#endif

class Foo 
public:
    foo(int x) IF_ENABLED(: x_(x)) 
    void add(int x)  IF_ENABLED(x_ += x;) 
    void add(const char *str)  IF_ENABLED(x_ += atoi(str);) 
    bool isset(void)  IF_ENABLED(return x_ > 0;) IF_DISABLED(return false;) 
private:
#ifdef ENABLE
    int x_;
#endif
;

【讨论】:

【参考方案2】:

一个完全避免宏(除了从构建系统输入的)的好解决方案如下:

#if ENABLE_FOO
constexpr bool g_use_foo = true;
#else
constexpr bool g_use_foo = false;
#endif

template <bool FooEnabled>
struct Foo 
  void bar1() 
;

template <>
void Foo<true>::bar1()  std::cerr << "not a mock\n"; 

using UserFoo = Foo<g_use_foo>;

现场示例:http://coliru.stacked-crooked.com/a/ecdb7c1a7f0a6068

基本上,我们声明了一个模板类,并内联了通用的琐碎模拟实现。我们为具有实际功能的类定义了一个特化。内联和外联的声明必须完全匹配是毫无价值的,否则您将专门针对尚未声明的内容。因此,如果您更新一个而不是另一个,则会出现编译时错误。请注意,这很好地反映了一个典型的非模板类,其中您有内联声明和定义,所以我认为这是非常合理的。

唯一没有涵盖的情况是您删除或忘记实现真实对象上的方法。这仍然会编译,即使您认为您使用的是真实对象,您也只会获得模拟功能。也就是说,我不认为这是一个主要问题。在这种情况下,即使是最基本的单行单元测试也会失败。如果你担心这个,你可以写:

template <bool FooEnabled>
struct Foo 
  void bar1()  static_assert(!FooEnabled, "");
;

谁会抓住这个。

你也可以很容易地覆盖状态。只是从具有状态的结构私下继承:

template <bool FooEnabled>
struct Foo : private std::conditional_t<FooEnabled, FooState, EmptyStruct> 
    void bar1() 
;

请注意,无论构建如何,这两个类都已完全定义并可使用,这对测试和工具有相当大的好处。

【讨论】:

以上是关于创建 C++ 类的存根版本的主要内容,如果未能解决你的问题,请参考以下文章

强制 Eclipse 为派生类中纯虚函数的实现提供存根

所有调用的 OCMock 存根类方法

创建存根 bigquery 表

c++ 创建类的实例作为引用类型

C++类的定义和创建

C++ 无法创建用户定义类的向量