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 as isXxx()/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, but M_ 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

  • Align case with switch; always end each case with break/return or ;.

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; use override. Consider final 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);

1.12. Declaring Variables

  • One per line; meaningful names; declare near first use.

// Wrong
int a,b;

// Correct
int width;
int height;

1.13. Line Length

  • Keep lines ≤ 100 chars; wrap long expressions/operators on new lines.

if (longExpr
    + otherExpr
    + finalExpr)
{
}

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.3. Enums: enum class

  • Scoped, type-safe.

enum class Mode { A, B, C };

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 and std::print (C++20/23) are safer and faster than iostreams (<<) or printf.

  • Feel++ currently uses the {fmt} library (the same backend that powers std::format) because not all compilers and standard libraries provide std::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:

  • Direct printing (std::println, fmt::println) should be restricted (e.g., only master rank writes) to avoid flooding the terminal.

  • Logging (LOG(INFO) << fmt::format(…​)) is MPI-aware and safe in Feel++:

    • In master-only mode → only rank 0 produces output.

    • In all-ranks mode → every process logs (useful for debugging).

    • In selective mode → fine-grained control.

// 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;
}

2.16. Casting: Use C++ Casts

  • Use static_cast, const_cast, reinterpret_cast; avoid C-style casts.

// Wrong
char *p = (char *)std::malloc(n);

// Correct
char *p = static_cast<char *>(std::malloc(n));

2.17. Glossary (C++ basics)

  • RAII: Resource Acquisition Is Initialization — acquire/release resources in ctor/dtor.

  • ODR: One Definition Rule — avoid multiple conflicting definitions across TUs.

  • SFINAE: Substitution Failure Is Not An Error — older template constraint idiom (prefer concepts now).

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