gpt4 book ai didi

c++ - 具有值语义的即席多态性和异构容器

转载 作者:IT老高 更新时间:2023-10-28 12:39:08 25 4
gpt4 key购买 nike

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

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 ,以及将自己的类型添加到已知类型的客户端代码。

通常的解决方案 是存储(智能)指向(抽象)接口(interface)(例如 vector<unique_ptr<IUsable>> )的指针,但这有许多缺点——从我的头顶来看:
  • 我必须将我当前的临时多态模型迁移到一个类层次结构,其中每个类都从公共(public)接口(interface)继承。哦啪!现在我必须为 int 编写包装器和 string还有什么不是......更不用说由于自由成员函数与接口(interface)(虚拟成员函数)密切相关而导致的可重用性/可组合性降低。
  • 容器失去了它的值语义:一个简单的赋值 vec1 = vec2如果我们使用 unique_ptr 是不可能的(强制我手动执行深拷贝),或者如果我们使用 shared_ptr,两个容器最终都处于共享状态(它有其优点和缺点——但由于我想要容器上的值语义,我再次被迫手动执行深度复制)。
  • 为了能够执行深拷贝,接口(interface)必须支持虚拟 clone()必须在每个派生类中实现的函数。你能认真地想出比这更无聊的事情吗?

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

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

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

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

    最佳答案

    不同的选择

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

  • 创建一个接口(interface)并有一个模板类,它为不同的类型实现这个接口(interface)。它应该支持克隆。
  • 使用 boost::variant和探访。

  • 混合静态和动态多态性

    对于第一个选择,您需要创建一个这样的界面:
    class UsableInterface 
    {
    public:
    virtual ~UsableInterface() {}
    virtual void use() = 0;
    virtual std::unique_ptr<UsableInterface> clone() const = 0;
    };

    很明显,每次你有一个具有 use() 的新类型时,你都不想手动实现这个接口(interface)。功能。因此,让我们有一个模板类来为你做这件事。
    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;
    };

    现在,您实际上已经可以用它做您需要的一切了。你可以把这些东西放在一个 vector 中:
    std::vector<std::unique_ptr<UsableInterface>> usables;
    // fill it

    您可以复制该 vector 保留基础类型:
    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 );

    并复制该 vector
    const auto copies = usables;

    你可以在肖恩 parent 的谈话中找到这个想法 Value Semantics and Concepts-based Polymorphism .他还给出了一个非常简短的版本 talk at Going Native 2013 ,但我认为这是为了快速遵循。

    此外,您可以采用比自己编写更通用的方法 Usable类并转发所有成员函数(如果您以后想添加其他)。想法是替换类 Usable带有模板类。此模板类将不提供成员函数 use()但是一个 operator T&()operator const T&() const .这为您提供了相同的功能,但您无需在每次使用此模式时都编写额外的值类。

    一个安全的、通用的、基于堆栈的可区分 union 容器

    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 容器中。你可以写一个 use()通过称为 visitor pattern 的模式为此类对象提供函数.它调用正确的 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;

    权衡取舍

    您可以在两个维度上扩展功能:
  • 添加满足静态接口(interface)的新类。
  • 添加类必须实现的新功能。

  • 在我介绍的第一种方法中,添加新类更容易。第二种方法可以更轻松地添加新功能。

    在第一种方法中,客户端代码不可能(或至少很难)添加新功能。在第二种方法中,客户端代码不可能(或至少很难)向混合中添加新类。一个出路是所谓的非循环访问者模式,它使客户端可以使用新类和新功能扩展类层次结构。这里的缺点是您必须在编译时牺牲一定数量的静态检查。这是一个 link which describes the visitor pattern包括非循环访问者模式以及其他一些替代方案。如果你对这个东西有疑问,我愿意回答。

    这两种方法都是 super 类型安全的。没有在那里进行权衡。

    第一种方法的运行时间成本可能要高得多,因为您创建的每个元素都涉及堆分配。 boost::variant方法是基于堆栈的,因此可能更快。如果第一种方法的性能有问题,请考虑切换到第二种方法。

    关于c++ - 具有值语义的即席多态性和异构容器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18856824/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com