- Table of Contents
- Background
- 1. C++ Version
- 2. Naming Conventions
- 3. Header Files
- 4. Scoping
- 5. Classes
- 6. Functions
- 7. Other C++ Features
- 7.1 Rvalue References
- 7.2 Friends
- 7.3 Exceptions
- 7.4 noexcept
- 7.5 Run-Time Type Information (RTTI)
- 7.6 Casting
- 7.7 alias declarations and typedefs
- 7.8 Streams
- 7.9 Printing Messages
- 7.10 Increment and Decrement
- 7.11 Use of const
- 7.12 Use of constexpr
- 7.13 Integer Types
- 7.14 Preprocessor Macros
- 7.15 0 and nullptr/NULL
- 7.16 sizeof
- 7.17 Type deduction
- 7.18 Concepts
- 7.19 Ranges
- 8. Comments
- 9. Formatting
- 10. Exceptions to the Rules
C++ is the main development language of DUNE's DAQ software processes. It is an unusually complex language, which can make code more bug-prone and harder to read and maintain.
The goal of this guide is to manage this complexity by describing in detail the dos and don'ts of writing C++ code for the DUNE data acquisition system. These rules exist to keep the code base manageable while still allowing coders to use C++ language features productively. While it will take a certain amount of time to learn these rules and adhering to them may mean that creating a piece of code that "just works" might take a little longer than it otherwise would, the payoff in terms of reduced debugging time and increased readability will be well worth it.
Two further points to close out this intro:
-
This guide is not a C++ tutorial: we assume that the reader is familiar with the language.
-
The DUNE DAQ C++ Style Guide is a modified fork of the Google C++ Style Guide https://google.github.io/styleguide/cppguide.html. Most of the modification involves loosening/simplifying Google's coding rules, as well as removing a lot of the justification for the rules in the interests of keeping this document (relatively) brief. If it's unclear why a given DAQ C++ guideline is in place, it's quite likely there's a relevant discussion on the topic in the Google Style Guide.
Currently, code should target C++20, i.e., should take advantage of C++20 features (unless otherwise indicated as described in this document) and not use C++23 features.
The most important consistency rules are those that govern naming. The style of a name immediately informs us what sort of thing the named entity is: a type, a variable, a function, a constant, etc., without requiring us to search for the declaration of that entity. The pattern-matching engine in our brains relies a great deal on these naming rules.
Naming rules are pretty arbitrary, but we feel that consistency is more important than individual preferences in this area, so regardless of whether you find them sensible or not, the rules are the rules.
Optimize for readability using names that would be clear even to people on a different team.
Use names that describe the purpose or intent of the object.
Do not worry about saving horizontal space as it is far
more important to make your code immediately
understandable by a new reader. Minimize the use of
abbreviations that would likely be unknown to someone outside
your project (especially acronyms and initialisms). Do not
abbreviate by deleting letters within a word unless it's obvious what's meant (e.g. "msg", "num", "max") or you define the full meaning of the abbreviation in a comment. Generally speaking, descriptiveness should be
proportional to the name's scope of visibility. For example,
n
may be a fine name within a 5-line function,
but within the scope of a class, it's likely too vague. Here's an example of well-chosen names:
class MyClass {
public:
int count_foo_errors(const std::vector<Foo>& foos) {
int n = 0; // Clear meaning given limited scope and context
for (const auto& foo : foos) {
...
++n;
}
return n;
}
void do_something_important() {
std::string daq_meltdown = ...; // Pretty clear what "daq" abbreviation is!
}
private:
const int m_max_allowed_connections = ...; // Clear meaning within context
};
And here's an example of poorly-chosen names:
class MyClass {
public:
int count_foo_errors(const std::vector<Foo>& foos) {
int total_number_of_foo_errors = 0; // Overly verbose; "n" or even "num_errors" would be simpler
for (int foo_index = 0; foo_index < foos.size(); ++foo_index) { // "i" or even "i_f" would be simpler
...
++total_number_of_foo_errors;
}
return total_number_of_foo_errors;
}
void do_something_important() {
int cstmr_id = ...; // What on earth is a cstmr?? Abbreviation hurts here.
}
private:
const int m_num = ...; // Unclear meaning within broad scope. Few names for an int could be worse than "num"
};
Note that certain universally-known abbreviations are OK, such as
i
for an iteration variable and T
for a
template parameter.
For the purposes of the naming rules below, a "word" is anything that you would write in English without internal spaces. This includes abbreviations, such as acronyms (e.g. "DAQ", "CERN"). Two naming conventions you need to be aware of for the discussion below are:
- Pascal case: Capitals used to distinguish words, with first letter capitalized: ThisIsInPascalCase
- Snake case: Underscores used to distinguish words, with all letters lowercase except optionally for acronyms: this_is_in_snake_case
In Pascal case, it's preferred that you treat acronyms like other words, e.g., StartRpc()
rather than
StartRPC()
.
Template parameters should follow the naming style for their category: type template parameters should follow the rules for type names, and non-type template parameters should follow the rules for variable names.
For DUNE DAQ software, if a variable name begins with a single letter followed by an underscore, that's meant to convey something about the variable's type (details below). For that reason, don't use this convention for any other purpose (e.g., use num_widgets
instead of n_widgets
).
Files ending in *.hpp
, *.cpp
and *.hxx
should use Pascal case (MyClass.hpp
). Files ending in *.cxx
should use snake case (my_application.cxx
). The meaning of these extensions is described later in this document.
The names of all types — classes, structs, type aliases, enums, and type template parameters — use Pascal case.
When making a type alias, however, additionally add a _t
to the end. So, e.g.,
template<typename T>
using MyAllocList_t = std::list<T, MyAlloc<T>>;
The names of local variables, function parameters and struct data members should use snake case.
E.g. cool_local_variable
, MyStruct.data_member
.
Non-static data members of classes should be prefixed with m_
.
For instance: MyClass.m_data_member
.
If a variable is a static data member in a class it should be preceded with an s_
.
E.g., MyClass.s_total_instances_of_this_class
. Static struct data members are discouraged.
If a variable is (unfortunately) a global, it should be preceded with a g_
.
E.g., g_total_warning_messages
.
Both standalone functions and class member functions should use snake case (as is the situation with, e.g., the STL <algorithm>
library).
Getters and setters should begin with get_
or set_
. E.g., MessageSender::get_num_messages_sent()
.
Namespace names are all lower-case, with words separated by underscores.
The top-level namespace of the DUNE DAQ codebase is dunedaq
. For a given package, the next level namespace should have the same name as the DUNE DAQ package. So, e.g., the code for the appfwk package should be placed in the dunedaq::appfwk
namespace.
.
Avoid nested namespaces that match well-known top-level
namespaces. Collisions between namespace names can lead to surprising
build breaks because of name lookup rules. In particular, do not
create any nested std
namespaces.
Enumerators should be in Pascal case, except prefaced with a k
. E.g.,
enum class UrlTableError {
kOk = 0,
kOutOfMemory,
kMalformedInput,
};
You're not really going to
define a macro, are you? If you do, they're like this:
MY_MACRO_THAT_SCARES_SMALL_CHILDREN_AND_ADULTS_ALIKE
.
Please see the description of macros; in general macros should not be used. However, if they are absolutely needed, then they should be named with all capitals and underscores.
#define UNAVOIDABLY_USEFUL_PLUGIN_LOADER(x) ...
Header files should have an .hpp
extension. They fall into one of two
categories: public header files (those meant to be included by code
using a library) and private header files (those only included by
library implementation files). Public header files shall be placed in
the include/package_name
directory (where package_name
is a
stand-in for the name of the package). Private headers typically are kept with
source files in the same directory.
In general, every .cpp
file should have an associated .hpp
file. There
are some common exceptions, such as files dedicated to unit tests. Files which contain a main()
function don't need a corresponding .hpp
file, and end in .cxx
rather than .cpp
.
Header files should be self-contained (compile on their own) and end in
.hpp
. Non-header files that are meant for inclusion should end in .inc
and be used very rarely, with an exception which will be mentioned in a moment.
Users and refactoring tools
should not have to adhere to special conditions to include the header.
Specifically, a header should have header guards
and include all other headers it needs.
Prefer placing the definitions for inline and template functions in the
same file as their declarations. If the definitions are lengthy, you can accomplish this de-facto by putting them in a file with an .hxx
extension in a subdirectory of the include directory called "detail" and including it after the declaration. E.g., if in Foo.hpp, we could have:
template <typename T>
class Foo {
public:
void print_value(const T& val) const;
};
// Foo.hxx has the definition of PrintValue
#include "detail/Foo.hxx"
The definitions of inline and template functions must be included into
every .cpp
file that uses them, or the program may fail to link in
some build configurations. If declarations and definitions are in
different files, including the former should transitively include the
latter, as in the .hxx
example.
As an exception, a template that is explicitly instantiated for all
relevant sets of template arguments, or that is a private implementation
detail of a class, is allowed to be defined in the one and only .cpp
file that instantiates the template.
All header files should have #define
guards to prevent multiple
inclusion. The format of the symbol name should be
<PACKAGE>_<PATH>_<FILE>_HPP_
. The symbol should appear three times, like so:
#ifndef FOOPACKAGE_INCLUDE_FOOPACKAGE_DAQ_PROCESS_HPP_
#define FOOPACKAGE_INCLUDE_FOOPACKAGE_DAQ_PROCESS_HPP_
...
#endif // FOOPACKAGE_INCLUDE_FOOPACKAGE_DAQ_PROCESS_HPP_
Define functions inline only when they are small, say, 10 lines or fewer. Feel free to inline getters and setters, and other short, performance-critical functions. Don't inline functions with loops or switch statements (unless, in the common case, the loop or switch statement is never executed).
In any file which performs an include, if the included header is the "related header" - meaning, you're editing foo.cpp and the header is foo.hpp - put it before all the other headers.
Then, in order:
- private headers
- public headers from current package
- public headers from other packages in project
- public headers from external dependencies
- standard library headers
All of a project's header files should be listed without use of UNIX directory aliases .
(the current directory) or ..
(the parent directory). For example, a private header
awesomedaqproject/src/base/GetAllSupernovaData.hpp
should be included as:
#include "base/GetAllSupernovaData.hpp"
As of dunedaq-v2.0.0, include directories which are automatically picked up by your package are:
include/<package>
(for public headers)src/
(private headers for files used in the package's main library and plugins)test/src/
(private headers for files used in test applications/plugins)
You should include all the headers that define the symbols you rely
upon, except in the case of forward declarations. Note that the order of header
declarations described above helps enforce this rule. If you rely on
symbols from Bar.hpp
, don't count on the fact that you included
Foo.hpp
which (currently) includes Bar.hpp
: include Bar.hpp
yourself,
unless Foo.hpp
explicitly demonstrates its intent to provide you the
symbols of Bar.hpp
.
If a header comes from the C++ Standard Library (e.g., <vector>
, <cstdlib>
) it should be enclosed in angle brackets. All other headers should be enclosed in quotes.
Currently, use of C++20 modules is disallowed.
With few exceptions, place code in a namespace. Avoid putting using-directives (e.g. using namespace foo
) in header files, as any files which include them may risk name collisions and, worse, unexpected behavior when the "wrong" function/class is picked up by the compiler. They're less damaging when employed in source files and can reduce code clutter, but make sure to only use them after including all your headers, and be aware of their risks.
Also in the vein of reducing code clutter, using-declarations (e.g., using heavily::nested:namespace::foo::FooClass
) can be useful for improving readability. For unnamed namespaces, see Unnamed Namespaces and Static
Variables.
When creating nonmember functions which work with a class, keep in mind that these functions are part of the class's interface and therefore should be in the same namespace as the class, though not necessarily the same files.
Namespaces should be used as follows:
-
Terminate namespaces with comments as shown in the given examples.
-
Namespaces wrap the entire source file after includes, definitions/declarations and forward declarations of classes from other namespaces.
// In the .hpp file
namespace mynamespace {
// All declarations are within the namespace scope.
// Notice the indentation of four spaces
class MyClass {
public:
...
void foo();
};
} // namespace mynamespace
// In the .cpp file
namespace mynamespace {
// Definition of functions is within scope of the namespace.
void MyClass::foo() {
...
}
} // namespace mynamespace
More complex .cpp
files might have additional details, like using-declarations.
#include "AHeader.hpp"
namespace mynamespace {
using ::foo::Bar;
...code for mynamespace...
} // namespace mynamespace
-
Do not declare anything in namespace
std
, including forward declarations of standard library classes. Declaring entities in namespacestd
is undefined behavior, i.e., not portable. To declare entities from the standard library, include the appropriate header file. -
Do not use Namespace aliases at namespace scope in header files except in explicitly marked internal-only namespaces, because anything imported into a namespace in a header file becomes part of the public API exported by that file.
The following are examples of proper use of a namespace alias:
// Shorten access to some commonly used names in .cc files.
namespace baz = ::foo::bar::baz;
// Shorten access to some commonly used names (in a .hpp file).
namespace librarian {
namespace impl { // Internal, not part of the API.
namespace sidetable = ::pipeline_diagnostics::sidetable;
} // namespace impl
inline void my_inline_function() {
// namespace alias local to a function (or method).
namespace baz = ::foo::bar::baz;
...
}
} // namespace librarian
When definitions in a .cpp
file do not need to be referenced outside
that file, place them in an unnamed namespace or declare them static
.
Do not use either of these constructs in .hpp
files.
Format unnamed namespaces like named namespaces. In the terminating comment, use a pair of double quotes in place of the (nonexistent) namespace name
Use unnamed namespaces/static variables for when it makes sense to maintain file scope (as opposed to local or global scope) for that variable. An example might be
namespace {
void utility_function_only_meaningful_to_this_file() {
...
}
} // namespace ""
-
Use completely global functions rarely, and only if there's a compelling reason
-
If a nonmember function can accomplish what a member function can, prefer a nonmember function. This is because the less code a class's data is exposed to, the less opportunity there is for bugs.
-
Nonmember functions should always be in a namespace, and unless there's a compelling reason to violate this rule, to go in the same namespace as the class it works with
-
Static methods of a class should generally be closely related to instances of the class or the class's static data.
Declare local variables in as local a scope as possible, and as close to the first use as possible. Always initialize variables in their declaration.
There is one caveat: if the variable is an object, its constructor is invoked every time it enters scope and is created, and its destructor is invoked every time it goes out of scope.
// Inefficient implementation:
for (int i = 0; i < 1000000; ++i) {
Foo f; // My constructor and destructor get called 1000000 times each!
f.do_something(i);
}
For pointer variables, this would translate to initializing the pointer to nullptr:
// Assume we don't yet know the Foo instance fptr will point to...
std::unique_ptr<Foo> fptr = nullptr;
if (able_to_read_data) {
fptr = std::make_unique<Foo>(foo_arg1, foo_arg2);
// fill the Foo instance with the data
}
if (fptr) {
// send data
}
The less complex the constructors and destructors of classes that are used as static and global variables anywhere in the code, the better. In particular, keep in mind there's no guarantee on the order of construction of these variables, and hence code should never rely on an assumed order.
Global variables are discouraged. When used, they should be const
or, if possible, constexpr
.
-
Don't call any of a class's virtual functions in its constructor. This will not result in the correct invocation of subclass implementations of those virtual functions.
-
If an error occurs that will prevent the class from being constructed, have it throw an exception. As its destructor won't execute in this scenario, make sure you clean up any resources the constructor allocated before throwing.
-
Initialize a class's member in the constructor's member initialization list rather than assign to it in the constructor's body. An exception to this might be if the member class's default constructor is much faster than its other constructors/assignment operator, but it's not guaranteed that it'll even need to be assigned to.
Type conversion operators, and constructors that are callable with a
single argument, should be marked explicit
in the class definition
to avoid having them used to perform an implicit conversion in user
code. As an exception, copy and move constructors should not be
explicit
, since they do not perform type conversion. Also, implicit
conversions can sometimes be necessary and appropriate for types that
are designed to transparently wrap other types; in this case an
exception to the rule is allowed.
Constructors that cannot be called with a single argument may omit
explicit
since the C++ language does not consider multi-argument or
argument-free constructors for implicit conversions.
If a class contains member data, each of its copy constructor, copy assignment operator, move constructor and move assignment operators must be either defined or explicitly deleted. "Defined" could be as simple as making explicit the use of the "default" keyword.
Always use a class
rather than struct
unless you're creating:
- A passive object only meant to carry data
- A small callable with an
operator()
defined
If using a struct to carry data, all fields must be public, and accessed directly rather than
through getter/setter methods. Any functions must not provide behavior
but should only be used to set up the data members, e.g., constructor,
destructor, initialize()
, reset()
.
Prefer to use a struct
instead of a pair or a tuple whenever the
elements can have meaningful names.
Exception: Pairs and tuples may be appropriate in generic code where there are not specific meanings for the elements of the pair or tuple. Their use may also be required in order to interoperate with existing code or APIs.
When class B inherits from class A, it should almost always be public inheritance ("inheritance of interface"). Protected and private inheritance is known as "inheritance of implementation" and results in less encapsulation than, say, having class B contain a member of class A and use its functionality ("composition"). Multiple inheritance of implementation is especially bad.
Explicitly annotate overrides of virtual functions or virtual
destructors with exactly one of an override
or final
specifier. Do
not use the keyword virtual
in this case as this is already denoted
by one of those two specifiers.
There's a limited set of circumstances in which it's OK to overload operators:
- For copying,
operator=
. - For type conversions,
operator()
. More in implicit conversions. - When defining comparison operators for a user-defined type
- Outputting a type's value where it makes sense, by streaming with
operator<<
. Note this should be a nonmember function, not a member function of the type.
In the interests of encapsulation, keep the access level of a class's member functions only as generous as necessary. I.e., prefer private functions over protected functions, protected functions over public functions. Of course, use common sense: if you're writing an abstract base class, your functions will be public!
In a class, never declare data as protected or public. Use accessor functions if you must.
A class definition should start with a public:
section,
followed by protected:
, then private:
. Omit sections that would be
empty.
Within each section, generally prefer grouping similar kinds of declarations together, and generally prefer the following order:
- types (including alias declarations/
typedef
s,using
, and nested structs and classes) - constants
- basic constructors (non-copy, non-move)
- normal functions
- copy constructor
- copy assignment
- move constructor
- move assignment
- destructor
- data members.
Do not put large method definitions inline in the class definition. See Inline Functions for more details.
- Have it do one thing, rather than many things (the "Swiss army knife" trap)
- If it starts getting long (say, beyond 40 lines) think about ways it could be broken up into other functions
- Prefer names that describe, to an appropriate level of precision, what the function does
If your function creates a single value and you don't anticipate it ever needing to return more than a single value, have it return it. Otherwise, use pass-by-reference in the argument list. These output arguments should appear after the input arguments. Parameters which serve both as input and output should be placed in-between.
[Section eliminated, material covered in "general guidelines"]
[Section eliminated for DUNE]
If a function is overloaded by the argument types alone, make sure its behavior is very similar across the types, especially if the types are themselves similar (e.g., std::string vs. const char*)
If the behavior is noticeably different, prefer different function names.
Default arguments are allowed on non-virtual functions when the default is guaranteed to always have the same value. Always define the value of the argument in the header, as it's part of the function's interface.
The only time it's OK to use a trailing return type is when specifying the return type of a lambda expression. In some cases the compiler is able to deduce a lambda's return type, but not in all cases.
-
You should find yourself using
std::unique_ptr
more often thanstd::shared_ptr
-
Use of raw pointers should be very rare. One of the few times it's OK is when you want to point to an object where you don't want to change anything about its ownership.
-
A corollary is that you should (almost) never use delete on a raw pointer because we expect that the use of raw pointers which own memory in DUNE DAQ will be limited to low-overhead access to pre-existing memory buffers, in which the user does not have ownership of the memory that is pointed to.
-
When using raw pointers, prefer
void*
to point to generic memory over a pointer to a specific type (such as char); this is because you can use astatic_cast
instead of areinterpret_cast
onvoid*
to cast it to a pointer to the desired type. Of course, use of generic memory should be rare and only in low-level code where knowledge of the type really is absent.
Writing coroutines is not formally disallowed. However, be aware that unlike in C++23, in C++20 coroutines require either the writing of a generator (which will require a good deal of expertise and effort) or the use of a generator from a third-party library (which will require consultation with Software Coordination). Ask yourself whether using a coroutine to solve the problem at hand is worth the difficulty.
Use rvalue references to:
-
Define move constructors and move assignment operators. You should always have these defined when you've created a new type and there's the possibility that its copy operations may be significantly slower.
-
Support perfect forwarding in generic code
-
Define pairs of overloads, one taking
Foo&&
and the other takingconst Foo&
, when this might improve performance
Use friend classes and functions when alternatives result in less encapsulation. An example of this would be if there's only one nonmember function which you could imagine would ever need a given member of a class - in this case, while you could make that given member public, it would result in less encapsulation than use of a friend function.
An appropriate use of a friend function is if you're overloading the streaming operator, operator<<
, and want to print a type's private data.
You should typically define your friend function in the same file as the class it's a friend of, and almost always in the same namespace as the class.
Thrown exceptions should be declared ERS Issues (for technical details on ERS, see the ERS documentation, and for ERS-specific usage recommendations, see the logging documentation).
Throw an exception if your code's encountered a problem it can't recover from on its own. Don't throw if you can implement a local recovery, and definitely don't throw exceptions as form of flow control in the absence of any problems.
Before you throw an exception, try to clean up as much as possible - release resources, etc. RAII is your friend here.
Like the parameters a function takes and a function's return value, the types of exception a function throws are part of the interface it presents to the caller. For this reason, think carefully when adding an exception throw to a function other callers are already using. Will they be able to handle the new exception? If not, can they at least release resources correctly?
Never throw exceptions out of a destructor
The swallow-all-exceptions construct catch(...)
should only rarely be used since in general it's better to have the failure behind an unexpected exception become blatantly obvious than hidden due to the exception being swallowed. However, there are scenarios where catch(...)
is appropriate. One example of this would be to use it directly inside of main()
, to clean up resources before terminating the program. Another would be in a thread. An uncaught exception escaping an std::thread
or an std::jthread
would cause a crash, and if the crash would have such negative consequences that it outweighs the argument against the prohibition, then it's OK to swallow all exceptions.
Catch by const reference, unless you plan to add info to the exception before rethrowing it, in which case you should a non-const reference.
When you catch, print as much info about the exception as would be useful to users of the program
If you've designed a type, strive to make its move and copy functions noexcept. This is because compilers can perform optimizations when it comes to STL functionality if noexcept is specified.
Otherwise, use noexcept judiciously. Keep in mind you can't take it back later, and that it's very hard to make this guarantee if you're writing generic code. For this reason, intelligently choose your conditionals inside of noexcept()
The only time Run Time Type Information (RTTI) can be used is in code meant to test other code.
Do not use C-style casts (e.g., "(float)3.5" or "float(3.5)")
Use reinterpret_cast
only for low level code, and only if you're sure
there's no safer approach
const_cast
should almost never be used. Its use is often indicative
of a deeper problem in the design of the code.
Use alias declarations and typedef
s to clarify the meaning of a type
in a given context. Prefer use of alias declarations; in particular,
use them when giving a templatized type a more-meaningful name, as
there's not a simple way to do this with typedef
s. E.g.
template<typename T>
using MyAllocList_t = std::list<T, MyAlloc<T>>;
MyAllocList_t<Foo> foos;
[Deleted; folded into the new "printing messages" section]
Use the messaging functions available in the DUNE DAQ logging package for output. Never use alternatives (this includes printf
, cout
, etc.)
See the guidelines in the Exceptions section for declaring new ERS Issues.
Include as much information useful for debugging in warning/error messages. E.g., rather than "Data found corrupt", go with "Data found corrupt in data packet ID #8294 with timestamp 0x3527378 (55735160 decimal) in run 34872"
TRACE may be used for internal code tracing. Only levels 4 (TLVL_TRACE) and higher should be used for this purpose.
Overload <<
for streaming only for types representing values, and write only
the user-visible value, not any implementation details.
Take care that a given print statement not print so often that it obscures the output of other equally (or even more) important messages
Unless in a loop construct, an increment (++
) or decrement (--
) of a
variable should exist on its own line. In particular, it should not be
used in an if statement.
Particularly since DUNE processes will involve many threads, intelligent use of const
is important.
Use const
on variables whose values can't be known at compile time but nonetheless aren't to be changed after they're initialized. The exception is if you need to pass this type of variable to a (poorly-designed) API
which doesn't change the variable's value but doesn't declare it const
in its function signatures. While it's more common for developers to underuse rather than overuse const
, a risk of overusing it is that it's a decision that's hard to reverse. If it turns out that const
needs to be removed from a variable, realize that his decision will likely break other people's code which may have relied on its const
-ness. If you make the decision to use const
, realize it should be a permanent one.
If a class method alters the class instance's physical state but not its logical state, declare it const and use "mutable" so the compiler allows the physical changes.
Compile-time constant initialization tools are even better than const
; use them when you can as described in the next section.
If a variable or function's return value is fixed at compile time and
you don't see this ever changing, use
constexpr
/consteval
/constinit
when possible. The phrase "don't see
this ever changing" is used since similar to "const" or "noexcept",
downgrading (e.g., changing a function from consteval
to
constexpr
, or dropping one of these qualifiers entirely) can break other people's code.
Unless you have a good reason not to, use int
. An obvious good
reason not to would be that you need to be guaranteed 64 bits to represent a value, e.g., a timestamp.
When you want a specific size in bytes, don't use C integer types
besides int
: no short
, long
, etc. Use intN_t
, N being the
number of bits.
You should not use the unsigned integer types such as uint32_t
, unless
there is a valid reason such as representing a bit pattern rather than a
number, or you need defined overflow modulo 2^N. In particular, do not
use unsigned types to say a number will never be negative. Instead, use
assertions for this.
If your code is a container that returns a size, be sure to use a type that will accommodate any possible usage of your container. When in doubt, use a larger type rather than a smaller type.
Use care when converting integer types. Integer conversions and promotions can cause undefined behavior, leading to security bugs and other problems.
Code should be 64-bit friendly. [does it need to be 32-bit friendly?]
While not explicitly forbidden, macros come with the very heavy price of the code you see not being the code the compiler sees, a problem compounded by their de-facto global scope. Avoid them if at all possible, using inline functions,
enums, const
variables, constexpr
, and putting repeated code inside of functions.
If you must write a macro, this will avoid many of their problems:
- Don't define macros in a header file.
- USE_ALL_CAPS_IN_YOUR_MACRO_NAME
#define
macros right before you use them, and#undef
them right after.- Do not just
#undef
an existing macro before replacing it with your own; instead, pick a name that's likely to be unique. - Try not to use macros that expand to unbalanced C++ constructs, or at least document that behavior well.
- Have the variable names in your macro be very unlikely to be used elsewhere in unrelated code
- Prefer not using
##
to generate function/class/variable names.
Use nullptr
for pointers, and '\0'
for the null character. Don't use NULL, and definitely don't use the number "0" in this context.
Prefer sizeof(varname)
to sizeof(type)
, unless you really do mean that you want the size of a particular type, and not a variable which happens to have the type in question.
The auto
and decltype
keywords save a lot of hassle for the writer of a piece of code, but not necessarily for the reader. Keep in mind the reader might be you in 18 months. Use your
best judgement as to when the benefits of these keywords (reduced code
clutter) outweigh the costs (the reader needs to know the type of a variable but has trouble determining it from the code).
While a function template can deduce the type of the argument, making this explicit will typically make it clearer to both the code's reader and to the compiler what it is you're trying to do.
Use a concept if you believe a set of conditions on a type will be applicable in multiple circumstances. This would be as opposed to if a set of conditions on a type will only apply for a single function or class, in which case a concept will only add additional boilerplate by restating the implicit conditions in the function or class. Prefer the requires(Concept<T>)
syntax over the template<Concept T>
syntax. Use pre-existing concepts from the STL when available rather than reinventing the wheel. E.g., rather than writing
template<typename T>
concept MyEqualityComparable = requires(T a, T b) {
{ a == b }
};
just use the existing std::equality_comparable
concept from the STL.
The use of ranges is encouraged where it will make code safer and more legible. For example, rather than using the traditional erase-remove idiom to filter elements out of a container, just pipe the elements through std::views::filter
. Or rather than passing the first and last iterators of a container to std::sort
, just pass the container itself to std::ranges::sort
.
Comments are absolutely vital to keeping our code readable. But remember: while comments are very important, the best code is self-documenting. Give sensible names to types and variables, and don't make code "clever" unless it creates clear performance improvements in bottleneck regions. If it's obvious what a function does, don't clutter the code with a comment. E.g., don't do something like this:
// "sqrt" calculates the square root of a variable
double sqrt(double);
Use the //
syntax instead of the old C-style /* */
syntax. An exception is if you're developing a function which doesn't (yet?) use its arguments but you want to avoid an unused parameter warning, e.g.:
void foo(int /* appropriate name for the integer */) {
// Code under development which doesn't (yet) use the integer
}
While the new C++17 attribute [[maybe_unused]]
could also prevent
unused parameter warnings, save this for situations where you've
declared a variable and it really depends on the control flow of the
code whether or not it ends up being used.
Always include the brief license info described below under Legal Notice.
File comments describe the contents of a file. If a file declares, implements, or tests exactly one abstraction that is documented by a comment at the point of declaration, file comments are not required. All other files must have file comments.
The comment at the top of a header file should never describe implementation details the user of a class doesn't need to worry about. Save those either for the source file, or for just above the definition of an inline function if it's in the header file.
Comments are always in danger of growing stale as code changes. This danger is greatest when it comes to the comment at the top of the file. Be aware of this when you modify code, and update the comments if necessary.
The following License stanza should be included in your Doxygen @file section:
* This is part of the DUNE DAQ Application Framework, copyright 2020.
* Licensing/copyright details are in the COPYING file that you should have received with this code.
[JCF, Mar-27-2020: The details of what license would be in the COPYING file are TBD, and should probably involve Giovanna]
If a .hpp
declares multiple abstractions, the file-level comment should
broadly describe the contents of the file, and how the abstractions are
related. A 1 or 2 sentence file-level comment may be sufficient. The
detailed documentation about individual abstractions belongs with those
abstractions, not at the file level.
Do not duplicate comments in both the .hpp
and the .cpp
. Duplicated
comments diverge.
Every non-obvious class declaration should have an accompanying comment that describes what it is for and how it should be used.
The class comment should provide the reader with enough information to know how and when to use the class, as well as any additional considerations necessary to correctly use the class. Document the synchronization assumptions the class makes, if any. If an instance of the class can be accessed by multiple threads, take extra care to document the rules and invariants surrounding multithreaded use.
Declaration comments describe use of the function (when it is non-obvious); comments at the definition of a function describe operation.
Function declaration should have comments immediately preceding it that describe what the function does and how to use it unless the function is simple and obvious.
Types of things to mention in comments at the function declaration:
- What the inputs and outputs are, if not obvious
- For class member functions: whether the object remembers reference arguments beyond the duration of the method call, and whether it will free them or not.
- Any non-obvious preconditions and postconditions. E.g., can a pointer argument be null?
- If there are any performance implications of how a function is used.
- If the function is re-entrant. What are its synchronization assumptions?
When documenting function overrides, focus on the specifics of the override itself, rather than repeating the comment from the overridden function. In many of these cases, the override needs no additional documentation and thus no comment is required.
When commenting constructors and destructors, remember that the person reading your code knows what constructors and destructors are for, so comments that just say something like "destroys this object" are not useful. Document what constructors do with their arguments (for example, if they take ownership of pointers), and what cleanup the destructor does. If this is trivial, just skip the comment. It is quite common for destructors not to have a header comment.
If there is anything tricky about how a function does its job, the function definition should have an explanatory comment. For example, in the definition comment you might describe any coding tricks you use, give an overview of the steps you go through, or explain why you chose to implement the function in the way you did rather than using a viable alternative. For instance, you might mention why it must acquire a lock for the first half of the function but why it is not needed for the second half.
Note you should not just repeat the comments given with the function
declaration, in the .hpp
file or wherever. It's okay to recapitulate
briefly what the function does, but the focus of the comments should be
on how it does it.
In general the actual name of the variable should be descriptive enough to give a good idea of what the variable is used for.
If there are any invariants (special values, relationships between members, lifetime requirements) not clearly expressed by the type and name, they must be commented.
In particular, add comments to describe the existence and meaning of sentinel values, such as nullptr or -1, when they are not obvious.
Along with the usual rules, a global variable should have a comment as to why it needs to be global unless it's completely clear.
In your implementation you should have comments in tricky, non-obvious, interesting, or important parts of your code. Of course, tricky and non-obvious code should be avoided unless absolutely necessary.
Pay attention to punctuation, spelling, and grammar; it is easier to read well-written comments than badly written ones. In particular, a spelling mistake can make it hard to grep for a comment in the future.
In general, comments should be in English. An exception might be if you know the only people working on your code will be the fellow speakers of your language, especially if they're not fluent in English. Since this is a C++ style guide and not an English style guide, it's understandable if English written by a non-native speaker is less than perfect; however, don't hesitate to have a native English speaker look over your comment if you feel it would help.
Generally, complete sentences are more readable than sentence fragments. Shorter comments, such as comments at the end of a line of code, can sometimes be less formal.
Use TODO
comments for code that is temporary, a short-term solution,
or good-enough but not perfect. If possible provide a time estimate
(even if just something like "next few weeks") as to when you expect
something should be done.
TODO
s should include the string TODO
in all caps, followed by the
name, date, e-mail address, bug ID, or other identifier of the person or issue
with the best context about the problem referenced by the TODO
.
// TODO: John Freeman ([email protected]), Apr-14-2020
// In the next month, determine whether we can declare this function
// noexcept and consequently benefit from compiler optimizations.
Stale TODO
comments should be reviewed. If they're no longer relevant,
they should be deleted. If they're still relevant, a message should be
sent to the person whose e-mail is given in the comment. When in
doubt, send an e-mail.
For proper formatting, process your code using the dbt-clang-format.sh
script from the daq-buildtools package; see more on this in the daq-buildtools documentation . Among other things, running the script will satisfy the following two rules:
- Indentation should involve two spaces. Tabs should NOT be used.
- Lines should (almost) always be less than 120 characters
For new code, deviations from this guide should be quite rare. However: to the extent that we reuse already-existing code in the DUNE DAQ codebase, the already-existing code won't adhere to directives in this guide. If the code is never going to be touched again, then this won't be a big issue. If we plan on altering it in the future, it may be worth at least getting it to be somewhat more conformant to the rules, especially if the changes are relatively non-invasive (e.g., running it through clang-format, as opposed to breaking up a long but well-tested function). If anything about the style in existing code may be confusing to future developers, it may be worth adding comments on how the style deviates from the standard.