36

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

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

当然,这必须是完全可扩展的。想想一个库 API,它需要一个vector<???>, 和客户端代码,它将自己的类型添加到已知的类型中。


通常的解决方案是存储(智能)指向(抽象)接口(例如)的指针,vector<unique_ptr<IUsable>>但这有很多缺点——从我的脑海中:

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

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


多年来,我一直在寻找一种可行的替代子类型多态性(又名接口继承)的方法。我经常使用 ad hoc 多态性(又名重载的自由函数),但我总是遇到同样的难题:容器必须是同质的,所以我总是不情愿地回到继承和智能指针,上面已经列出了所有缺点(可能还有更多)。

理想情况下,我希望只vector<IUsable>具有适当的值语义,而不更改我当前(不存在)类型层次结构的任何内容,并保持临时多态性而不是需要子类型多态性。

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

4

5 回答 5

26

不同的选择

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

  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;

你可以把 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方法是基于堆栈的,因此可能更快。如果第一种方法的性能存在问题,请考虑切换到第二种方法。

于 2013-09-18T09:16:41.667 回答
18

归功于它:当我观看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

如您所见,这是一个相当简单的 a 包装器unique_ptr<Interface>,带有一个实例化派生的模板构造函数Implementation<T>。所有(不完全)血淋淋的细节都是私有的,公共接口再干净不过了:包装器本身除了构造/复制/移动之外没有任何成员函数,接口是作为use()重载现有函数的免费函数提供的。

显然,选择unique_ptr意味着我们需要实现一个私有clone()函数,每当我们想要复制一个IUsable对象时调用它(这反过来又需要堆分配)。诚然,每个副本分配一个堆是非常不理想的,但如果公共接口的任何函数可以改变底层对象(即,如果use()采用非常量引用并修改它们),这是一个要求:这样我们确保每个对象都是唯一的因此可以自由变异。


现在,如果像问题一样,对象是完全不可变的(不仅通过暴露的接口,请注意,我意思是整个对象始终是完全不可变的),那么我们可以引入共享状态而不会产生有害的副作用。最直接的方法是使用-to shared_ptr- const而不是 a 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被变异,而不是底层对象本身,所以它不会影响其他共享引用)

于 2013-09-17T18:05:53.007 回答
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

于 2013-09-18T08:27:08.903 回答
3

之前的其他答案(使用 vtabled 接口基类,使用 boost::variant,使用虚拟基类继承技巧)都是解决这个问题的完美有效的解决方案,每个解决方案在编译时间与运行时间成本之间都有不同的平衡。我建议尽管在 C++ 11 及更高版本上使用 egg::variant 而不是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 函数式编程非常简单地使用这些异构集合进行编程,并且可能目前领先的工具包库现在是建议 Boost.Hana理想情况下需要 clang 3.6 或 GCC 5.0。

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

于 2015-02-24T16:10:13.247 回答
1

这是我最近从std::functionlibstdc++ 的实现中得到的一个想法:

创建一个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),并且根本不使用动态多态性。这是很酷的东西。

于 2013-09-18T07:01:48.720 回答