The project's structure largely follows the conventions from John Lakos' book "Large Scale C++ Software Design" ([1], [2]).
All library code belongs to a component; all components belong to a package; all packages belong to a package group; and there are no dependency cycles between components, packages, and package groups (§2.1 of [1]). For example,
sxt/base/iterator/counting_iterator.{h,cc,t.cc}
are the header, source, and unit tests for the component "counting_iterator". "counting_iterator" belongs to the package "iterator", and "iterator" belongs to the package group "base".
If another component sxt/multiexp/pippenger_multiprod/multiproduct
depends on
base/iterator/counting_iterator
, then no component in the package group "base"
can depend on a component in the package group "multiexp" as that would introduce
a cycle (§3.5 of [1], [3])
All packages are given a unique namespace. For example, all components within
the package base/iterator
use the namespace "basit". The first three letters
of a package namespace uniquely identify the package group (e.g. "bas"
identifies the package group "base"); following letters uniquely identify the
package within the package group (§2.10 of [1]). Package namespace names don't have to
be descriptive, but should be short and satisfy the uniqueness requirements.
We follow the C++ standard library naming convention. Class names, variables, functions, and concepts use snake case.
Class member names are suffixed with an underscore:
class abc {
private:
int x_;
};
Template parameters use camel case with the first letter capitalized:
template <class MyType>
void f(MyType t) {
// ...
}
Our error handling guidelines are inspired by Envoy's style guide ([4]).
Error codes and exceptions should be used to handle normal control flow. Crashing is a valid error handling strategy and should be used for errors not considered part of normal control flow ([5]).
To make errors more explicit, we use noexcept for functions that either don't throw an exception or would only throw exceptions outside of normal control flow (Item 14 of [6]). For example,
std::vector<int> copy_and_sort(const std::vector<int>& xv) noexcept {
std::vector<int> res{xv.begin(), xv.end()};
std::sort(res.begin(), res.end());
return res;
}
copy_and_sort might throw std::bad_alloc and noexcept will cause the function to terminate; but such an error would be outside of normal control flow, so termination is acceptable.
We make extensive use of RAII and allocator-aware containers to manage device memory and achieve certain host-side optimizations.
See [9], [10], and [11].
In order to get the most out of GPUs and eventually scale to using multiple GPUs, we use the asynchronous versions of CUDA API functions.
To make writing async code easier, we adopt the future-promise design from seastar.io ([12]) and use c++20 coroutines ([13]).
We try to follow the guidelines from Kevlin Henney's talk "Structure and Interpretation of Test Cases" ([7], [8]).
In additional to checking correctness, tests also serve as documentation and should be readable and describe code's behavior.
1: John Lakos. 2019. Large-scale C++ software design, Volume 1.
2: John Lakos. Overview of [1].
3: John Lakos. Advanced Levelization Techniques.
4: https://github.com/envoyproxy/envoy/blob/main/STYLE.md#error-handling
5: Matt Klein. Crash early and crash often for more reliable software
6: Scott Meyers. Effective Modern C++.
7: Kevlin Henney. Structure and Interpretation of Test Cases.
8: Kevlin Henney. Programming with GUTs.
9: Pablo Halpern, John Lakos. Value Proposition: Allocator-Aware (AA) Software.
10: John Lakos. Local ('Arena') Memory Allocators.
11: Pablo Halpern. Allocators: The Good Parts.
12: Avi Kivity. Building efficient I/O intensive applications with Seastar.
13: Gor Nishanov. C++ Coroutines: Under the covers.