具有值语义的临时多态性和异构容器

Ad hoc polymorphism and heterogeneous containers with value semantics

本文关键字:多态性 异构 语义      更新时间:2023-10-16

我有许多不相关的类型,它们都通过重载的自由函数(临时多态性)支持相同的操作:

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()它们。容器必须具有值语义(即两个容器之间的赋值复制数据,它不共享数据)。

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>> ),但这有很多缺点——从我的头顶上看:

  • 我必须将我当前的临时多态模型迁移到一个类层次结构中,其中每个类都继承自公共接口。哦,啪!现在我必须为intstring编写包装器,而不是什么......更不用说由于自由成员函数与接口(虚拟成员函数)紧密绑定而导致的可重用性/可组合性降低。
  • 容器失去了它的价值语义:如果我们使用 unique_ptr(迫使我手动执行深度复制),一个简单的赋值vec1 = vec2是不可能的),或者如果我们使用 shared_ptr,两个容器最终都会达到共享状态(这有其优点和缺点 - 但由于我想在容器上获得值语义,我再次被迫手动执行深度复制)。
  • 为了能够执行深度复制,接口必须支持虚拟clone()函数,该函数必须在每个派生类中实现。 你能认真地想到比这更无聊的东西吗?

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


多年来,我一直在寻找亚型多态性(又名界面继承)的可行替代方案。我经常玩临时多态性(又名重载的自由函数),但我总是碰到同样的硬墙:容器必须是同质的,所以我总是勉强回到继承和智能指针,上面已经列出了所有缺点(可能更多)。

理想情况下,我希望有一个具有适当值语义的vector<IUsable>,而不对我当前(没有)类型层次结构进行任何更改,并保持临时多态性而不是要求子类型多态性。

这可能吗?如果是这样,如何?

不同的选择

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

  1. 创建一个接口,并拥有一个模板类,该类为不同类型的接口实现此接口。它应该支持克隆。
  2. 使用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;

您可以将可用物品放在std::vector

std::vector<Usable> usables;
usables.emplace_back( std::string("Hello!") );
usables.emplace_back( 42 );

并复制该向量

const auto copies = usables;

你可以在肖恩父母谈论价值语义和基于概念的多态性中找到这个想法。他还在2013年本土化大会上对这个演讲做了一个非常简短的版本,但我认为这是要快速跟进的。

此外,您可以采用更通用的方法,而不是编写自己的 Usable 类并转发所有成员函数(如果要稍后添加其他函数)。这个想法是将类Usable替换为模板类。此模板类不会提供成员函数use()而是提供operator T&()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方法是基于堆栈的,因此可能更快。如果第一种方法的性能有问题,请考虑切换到第二种方法。

功劳:当我观看肖恩·父母(Sean Parent)2013年的《走向本土》(Go Native Native 2013)"继承是邪恶的基础阶级"(Inheritance Is The Base Class of Evil)的演讲时,我意识到解决这个问题实际上是多么简单。我只能建议你观看它(在短短20分钟内有更多有趣的东西,这个问答几乎没有触及整个演讲的表面),以及其他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()获取了非常量引用并修改了它们),这是一个要求:通过这种方式,我们确保每个对象都是唯一的,因此可以自由改变。


现在,如果像问题中一样,对象是完全不可变的(不仅通过公开的接口,请注意,我的意思是整个对象总是完全不可变的),那么我们可以引入共享状态而不会产生恶意的副作用。执行此操作的最直接方法是使用 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() 函数是如何消失的(我们不再需要它,我们只是共享底层对象,这并不麻烦,因为它是不可变的),以及由于shared_ptr保证,现在如何noexcept复制。

有趣的是,底层对象必须是不可变的,但你仍然可以改变它们的IUsableImmutable包装器,所以这样做仍然是完全可以的:

  std::vector<IUsableImmutable> items;
  items.emplace_back(3);
  items[0] = std::string{ "hello" };

(只有shared_ptr是突变的,而不是底层对象本身,因此它不会影响其他共享引用)

也许 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

前面的其他答案(使用 vtabled 接口基类,使用 boost::variant,使用虚拟基类继承技巧)都是这个问题的完美和有效的解决方案,每个解决方案都有编译时间与运行时成本的差异平衡。我建议不要使用 boost::variant,但在 C++ 11 及更高版本上使用 eggs::variant 代替,这是使用 C++ 11/14 对 boost::variant 的重新实现,它在设计、性能、易用性、抽象能力方面非常优越,它甚至在 VS2013 上提供了一个相当完整的功能子集(在 VS2015 上提供了完整的功能集)。它也是由Boost的主要作者编写和维护的。

如果你能够稍微重新定义这个问题 - 具体来说,你可以失去类型擦除std::vector,转而使用更强大的东西 - 你可以使用异构类型容器来代替。这些工作原理是为容器的每次修改返回新的容器类型,因此模式必须是:

类型新容器=oldcontainer.push_back(新项);

这些在 03 C++中使用起来很痛苦,尽管 Boost.Fusion 在使它们可能有用方面做出了公平的拳头。实际上有用的可用性只能从 C++ 11 开始,尤其是从 C++ 14 开始,这要归功于通用 lambda,这使得使用这些异构集合非常简单地使用 constexpr 函数式编程进行编程,并且可能目前领先的工具包库是 Boost.Hana,理想情况下需要 clang 3.6 或 GCC 5.0。

异构类型容器几乎是 99% 编译时间 1% 运行时成本的解决方案。你会看到很多编译器优化器面对工厂使用当前的编译器技术,例如我曾经看到clang 3.5为应该生成两个操作码的代码生成2500个操作码,而对于相同的代码,GCC 4.9吐出15个操作码,其中12个实际上没有做任何事情(他们将内存加载到寄存器中,对这些寄存器什么也没做)。综上所述,在几年内,您将能够为异构类型容器实现最佳代码生成,此时我预计它们将成为C++元编程的下一代形式,我们将能够使用实际函数对C++编译器进行功能编程,而不是使用模板!!

这是我

最近从libstdc++中的std::function实现中获得的一个想法:

使用知道如何在 T 上复制、删除和执行其他操作的静态成员函数创建一个 Handler<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),并且根本不使用动态多态性。 这是很酷的东西。