sya*_*yam 32 c++ polymorphism containers heterogeneous c++11
我有许多不相关的类型,它们都通过重载的自由函数(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; }
Run Code Online (Sandbox Code Playgroud)
正如问题的标题所暗示的那样,我希望将这些类型的实例存储在异构容器中,这样use()
无论它们具体是什么类型,我都可以使用它们.容器必须具有值语义(即两个容器之间的赋值复制数据,它不共享它).
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);
Run Code Online (Sandbox Code Playgroud)
当然,这必须是完全可扩展的.考虑一个带有a的库API vector<???>
,以及将自己的类型添加到已知类型的客户端代码.
通常的解决方案是将(智能)指针存储到(抽象)接口(例如vector<unique_ptr<IUsable>>
),但这有许多缺点 - 从我的头脑:
int
和string
什么不...不向减少可重用性/可组合何况由于免费会员功能成为紧密相连的接口(虚成员函数).vec1 = vec2
如果我们使用unique_ptr
(强制我手动执行深拷贝),则不可能进行简单的赋值,或者如果我们使用shared_ptr
(这有其优点和缺点 - 两个容器最终都是共享状态- 但是因为我想要值容器上的语义,我再次被迫手动执行深层复制).clone()
在每个派生类中实现的虚函数.你能认真考虑一些比这更无聊的事情吗?总结一下:这增加了许多不必要的耦合,并且需要大量(可以说是无用的)样板代码.这绝对不能令人满意,但到目前为止,这是我所知道的唯一实用解决方案.
我一直在寻找一种可行的替代亚型多态性(又名界面继承)的方法.我使用ad hoc多态(也就是重载的自由函数)玩了很多但是我总是遇到同样的困难:容器必须是同构的,所以我总是勉强回到继承和智能指针,上面已经列出了所有的缺点(而且可能更多).
理想情况下,我希望只vector<IUsable>
使用正确的值语义,而不改变我当前(缺少)类型层次结构的任何内容,并保持ad hoc多态而不需要子类型多态.
这可能吗?如果是这样,怎么样?
Ral*_*zky 23
有可能的.有几种替代方法可以解决您的问题.每个人都有不同的优点和缺点(我将解释每一个):
boost::variant
和访问.对于第一个替代方案,您需要创建一个这样的接口:
class UsableInterface
{
public:
virtual ~UsableInterface() {}
virtual void use() = 0;
virtual std::unique_ptr<UsableInterface> clone() const = 0;
};
Run Code Online (Sandbox Code Playgroud)
显然,每当你有一个具有该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;
};
Run Code Online (Sandbox Code Playgroud)
现在,您实际上已经可以使用它完成所需的一切.你可以将这些东西放在一个向量中:
std::vector<std::unique_ptr<UsableInterface>> usables;
// fill it
Run Code Online (Sandbox Code Playgroud)
您可以复制保留基础类型的向量:
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(); } );
Run Code Online (Sandbox Code Playgroud)
你可能不希望用这样的东西乱丢你的代码.你想写的是
copies = usables;
Run Code Online (Sandbox Code Playgroud)
好吧,通过将其包装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;
};
Run Code Online (Sandbox Code Playgroud)
由于漂亮的模板化构造函数,您现在可以编写类似的东西
Usable u1 = 5;
Usable u2 = std::string("Hello usable!");
Run Code Online (Sandbox Code Playgroud)
您可以使用适当的值语义分配值:
u1 = u2;
Run Code Online (Sandbox Code Playgroud)
你可以把Usables放进去 std::vector
std::vector<Usable> usables;
usables.emplace_back( std::string("Hello!") );
usables.emplace_back( 42 );
Run Code Online (Sandbox Code Playgroud)
并复制该矢量
const auto copies = usables;
Run Code Online (Sandbox Code Playgroud)
你可以在肖恩父母谈论价值语义学和基于概念的多态性中找到这个想法.他还在Going Native 2013上给出了这个演讲的一个非常简短的版本,但我认为这是要快速跟进的.
此外,您可以采用更通用的方法,而不是编写自己的Usable
类并转发所有成员函数(如果您想稍后添加其他函数).我们的想法是Usable
用模板类替换类.此模板类不提供成员函数,use()
而是提供operator T&()
和operator const T&() const
.这为您提供了相同的功能,但每次促进此模式时,您都不需要编写额外的值类.
该模板类boost::variant
正是提供像一个C风格union
,但安全和适当的值语义.使用它的方法是这样的:
using Usable = boost::variant<int,std::string,A>;
Usable usable;
Run Code Online (Sandbox Code Playgroud)
您可以从任何这些类型的对象分配给a Usable
.
usable = 1;
usable = "Hello variant!";
usable = A();
Run Code Online (Sandbox Code Playgroud)
如果所有模板类型都具有值语义,那么它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 );
}
Run Code Online (Sandbox Code Playgroud)
现在你可以写了
Usable u = "Hello";
use( u );
Run Code Online (Sandbox Code Playgroud)
而且,正如我已经提到的,您可以将这些东西放入STL容器中.
std::vector<Usable> usables;
usables.emplace_back( 5 );
usables.emplace_back( "Hello world!" );
const auto copies = usables;
Run Code Online (Sandbox Code Playgroud)
您可以在两个维度中扩展功能:
在我提出的第一种方法中,添加新类更容易.第二种方法可以更轻松地添加新功能.
在第一种方法中,客户端代码无法(或至少很难)添加新功能.在第二种方法中,客户端代码不可能(或至少很难)在混合中添加新类.一种出路是所谓的非循环访问者模式,它使客户端可以使用新类和新功能扩展类层次结构.这里的缺点是你必须在编译时牺牲一定数量的静态检查.这是一个描述访客模式的链接,包括非循环访客模式以及其他一些替代方案.如果您对这些内容有疑问,我愿意回答.
两种方法都是超类型安全的.在那里没有权衡取舍.
第一种方法的运行时成本可以高得多,因为您创建的每个元素都涉及堆分配.该boost::variant
方法基于堆栈,因此可能更快.如果性能是第一种方法的问题,请考虑切换到第二种方法.
sya*_*yam 16
应有的信用:当我看到Sean Parent的Going Native 2013 "继承是邪恶的基础"谈话时,我意识到事后看来解决这个问题实际上是多么简单.我只能建议你观看它(有更多有趣的东西在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
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,这是一个相当简单的包装器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;
}
Run Code Online (Sandbox Code Playgroud)
注意clone()
函数是如何消失的(我们不再需要它,我们只是共享底层对象,因为它是不可变的,所以没有麻烦),以及如何noexcept
通过shared_ptr
保证来复制.
有趣的是,底层对象必须是不可变的,但你仍然可以改变它们的IUsableImmutable
包装器,所以它仍然完全可以做到这一点:
std::vector<IUsableImmutable> items;
items.emplace_back(3);
items[0] = std::string{ "hello" };
Run Code Online (Sandbox Code Playgroud)
(只有shared_ptr
变异,而不是底层对象本身,所以它不会影响其他共享引用)
小智 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;
}
Run Code Online (Sandbox Code Playgroud)
实例:http://coliru.stacked-crooked.com/a/e4f4ccf6d7e6d9d8
归档时间: |
|
查看次数: |
5275 次 |
最近记录: |