Code¶
Some general tips to keep in mind:
If you add functionality, please update the documentation accordingly.
If you add functionality, add tests if applicable. This helps make sure Arbor is stable and functionality does what it’s supposed to do.
If you work on the public C++ API, provide Python wrappings.
Make sure Arbor compiles and has no new warnings.
Consider the coding guidelines below.
Python¶
We follow the black coding style. It is enforced by an automated check on each pull request. You can run the following commands to apply it:
# Install the formatter if not present
pip install black
# Automatically apply style to all Python files. If unsure what this does read on.
black .
The formatter can also be run with --check
to list offending files and
--diff
to preview changes. Most editors can integrate with black.
C++¶
The main development language of Arbor is C++. For Arbor, we start with the community guidelines set out in the C++ Core Guidelines. These guidelines are quite generic and only give loose guidelines for some important topics like variable naming and indentation.
This wiki will describe the specific extensions and differences to the
C++ Core Guidelines - variable naming. - code formatting (indentation,
placement of curly braces, int const&
vs const int&
etc.) -
rules for topics not covered in the Core Guidelines (e.g., CUDA). -
exceptions to the rules given in the Core Guidelines. - rules for the
CMake build system and directory structure. - rules for external
dependencies (e.g., Boost).
Todo
This page needs revision.
Code organisation¶
Source files naming conventions¶
Todo
describe the public/private header and source code organization.
.cpp
extension for source files.hpp
extension for header filesNames must be lowercase with words separated with underscores.
Namespaces¶
All code is in namespace arb
.
As an example, the std::make_unique<>
function template was not
provided as part of C++11 (it wasn’t introduced until C++14), and we
would like to use it in Arbor. The code sample below shows how
namespaces are declared and formatted in Arbor:
namespace arb {
namespace util {
// just because we aren't using C++14, doesn't mean we shouldn't have make_unique
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args) ...));
}
} // namespace util
} // namespace arb
In the example above, the namespaces are not indented. However, namespaces should be indented if they are declared in the middle of code to make their existence obvious to the person reading the code.
Use an impl
namespace to hide implementation details that should not
be exposed to user space.
You can use using
statements to import individual types or functions
from a namespace - only if it really improves the readability of your
code - only in a function or class scope: don’t pollute namespaces
Formatting statements¶
A lot of the rules here are purely a matter of personal taste, imposed by the guy who got to set the rules. That said, it follows the accepted practice in the C++ community (if we accept that not everybody has the same accepted practice), and if followed consistently, will make code easier to understand.
template <typename T>
class array {
public:
using value_type = T;
value_type& operator[] (std::size_t i) {
return data_[i];
}
const value_type& operator[] (std::size_t i) const {
return data_[i];
}
std::size_t size() const {
return size_;
}
private:
value_type* data_;
std::size_t size_;
};
// use new lines and indentation to make complex template expressions
// human readable
template <
typename T,
typename = typename // assert that T is a built-in arithmetic type
std::enable_if<
std::is_arithmetic<T>::value
>
>
T sum(const array<T>& in) {
return std::accumulate(in.begin(), in.end(), 0);
}
TODO: When declaring an operator, should we leave a space between the operator and the following opening parenthesis, or should we follow the convention we use for functions where we don’t leave a space?
Indentation and whitespace cleanup¶
No tabs, 4 spaces
Take the extra effort to remove trailing whitespace (at the end of the lines and the file).
Respect 80-column limit, but go for longer lines when they make sense (and make the code clearer)
Variable naming conventions¶
All lowercase, words separated by _
, but template parameters follow
camel case:
template <typename ValueType>
class my_class {
public:
// ...
private:
ValueType val_;
};
Single letter template parameters should be preferred.
TODO: Or should we force single letter parameters aliased by more meaningful type names inside the class (either public or private depending on our intent)?
Avoid obfuscated names of old C heritage.
Recurring variables naming conventions¶
TODO: Some variable names are recurring a lot inside every code. It would be nice if we could decide on the most common ones.
count
orcnt
index
oridx
iter
orit
…
Ben says “depends… I would use count
or index
unless the scope
of the variable is very small. Using it
is standard C++ shorthand,
but again for fairly limited scope.”
Member variables¶
Private member variables must be suffixed by _
, while public member
variables must not.
TODO: Any conventions about static
variables, const
s or global
const
s?
Member initialisation lists¶
Constructors member initialisation lists should be as follows:
// everything goes on one line if clear
class my_class {
public:
my_class(int a):
a_(a)
{}
my_class(int a, int b, int c):
a_(a), b_(b) , c_(c)
{}
private:
int a_ = 0;
int b_ = 0;
int c_ = 0;
};
// use one entry per line if multiple lines needed
class my_class {
public:
my_class(int a, int o, int p):
apple_(a),
orange_(o),
pear_(find_pair_type(p))
{}
private:
int apple_;
int orange_;
int pear_;
};
Member functions¶
Make sure to declare const
if it is not changing the object’s state.
Getters and Setters¶
Before filling up a class with getters and setters, consider seriously
if those members are meant actually to be public. If, nonetheless, getters
and/or setters are needed, don’t use the get_
and set_
prefixes.
template <typename T>
class my_class {
public:
// ...
T value() const {
return value_;
}
void value(const T& val) {
// perhaps do something before assigning, otherwise it could be just public
value_ = val;
}
private:
T value_;
};
Declaring references and pointers¶
// ok
std::string& s = ...;
const std::string& s = ...;
std::string* s = ...;
const std::string* s = ...;
std::string* const s = ...;
// not ok
std::string &s = ...;
const std::string &s = ...;
std::string *s = ...;
const std::string *s = ...;
std::string *const s = ...;
Generally, we follow C++’s convention for references and pointers, as it is the style used in the C++ standard, and also the recommendation of the C++ Core Guidelines NL.18. Precedence and the C++ language grammar may offer some support the other convention, but not enough support!
Macros¶
Macros are C-ish, so they must be avoided. If not possible, they must be written in capitals, with words separated by underscores.
Always use {}
, even for single statement if
, for
, etc¶
It makes code clearer and avoids nasty bugs that occur when refactoring. It also avoids some errors when merging with git.
// ok
for (auto& v: vector) {
// increment the value!
v++;
}
// bad
for (auto& v: vector)
// increment the value!
v++;
don’t put {
on a new line¶
Except when indentation of arguments or when doing member initialization in constructors.
// it makes sense to have the { on a new line here for clarity
std::vector<std::string> foo(
std::vector<std::vector<int>>& values,
std::map<int, std::string>& name_table)
{
// do some work
}
leave a space between if
, for
etc and following parenthesis¶
Accords with K&R style, and makes a visual distinction with function evaluation
// ok
for (auto& v: vector) {
v++;
}
// not ok
for(auto& v: vector) {
v++;
}
use using
instead of typedef
¶
It is easier to read, consistent with auto
:
// good
using int_container = std::vector<int>;
// bad
typedef std::vector<int> int_container;
and can be used for template aliases
template <typename T>
using aligned_container = std::vector<T, my_fancy_aligned_allocator<T>>;
Use scoped enum instead of enum¶
// good
enum class ionKind {sodium, calcium};
// bad
enum ionKind {ion_sodium, ion_calcium};
And stick to the naming scheme for all enums of xxxKind
to make it
clear throughout the code whenever an enum is being used, for example:
auto i = current(voltage, ionKind::calcium);
Use struct
for POD wrappers¶
But class
if it has any sort of fancy logic associated with it
Memory management¶
use unique_ptr
¶
Actually, feel free to use naked pointers in your code, but make sure
that you use smart pointers to handle allocation and freeing of memory.
When a developer sees a naked pointer in Arbor, they can think, “good, I
don’t have to worry about responsibility for freeing that memory”.
Furthermore, if unique_ptr
handles allocation and freeing of memory,
the user doesn’t have to concern themselves with freeing memory ever.
This practice implies that care must be taken to ensure that the
resource managed by a unique_ptr
has to outlive any raw pointers
that are obtained from its get()
member.
Header files¶
use pragma once¶
Use #pragma once
to guard against including the same header twice.
This might not be completely standard compliant, but it is supported by
every compiler under the sun, and is much cleaner than #ifdef
guards.
don’t rely on headers being included elsewhere¶
For example, if you use std::vector<int>
in a file, make sure to
have #include <vector>
at the top of the source file.
Relying on headers being included elsewhere can lead to portability
problems, for example, on MacOS you have to #include <cmath>
for some
math functions that are imported via other header files with gcc on
Linux.
Sort headers alphabetically¶
To make it easy to search for a header in a long list of includes.
For example:
#include <algorithm>
#include <fstream>
#include <map>
#include <queue>
#include <set>
use C++ wrappers for C standard headers¶
// good
#include <cmath>
#include <cstdio>
// bad
#include <math.h>
#include <stdio.h>
when calling C stdlib functions, use the std::
-prefix versions,
e.g., std::printf(...)
instead of printf
. Most of the times C++
wrappers just bring into std
the C declarations, but sometimes the
wrappers have more syntactic sugar and call the same internal builtins
that their C counterparts call (for example GCC).
group headers together¶
In the following order
C++ standard libary
system C headers (POSIX, kernel interfaces, etc.)
external libraries
public arbor headers
private arbor headers
For example:
// first C++ standard headers
#include <algorithm>
#include <fstream>
#include <map>
// then system C headers
#include <signal.h>
#include <sys/select.h>
// externals
#include <vector/Vector.hpp>
// public arbor headers
#include <arbor/common_types.hpp>
#include <arbor/simulation.hpp>
// private arbor headers (note we use quotes for private project headers).
#include "cell_group.hpp"
#include "util/optional.hpp"