Consider Safety

Regarding the consideration of safety, we refer again to the safety instructions of C++ Best Practice and point out and extend some cases for you to keep in mind for coding in NebulaStream below.

Use Const as Much as Possible

const tells the compiler that a variable or method is immutable. This helps the compiler optimize the code and helps the developer know if a function has a side effect. Also, using const & prevents the compiler from copying data unnecessarily. The comments on const from John Carmack are also a good read.

/// Only pay attention to the const-ness of variables / functions in this example
class MyClass {
  std::string str;
public:

/// Bad: The caller doesn't care whether you change i in the implementation or not, because that
/// has no side effect on the caller whatsoever.
/// What is more, the implementation might even omit the `const` (see below).
void bad(int const i);

/// Unnecessary const, that, depending on the return type, has detrimental
/// effects on the performance because it interferes with move semantics.
const int also_bad(int const i);

// Good! Be aware of the tradeoff of returning by value or by const ref from the next section!
std::string const &getString() const;

/// Good as far as const-ness is concerned, but maybe consider passing a rvalue in this case.
void setString(std::string const &s);
};

/// Marking var const here would be fine and legit as it'ld be meaningful in the implementation!
void MyClass::bad(int var) { }

std::string const &MyClass::getString() const { return str; }

void MyClass::setString(std::string const &str) { this->str = str; }

Carefully Consider Your Return Types

  • Getters

    • Returning by & or const & can have significant performance savings when the normal use of the returned value is for observation

    • Returning by value is better for thread safety and if the normal use of the returned value is to make a copy anyhow, there’s no performance lost

    • If your API uses covariant return types, you must return by & or *

  • Temporaries and local values

    • Always return by value.

Do Not Pass and Return Simple Types by Const Ref

// Very Bad

class MyClass

{
public:

explicit MyClass(const int& t_int_value): m_int_value(t_int_value)
{}

const int& get_int_value() const
{
return m_int_value;
}

private:

int m_int_value;

}

Instead, pass and return simple types by value. If you plan not to change passed value, declare them as const, but not const refs:

// Good

class MyClass
{

public:

explicit MyClass(const int t_int_value): m_int_value(t_int_value)
{}

int get_int_value() const
{
return m_int_value;
}

private:

int m_int_value;

}

Why? Because passing and returning by reference leads to pointer operations instead by much faster passing values in processor registers.

Avoid Raw Memory Access

Raw memory access, allocation and deallocation, are difficult to get correct in C++ without risking memory errors and leaks. C++11 provides tools to avoid these problems.

// Bad

MyClass *myobj = new MyClass;

delete myobj;

// Good

auto myobj = std::make_unique<MyClass>(constructor_param1, constructor_param2); // C++14

auto myobj = std::unique_ptr<MyClass>(new MyClass(constructor_param1, constructor_param2)); // C++11

auto mybuffer = std::make_unique<char[]>(length); // C++14

auto mybuffer = std::unique_ptr<char[]>(new char[length]); // C++11

// or for reference counted objects

auto myobj = std::make_shared<MyClass>();

// myobj is automatically freed for you whenever it is no longer used.

Use Exceptions

Exceptions cannot be ignored. Return values, such as using boost::optional, can be ignored and if not checked can cause crashes or memory errors. An exception, on the other hand, can be caught and handled. Potentially all the way up the highest level of the application with a log and automatic restart of the application.

Stroustrup, the original designer of C++, makes this point much better than we ever could.

While exceptions can be helpful, throwing an exception is very expensive. Use exceptions only when beneficial.

Use Noexcept Specifiers and Operators

Noexcept Specifiers

At least if the function is not too complex, use the noexcept specifier to indicate whether a function can or cannot throw.

Noexcept Operators

Consider using the noexcept operator for short, highly templated functions in which the exception specification depends on those of a callee like in the following sketch:

template< typename... T> auto f(T &&t) noexcept(noexcept(u(std::forward<T>(t)...))) {
    return u(std::forward<T>(t)...);
}

Do Not Define Variadic Functions

Variadic functions can accept a variable number of parameters. The probably best known example is printf(). You have the possibility to define this kind of functions by yourself but this is a possible security risk. The usage of variadic functions is not type safe and the wrong input parameters can cause a program termination with an undefined behavior. This undefined behavior can be exploited to a security problem.

If you have the possibility to use a compiler that supports C++11, you can use variadic templates instead.

Inheritance

Virtual Destructors

Always make base class destructors either virtual or make them protected (if there is some class which inherits from base).

Use final

Use the final keyword in virtual functions and class declarations whenever we expect a function not to be implemented from a deriving class / a class not to be derived from. In theory this should come with two benefits:

  1. Performance benefit
  2. Expressiveness (we do not expect this class to be inherited further)

Speed Showcase

If we make neither the function nor the class final and the compiler is asked to execute the function in question on a reference of said type, we load the vtable and jump to whatever is stored in there. This is only one instruction but can eventually entail a branch misprediction and be costly. On the other hand, marking the functions and class final does not cost us anything and conveys the message: “If you derive from this, you do something we did not expect you to do”. If, eventually, we decide that it is beneficial to further implement said class, we can simply remove the final specifier.