Ad hoc多态和具有值语义的异构容器

时间:2013-09-17 18:05:53

标签: c++ c++11 polymorphism containers heterogeneous

我有许多不相关的类型,它们都通过重载的自由函数(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>>),但这有很多缺点 - 从我的脑海中浮现出来:

  • 我必须将当前的ad hoc多态模型迁移到类层次结构中,其中每个类都继承自公共接口。 哦快攻!现在我必须为intstring编写包装器而不是......更不用说由于免费成员函数变得密切而导致可重用性/可组合性降低绑定到接口(虚拟成员函数)。
  • 容器失去了它的值语义:如果我们使用vec1 = vec2(强制我手动执行深层复制),则不可能使用简单的赋值unique_ptr,或者如果我们使用{最后两个容器都以共享状态结束} {1}}(它有它的优点和缺点 - 但由于我想要容器上的值语义,我再次被迫手动执行深层复制)。
  • 为了能够执行深层复制,接口必须支持虚拟shared_ptr功能,该功能必须在每个派生类中实现。 你能认真考虑一些比这更无聊的东西吗?

总结一下:这增加了许多不必要的耦合,需要大量(可以说是无用的)样板代码。这是绝对不满意但到目前为止这是我所知道的唯一实用解决方案。


我一直在寻找一种可行的替代亚型多态性(又名界面继承)的年龄。我使用ad hoc多态(也就是重载的自由函数)玩了很多但是我总是遇到同样的困难:容器是同构的,所以我总是勉强回到继承和智能指针,上面列出的所有缺点(可能还有更多)。

理想情况下,我希望只有clone()具有正确的值语义,而不会将任何内容更改为当前(缺少)类型层次结构,并且保持ad hoc多态性而不是要求子类型多态性。

这可能吗?如果是这样,怎么样?

5 个答案:

答案 0 :(得分:23)

不同的选择

有可能。有几种替代方法可以解决您的问题。每个人都有不同的优点和缺点(我将解释每一个):

  1. 创建一个接口并拥有一个模板类,该类为不同类型实现此接口。它应该支持克隆。
  2. 使用boost::variant和访问。
  3. 混合静态和动态多态

    对于第一种选择,您需要创建一个这样的界面:

    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;
}

实例:http://coliru.stacked-crooked.com/a/e4f4ccf6d7e6d9d8

答案 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),也根本不使用动态多态。这很酷。