Projects/Edu/KStars/C++ Best Practices for KStars
This page describes how to use some C++ features while developing KStars.
Smart Pointers
std::unique_ptr
std::unique_ptr hold an object in a scope (local or class variable) and when the unique pointer is destroyed, the owned object is also destroyed. This smart pointer has a release() function to release the ownership and get the pointer to the held object. This smart pointer is the most lightweight because it does not have any internal reference counting. On the other side, it cannot be copied thus you cannot return a std::unique_ptr from a function and containers (vector, map) cannot hold these pointers.
This approach is good for:
- Temporary allocated variables in functions which are destroyed when an error path is executed before leaving the function, we can write code without worrying the forgotten undeleted object what would be a memory leak. The allocated object can be returned by release() function when leaving the function if everything is okay. Just imagine a 100 lines long function with many ifs and while cycle, it is easy to forget to delete a temporary allocated object:
MyObj* MyFunction() { std::unique_ptr<MyObj> LocalObj(new MyObj); if (something_is_wrong1) { return nullptr; } if (something_is_wrong2) { return nullptr; } return LocalObj.release(); }
- Declare a class variable which is not necessarily allocated, but you want to delete it after the class itself destroyed automatically. This design pattern spares the initialization of the class variable to nullptr because a unique pointer is set to nullptr by default. On the other hand, the delete functions in the destructor are not needed and a source of the possible memory leaks is eliminated by design, you cannot forget it:
struct MyClass { ~MyClass { } // We don't have to delete MyObj, it is destroyed automatically by ObjVar. void Allocate() { ObjVar.reset(new MyObj); // Allocate a MyObj instance and set to ObjVar } void Release() { ObjVar.reset(); // We can delete MyObj any time by this call } void DoSomething() { // The get() function gives access to the raw pointer of the owned object (MyObJ*) if (ObjVar.get() == nullptr) return; // We can call MyObj::DoThings() transparently with -> like the syntax of a raw pointer (MyObj*) ObjVar->DoThings(); } std::unique_ptr<MyObj> ObjVar; };
Several std::shared_ptr pointers can hold "shared" ownership of an object what will be destroyed automatically once the last shared_ptr is destroyed. The shared pointers can be copied or cloned because these operations will just share the ownership with additional shared pointers which point to the same object instance.
std::shared_ptr is ideal for:
- Creating containers of smart pointers. Since the shared pointers can be copied, it is possible to create containers (std::vector, std::map etc.) with std::shared_ptr.
struct SkyCatalog { int ID { 0 }; }; std::vector<std::shared_ptr<SkyCatalog>> Catalogs; void DoTask() { std::shared_ptr<SkyCatalog> CatalogA; std::shared_ptr<SkyCatalog> CatalogB; Catalogs.push_back(CatalogA); Catalogs.push_back(CatalogB); // CatalogA and CatalogB variables go out of scope here and destroyed, but they were // added to Catalogs vector, therefore, the owned SkyCatalog instances won't be destroyed // because there are still smart pointers pointing to these objects in Catalogs. }
- Having a reference to an object (class A) in different objects (class B, C, D...) to ensure the safe memory handling and maintain object lifetime properly.
struct SkyCatalog { int ID { 0 }; }; struct CatalogManager { CatalogManager() { Catalog.reset(new SkyCatalog); Star.reset(new StarObject); Star.Catalog = Catalog; Galaxy.reset(new GalaxyObject(Catalog)); } std::shared_ptr<SkyCatalog> Catalog; std::unique_ptr<StarObject> Star; std::unique_ptr<GalaxyObject> Galaxy; }; struct StarObject { void DoTask() { if (Catalog.get() != nullptr) printf("Catalog ID: %d\n", Catalog->ID); } std::shared_ptr<SkyCatalog> Catalog; }; struct GalaxyObject { GalaxyObject (std::shared_ptr<SkyCatalog> &catalog) : Catalog(catalog) { } void DoTask() { if (Catalog.get() != nullptr) printf("Catalog ID: %d\n", Catalog->ID); } std::shared_ptr<SkyCatalog> Catalog; };
Initialization of class variables
A class can have multiple constructors with different parameters. The class variables had to be initialized by hand in each constructor in the past, but C++11 eliminated this error-prone situation by allowing the initial value declaration in the header file. In this way, the initial values are declared in one place and code editors can also view it in their context-sensitive help. For example, CLion shows the type and the initial value if you go over a class variable with the mouse pointer while holding the Ctrl button. It is much easier than digging around the source code if you are interested in an initial value during coding.
The initial value can be written in braces (int myVar { 0 };) or with assignment operator (int myVar = 0;), these styles are equivalent, but the braces were chosen for the common pattern in KStars.
Example myclass.h:
struct MyClass { int Counter { 1 }; float Counter2 { 0 }; };