简体   繁体   中英

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

I'm fairly new to C++ and just learned about smart pointers. Now I'm wondering what the best practice is to make sure that they are efficiently used (let's say my optimization algorithm requires hundreds of millions of objects to be created, evaluated [in relatives ] and then then safely discarded - ideally in a concurrent way).

I would appreciate if you could tell me if this is alright or if there is something else I'm supposed to do differently/better.

Edit: This sa simplified example to focus on the correct creation of the objects. Obviously, simple properties could be represented differently. Here, I am trying to focus on learning how to correctly create objects that reference other objects and may be referenced by many other objects before they are destroyed. So my question is if the code below is a reasonably fast and, more important, safe way to create objects.

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);
    }
};

As I mentioned before, eventually I hope to be able to implement something so multiple threads can access relatives , but don't think this is relevant for the object creation (is it?).

Thank you in advance!

The concept of std::shared_ptr is so cool that I had to use it everywhere in my first new project after it appeared! It enables RAII (A C++ explicit garbage collection method) for all objects pointed to being destroyed when the object owning the std::shared_ptr is destroyed.

Now std::shared_ptr has a few drawbacks, some with the concept and some with the implementation.

  • It is possible to make circular dependences that will leave the data unreferenced by the rest of the program, effectively leaking the memory.
  • If there is only one logical owner, there is a simpler smart pointer, std::unique_ptr
  • If there are none-owning references, raw pointers can be used instead. (the none-owners must be destroyed before the owning smart pointers)
  • It introduces an extra indirection.
    std::shared_ptr<Pet> pet;
pet->control block->Pet object

Which is an external reference count, as opposed to where the reference count is part of the object.

pet->Counter + Pet object

So if your goal is to delete a Pet when there are no more refences to it there are different methods to do it.

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.
}

So before erasing the pet you will have to make the relatives forget the pet, there are at least three ways to do this

  • change relatives to use std::weak_ptr
    • requires extra checking before using elements of relatives as the relation could have be erased.
  • change relatives to use the PetId instead of the std::shared_ptr<Pet>
    • requires an extra lookup in PetDict each time before using elements of relatives as the relation could have be erased.
  • iterate through the relatives to erase the relation in each of those.
    • as relatives is not indexed by PetId , all nodes of relatives must be check for the right PetId , rather horrible O(m) but then how often ar pets deleted?

The first two options are a kind of lazy delete as they can remove the relation once it is found that it is no longer active, though it will still cost in alive checking for every usage.

Regarding the thread safety, there are several variants.

  • If you only read after creating the pets everything is fine
  • If you change the reference count by making a copy of a std::shared_ptr that is also OK, as the counter is the only part of std::shared_ptr that is atomic.
  • If you change the object that the std::shared_ptr points to in the control block, you get a race condition
  • If you change the values of an object that the std::shared_ptr points to you get a race condition.

In C++20 std::atomic(std::shared_ptr) can help with atomic updates of the control block, but you still needs to make access to the pointed to object thread safe yourself. The usual cost benefits for how many readers/writes must be evaluated.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM