Feel Coding Styles (C20/23 + HPC)
This document is a teaching-first style guide for Feel++. Every rule has a short explanation and a tiny C++ example.
1. Part I — General Coding & Naming
1.1. Formatting & Tools
-
Use
.clang-format
at the repo root; Allman braces, 4 spaces, column ≤ 100. -
Run
git clang-format
before committing. Style violations fail CI.
Example (auto-formatted spacing & braces):
// Wrong
if(ok){doWork();}
// Correct
if (ok)
{
doWork();
}
1.2. Headers & Include Guards
-
Every header uses a FEELPP-style include guard.
// feel/mesh/my_header.hpp
#if !defined(FEELPP_MESH_MY_HEADER_HPP)
#define FEELPP_MESH_MY_HEADER_HPP 1
// header content
#endif // FEELPP_MESH_MY_HEADER_HPP
Include order (in .cpp): own header, C std, 3rd-party, Feel headers.
// Correct
#include "feel/mesh/my_header.hpp"
#include <string>
#include <vector>
#include <boost/mpi3/communicator.hpp>
#include "feel/mesh/mesh.hpp"
1.3. Namespaces
-
Put code in
Feel
(and sub-namespaces). No extra indent inside namespaces.
namespace Feel::mesh
{
class A
{
public:
A() = default;
};
} // namespace Feel::mesh
1.4. Naming Conventions
-
Classes/structs: PascalCase (
MeshAdaptation
) -
Functions/variables: camelCase (
getValue
,counter
) -
Accessors: property name (e.g.,
matrix()
); bool asisXxx()
/hasXxx()
-
Acronyms lowercased inside names (e.g.,
isFemEnabled()
)
// Wrong
class meshadapter {};
int Counter;
bool isFEMEnabled();
// Correct
class MeshAdapter {};
int counter;
bool isFemEnabled();
1.5. Data Members & Statics
-
Official: non-static members use
M_
(e.g.,int M_value;
). -
Statics:
S_
(e.g.,static inline int S_counter = 0;
). -
A trailing underscore (e.g.,
value_
) is tolerated, butM_
is the rule for new code.
// Tolerated
class Foo { int value_; };
// Preferred (official)
class Foo { int M_value = 0; };
// Static
class Bar { static inline int S_instances = 0; };
1.6. Reserved Identifiers
-
Don’t start identifiers with
and never use
_
.
// Wrong
int _count, __impl;
// Correct
int count, impl;
1.7. Indentation & Whitespace
-
4 spaces; no tabs; no extra indent in namespaces.
-
Space around binary operators; one space after keywords.
-
Pointer/reference style:
char *p
,const T &x
.
// Wrong
if(foo){a=b+1;}
// Correct
if (foo)
{
a = b + 1;
}
1.8. Braces & Parentheses
-
Allman braces; always use braces for multi-line or complex bodies.
-
Parenthesize to make precedence explicit.
// Wrong
if (a && b || c)
// Correct
if ((a && b) || c)
1.9. Switch
switch (mode)
{
case Mode::A:
doA();
break;
case Mode::B:
doB();
[[fallthrough]];
case Mode::C:
doC();
break;
default:
handleDefault();
break;
}
1.10. Inheritance & Virtuals
-
Don’t repeat
virtual
in overrides; useoverride
. Considerfinal
where relevant. -
Polymorphic base classes need a
virtual
(or protected) destructor.
// Wrong
class Derived : public Base
{
virtual void run();
};
// Correct
class Derived : public Base
{
void run() override;
};
1.11. Comments & Doxygen
-
Prefer
//
comments; reserve/* … */
for license blocks. -
Use
//!
Doxygen for public APIs (\param
,\return
,\tparam
).
//!
//! \brief Compute flux on a boundary.
//! \param mesh input mesh
//! \param bid boundary id
//! \return integrated flux
double computeFlux(const Mesh &mesh, int bid);
2. Part II — C++20/23: Prefer This / Prefer That
Each item: a short explanation + example. Acronyms expanded at first use.
2.1. Special Members: = default
/ = delete
-
Clear intent, zero boilerplate, follows Rule of 0/5/6.
class Solver { public: Solver() = default; ~Solver() = default; Solver(Solver const &) = delete; Solver &operator=(Solver const &) = delete; };
2.2. Overrides: override
(and final
when needed)
-
Don’t repeat
virtual
in derived classes.
class Derived : public Base { public: void run() override; };
2.4. Defaulted Comparisons: <⇒
-
Generate comparisons with one line.
struct Point { int x{}, y{}; friend auto operator<=>(Point const &, Point const &) = default; };
2.5. use nodiscard
-
Mark results that must not be ignored.
[[nodiscard]] double computeEnergy(Mesh const &mesh);
2.6. constexpr
/ consteval
/ constinit
-
constexpr
: usable at compile time;consteval
: must be compile-time;constinit
: constant-initialized static (avoids static init order issues).
constexpr int square(int x) { return x*x; } consteval int scaled(int x) { return x*42; } constinit static int S_threshold = 10;
2.7. inline constexpr
Globals in Headers
-
Avoid ODR issues.
inline constexpr double kPi = 3.14159265358979323846;
2.8. Views: std::string_view
, std::span<T>
-
Accept data without owning/copying.
void logName(std::string_view name); void scale(std::span<double const> v, double factor);
2.9. Ranges: std::ranges
/ std::views
-
Clearer intent, fewer errors than hand-written loops.
for (int x : values | std::views::drop(1)) { std::println("{}", x); }
2.10. Concepts / requires
-
Express constraints directly; simpler than SFINAE.
template <std::integral T> T gcd(T a, T b);
2.11. auto
for Type Deduction
-
Avoids accidental conversions/copies; clarifies intent.
auto s = getStringLike();
2.12. Formatting & Printing: prefer std::format
/ std::print
-
std::format
andstd::print
(C++20/23) are safer and faster than iostreams (<<
) orprintf
. -
Feel++ currently uses the {fmt} library (the same backend that powers
std::format
) because not all compilers and standard libraries providestd::format
yet. -
Both styles are acceptable:
// Standard (C++20/23)
std::string msg = std::format("rank {} of {}", rank, size);
std::println("{}", msg);
// With {fmt} (used in Feel++)
std::string msg = fmt::format("rank {} of {}", rank, size);
fmt::println("{}", msg);
In parallel/HPC codes:
|
// Logging with {fmt} and glog (preferred in Feel++)
LOG(INFO) << fmt::format("N={}", N);
2.13. Strong Types over Flags
-
Replace “parameter soup” with self-documenting types.
struct Position { int x, y; }; struct Size { int w, h; }; Rectangle r{ Position{0,0}, Size{640,480} };
2.14. Explicit Initialization
-
Never read uninitialized memory.
float sum(std::span<float> v) { float result = 0.0f; for (float x : v) { result += x; } return result; }
2.15. Switch with ;
-
Make intentional fallthrough explicit.
switch (mode) { case Mode::A: doA(); [[fallthrough]]; case Mode::B: doB(); break; }
3. Part III — HPC Practices (MPI, GPU, Perf, Determinism, Logging)
Each rule has a short explanation + example. Acronyms are expanded at first sight.
3.1. Determinism & Reproducibility
-
Avoid hidden global state; seed RNGs explicitly; document nondeterministic paths.
-
RNG = Random Number Generator.
// Deterministic RNG (repeatable experiments)
std::mt19937 gen(1337);
std::uniform_real_distribution<double> dist(0.0, 1.0);
double u = dist(gen);
3.2. Memory & Data Layout
-
Reuse allocations in loops; prefer contiguous memory; consider SoA (Structure of Arrays) for SIMD/vectorization.
// Wrong: allocates each iteration
for (int i = 0; i < N; ++i)
{
std::vector<double> buf(1024);
process(buf);
}
// Correct: reuse
std::vector<double> buf(1024);
for (int i = 0; i < N; ++i)
{
process(buf);
}
Glossary: SoA = Structure of Arrays (e.g., struct { float x; float *y; }
) vs AoS = *Array of Structures (struct { float x,y; } p[N];
). SoA often vectorizes better.
3.3. Concurrency & Atomics
-
Minimize shared mutable state; prefer message passing; when needed, use
std::atomic
with explicit memory order.
std::atomic<int> S_counter{0};
S_counter.fetch_add(1, std::memory_order_relaxed);
3.4. MPI (Message Passing Interface)
-
Prefer collectives over manual send/recv loops — they are simpler, faster, and less error-prone.
-
Logging is MPI-aware and integrated with Google glog:
-
In master-only mode, only rank 0 logs; on other ranks the logging stream is a NoOp.
-
In all-ranks mode, every process logs (debugging).
-
In selective mode, rank 0 logs info while specific ranks may log debug output. Users do not need to write
if (comm.rank()==0)
guards —LOG(…)
handles this transparently.
-
-
Avoid gratuitous barriers; synchronize only when required (timing, algorithm phases).
3.4.1. Prefer collectives over manual loops
// Wrong: manual broadcast via send/recv
if (comm.rank() == 0)
{
for (int r = 1; r < comm.size(); ++r)
{
MPI_Send(buf.data(), count, MPI_DOUBLE, r, 0, MPI_COMM_WORLD);
}
}
else
{
MPI_Recv(buf.data(), count, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
}
// Correct: use a collective broadcast
MPI_Bcast(buf.data(), count, MPI_DOUBLE, 0, MPI_COMM_WORLD);
// (boost::mpi3 also provides collective methods, e.g. comm.broadcast_n)
3.4.2. MPI-aware logging with glog
// Transparent: behavior depends on logging configuration
LOG(INFO) << fmt::format("Starting computation with N={}", N);
// In selective/debug mode, all ranks may emit if configured
LOG(DEBUG) << fmt::format("[rank {}] localNorm={}", comm.rank(), localNorm);
3.4.3. Avoid unnecessary barriers
// Wrong: barrier in every loop step (expensive!)
for (int step = 0; step < steps; ++step)
{
compute_local();
MPI_Barrier(MPI_COMM_WORLD); // unnecessary
}
// Correct: rely on nonblocking ops or collectives
std::vector<MPI_Request> reqs;
// issue nonblocking Isend/Irecv into reqs...
MPI_Waitall(static_cast<int>(reqs.size()), reqs.data(), MPI_STATUSES_IGNORE);
// Barrier only when timing phases
MPI_Barrier(MPI_COMM_WORLD);
double t0 = MPI_Wtime();
do_work();
MPI_Barrier(MPI_COMM_WORLD);
double t1 = MPI_Wtime();
LOG(INFO) << fmt::format("Phase time = {:.6f}s", t1 - t0);
3.5. GPU / Accelerators
-
Don’t leak CUDA/HIP types in public headers; keep device pointers opaque; keep kernels focused.
// header (opaque device handle)
class DeviceBuffer
{
public:
void * M_dev = nullptr; // opaque; defined/managed in .cu/.hip
};
3.6. Logging with Google glog (GLOG)
-
Feel++ integrates Google glog with MPI-aware logging.
-
Logging behavior is controlled by configuration: 1) Master-only: only rank 0 produces output (other ranks get a NoOp stream). 2) All ranks: every rank logs (useful for debugging). 3) Selective: rank 0 logs info; individual ranks can be configured to emit debug output.
Important: Users do not need to write if (comm.rank()==0)
guards.
Logging transparently respects the current MPI logging mode.
On ranks where logging is disabled, the LOG(…)
call compiles to a NoOp.
// Transparent to the user:
// In master-only mode → only rank 0 produces output
// In all-ranks mode → every rank produces output
LOG(INFO) << fmt::format("Starting computation with N={}", N);
// Selective debug example (controlled via Feel++ logging config/flags)
LOG(WARNING) << fmt::format("[rank {}] localNorm={}", comm.rank(), localNorm);
Initialization (done once per process):
int main(int argc, char **argv)
{
google::InitGoogleLogging(argv[0]);
// Feel++ logging setup chooses master-only / all-ranks / selective
// based on command-line flags or configuration
}
3.7. Testing & Benchmarking
-
Fix RNG seeds; make tests deterministic.
-
Separate microbenchmarks from unit tests.
-
Use sanitizers (ASan/UBSan/TSan) in dedicated CI jobs.
std::mt19937 gen(42); // fixed seed for tests
3.8. Glossary (HPC)
-
MPI: Message Passing Interface — standard for distributed-memory parallelism.
-
GPU: Graphics Processing Unit — accelerator used for massively parallel workloads.
-
SoA/AoS: Structure of Arrays / Array of Structures — data layout patterns affecting vectorization and cache behavior.
-
SIMD: Single Instruction, Multiple Data — CPU vector instructions (e.g., AVX).
-
Barrier: a global synchronization point across MPI ranks (use sparingly).
4. Appendix — Full Example (Integrating Rules)
#if !defined(FEELPP_MESH_ADAPT_HPP)
#define FEELPP_MESH_ADAPT_HPP 1
namespace Feel::mesh
{
class MeshAdaptation
{
public:
MeshAdaptation() = default;
explicit MeshAdaptation(std::vector<int> dirs) noexcept
: M_directions(std::move(dirs))
{}
[[nodiscard]] const std::vector<int> & directions() const noexcept
{
return M_directions;
}
void setDirections(std::vector<int> dirs)
{
M_directions = std::move(dirs);
}
virtual ~MeshAdaptation() = default;
private:
std::vector<int> M_directions; // non-static → M_
static inline std::atomic<int> S_instances; // static → S_
};
} // namespace Feel::mesh
#endif // FEELPP_MESH_ADAPT_HPP