memgraph/docs/dev/code-review.md
Teon Banek a3d99d7628 Update Code Review Guidelines with Exceptions section
Reviewers: mtomic, mferencevic, msantl, ipaljak, vkasljevic, dlozic, buda

Reviewed By: mferencevic, msantl, buda

Differential Revision: https://phabricator.memgraph.io/D2286
2019-08-07 15:58:38 +02:00

270 lines
9.5 KiB
Markdown

# Code Review Guidelines
This chapter describes some of the things you should be on the lookout when
reviewing someone else's code.
## Exceptions
Although the Google C++ Style Guide forbids exceptions, we do allow them in
our codebase. As a reviewer you should watch out for the following.
The documentation of throwing functions needs to be in-sync with the
implementation. This must be enforced recursively. I.e. if a function A now
throws a new exception, and the function B uses A, then B needs to handle that
exception or have its documentation updated and so on. Naturally, the same
applies when an exception is removed.
Transitive callers of the function which throws a new exception must be OK
with that. This ties into the previous point. You need to check that all users
of the new exception either handle it correctly or propagate it.
Exceptions should not escape out of class destructors, because that will
terminate the program. The code should be changed so that such cases are not
possible.
Exceptions being thrown in class constructors. Although this is well defined
in C++, it usually implies that a constructor is doing too much work and the
class construction & initialization should be redesigned. Usual approaches are
using the (Static) Factory Method pattern or having some sort of an
initialization method that needs to be called after the construction is done.
Prefer the Factory Method.
Don't forget that STL functions may also throw!
## Pointers & References
In cases when some code passes a pointer or reference, or if a code stores a
pointer or reference you should take a careful look at the following.
* Lifetime of the pointed to value (this includes both ownership and
multithreaded access).
* In case of a class, check validity of destructor and move/copy
constructors.
* Is the pointed to value mutated, if not it should be `const` (`const Type
*` or `const Type &`).
## Allocators & Memory Resources
With the introduction of polymorphic allocators (C++17 `<memory_resource>` and
our `utils/memory.hpp`) we get a more convenient type signatures for
containers so as to keep the outward facing API nice. This convenience comes
at a cost of less static checks on the type level due to type erasure.
For example:
std::pmr::vector<int> first_vec(std::pmr::null_memory_resource());
std::pmr::vector<int> second_vec(std::pmr::new_delete_resource());
second_vec = first_vec // What happens here?
// Or with our implementation
utils::MonotonicBufferResource monotonic_memory(1024);
std::vector<int, utils::Allocator<int>> first_vec(&monotonic_memory);
std::vector<int, utils::Allocator<int>> second_vec(utils::NewDeleteResource());
second_vec = first_vec // What happens here?
In the above, both `first_vec` and `second_vec` have the same type, but have
*different* allocators! This can lead to ambiguity when moving or copying
elements between them.
You need to watch out for the following.
* Swapping can lead to undefined behaviour if the allocators are not equal.
* Is the move construction done with the right allocator.
* Is the move assignment done correctly, also it may throw an exception.
* Is the copy construction done with the right allocator.
* Is the copy assignment done correctly.
* Using `auto` makes allocator propagation rules rather ambiguous.
## Classes & Object Oriented Programming
A common mistake is to use classes, inheritance and "OOP" when it's not
needed. This sections shows examples of encountered cases.
### Classes without (Meaningful) Members
class MyCoolClass {
public:
int BeCool(int a, int b) { return a + b; }
void SaySomethingCool() { std::cout << "Hello!"; }
};
The above class has no members (i.e. state) which affect the behaviour of
methods. This class should need not exist, it can be easily replaced with a
more modular (and shorter) design -- top level functions.
int BeCool(int a, int b) { return a + b; }
void SaySomethingCool() { std::cout << "Hello!"; }
### Classes with a Single Public Method
clas MyAwesomeClass {
public:
MyAwesomeClass(int state) : state_(state) {}
int GetAwesome() { return GetAwesomeImpl() + 1; }
private:
int state_;
int GetAwesomeImpl() { return state_; }
};
The above class has a `state_` and even a private method, but there's only one
public method -- `GetAwesome`.
You should check "Does the stored state have any meaningful influence on the
public method?", similarly to the previous point.
In the above case it doesn't, and the class should be replaced with a public
function in `.hpp` while the private method should become a private function
in `.cpp` (static or in anonymous namespace).
// hpp
int GetAwesome(int state);
// cpp
namespace {
int GetAwesomeImpl(int state) { return state; }
}
int GetAwesome(int state) { return GetAwesomeImpl(state) + 1; }
A counterexample is when the state is meaningful.
class Counter {
public:
Counter(int state) : state_(state) {}
int Get() { return state_++; }
private:
int state_;
};
But even that could be replaced with a closure.
auto MakeCounter(int state) {
return [state]() mutable { return state++; };
}
### Private Methods
Instead of private methods, top level functions should be preferred. The
reasoning is completely explained in "Effective C++" Item 23 by Scott Meyers.
In our codebase, even improvements to compilation times can be noticed if
private methods in interface (`.hpp`) files are replaced with top level
functions in implementation (`.cpp`) files.
### Inheritance
The rule is simple -- if there are no virtual methods (but maybe destructor),
then the class should be marked as `final` and never inherited.
If there are virtual methods (i.e. class is meant to be inherited), make sure
that either a public virtual destructor or a protected non-virtual destructor
exist. See "Effective C++" Item 7 by Scott Meyers. Also take a look at
"Effective C++" Items 32---39 by Scott Meyers.
An example of how inheritance with no virtual methods is replaced with
composition.
class MyBase {
public:
virtual ~MyBase() {}
void DoSomethingBase() { ... }
};
class MyDerived final : public MyBase {
public:
void DoSomethingNew() { ... DoSomethingBase(); ... }
};
With composition, the above becomes.
class MyBase final {
public:
void DoSomethingBase() { ... }
};
class MyDerived final {
MyBase base_;
public:
void DoSomethingNew() { ... base_.DoSomethingBase(); ... }
};
The composition approach is preferred as it encapsulates the fact that
`MyBase` is used for the implementation and users only interact with the
public interface of `MyDerived`. Additionally, you can easily replace `MyBase
base_;` with a C++ PIMPL idiom (`std::unique_ptr<MyBase> base_;`) to make the
code more modular with regards to compilation.
More advanced C++ users will recognize that the encapsulation feature of the
non-PIMPL composition can be replaced with private inheritance.
class MyDerived final : private MyBase {
public:
void DoSomethingNew() { ... MyBase::DoSomethingBase(); ... }
};
One of the common "counterexample" is the ability to store objects of
different type in a container or pass them to a function. Unfortunately, this
is not that good of a design. For example.
class MyBase {
... // No virtual methods (but the destructor)
};
class MyFirstClass final : public MyBase { ... };
class MySecondClass final : public MyBase { ... };
std::vector<std::unique_ptr<MyBase>> first_and_second_classes;
first_and_second_classes.push_back(std::make_unique<MyFirstClass>());
first_and_second_classes.push_back(std::make_unique<MySecondClass>());
void FunctionOnFirstOrSecond(const MyBase &first_or_second, ...) { ... }
With C++17, the containers for different types should be implemented with
`std::variant`, and as before the functions can be templated.
class MyFirstClass final { ... };
class MySecondClass final { ... };
std::vector<std::variant<MyFirstClass, MySecondClass>> first_and_second_classes;
// Notice no heap allocation, since we don't store a pointer
first_and_second_classes.emplace_back(MyFirstClass());
first_and_second_classes.emplace_back(MySecondClass());
// You can also use `std::variant` here instead of template
template <class TFirstOrSecond>
void FunctionOnFirstOrSecond(const TFirstOrSecond &first_or_second, ...) { ... }
Naturally, if the base class has meaningful virtual methods (i.e. other than
destructor) it maybe is OK to use inheritance but also consider alternatives.
See "Effective C++" Items 32---39 by Scott Meyers.
### Multiple Inheritance
Multiple inheritance should not be used unless all base classes are pure
interface classes. This decision is inherited from [Google C++ Style
Guide](https://google.github.io/styleguide/cppguide.html#Inheritance). For
example on how to design with and around multiple inheritance refer to
"Effective C++" Item 40 by Scott Meyers.
Naturally, if there *really* is no better design, then multiple inheritance is
allowed. An example of this can be found in our codebase when inheriting
Visitor classes (though even that could be replaced with `std::variant` for
example).
## Code Format & Style
If something doesn't conform to our code formatting and style, just refer the
author to either [C++ Style](cpp-code-conventions.md) or [Other Code
Conventions](other-code-conventions.md).