Modern CPP - Hyp-ed/hyped-2024 GitHub Wiki

C++ let's you do many things - but with great power comes great responsibility. There are plenty of language features that should be avoided. If you feel like you have to use any of those, it's very likely that there is a bigger problem with your code.

Things to avoid

Includes

Headers should only be imported where they are required!

  • Don't include any files you don't need.
  • Don't include files in the .hpp because they are required in the .cpp.

Null values

There are many ways to represent the absence of a value in C++:

  1. Pointers and nullptr
  2. Sentinel values, e.g. 0, -1 or nullptr
  3. std::pair<bool, T>

However, none of these is ideal. Since C++17, the language supports optionals. These solve all the problems the above constructs have and are superior in (almost) all cases. Avoid all of the above and use std::optional!

Further reading.

Raw pointers

If you find yourself passing around raw pointers, e.g. MyObject *objectPtr, there is likely a better way of doing this. Raw pointers introduce many source of bugs related to the underlying memory as well as pointer arithmetic. Depending on what aspect of the pointer you require, there are different alternative:

Heap allocations

If you are using new/delete to allocate objects on the heap, you are required to use raw pointers. The first question you should ask yourself is this: do you really need heap memory? Most tasks in HYPED can be done with stack allocated objects, but there are some that require the heap. However, there are better ways to solve those problems: unique pointers and share pointers! Consider the following code:

class MyClass {
  LargeObject *large_object_;
};

MyClass::MyClass() {
    large_object_ = new LargeObject();
}

MyClass::~MyClass() {
    delete large_object_;
}

Here we create an object of the heap because it may be too large for the stack. However, we neither need to share the object around nor do we require any features that are inherint to pointers (e.g. pointer arithmetic). So, we can make use of std::uniqe_ptr. Here's how that looks:

#include <memory>

class MyClass {
  std::unique_ptr<LargeObject> large_object_;
};

MyClass::MyClass() {
  large_object_ = std::make_unique<LargeObject>();
}

Note how we don't even need a destructor. This is because the unique_ptr deallocates it's own heap memory once it goes out of scope. You can even pass const references to the underlying object around as much as we want:

#include <memory>
#include <vector>

class MyClass {
 public:
  class MySubClass {
    const LargeObject &large_object_;

   public:
    MySubClass(const LargeObject &large_object) {
        large_object_ = large_object;
    }
  };
  std::unique_ptr<LargeObject> large_object_;
  std::vector<MySubClass> sub_objects_;
};

MyClass::MyClass() {
  large_object_ = std::make_unique<LargeObject>();
  for (int i = 0; i < 1000; i++) {
    sub_objects_.emplace_back(*large_object_);
  }
}

But what if MySubClass was to outlive an instance of MyClass? In that case you can use shared pointers. They work just like unique pointers but they count how many instances point to the same memory location. Once the last instace goes out of scope, the underlying memory gets deallocated. This doesn't come with the same compile time guarantees and should only be used if there is a clear reason for it, but it's much safer than using raw pointers with manual allocations and deallocations.

References

Pointers allow you to modify the underlying memory. In fact, this is the only way to avoid copying memory around in C. However, we are writing C++ so there is something more suited for the job: references. Consider this:

void squareInPlace(int *x) {
  *x = *x * *x;
}

int main() {
    int x = 3;
    squareInPlace(&x);
}

With references, this becomes

void squareInPlace(int &x) {
    x = x * x;
}

int main() {
    int x = 3;
    squareInPlace(x);
}

Nullable references

With references you are always forced to have some underlying object. Not only does a pointer allows you to point anywhere you want, it can also point nowhere. This is why we have nullptrs. Consider the following code:

#include <unordered_map>

MyClass *lookup(std::unordered_map<int, MyClass> &values, int key) {
    auto value_it = values.find();
    if (value_it == values.end()) {
        return nullptr;
    }
    return value_it->second;
}

This lookup may fail! In that case, we need a value to indicate said failure. Your first instinct should be to use optionals. However, there are problems that come with optionals of reference types. Therefore, this is a valid reason to use a pointer instead of a reference until an alternative is provided by the C++ standard!

using and using namespace

This should never be used unless you are aliasing a complicated type within a class. Otherwise you are hiding crucial information about the origin of functions and classes.

Be careful with aliasing however, as complicated types may indicate shortcomings of the code.

For example, this is fine:

class Listener {
 public:
  using MessageHandler 
    = std::function<void(const int64_t timestamp, const MessageHeader &header, const MessageBody &body)>

  Listener(MessageHandler);

 private:
  MessageHandler message_handler_;
};

But this is not:

using data::Data;
using data::StripeCounter;
using hyped::utils::Logger;
using utils::io::GPIO;
⚠️ **GitHub.com Fallback** ⚠️