在 C++ 中创建新对象的最佳实践是什么?

问题描述

我对 C++ 还很陌生,刚刚了解了智能指针。现在我想知道确保它们被有效使用的最佳实践是什么(假设我的优化算法需要创建数亿个对象,[在 relatives] 中进行评估,然后安全地丢弃 - 理想情况下以并发方式)。

如果您能告诉我这是否可以,或者我是否应该做一些不同/更好的其他事情,我将不胜感激。

编辑:这是一个简化的示例,重点关注对象的正确创建。显然,简单的属性可以用不同的方式表示。在这里,我试图专注于学习如何正确创建引用其他对象的对象,并且在销毁它们之前可能会被许多其他对象引用。所以我的问题是,下面的代码是否是一种相当快速、更重要、更安全的创建对象的方法

class Property {
public:
    int id;
};

class Pet {
    const int id_;
    const std::shared_ptr<Property> myProperty_;
    const std::shared_ptr<Property> myProperty2_;
    std::map<double,std::shared_ptr<Pet>> relatives;  // one of the places where the object will be referenced,I use "HashMap" in my Java prototype

    Pet(const int id,const std::shared_ptr<Property> myProperty,const std::shared_ptr<Property> myProperty2) : 
        id_(id),myProperty_(myProperty),myProperty2_(myProperty2) { }

    const std::shared_ptr<Pet> makePet(const int id,const std::shared_ptr<Property> myProperty2) {
        return std::make_shared<Pet>(id,myProperty,myProperty2);
    }
};

正如我之前提到的,最终我希望能够实现一些东西,以便多个线程可以访问 relatives,但不要认为这与对象创建相关(是吗?)。

先谢谢你!

解决方法

std::shared_ptr 的概念非常酷,它出现后我不得不在我的第一个新项目中到处使用它!当拥有 std::shared_ptr 的对象被销毁时,它为所有指向被销毁的对象启用 RAII(一种 C++ 显式垃圾收集方法)。

现在 std::shared_ptr 有一些缺点,一些是概念上的,一些是实现上的。

  • 有可能产生循环依赖,使程序的其余部分无法引用数据,从而有效地泄漏内存。
  • 如果只有一个逻辑所有者,则有一个更简单的智能指针,std::unique_ptr
  • 如果存在非拥有引用,则可以使用原始指针代替。 (非所有者必须在拥有智能指针之前销毁)
  • 它引入了额外的间接寻址。
    std::shared_ptr<Pet> pet;
pet->control block->Pet object

这是一个外部引用计数,而不是引用计数是对象的一部分。

pet->Counter + Pet object

因此,如果您的目标是在没有更多引用的情况下删除宠物,则可以采用不同的方法来实现。

using PetId = int;
std::unordered_map<PetId,std::shared_ptr<Pet>> PetDict; // global pet owner,hash map/dictionary

void ErasePet(PetId id) {
  auto pet = PetDict.find(id);
  if (pet != PetDict.end()) // can be avoided if you know id is in PetDict ... no,you can't be sure.
    PetDict.erase(pet); // this is not enough as the relatives still keep the pet alive and will cause a dead pet to still have relations while not being findable anymore.
}

因此,在删除宠物之前,您必须让 relatives 忘记宠物,至少有三种方法可以做到这一点

  • relatives 改为使用 std::weak_ptr
    • 在使用 relatives 的元素之前需要额外检查,因为关系可能已被删除。
  • 更改 relatives 以使用 PetId 而不是 std::shared_ptr<Pet>
    • 每次使用 relatives 的元素之前都需要在 PetDict 中进行额外查找,因为关系可能已被删除。
  • 遍历 relatives 以删除每个中的关系。
    • 因为 relatives 没有被 PetId 索引,所以 relatives 的所有节点都必须检查正确的 PetId,相当可怕的 O(m) 但是那么多久 ar宠物被删除了?

前两个选项是一种惰性删除,因为一旦发现它不再处于活动状态,它们就可以删除该关系,尽管它仍然会在每次使用时进行活动检查。

关于线程安全,有几种变体。

  • 如果你只在创建宠物后阅读,一切都很好
  • 如果您通过复制 std::shared_ptr 来更改引用计数也是可以的,因为计数器是 std::shared_ptr 的唯一原子部分。
  • 如果在控制块中更改 std::shared_ptr 指向的对象,则会出现竞争条件
  • 如果更改 std::shared_ptr 指向的对象的值,则会出现竞争条件。

在 C++20 中,std::atomic(std::shared_ptr) 可以帮助控制块的原子更新,但您仍然需要自己访问指向的对象线程安全。 必须评估多少读取/写入的通常成本收益。