我有两个具有1:1关系的类层次结构:一些普通类A
,B
具有共同的Root
接口,以及一个WrapperRoot<T>
接口两个具体的实例WrapperA<T>
和WrapperB<T>
。我现在正在寻求实现一个函数auto wrap<T>(Root& elem) -> unique_ptr<WrapperRoot<T>>
,它将每个普通类映射到它的包装类。
包装器的确切类型很重要,因为它们将具有虚拟方法,并且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();
}
我该如何使这项工作?或者我是否需要诉诸类型违规黑客?
答案 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");
}
答案 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
返回类型的专业化(问题衰退为“正常”访问者模式)void
可以作为const和非const)提供。