我有许多不相关的类型,它们都通过重载的自由函数(ad hoc多态)支持相同的操作:
struct A {};
void use(int x) { std::cout << "int = " << x << std::endl; }
void use(const std::string& x) { std::cout << "string = " << x << std::endl; }
void use(const A&) { std::cout << "class A" << std::endl; }
正如问题的标题所暗示的那样,我希望将这些类型的实例存储在异构容器中,以便无论它们具体是什么类型,我都可以use()
。容器必须具有值语义(即两个容器之间的赋值 copy 数据,它不共享它)。
std::vector<???> items;
items.emplace_back(3);
items.emplace_back(std::string{ "hello" });
items.emplace_back(A{});
for (const auto& item: items)
use(item);
// or better yet
use(items);
当然,这必须是完全可扩展的。想象一下采用vector<???>
的库API,以及将自己的类型添加到已知类型的客户端代码。
通常的解决方案是将(智能)指针存储到(抽象)接口(例如vector<unique_ptr<IUsable>>
),但这有很多缺点 - 从我的脑海中浮现出来:
int
和string
编写包装器而不是......更不用说由于免费成员函数变得密切而导致可重用性/可组合性降低绑定到接口(虚拟成员函数)。vec1 = vec2
(强制我手动执行深层复制),则不可能使用简单的赋值unique_ptr
,或者如果我们使用{最后两个容器都以共享状态结束} {1}}(它有它的优点和缺点 - 但由于我想要容器上的值语义,我再次被迫手动执行深层复制)。shared_ptr
功能,该功能必须在每个派生类中实现。 你能认真考虑一些比这更无聊的东西吗? 总结一下:这增加了许多不必要的耦合,需要大量(可以说是无用的)样板代码。这是绝对不满意但到目前为止这是我所知道的唯一实用解决方案。
我一直在寻找一种可行的替代亚型多态性(又名界面继承)的年龄。我使用ad hoc多态(也就是重载的自由函数)玩了很多但是我总是遇到同样的困难:容器有是同构的,所以我总是勉强回到继承和智能指针,上面列出的所有缺点(可能还有更多)。
理想情况下,我希望只有clone()
具有正确的值语义,而不会将任何内容更改为当前(缺少)类型层次结构,并且保持ad hoc多态性而不是要求子类型多态性。
这可能吗?如果是这样,怎么样?
答案 0 :(得分:23)
有可能。有几种替代方法可以解决您的问题。每个人都有不同的优点和缺点(我将解释每一个):
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中找到这个想法。他还给出了这个talk at Going Native 2013的非常简短的版本,但我认为这是要快速遵循的。
此外,您可以采用更通用的方法,而不是编写自己的Usable
类并转发所有成员函数(如果您想稍后添加其他函数)。我们的想法是用模板类替换类Usable
。此模板类不会提供成员函数use()
,而是operator T&()
和operator const T&() const
。这为您提供了相同的功能,但每次促进此模式时,您都不需要编写额外的值类。
template class 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容器中。您可以通过名为visitor pattern的模式为此类对象编写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;
您可以在两个方面扩展功能:
在我提出的第一种方法中,添加新类更容易。第二种方法可以更轻松地添加新功能。
在第一种方法中,客户端代码无法(或至少很难)添加新功能。在第二种方法中,客户端代码不可能(或至少很难)在混合中添加新类。一种出路是所谓的非循环访问者模式,它使客户端可以使用新类和新功能扩展类层次结构。这里的缺点是你必须在编译时牺牲一定数量的静态检查。这是一个link which describes the visitor pattern,包括非循环访客模式以及其他一些替代方案。如果您对这些内容有疑问,我愿意回答。
这两种方法都是超类型安全的。在那里没有权衡取舍。
第一种方法的运行时成本可能要高得多,因为您创建的每个元素都涉及堆分配。 boost::variant
方法基于堆栈,因此可能更快。如果性能是第一种方法的问题,请考虑切换到第二种方法。
答案 1 :(得分:16)
归功于它应有的地方:当我看到Sean Parent's Going Native 2013 "Inheritance Is The Base Class of Evil" talk时,我意识到实际上很简单,事后才能解决这个问题。我只能建议你观看它(在20分钟内有更多有趣的东西,这个Q / A几乎没有触及整个演讲的表面),以及其他 Going Native 2013 会谈
实际上它很简单,根本不需要任何解释,代码说明了一切:
struct IUsable {
template<typename T>
IUsable(T value) : m_intf{ new Impl<T>(std::move(value)) } {}
IUsable(IUsable&&) noexcept = default;
IUsable(const IUsable& other) : m_intf{ other.m_intf->clone() } {}
IUsable& operator =(IUsable&&) noexcept = default;
IUsable& operator =(const IUsable& other) { m_intf = other.m_intf->clone(); return *this; }
// actual interface
friend void use(const IUsable&);
private:
struct Intf {
virtual ~Intf() = default;
virtual std::unique_ptr<Intf> clone() const = 0;
// actual interface
virtual void intf_use() const = 0;
};
template<typename T>
struct Impl : Intf {
Impl(T&& value) : m_value(std::move(value)) {}
virtual std::unique_ptr<Intf> clone() const override { return std::unique_ptr<Intf>{ new Impl<T>(*this) }; }
// actual interface
void intf_use() const override { use(m_value); }
private:
T m_value;
};
std::unique_ptr<Intf> m_intf;
};
// ad hoc polymorphic interface
void use(const IUsable& intf) { intf.m_intf->intf_use(); }
// could be further generalized for any container but, hey, you get the drift
template<typename... Args>
void use(const std::vector<IUsable, Args...>& c) {
std::cout << "vector<IUsable>" << std::endl;
for (const auto& i: c) use(i);
std::cout << "End of vector" << std::endl;
}
int main() {
std::vector<IUsable> items;
items.emplace_back(3);
items.emplace_back(std::string{ "world" });
items.emplace_back(items); // copy "items" in its current state
items[0] = std::string{ "hello" };
items[1] = 42;
items.emplace_back(A{});
use(items);
}
// vector<IUsable>
// string = hello
// int = 42
// vector<IUsable>
// int = 3
// string = world
// End of vector
// class A
// End of vector
正如您所看到的,这是一个围绕unique_ptr<Interface>
的相当简单的包装器,带有一个模板化构造函数,用于实例化派生的Implementation<T>
。所有(不完全)血腥细节都是私有的,公共接口不能更清晰:包装器本身没有构造函数,除了构造/复制/移动,接口是作为一个重载的免费use()
函数提供的现有的。
显然,unique_ptr
的选择意味着我们需要实现一个私有的clone()
函数,只要我们想要复制一个IUsable
对象(这反过来又要求)堆分配)。不可否认,每个副本的一个堆分配是非常不理想的,但如果公共接口的任何函数可以改变底层对象(即,如果use()
采用非const 引用并修改了它,则这是一个要求)他们):这样我们就可以确保每个对象都是唯一的,因此可以自由地进行变异。
现在,如果在问题中,对象是完全不可变的(不仅通过暴露的界面,请注意,我真的意味着整个对象始终是完全不可变的强>)然后我们可以引入共享状态而没有恶意的副作用。最直接的方法是使用shared_ptr
- to-const 而不是unique_ptr
:
struct IUsableImmutable {
template<typename T>
IUsableImmutable(T value) : m_intf(std::make_shared<const Impl<T>>(std::move(value))) {}
IUsableImmutable(IUsableImmutable&&) noexcept = default;
IUsableImmutable(const IUsableImmutable&) noexcept = default;
IUsableImmutable& operator =(IUsableImmutable&&) noexcept = default;
IUsableImmutable& operator =(const IUsableImmutable&) noexcept = default;
// actual interface
friend void use(const IUsableImmutable&);
private:
struct Intf {
virtual ~Intf() = default;
// actual interface
virtual void intf_use() const = 0;
};
template<typename T>
struct Impl : Intf {
Impl(T&& value) : m_value(std::move(value)) {}
// actual interface
void intf_use() const override { use(m_value); }
private:
const T m_value;
};
std::shared_ptr<const Intf> m_intf;
};
// ad hoc polymorphic interface
void use(const IUsableImmutable& intf) { intf.m_intf->intf_use(); }
// could be further generalized for any container but, hey, you get the drift
template<typename... Args>
void use(const std::vector<IUsableImmutable, Args...>& c) {
std::cout << "vector<IUsableImmutable>" << std::endl;
for (const auto& i: c) use(i);
std::cout << "End of vector" << std::endl;
}
注意clone()
函数是如何消失的(我们不再需要它,我们只是共享底层对象,因为它是不可变的而没有麻烦),以及现在如何复制noexcept
谢谢到shared_ptr
保证。
有趣的是,底层对象必须是不可变的,但你仍然可以改变它们的IUsableImmutable
包装器,所以它仍然可以完成这个:
std::vector<IUsableImmutable> items;
items.emplace_back(3);
items[0] = std::string{ "hello" };
(只有shared_ptr
被改变,而不是基础对象本身,所以它不会影响其他共享引用)
答案 2 :(得分:5)
也许是boost :: variant?
#include <iostream>
#include <string>
#include <vector>
#include "boost/variant.hpp"
struct A {};
void use(int x) { std::cout << "int = " << x << std::endl; }
void use(const std::string& x) { std::cout << "string = " << x << std::endl; }
void use(const A&) { std::cout << "class A" << std::endl; }
typedef boost::variant<int,std::string,A> m_types;
class use_func : public boost::static_visitor<>
{
public:
template <typename T>
void operator()( T & operand ) const
{
use(operand);
}
};
int main()
{
std::vector<m_types> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(std::string("hello"));
vec.push_back(A());
for (int i=0;i<4;++i)
boost::apply_visitor( use_func(), vec[i] );
return 0;
}
答案 3 :(得分:3)
之前的其他答案(使用vtabled接口基类,使用boost :: variant,使用虚拟基类继承技巧)对于这个问题都是非常好的有效解决方案,每个都有不同的编译时间与运行时间成本的平衡。我建议使用C ++ 11及更高版本use eggs::variant instead而不是boost :: variant,这是使用C ++ 11/14重新实现boost :: variant,它在设计,性能,易用性方面非常优越,抽象的力量,它甚至在VS2013上提供了一个相当完整的功能子集(以及VS2015上的完整功能集)。它也是由Boost主要作者编写和维护的。
如果你能够稍微重新定义问题 - 具体来说,你可以丢失类型擦除std :: vector以支持更强大的东西 - 你可以使用异类型容器代替。这些工作通过为容器的每个修改返回一个新的容器类型,因此模式必须是:
newtype newcontainer = oldcontainer.push_back(newitem);
这些是在C ++ 03中使用的痛苦,尽管Boost.Fusion公平地说它们可能有用。实际上有用的可用性只能从C ++ 11开始实现,尤其是从C ++ 14开始,这要归功于通用lambda,这使得使用constexpr函数编程可以非常直接地使用这些异构集合进行编程,现在可能是当前领先的工具包库。 proposed Boost.Hana理想情况下需要clang 3.6或GCC 5.0。
异构类型容器几乎是99%编译时间1%运行时成本解决方案。您将看到许多编译器优化器面向具有当前编译器技术的工厂,例如我曾经看到clang 3.5为代码生成2500个操作码应该生成两个操作码,并且对于相同的代码,GCC 4.9吐出15个操作码,其中12个实际上没有做任何事情(他们将内存加载到寄存器中并且对这些寄存器没有做任何事) 。所有这一切,在几年后你将能够实现异构类型容器的最佳代码生成,在这一点上,我希望它们将成为C ++元编程的下一代形式,而不是用模板来徘徊我们将能够使用实际函数对C ++编译器进行功能编程!!!
答案 4 :(得分:1)
最近我从libstdc ++中的std::function
实现获得了一个想法:
使用静态成员函数创建一个Handler<T>
模板类,该类知道如何在T上复制,删除和执行其他操作。
然后在Any类的构造函数中存储一个指向该静态函数的函数指针。你的Any类不需要知道T,它只需要这个函数指针来调度T特定的操作。请注意,函数的签名与T无关。
大致如此:
struct Foo { ... }
struct Bar { ... }
struct Baz { ... }
template<class T>
struct Handler
{
static void action(Ptr data, EActions eAction)
{
switch (eAction)
{
case COPY:
call T::T(...);
case DELETE:
call T::~T();
case OTHER:
call T::whatever();
}
}
}
struct Any
{
Ptr handler;
Ptr data;
template<class T>
Any(T t)
: handler(Handler<T>::action)
, data(handler(t, COPY))
{}
Any(const Any& that)
: handler(that.handler)
, data(handler(that.data, COPY))
{}
~Any()
{
handler(data, DELETE);
}
};
int main()
{
vector<Any> V;
Foo foo; Bar bar; Baz baz;
v.push_back(foo);
v.push_back(bar);
v.push_back(baz);
}
这为您提供了类型擦除,同时仍然保持了值语义,并且不需要修改包含的类(Foo,Bar,Baz),也根本不使用动态多态。这很酷。