关于stl:我可以在C中使用具有值语义的多态容器吗?

关于stl:我可以在C中使用具有值语义的多态容器吗?

Can I have polymorphic containers with value semantics in C++?

通常,我更喜欢在C语言中使用值而不是指针语义(即,使用vector<Class>代替vector<Class*>)。通常,由于不必记住删除动态分配的对象,可以弥补轻微的性能损失。

不幸的是,当您要存储所有都源自同一基础的各种对象类型时,值集合不起作用。请参见下面的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#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

}

我的问题是:我可以吃蛋糕(值语义),也可以吃它(多态容器)吗?还是我必须使用指针?


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

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

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

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


我只想指出vector 通常比vector 更有效。在vector 中,所有的Foos在内存中将彼此相邻。假设TLB和缓存处于冷状态,则第一次读取会将页面添加到TLB,并将向量的一部分拉入L#缓存;随后的读取将使用热缓存和已加载的TLB,偶尔出现缓存未命中和较少发生的TLB故障。

将其与向量进行对比:填充向量时,将从内存分配器中获取Foo *。假设您的分配器不是非常聪明(tcmalloc?),或者随着时间的推移缓慢地填充向量,则每个Foo的位置可能与另一个Foos相距很远:也许只是数百个字节,也许是兆字节。 >

在最坏的情况下,当您扫描向量并取消对每个指针的引用时,将导致TLB错误和缓存未命中-这将比使用向量时要慢得多。 (好吧,在最坏的情况下,每个Foo都已被调出到磁盘上,并且每次读取都会产生磁盘seek()和read()来将页面移回RAM。)

因此,请在适当的时候继续使用vector 。 :-)


是的,可以。

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


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

我相信,如果您尝试将一个派生类广播到其基类,它将抛出bad_any_cast。我试过了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  // But you sort of can do it with boost::any.

  vector 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

话虽如此,我也将先走shared_ptr路线!只是认为这可能会引起一些兴趣。


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

  • 使用std :: optional或boost :: optional和访问者模式。该解决方案使添加新类型变得困难,但是却易于添加新功能。
  • 使用类似于Sean Parent在演讲中介绍的包装器类。该解决方案很难添加新功能,但是很容易添加新类型。
  • 包装器定义您的类所需的接口,并包含一个指向此类对象的指针。接口的实现是通过自由函数完成的。

    这是此模式的示例实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    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;
    };

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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<Shape>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int main() {
        std::vector<Shape> shapes;
        shapes.emplace_back(Circle());
        shapes.emplace_back(Rectangle());
        for (const auto &shape : shapes) {
            draw(shape);
        }
        return 0;
    }

    此模式的缺点是,由于每个功能需要定义三次,因此它在界面中需要大量样板。
    好处是您可以获得复制语义:

    1
    2
    3
    4
    5
    6
    7
    8
    int main() {
        Shape a = Circle();
        Shape b = Rectangle();
        b = a;
        draw(a);
        draw(b);
        return 0;
    }

    这将产生:

    1
    2
    Drew rectangle with width 2
    Drew rectangle with width 2

    如果您担心shared_ptr,可以将其替换为unique_ptr
    但是,它将不再是可复制的,您将不得不移动所有对象或手动实施复制。
    肖恩·帕恩特(Sean Parent)在演讲中对此进行了详细讨论,并在上述答案中给出了一个实现。


    只需在已经说过的1800条信息中添加一件事即可。

    您可能想看看Scott Mayers的"更有效的C","项目3:从不对数组进行多态处理",以便更好地理解此问题。


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

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

    这两个既定目标之间存在基本的紧张关系,因此您应该确定所需的内容,然后尝试设计一些基本可以使您获得所需内容的东西。可以做一些不错的和意想不到的技巧,以使指针看起来像具有足够聪明的引用计数和足够聪明的工厂实现的值。基本思想是使用引用计数,按需复制和constness,以及(针对该因素)预处理器,模板和C的静态初始化规则的组合,以获取关于自动化指针转换的尽可能智能的信息。

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

    我认为可以做到,但是您必须在C中提供一个相当封闭,类似C#托管代码的世界(尽管您可以在需要时从中进入基础C)。因此,我很同情您的思路。


    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.

    请勿执行此操作。首先,这会否定您要实现的多态性!


    我正在使用自己的具有公开值类型语义的模板化集合类,但在内部它存储指针。它使用了一个自定义的迭代器类,该类在取消引用时会得到值引用,而不是指针。复制集合将进行深层项目复制,而不是重复的指针复制,这就是大多数开销所在的地方(这是一个很小的问题,考虑到我所得到的)。

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


    推荐阅读