Reviewers: mtomic, mferencevic, msantl, ipaljak, vkasljevic, dlozic, buda Reviewed By: mferencevic, msantl, buda Differential Revision: https://phabricator.memgraph.io/D2286
9.5 KiB
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 *
orconst 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. 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 or Other Code Conventions.