我可以在 C++ 中拥有具有值语义的多态容器吗?

发布于 2024-07-04 20:40:26 字数 1405 浏览 4 评论 0原文

作为一般规则,我更喜欢在 C++ 中使用值而不是指针语义(即使用 vector 而不是 vector)。 通常,性能上的轻微损失可以通过不必记住删除动态分配的对象来弥补。

不幸的是,当您想要存储全部派生自公共基础的各种对象类型时,值集合不起作用。 请参阅下面的示例。

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

我的问题是:我可以鱼与熊掌兼得(值语义)(多态容器)吗? 或者我必须使用指针?

As a general rule, I prefer using value rather than pointer semantics in C++ (ie using vector<Class> instead of vector<Class*>). Usually the slight loss in performance is more than made up for by not having to remember to delete dynamically allocated objects.

Unfortunately, value collections don't work when you want to store a variety of object types that all derive from a common base. See the example below.

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

My question is: Can I have have my cake (value semantics) and eat it too (polymorphic containers)? Or do I have to use pointers?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

2024-07-11 20:40:31

我使用自己的模板化集合类,具有公开的值类型语义,但它在内部存储指针。 它使用自定义迭代器类,当取消引用时,它会获取值引用而不是指针。 复制集合会产生深层项目副本,而不是重复的指针,这就是最大开销所在的地方(一个非常小的问题,考虑到我得到的是什么)。

这是一个可以满足您需求的想法。

I'm using my own templated collection class with exposed value type semantics, but internally it stores pointers. It's using a custom iterator class that when dereferenced gets a value reference instead of a pointer. Copying the collection makes deep item copies, instead of duplicated pointers, and this is where most overhead lies (a really minor issue, considered what I get instead).

That's an idea that could suit your needs.

我的鱼塘能养鲲 2024-07-11 20:40:30

看看static_castreinterpret_cast
在 C++ 编程语言,第 3 版中,Bjarne Stroustrup 在第 130 页对此进行了描述。第 6 章中有一个完整的部分。
您可以将您的父类重新转换为子类。 这需要你知道每一个是什么时候的。 Stroustrup 博士在书中讨论了避免这种情况的不同技术。

不要这样做。 这否定了您首先想要实现的多态性!

Take a look at static_cast and reinterpret_cast
In C++ Programming Language, 3rd ed, Bjarne Stroustrup describes it on page 130. There's a whole section on this in Chapter 6.
You can recast your Parent class to Child class. This requires you to know when each one is which. In the book, Dr. Stroustrup talks about different techniques to avoid this situation.

Do not do this. This negates the polymorphism that you're trying to achieve in the first place!

祁梦 2024-07-11 20:40:30

大多数容器类型都希望抽象出特定的存储策略,无论是链表、向量、基于树还是其他的。 因此,你在拥有和消费上述蛋糕时都会遇到麻烦(即,蛋糕是谎言(注意:有人不得不开这个玩笑))。

那么该怎么办? 嗯,有一些可爱的选项,但大多数都会简化为几个主题之一或其组合的变体:选择或发明合适的智能指针,以某种巧妙的方式使用模板或模板模板,使用容器的通用界面它提供了一个用于实现每个容器的双重调度的钩子。

你的两个既定目标之间存在基本的张力,因此你应该决定你想要什么,然后尝试设计一些基本上可以满足你想要的东西。 可以通过一些巧妙且意想不到的技巧来让指针看起来像具有足够聪明的引用计数和足够聪明的工厂实现的值。 基本思想是使用引用计数、按需复制和常量以及(对于因子)预处理器、模板和 C++ 的静态初始化规则的组合,以尽可能智能地实现自动指针转换。

过去,我花了一些时间尝试设想如何使用虚拟代理/信封字母/带有引用计数指针的可爱技巧来完成类似 C++ 中值语义编程的基础之类的事情。

我认为这是可以做到的,但您必须在 C++ 中提供一个相当封闭的、类似 C# 托管代码的世界(尽管您可以在需要时突破到底层 C++)。 所以我非常同情你的想法。

Most container types want to abstract the particular storage strategy, be it linked list, vector, tree-based or what have you. For this reason, you're going to have trouble with both possessing and consuming the aforementioned cake (i.e., the cake is lie (NB: someone had to make this joke)).

So what to do? Well there are a few cute options, but most will reduce to variants on one of a few themes or combinations of them: picking or inventing a suitable smart pointer, playing with templates or template templates in some clever way, using a common interface for containees that provides a hook for implementing per-containee double-dispatch.

There's basic tension between your two stated goals, so you should decide what you want, then try to design something that gets you basically what you want. It is possible to do some nice and unexpected tricks to get pointers to look like values with clever enough reference counting and clever enough implementations of a factory. The basic idea is to use reference counting and copy-on-demand and constness and (for the factor) a combination of the preprocessor, templates, and C++'s static initialization rules to get something that is as smart as possible about automating pointer conversions.

I have, in the past, spent some time trying to envision how to use Virtual Proxy / Envelope-Letter / that cute trick with reference counted pointers to accomplish something like a basis for value semantic programming in C++.

And I think it could be done, but you'd have to provide a fairly closed, C#-managed-code-like world within C++ (though one from which you could break through to underlying C++ when needed). So I have a lot of sympathy for your line of thought.

带上头具痛哭 2024-07-11 20:40:30

只是为了向所有 1800 条信息添加一件事< /a> 已经说过了。

您可能想看看 Scott Mayers 的 “更有效的 C++” “第 3 条:永远不要以多态方式对待数组”,以便更好地理解这个问题。

Just to add one thing to all 1800 INFORMATION already said.

You might want to take a look at "More Effective C++" by Scott Mayers "Item 3: Never treat arrays polymorphically" in order to better understand this issue.

浅听莫相离 2024-07-11 20:40:29

您也可以考虑 boost::any。 我已将其用于异构容器。 当读回该值时,您需要执行any_cast。 如果失败,它将抛出 bad_any_cast 。 如果发生这种情况,您可以抓住并继续下一种类型。

相信如果您尝试将派生类进行any_cast到其基类,它会抛出bad_any_cast。 我尝试过这个:

 // 但你可以用 boost::any 来做到这一点。 

    向量<任意>   值向量; 

    valueVec.push_back(any(Parent())); 
    valueVec.push_back(any(Child()));   // 仍然是一个 Child,包裹在 Any 中。 

    父 p = any_cast(valueVec[0]); 
    子 c = any_cast(valueVec[1]); 
    p.write(); 
    c.write(); 

    // 输出: 
    // 
    // 父级:1 
    // 孩子:2, 2 

    // 现在尝试将子对象转换为父对象。 
    尝试 { 
        父 p2 = any_cast(valueVec[1]); 
        p2.write(); 
    } 
    catch (const boost::bad_any_cast &e) 
    { 
        计算<<   e.what() <<   结束; 
    } 

    // 输出: 
    // boost::bad_any_cast: 使用 boost::any_cast 转换失败 
  

话虽这么说,我也会先走shared_ptr路线! 只是觉得这可能会引起一些兴趣。

You might also consider boost::any. I've used it for heterogeneous containers. When reading the value back, you need to perform an any_cast. It will throw a bad_any_cast if it fails. If that happens, you can catch and move on to the next type.

I believe it will throw a bad_any_cast if you try to any_cast a derived class to its base. I tried it:

  // But you sort of can do it with boost::any.

  vector<any> valueVec;

  valueVec.push_back(any(Parent()));
  valueVec.push_back(any(Child()));        // remains a Child, wrapped in an Any.

  Parent p = any_cast<Parent>(valueVec[0]);
  Child c = any_cast<Child>(valueVec[1]);
  p.write();
  c.write();

  // Output:
  //
  // Parent: 1
  // Child: 2, 2

  // Now try casting the child as a parent.
  try {
      Parent p2 = any_cast<Parent>(valueVec[1]);
      p2.write();
  }
  catch (const boost::bad_any_cast &e)
  {
      cout << e.what() << endl;
  }

  // Output:
  // boost::bad_any_cast: failed conversion using boost::any_cast

All that being said, I would also go the shared_ptr route first! Just thought this might be of some interest.

汹涌人海 2024-07-11 20:40:29

在寻找这个问题的答案时,我遇到了这个和 类似的问题。 在另一个问题的答案中,您将找到两个建议的解决方案:

  1. 使用 std::Optional 或 boost::Optional 和访问者模式。 这种解决方案使得添加新类型变得困难,但添加新功能却很容易。
  2. 使用类似于 Sean Parent 在他的演讲中介绍的的包装类。 这种解决方案使得添加新功能变得困难,但添加新类型却很容易。

包装器定义类所需的接口,并保存指向此类对象的指针。 接口的实现是通过自由函数完成的。

以下是此模式的示例实现:

class Shape
{
public:
    template<typename T>
    Shape(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {}

    friend void draw(const Shape &shape)
    {
        shape.container->drawImpl();
    }
    // add more functions similar to draw() here if you wish
    // remember also to add a wrapper in the Concept and Model below

private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void drawImpl() const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x) : m_data(move(x)) { }
        void drawImpl() const override
        {
            draw(m_data);
        }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

然后将不同的形状实现为常规结构/类。 您可以自由选择是否要使用成员函数或自由函数(但您必须更新上述实现才能使用成员函数)。 我更喜欢自由函数:

struct Circle
{
    const double radius = 4.0;
};

struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

void draw(const Circle &circle)
{
    cout << "Drew circle with radius " << circle.radius << endl;
}

void draw(const Rectangle &rectangle)
{
    cout << "Drew rectangle with width " << rectangle.width << endl;
}

您现在可以将 CircleRectangle 对象添加到同一个 std::vector

int main() {
    std::vector<Shape> shapes;
    shapes.emplace_back(Circle());
    shapes.emplace_back(Rectangle());
    for (const auto &shape : shapes) {
        draw(shape);
    }
    return 0;
}

这样做的缺点模式的一个缺点是它需要在界面中使用大量的样板文件,因为每个函数需要定义三次。
好处是您可以获得复制语义:

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}

这会产生:

Drew rectangle with width 2
Drew rectangle with width 2

如果您担心 shared_ptr,您可以将其替换为 unique_ptr
但是,它将不再可复制,您必须移动所有对象或手动实施复制。
Sean Parent 在他的演讲中详细讨论了这一点,上面提到的答案中显示了一个实现。

While searching for an answer to this problem, I came across both this and a similar question. In the answers to the other question you will find two suggested solutions:

  1. Use std::optional or boost::optional and a visitor pattern. This solution makes it hard to add new types, but easy to add new functionality.
  2. Use a wrapper class similar to what Sean Parent presents in his talk. This solution makes it hard to add new functionality, but easy to add new types.

The wrapper defines the interface you need for your classes and holds a pointer to one such object. The implementation of the interface is done with free functions.

Here is an example implementation of this pattern:

class Shape
{
public:
    template<typename T>
    Shape(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {}

    friend void draw(const Shape &shape)
    {
        shape.container->drawImpl();
    }
    // add more functions similar to draw() here if you wish
    // remember also to add a wrapper in the Concept and Model below

private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void drawImpl() const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x) : m_data(move(x)) { }
        void drawImpl() const override
        {
            draw(m_data);
        }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

Different shapes are then implemented as regular structs/classes. You are free to choose if you want to use member functions or free functions (but you will have to update the above implementation to use member functions). I prefer free functions:

struct Circle
{
    const double radius = 4.0;
};

struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

void draw(const Circle &circle)
{
    cout << "Drew circle with radius " << circle.radius << endl;
}

void draw(const Rectangle &rectangle)
{
    cout << "Drew rectangle with width " << rectangle.width << endl;
}

You can now add both Circle and Rectangle objects to the same std::vector<Shape>:

int main() {
    std::vector<Shape> shapes;
    shapes.emplace_back(Circle());
    shapes.emplace_back(Rectangle());
    for (const auto &shape : shapes) {
        draw(shape);
    }
    return 0;
}

The downside of this pattern is that it requires a large amount of boilerplate in the interface, since each function needs to be defined three times.
The upside is that you get copy-semantics:

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}

This produces:

Drew rectangle with width 2
Drew rectangle with width 2

If you are concerned about the shared_ptr, you can replace it with a unique_ptr.
However, it will no longer be copyable and you will have to either move all objects or implement copying manually.
Sean Parent discusses this in detail in his talk and an implementation is shown in the above mentioned answer.

大姐,你呐 2024-07-11 20:40:28

我只是想指出向量通常比向量更有效。 在向量中,所有Foo在内存中将彼此相邻。 假设 TLB 和缓存是冷的,第一次读取会将页面添加到 TLB 并将矢量块拉入 L# 缓存; 后续读取将使用热缓存和已加载的 TLB,偶尔会出现缓存未命中和不太频繁的 TLB 故障。

将此与向量进行对比:当您填充向量时,您会从内存分配器中获取 Foo*。 假设您的分配器不是非常智能(tcmalloc?)或者您随着时间的推移慢慢填充向量,每个 Foo 的位置可能与其他 Foo 相距很远:可能仅相距数百字节,也可能相距兆字节。

在最坏的情况下,当您扫描向量时, 取消引用每个指针将导致 TLB 错误和缓存未命中——这最终会比使用向量很多。 (好吧,在最坏的情况下,每个 Foo 都已被分页到磁盘,并且每次读取都会引发磁盘eek()和read()以将页面移回到RAM中。)

因此,继续使用向量每当适当的时候。 :-)

I just wanted to point out that vector<Foo> is usually more efficient than vector<Foo*>. In a vector<Foo>, all the Foos will be adjacent to each other in memory. Assuming a cold TLB and cache, the first read will add the page to the TLB and pull a chunk of the vector into the L# caches; subsequent reads will use the warm cache and loaded TLB, with occasional cache misses and less frequent TLB faults.

Contrast this with a vector<Foo*>: As you fill the vector, you obtain Foo*'s from your memory allocator. Assuming your allocator is not extremely smart, (tcmalloc?) or you fill the vector slowly over time, the location of each Foo is likely to be far apart from the other Foos: maybe just by hundreds of bytes, maybe megabytes apart.

In the worst case, as you scan through a vector<Foo*> and dereferencing each pointer you will incur a TLB fault and cache miss -- this will end up being a lot slower than if you had a vector<Foo>. (Well, in the really worst case, each Foo has been paged out to disk, and every read incurs a disk seek() and read() to move the page back into RAM.)

So, keep on using vector<Foo> whenever appropriate. :-)

遗心遗梦遗幸福 2024-07-11 20:40:28

是的你可以。

boost.ptr_container 库提供标准容器的多态值语义版本。 您只需传入一个指向堆分配对象的指针,容器将获得所有权,并且所有进一步的操作都将提供值语义,除了回收所有权之外,这通过使用智能指针为您提供了几乎所有值语义的好处。

Yes, you can.

The boost.ptr_container library provides polymorphic value semantic versions of the standard containers. You only have to pass in a pointer to a heap-allocated object, and the container will take ownership and all further operations will provide value semantics , except for reclaiming ownership, which gives you almost all the benefits of value semantics by using a smart pointer.

﹎☆浅夏丿初晴 2024-07-11 20:40:27

由于不同类的对象具有不同的大小,因此如果将它们存储为值,最终会遇到切片问题。

一种合理的解决方案是存储容器安全的智能指针。 我通常使用 boost::shared_ptr ,它可以安全地存储在容器中。 请注意, std::auto_ptr 不是。

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

shared_ptr 使用引用计数,因此在删除所有引用之前它不会删除底层实例。

Since the objects of different classes will have different sizes, you would end up running into the slicing problem if you store them as values.

One reasonable solution is to store container safe smart pointers. I normally use boost::shared_ptr which is safe to store in a container. Note that std::auto_ptr is not.

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

shared_ptr uses reference counting so it will not delete the underlying instance until all references are removed.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文