【发布时间】: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 _dummy{std::forward<Args>(args)...}; }
这个想法是定义一个模板,该模板要求存根类中存在具有适当参数和返回类型的方法才能正确编译,但编译器将优化为无。
有没有办法避免使用宏,并且只使用模板来做到这一点?似乎有人希望方法的名称成为模板参数,但我不知道该怎么做。
有没有办法避免在CTOR_STUB() 中提供当前类的名称?编译器确实知道名称,但我看不到将名称作为可用于定义构造函数模板的符号的方法,而不是将类名称作为文本字符串或类型获取。
当类的真实版本不能做同样的事情时,是否存在使存根正确编译或编译失败的缺陷?
【问题讨论】:
-
传播签名中的更改与传播具有单独定义和声明的典型类(即非模板)中的更改没有任何不同。如果你忘记这样做,你会得到编译时错误。我认为这里的好处并不比使用宏更重要。
-
(RetType)0可能很危险。你的意思可能是static_cast<RetType>(0)。 -
@NirFriedman,除了在签名更改时还有一个要更新的地方之外,不同之处在于条件编译。如果忘记更新存根并在“启用”情况下构建,则不会出现错误,因为未使用存根。如果未能更新该类的用户,则在“禁用”情况下没有错误。自动链接它们可确保在启用和禁用情况下获得相同的编译时间检查。
-
@TrentP 好吧,可以采取多种方法,可能会有少量重复,恕我直言,仍然比宏更好。关键点是存根和真正的类都必须在两个构建中都定义。像这样使用宏进行条件编译是个坏主意。该宏可用于定义一个 constexpr bool,可以将其输入
std::conditional以选择正在使用的类。