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

What's the best practice to create new objects in C++?

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

如果你能告诉我这是否合适或者我是否应该做其他事情,我将不胜感激differently/better。

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

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> myProperty, 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
  • 如果有 none 拥有引用,则可以使用原始指针代替。 (none-所有者必须在拥有智能指针之前销毁)
  • 它引入了一个额外的间接寻址。
    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) 但是如何经常删除宠物?

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

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

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

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