减少非虚拟多态类中从基委派到派生委派的样板

Reducing boilerplate for base to derived delegation in non-virtual polymorphic classes

本文关键字:委派 派生 虚拟 多态      更新时间:2023-10-16

考虑一个封闭的1类层次结构,如下所示:

class B {...}; 
class D1 final : public B {...};
class D2 final : public B {...};

其中B是一个抽象的2基类,D1D2是它的派生类。

由于实现约束或设计,这些类都没有任何virtual方法,而是B上在D1D2中具有不同实现的成员函数只是通过对派生类型进行运行时检查来委托给实际的最派生类型,如下所示:

class B {
bool isD1;
protected:
B(bool isD1) : isD1{isD1} {}
public:
std::string to_string() {
return isD1 ? static_cast<D1*>(this)->to_string() : static_cast<D2*>(this)->to_string();
}
}
class D1 final : public B {
public:
D1() : B(true) {}
std::string to_string() { // D1 specific implementation ... }
}
class D2 final : public B {
public:
D2() : B(false) {}
std::string to_string() { // D2 specific implementation ... }
}

在这里,B上的to_string方法只是检查派生最多的B类型是D1还是D2,并调用适当的方法(在这两种情况下也称为to_string)。

凉。

现在想象一下还有 10 种像B::to_string这样的方法。我可以在 C++11 中做些什么来减少B中的委派样板,而无需诉诸宏?

在 C++14 中,似乎一个合理的方法是通用授权机制,例如:

class B {
...
template <typename F>
auto delegate(F&& f) -> decltype(f(D1{})) {
return isD1 : f(*static_cast<D1*>(this)) : f(*static_cast<D2*>(this));
}
std::string to_string() {
return delegate([](auto&& b){ return b.to_string(); });
}
}

在这里,无论最终传递D1还是D2[](auto&& b){ return b.to_string(); }通用 lambda 都有效(因为两者都有to_string方法)。在C++11中,我没有看到同样简洁的方式来表达这一点。

有什么想法吗?

当然,您可以使用宏来复制非通用宏并将其传递给 2 参数delegate方法(该方法为D1D2使用单独的函子),但我想避免宏。


1这里的封闭意味着B的派生类集是固定的,并且在运行时是已知的。

2概念抽象,但不是"纯virtual"意义上的抽象。也就是说,不应直接实例化此类 - 唯一有意义的整个对象是其派生类。各种构造函数protected强制执行这一点。

这个怎么样?

template <typename F1, typename F2>
auto delegate(F1 f1, F2 f2) -> decltype((D1{}.*f1)()) {
return isD1 ? (static_cast<D1*>(this)->*f1)() : (static_cast<D2*>(this)->*f2)();
}
std::string to_string() {
return delegate(&D1::to_string, &D2::to_string);
}

您还可以使其类型更强:

template <typename Result>
Result delegate(Result (D1::*f1)(), Result (D2::*f2)()) {
return isD1 ? (static_cast<D1*>(this)->*f1)() : (static_cast<D2*>(this)->*f2)();
}

这不是一个答案,这是一个可憎的东西。但我想我会分享,因为

  • OP暗示他/她正在使用更简单的基于宏观的方案,并且
  • 它接近"零样板解决方案"。

下面是一个工作示例。

#include <string>
#include <sstream>
#include <iostream>
#include <delegate_macros>
#define FOREACH_DELEGATE(A) 
A(std::string, to_string, (),      ())
A(void,        setInt,    (int a), (a))
class B
{
DECLARE_VTAB_MEMBERS
public:
B(DELEGATE_ARGS) : INITIALIZER_LIST { }
DEFINE_DELEGATORS
};
class D1 : public B
{
int m_i;
public:
D1() : B(PASS_DELEGATES) {}
void setInt(int i) {m_i = i;}
std::string to_string() {std::stringstream ss; ss << "D1:" << m_i; return ss.str();}
};
class D2 : public B
{
int m_i;
public:
D2() : B(PASS_DELEGATES) {}
void setInt(int i) {m_i = i * 5;}
std::string to_string() {std::stringstream ss; ss << "D2:" << m_i; return ss.str();}
};

哪里

int main(int argc, char *argv[])
{
D1 d1;
D2 d2;
B *ref = &d1;
ref->setInt(2);
std::cout << "((B*)&d1)->toString: " << ref->to_string() << std::endl;
ref = &d2;
ref->setInt(2);
std::cout << "((B*)&d2)->toString: " << ref->to_string() << std::endl;
}

收益 率

$ ./a.out
((B*)&d1)->toString: D1:2
((B*)&d2)->toString: D2:10

delegate_macros中的宏独立于B及其子类的结构:

#define MAKE_DELEGATOR(ret, name, params, args)
ret name params
{
return (this ->* m_##name) args;
}
#define MAKE_DELEGATE_REF(ret, name, params, args) (ret (B::*) params)&name,
#define DECLARE_VTAB_MEMBER(t,n,p,a) t (B::*m_##n)p;
#define MAKE_CTOR_INITIALIZER(t,n,p,a) m_##n(n),
#define MAKE_CTOR_ARG(t,n,p,a) t (B::*n) p,
#define MAKE_CTOR_PARAMS(t,n,p,a) t (B::*m_##n)p,
#define DECLARE_VTAB_MEMBERS FOREACH_DELEGATE(DECLARE_VTAB_MEMBER) char dummy;
#define INITIALIZER_LIST     FOREACH_DELEGATE(MAKE_CTOR_INITIALIZER) dummy()
#define DEFINE_DELEGATORS    FOREACH_DELEGATE(MAKE_DELEGATOR)
#define DELEGATE_ARGS        FOREACH_DELEGATE(MAKE_CTOR_ARG) void *
#define PASS_DELEGATES       FOREACH_DELEGATE(MAKE_DELEGATE_REF) NULL

我称之为可憎之物有几个原因:

  • 它手动制作一个 VTABLE...如果您想要一个 VTABLE,只需使用虚拟。
  • 它将简单的拼写错误转换为输出页面。
  • 它对每个子类指向(B::*)变体的方法的指针执行未经检查的强制转换。
    • 并使用指针到方法 so-cast 调用子类方法。
  • 它添加了虚拟构造函数参数和成员变量,以使宏扩展更容易。
  • 没有人能读懂它,除非他们花了数年时间写这种宏。
  • FOREACH_DELEGATE必须在要使用宏的每个翻译单元中定义,因此仅当B及其所有子类都定义在一个文件中时才适用。 如果要将 B 与 FOREACH_DELEGATE 放在标题中,则必须创建其他宏来区分声明委托人与定义委托人。