是什么激发了 Stroustrup 对资源管理技术的偏好顺序?

What motivates Stroustrup's order of preference for resource management techniques?

以下幻灯片来自 Bjarne Stroustrups 的演讲 "The Essence of C++":

我是否通过以下简单示例(值、原始指针、资源智能指针 members/subobjects)理解了他的技术?:
1.

class foo{  
    std::vector<bar> subObj;  
    int n;  
public:  
    foo(int n) : n(n) {
        subObj.push_back(bar("snickers"));
    } 
};

2.

class foo{  
    std::vector<bar>* subObj;  
    int n;  
public:
    foo(int n) : n(n){
        subObj = new std::vector<bar>();
        subObj->push_back(bar("snickers"));
    } 

    ~foo() {
        delete subObj;
    }
};

3.

class foo{    
    std::unique_ptr<std::vector<bar> > subObj;  
    int n;  
public:   
    foo(int n) : n(n){  
        subObj(new std::vector<bar>());  
        subObj->push_back(bar("snickers"));  
    }
}; 

为什么 1. 优于 2.?如果我实例化一个 foo 对象并取消引用它以获得 small n 成员的值,那么 vector 成员也将被加载到内存中,对吗?有了2,只有指针被加载到内存中!对于这个问题,我想假设向量在执行过程中会有点大。

此外,为什么 2 (RAII) 优于智能指针?开销是不是很相似(都在生命周期后销毁资源)?

我认为项目符号 2 针对的是其他资源,例如网络连接、文件句柄、与 audio/video 设备的连接等

如果您的 class 实例打开网络连接,您需要确保 class 的析构函数关闭连接。

如果您的 class 实例打开文件或目录,您需要确保 class 的析构函数关闭文件或目录。

如果您的 class 实例打开了与您的音频设备的连接,您需要确保 class 的析构函数关闭连接。

如果您的 class 的实例将程序的显示更改为使用整个屏幕而不是 window,class 的析构函数需要确保显示恢复到 window 模式。

无论 2 是什么,它都不比任何东西更可取。一旦你复制了一个 foo 对象,这是一个等待发生的意外。

Why is 1. preferable over 2.? If I instantiate a foo object and dereference it in order to get the value of the small n member, the vector member will be loaded into memory as well, right? With 2, only the pointer is loaded into memory!

这很令人困惑 - 这里的所有内容都已经在内存中了,那么您究竟要将什么加载到内存中?的确,2 中的 foo 小于 1 中的 foo,因此您可以在同一缓存行中容纳更多 foo,当您对foo 的数组而不触及 vector.

但是 vector 本身只不过是三个指针(用于开始、结束、容量),所以这并不是一个巨大的位置损失。 vector 的内容可以任意大(当然有一个限制)但它们在其他地方。当您实际需要使用 vector.

时,foo 的较小尺寸带来的微小局部性增益很可能会被额外的间接级别擦除

首先,关于那个presentation/slide的范围:据我所知,Strostrup主要是在谈论函数中的局部变量,而不是成员变量。此外,他想指出的一点是,C++ 中的垃圾收集器(几乎)是不必要的,因为还有其他(可以说是更好的)机制(构建在析构函数之上)。所以你不应该太看重第 1-3 点的顺序。您使用哪种技术更多地取决于用例以及我们实际谈论的资源类型。
现在介绍不同的技术:

  1. 容器:显然,如果您真的想管理多个项目,您应该只使用容器。这主要与 C-Style 数组形成对比。所以你不应该做的是:

    Ressource rc[]=new Ressource[10]; 
    

    而是使用例如向量:

    std::vector<Ressource> rc(10);
    

    它的优点是复制 vector 也会复制包含的元素(值语义),并且一旦 vector 被销毁(例如超出范围),元素就会自动销毁。 C 样式数组不提供这两种机制。

  2. RAII:如果必须确保某个资源必须在某个时间点释放,则应将其包装到 RAII class 中。这可以是内存(例如考虑一个大矩阵),但在与线程、互斥体、文件或套接字等系统资源结合时尤其知名。明确一点:在您的示例 1 中,您是 "managing" 的资源是 bar 的集合,向量是执行此操作的技术。在示例 2 中,资源是 std::vector<bar>foo 是 RAII-class。现在,如果您的 RAII class 管理内存(如您的情况),它应该在内部使用智能指针。
    一条经验法则:永远不要使用 newdelete - 始终使用 std::make_uniquestd::make_shared (一如既往,可能会有特殊情况,你有打破那个规则)。

  3. 这给我们带来了智能指针。基本上它们是 classic new/ delete 组合的替代品。虽然它们擅长自动释放内存,但与成熟的 RAII class 相比,它们有一些缺点:1)它们仅适用于内存,但例如无法获取锁或文件(但当然可以用来保存其他 RAII classes)。 2) 它们不提供值语义(复制 share_ptr 不会复制其内容,而复制 unique_ptr 是不可能的)。但同样,它们非常适合实现某些类型的 RAII classes,并且如果您只想在堆上创建单个对象(免费存储)。

因此,总结一下:您的示例并未针对同一类型的资源显示不同的技术(容器、RAII、智能指针),而是您通过不同的机制管理不同类型的资源。从您的示例中,数字 2 严格来说比 1 和 3 差,因为它手动调用 newdelete(需要额外的函数并添加额外的错误源)。 1 还是 3 更好取决于您的应用程序。一般来说,我更喜欢 1,因为它自动实现值语义,而 3 需要手动实现 copy/move 赋值运算符/构造函数。但是,如果您绝对需要最小化 foo 本身的内存(而不是总内存占用),那么 3 与 1 相比节省了 2 个指针的大小(忽略由于填充可能产生的其他影响)。