将运行时类型映射到并行类层次结构...使用模板

时间:2015-07-02 10:03:30

标签: c++ templates c++11 type-erasure virtual-method

我有两个具有1:1关系的类层次结构:一些普通类AB具有共同的Root接口,以及一个WrapperRoot<T>接口两个具体的实例WrapperA<T>WrapperB<T>。我现在正在寻求实现一个函数auto wrap<T>(Root& elem) -> unique_ptr<WrapperRoot<T>>,它将每个普通类映射到它的包装类。

UML class diagram of the described hierarchy

  

包装器的确切类型很重要,因为它们将具有虚拟方法,并且Root对象的确切类型不是静态知道的。

尝试解决方案

我的第一个想法是在Root中声明模板虚拟方法:

class Root {
  ...
public:
  template<typename T>
  virtual auto wrap() -> unique_ptr<WrapperRoot<T>> = 0;
}

然后可以在子类中实现

class A : public Root {
  ...
  template<typename T>
  virtual auto wrap() -> unique_ptr<WrapperRoot<T>> override {
    return make_unique<WrapperA<T>>();
  }
}

正如我要发现的那样,C ++不允许使用虚拟方法的模板。我做了一些进一步的研究,发现了类型擦除的技术,它允许我们突破这个虚拟模板与模板dichtomy。也许有可能让每个类通过传入已删除模板参数<T>的类似访问者的对象来选择其包装类型?但是,我仍然是C ++的新手,我实现这一点的所有尝试都只是将问题转移到了另一个层面,但没有解决它们。

这是特别令人沮丧的,因为我熟悉的其他语言表达这种结构没有问题。例如。在Java中定义虚方法<T> WrapperRoot<T> wrap() { return new WrapperA<T>(); }没有问题,但这是因为Java通过重新解释强制转换来实现模板。 Java的实现将在C ++中表达为:

template<typename T>
WrapperRoot<T>* wrap() { return reinterpret_cast<WrapperRoot<T>*>(wrapper_impl()); }
virtual void* wrapper_impl() { return new WrapperA<void*>() }

但是,我想使用使用 C ++类型系统而不是通过转换void指针来违反它。

测试用例

为了明确地解释我的问题,我创建了以下测试用例。正确实施wrap后,应输出:

WrapperA
WrapperB

不应修改main方法,但可以添加任意方法,帮助程序类型和wrap函数的实现。

#include <iostream>
#include <memory>
using namespace std;

// the Root hierarchy

class Root {
public:
  virtual ~Root() {}
};

class A : public Root {};

class B : public Root {};


// the Wrapper hierarchy

template<typename T>
class WrapperRoot {
public:
  virtual ~WrapperRoot() {}
  virtual T name() = 0;
};

template<typename T>
class WrapperA : public WrapperRoot<T> {
public:
  virtual T name() { return T("WrapperA\n"); }
};

template<typename T>
class WrapperB : public WrapperRoot<T> {
public:
  virtual T name() { return T("WrapperB\n"); }
};


// the "wrap" function I want to implement

template<typename T>
auto wrap(Root& ) -> unique_ptr<WrapperRoot<T>>;

// util
template<typename T, typename... Args>
auto make_unique(Args... args) -> unique_ptr<T> {
  return unique_ptr<T>(new T(forward<Args>(args)...));
}

int main() {
  unique_ptr<Root> a = make_unique<A>();
  unique_ptr<Root> b = make_unique<B>();
  cout << wrap<string>(*a)->name()
       << wrap<string>(*b)->name();
}

我该如何使这项工作?或者我是否需要诉诸类型违规黑客?

2 个答案:

答案 0 :(得分:2)

让这个工作的最简单方法就是dynamic_cast Root&来计算它的运行时类型:

template<typename T>
auto wrap(Root& root) -> unique_ptr<WrapperRoot<T>>
{
    if (dynamic_cast<A*>(&root)) {
        //root is an A, return a WrapperA
        return make_unique<WrapperA<T>>();
    } 
    else if (dynamic_cast<B*>(&root)) {
        //root is a B, return a WrapperB
        return make_unique<WrapperB<T>>();
    } 

    throw std::runtime_error("No wrapper for that type");
}

https://jsfiddle.net/hLg2cr9v/

答案 1 :(得分:0)

事实证明,这可以通过类型擦除,访问者模式和一些间接来解决。解决方案很简洁,不需要我们在wrap函数内重新实现动态调度。

核心思想是引入WrapperSelector访问者界面:

class WrapperSelector {
public:
  virtual auto visit(A&) -> void = 0;
  virtual auto visit(B&) -> void = 0;
};

Root层次结构需要稍微修改以接受此访问者,并执行双重调度:

class Root {
public:
  virtual ~Root() {}
  virtual auto accept(WrapperSelector&) -> void = 0;
};

class A : public Root {
public:
  virtual auto accept(WrapperSelector& wrapper) -> void {
    wrapper.visit(*this);
  }
};

class B : public Root {
public:
  virtual auto accept(WrapperSelector& wrapper) -> void {
    wrapper.visit(*this);
  }
};

到目前为止,这是C ++中的标准访问者模式。我们现在要做的是引入一个模板化的类WrapperSelectorImpl<T> : public WrapperSelector。由于它是模板化的,但仅通过非模板化接口使用,因此实现了类型擦除。在内部,我们将此WrapperSelectorImpl构造为借用的WrapperRoot<T>指针的容器,我们在其中编写所选的包装器。在accept / visit序列结束后,该指针将被包装器填充,因此没有虚拟方法需要返回模板参数化类型。此外,accept方法除了选择相应的visit方法之外什么都不做,因此Root层次结构中的类型不需要知道WrapperRoot层次结构 - 具体WrapperSelector将处理此映射。

template<typename T>
class WrapperSelectorImpl : public WrapperSelector {
  unique_ptr<WrapperRoot<T>>& _wrapper;
public:
  explicit WrapperSelectorImpl(unique_ptr<WrapperRoot<T>>& wrapper)
    : _wrapper(wrapper)
  {}

  virtual auto visit(A&) -> void override {
    _wrapper = make_unique<WrapperA<T>>();
  }

  virtual auto visit(B&) -> void override {
    _wrapper = make_unique<WrapperB<T>>();
  }
};

我们的wrap函数现在必须设置一个指针,WrapperSelectorImpl借用该指针,让Root层次结构中的给定对象通过WrapperSelector选择包装器,并返回现在填充的指针:

template<typename T>
auto wrap(Root& obj) -> unique_ptr<WrapperRoot<T>> {
  unique_ptr<WrapperRoot<T>> wrapper;
  WrapperSelectorImpl<T> wrapper_selector(wrapper);
  obj.accept(wrapper_selector);
  return wrapper;
}

概括

上述技术可用于实现任意模板化虚拟方法,或具有任意返回类型的访问者模式。这方面的先决条件是对访客模式的最小支持:

  • 某个Subject类或类层次结构,其中包含virtual void accept(SubjectVisitor& v) { v.visit(*this); }
  • SubjectVisitor类层次结构中每个类virtual void visit(S&) = 0的{​​{1}}个S方法接口。

现在假设我们希望在Subject层次结构中实现具有以下伪签名的方法:

Subject

然后,我们可以使用以下步骤来实现此目的:

首先,我们创建一个包装函数,公开我们的调度逻辑的公共接口。这可能是class Subject { ... template<typename R, typename T, typename... Args> virtual R frobnicate(Args... args) = 0; } 中的非虚拟模板化方法,也可能是自由函数。内部结构与上例中的相同:设置返回值,设置访问者(借用对返回值的引用),执行调度以及返回值。

由于这是如此普遍,我们可以将其打包成可重复使用的模板化函数:

Subject

请注意,这假定返回值是默认可构造的。如果不是这种情况,用// most general implementation template<typename ReturnType, typename Subject, typename Visitor, typename... Args> auto manage_visitor(Subject& subject, Args... args) -> ReturnType { ReturnType return_value; Visitor visitor(return_value, std::forward(args)...); subject.accept(visitor); return return_value; } class Subject { ... template<typename R, typename T, typename... Args> R frobnicate(Args... args) { return manage_visitor<R, Subject, ConcreteSubjectVisitor<R, T>>(*this, std::forward(args)...); } }; 代替unique_ptr<ReturnType>可能是一个解决方案。

我们现在必须提供一个提供实际实施的ReturnType

class ConcreteSubjectVisitor : public SubjectVisitor

唯一重要的是它可以写​​入返回值。请注意,访问者可以通过构造函数获取其他参数,这使得它与template<typename ReturnType, typename T> class ConcreteSubjectVisitor : public SubjectVisitor { ReturnType& return_value; ArgType something; public: ConcreteSubjectVisitor(ReturnType& ret, ArgType& other_arg) : return_value(ret), something(other_arg) {} virtual void visit(S1&) override { ... } virtual void visit(S2&) override { ... } ... }; 函数或构造lambda有些相关。然后std::bind定义包含实际代码,该代码可以访问访问者的所有类型参数以及访问者的所有构造函数参数。

打开问题:

  • 处理非默认构造的返回类型(指针或自定义默认值)
  • visit返回类型的专业化(问题衰退为“正常”访问者模式)
  • 对完全多方法的推广(如果方法是咖喱形式则是微不足道的)
  • 提供方便的界面
  • const-correctness(必须在每个访问者的基础上应用,void可以作为const和非const)提供。