我想基于预处理器定义创建类的存根或模拟版本。当预处理器宏设置为“启用”时,该类只是一个普通类。当设置为“禁用”时,该类是一个空存根,编译器可以完全远离优化。然而,编译或不与真实类完全编译的代码也应该与存根类具有相应的行为。
以下是一个示例:
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 _dummy{std::forward<Args>(args)...}; }
我们的想法是定义一个模板,该模板要求在存根类中存在具有适当参数和返回类型的方法,以便正确编译,但编译器将优化为零。
有没有办法避免使用宏,只使用模板执行此操作?似乎有人希望方法的名称是模板参数,我不知道如何做到这一点。
有没有办法避免需要在CTOR_STUB()
中提供当前类的名称?编译器知道名称,但我看不到将名称作为可用于定义构造函数模板的符号的方法,而不是将类名称作为文本字符串或类型。
是否有一些缺陷会导致存根正确编译或者在类的真实版本不同的情况下无法编译?
答案 0 :(得分: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
};
答案 1 :(得分:0)
一个完全避免宏的好解决方案(除了从构建系统输入的内容)如下:
#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, "");}
};
哪个会抓住这个。
你可以很容易地覆盖状态。只需从具有state:
的结构中私下继承template <bool FooEnabled>
struct Foo : private std::conditional_t<FooEnabled, FooState, EmptyStruct> {
void bar1() {}
};
请注意,无论构建如何,这两个类都是完全定义和可用的,这对测试和工具具有相当大的好处。