c++ 处理存储在不同类中的类的对象的有效性

zvms9eto  于 2022-11-19  发布在  其他
关注(0)|答案(3)|浏览(134)

我试图理解类处理 * 另一个 * 类的对象的有效性(引用)的首选方法。
在这里,C有一个存储D对象引用的向量。如果DC是库的一部分,C应该如何处理D对象超出调用者范围的情况?
我有几种方法,虽然不确定可行性:

  • D知道_list存储了什么,并且一旦所述D超出范围,~D()就运行,其将自身从_list中移除。
  • _list存储weak_ptr,而不是raw,并且在访问D时,在访问之前调用weak_ptr::lock(),尽管这将需要示例化shared_ptr,而这在生产中似乎并不常见。
struct D
{
    ~D()
    {
        printf ("~D()\n");
    }
};

class C
{
    vector<D*> _list;

    public:
    void add(D* dObject)
    {
        _list.push_back(dObject);
        printf ("Adding D => size = %ld\n", _list.size());
    }

    ~C()
    {
        printf ("~C()\n");
    }
};

int main()
{
    C c1;
    {
        D d1;
        c1.add(&d1);
    }

    /**
     _list[0] is garbage now. How to avoid accessing it i.e 
      C being aware to not access it?
    */
    printf ("----out of scope---\n"); 
    D d2;                   
    c1.add(&d2);
}
ltqd579y

ltqd579y1#

备选项为

1.使用scope_exit(也称为作用域保护):

int main() {
    C c1;
    {
        D d1;
        c1.add(&d1);
        scope_exit d1_erase([&] {
            c1.erase(&d1);
        });
        // ... insert here other code within the inner scope
    }
    // ... insert here other code within the outer scope
    return 0;
}

你应该在D中使用std::set而不是std::vector来更有效地查找和删除条目。当然D应该公开提供erasedelete函数。
scope_exit是即将发布的C++库基础v3技术规范(https://en.cppreference.com/w/cpp/experimental/scope_exit)的一部分。是的。
1.创建一个自定义类,用于管理从两个类外部添加和自动删除(也在范围退出时)指向列表的指针和从列表中删除指针:

class E
{
public:
    E(C& c, D* dp) : _c(c), _dp(dp) { _c.add(_dp); }; // add to list
    ~E() { _c.erase(_dp); };                          // remove from list

    E(const E&) = delete;            // rule-of-3
    E& operator=(const E&) = delete;

private:
    C& _c;
    D* _dp;
};

int main() {
    C c1;
    {
        D d1;
        E e(c1, &d1); // adds pointer to d1 to c1 list
                      // and removes it automatically at the end of the scope
    }

    return 0;
}

您也可以将D储存在E中,而不是透过储存的指涉来指涉它。
顺便说一句:本地范围的销毁顺序与施工顺序相反。
这两个解决方案保持CD不变。您必须自己决定,C是否应该以入侵方式了解其D对象,还是像这两个解决方案那样以非入侵方式处理其在D中的添加和删除。
对于对象存储在本地并且销毁时间明确的情况(在结束时或由于异常或return语句而离开作用域时),我不会使用shared_ptr/weak_ptr

omhiaaxx

omhiaaxx2#

基本上在c++中,你不应该保留栈示例对某些容器的引用。悬空内存访问是危险的,你应该把在堆上创建的示例添加到你喜欢的容器中。在这种情况下,shared_ptr对于管理示例的生存期是有用和安全的。

#include <vector>
#include <string>
#include <memory>
using namespace std;

struct D{
public:
    string _name;
    D(const string& name) : _name(name){}
    ~D(){
        printf("~D()\n");
    }
};

class C{
    vector<shared_ptr<D>> _list;
public:
    void add(shared_ptr<D> dObject){
        _list.push_back(dObject);
        printf("Adding D => size = %ld\n", _list.size());
    }
    void print() {
        for (const auto& d : _list) {
            printf("%s\n", d->_name.c_str());
        }
    }
    ~C(){
        printf("~C()\n");
    }
};

int main(){
    C c1;
    {
        c1.add(make_shared<D>("first D"));
    }
    c1.add(make_shared<D>("second D"));
    c1.print();
}

输出:

Adding D => size = 1
Adding D => size = 2
first D
second D
~C()
~D()
~D()

附加:按块范围删除对象

C c1;
    {
        auto d = make_shared<D>("first D");
        shared_ptr<nullptr_t> D_deleter{ nullptr, [&](auto) { c1.remove(d); } };
        c1.add(d);
    } // d would be deleted here
9nvpjoqh

9nvpjoqh3#

从本质上讲,最初的问题是问:“如何知道局部变量何时超出范围?”
答案是:当调用其析构函数时。

对于您问题中的代码,您可以让~D()c1中删除自身。这很尴尬,问题下面的讨论会提到std::shared_ptr,但示例中的代码使用了局部变量。您不能在局部变量上使用std::shared_ptr,只能在堆变量上使用。这在讨论中没有明确说明。

以下是与shared_ptr和局部变量特别相关的问题的两个参考:
Create shared_ptr to stack object

Set shared_ptr to point existing object
更好的解决方案是不使用局部变量,而使用堆变量。
局部变量和堆变量之间的区别似乎会引起一些混乱。
C++有几个变量存储类:

  • 堆栈-用于局部变量和函数参数
  • 寄存器-有时用于局部变量-主要用于优化
  • 全局/静态-“一个且仅一个”,在程序加载时分配的空间中。
  • 线程本地-全局/静态但特定于线程-每个线程都有自己副本。
  • 堆-由程序通过new或malloc(或等效的)动态分配。2有时称为“自由存储”。

C++对newdelete有一些技巧,在这些技巧中,您可以使用自定义堆,即您自己管理的内存,但该内存将从通用堆中分配,或者可能存在于其他变量中。
下面是三个不同的示例:

局部变量:

void some_function(C& c1)
{
    D d1;
    c1.add(&d1);
}

当这个函数退出时,包含d1的堆栈帧消失,变量也不再存在。~D()将被调用,如果你添加了一个机制,比如让d1持有一个指向c1的指针,你可以从c1中删除d1,等等。
std::shared_ptr对局部变量没有帮助。

使用new分配的堆变量:

void some_function(C& c1)
{
    D *d1p = new D();
    c1.add(d1p);
}

当这个函数退出时,d1p指向的D变量仍然存在。它将继续存在,直到它被显式删除。在这个上下文中,c1变量“拥有”这个指针,并且它负责在使用完它后删除它。
在这种情况下,添加智能指针可以简化引用管理,如下所示:

通过make_shared创建的堆变量和shared_ptr:

void some_function(C& c1)
{
    std::shared_ptr<D> d1p = std::make_shared<D>();
    c1.add(d1p);
}

此示例在堆上创建一个D变量,并创建一个本地shared_ptr,该本地shared_ptr通过一个小计数器对象(也在堆上)保存对该D变量的引用。当shared_ptr传递给C::add方法时,计数器对象中的引用计数将递增。当shared_ptr超出范围时,计数器对象中的引用计数递减。c1对象中的shared_ptr现在保存对该shared_ptr的唯一引用,当c1对象被销毁时,它所包含的shared_ptr也将被销毁,释放其引用--如果计数变为零,它所指向的对象也将被销毁。
除了这种自动销毁之外,这种方法的另一个优点是,您可以从c1获取对D变量的引用,并在其他地方使用它们,这些引用(以及D变量本身)可以比c1更持久。
C类必须针对这种情况进行修改,例如

std::vector<std::shared_ptr<D>> _list;

void add(std::shared_ptr<D> dObject)

以下是共享指针工作方式的参考:
How do shared pointers work?
和一些关于make_shared文档:
https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared

相关问题