不同的选择
有可能的。您的问题有几种替代方法。每个都有不同的优点和缺点(我将解释每个):
- 创建一个接口并拥有一个模板类,该类为不同类型实现此接口。它应该支持克隆。
- 使用
boost::variant
和访问。
混合静态和动态多态性
对于第一个替代方案,您需要创建一个这样的界面:
class UsableInterface
{
public:
virtual ~UsableInterface() {}
virtual void use() = 0;
virtual std::unique_ptr<UsableInterface> clone() const = 0;
};
显然,您不希望每次拥有具有该use()
功能的新类型时都手动实现此接口。因此,让我们有一个模板类来为你做这件事。
template <typename T> class UsableImpl : public UsableInterface
{
public:
template <typename ...Ts> UsableImpl( Ts&&...ts )
: t( std::forward<Ts>(ts)... ) {}
virtual void use() override { use( t ); }
virtual std::unique_ptr<UsableInterface> clone() const override
{
return std::make_unique<UsableImpl<T>>( t ); // This is C++14
// This is the C++11 way to do it:
// return std::unique_ptr<UsableImpl<T> >( new UsableImpl<T>(t) );
}
private:
T t;
};
现在你实际上已经可以用它做你需要的一切了。你可以把这些东西放在一个向量中:
std::vector<std::unique_ptr<UsableInterface>> usables;
// fill it
您可以复制该向量并保留基础类型:
std::vector<std::unique_ptr<UsableInterface>> copies;
std::transform( begin(usables), end(usables), back_inserter(copies),
[]( const std::unique_ptr<UsableInterface> & p )
{ return p->clone(); } );
你可能不想在你的代码中乱扔这样的东西。你想写的是
copies = usables;
好吧,您可以通过将 包装std::unique_ptr
到支持复制的类中来获得这种便利。
class Usable
{
public:
template <typename T> Usable( T t )
: p( std::make_unique<UsableImpl<T>>( std::move(t) ) ) {}
Usable( const Usable & other )
: p( other.clone() ) {}
Usable( Usable && other ) noexcept
: p( std::move(other.p) ) {}
void swap( Usable & other ) noexcept
{ p.swap(other.p); }
Usable & operator=( Usable other )
{ swap(other); }
void use()
{ p->use(); }
private:
std::unique_ptr<UsableInterface> p;
};
由于漂亮的模板化构造器,您现在可以编写类似的东西
Usable u1 = 5;
Usable u2 = std::string("Hello usable!");
您可以使用适当的值语义分配值:
u1 = u2;
你可以把 Usables 放在一个std::vector
std::vector<Usable> usables;
usables.emplace_back( std::string("Hello!") );
usables.emplace_back( 42 );
并复制该向量
const auto copies = usables;
你可以在 Sean Parents talk Value Semantics and Concepts-based Polymorphism中找到这个想法。他还在Going Native 2013 上给出了这个演讲的一个非常简短的版本,但我认为这很快就会跟进。
此外,您可以采用比编写自己的Usable
类并转发所有成员函数(如果您想稍后添加其他函数)更通用的方法。这个想法是Usable
用模板类替换该类。该模板类将不提供成员函数use()
,而是提供一个operator T&()
and operator const T&() const
。这为您提供了相同的功能,但您无需在每次促进此模式时都编写额外的值类。
一个安全、通用、基于堆栈的可区分联合容器
模板类boost::variant
正是如此,它提供了类似于 C 风格的东西,但union
安全且具有适当的值语义。使用方法是这样的:
using Usable = boost::variant<int,std::string,A>;
Usable usable;
您可以将任何这些类型的对象分配给Usable
.
usable = 1;
usable = "Hello variant!";
usable = A();
如果所有模板类型都具有值语义,那么boost::variant
也具有值语义并且可以放入 STL 容器中。您可以通过称为访问者模式use()
的模式为此类对象编写函数。它根据内部类型为包含的对象调用正确的函数。use()
class UseVisitor : public boost::static_visitor<void>
{
public:
template <typename T>
void operator()( T && t )
{
use( std::forward<T>(t) );
}
}
void use( const Usable & u )
{
boost::apply_visitor( UseVisitor(), u );
}
现在你可以写
Usable u = "Hello";
use( u );
而且,正如我已经提到的,您可以将这些东西放入 STL 容器中。
std::vector<Usable> usables;
usables.emplace_back( 5 );
usables.emplace_back( "Hello world!" );
const auto copies = usables;
权衡取舍
您可以在两个维度上扩展功能:
- 添加满足静态接口的新类。
- 添加类必须实现的新功能。
在我提出的第一种方法中,添加新类更容易。第二种方法更容易添加新功能。
在第一种方法中,客户端代码不可能(或至少很难)添加新功能。在第二种方法中,客户端代码不可能(或至少很难)将新类添加到混合中。一个出路是所谓的非循环访问者模式,它使客户端可以使用新类和新功能扩展类层次结构。这里的缺点是您必须在编译时牺牲一定数量的静态检查。这是一个描述访问者模式的链接,包括非循环访问者模式以及其他一些替代方案。如果您对这些东西有任何疑问,我愿意回答。
这两种方法都是超级类型安全的。那里没有权衡取舍。
第一种方法的运行时成本可能要高得多,因为您创建的每个元素都涉及堆分配。该boost::variant
方法是基于堆栈的,因此可能更快。如果第一种方法的性能存在问题,请考虑切换到第二种方法。