Coding Practice Rules
This section covers general C++ coding practices in place for the GEOS project.
All contributors should familiarize themselves with these rules to maintain code quality, consistency, and reliability across the GEOS codebase.
Note
This document uses collapsible code blocks. Click on “Show/Hide Code” to expand examples.
Memory Managment
Allocations
Minimize dynamic memory allocation as much as possible, particularly in performance-critical code,
For frequently allocated/deallocated objects, consider memory pools,
For containers with known size, reserve capacity upfront.
As they are intended to low-level allocator codes, avoid new / delete and malloc / free as much as possible:
When applicable, Prefer composition over pointers,
Use RAII principles as much as possible,
Prefer
std::unique_ptroverstd::shared_ptr.
Pointer / Reference Function Arguments
Pass object by reference rather than passing pointer types.
When passing nullable objects as parameter, pointers can be passed.
Pointer parameters must always be null-checked when dereferenced. If applying this rule produces repetitive code, consider using a const reference.
This rule imply the following:
A reference may never be null,
In a given method,
thismay never be null.
Why?
Passing a pointer or reference parameter show the intention of modifying the underlying object. The difference of the two practices is that passing a pointer means that we consider that the underlying object can be null.
Provide Views to Arrays
When possible, prefer provide views to arrays (to const data if possible).
views must be passed / captured by value for function / lambdas.
The rule is generalizable to string_view for strings, but not applicable in GPU context.
Why Use Views?
No memory allocation: Views are lightweight references
Mutability correctness: Depending on the view type, can provide
constread-only access to inner dataGPU compatibility: LvArray views work seamlessly on device
Example: Views for arrays
// BAD - Creates a copy
array1d< real64 > copy = originalArray;
// GOOD - Creates a view (lightweight)
arrayView1d< real64 > view = originalArray.toView();
// GOOD - Const view for read-only access
arrayView1d< real64 const > constView = originalArray.toViewConst();
View Lifetime Management
Never Outlive Parent Arrays
Why?
Dangling views cause segmentation faults and undefined behavior, that can be particularly hard to diagnose.
The rule is applicable to arrayView* types and string_view.
Example: Lifetime Management
// WRONG - returning view of local array
arrayView1d< real64 > getDanglingView()
{
array1d< real64 > tempArray( 100 );
return tempArray.toView(); // DANGER: tempArray destroyed, view dangles!
}
// WRONG - storing view of temporary
class DataHolder
{
arrayView1d< real64 > m_view;
void setData()
{
array1d< real64 > temp( 100 );
m_view = temp.toView(); // DANGER: temp destroyed at end of scope!
}
};
// CORRECT - return the array
array1d< real64 > getSafeArray()
{
array1d< real64 > result( 100 );
// populate...
return result; // Move semantics ensure safety
}
// CORRECT - store array, create view when needed
class SafeDataHolder
{
public:
arrayView1d< real64 > getView() { return m_data.toView(); }
arrayView1d< real64 const > getViewConst() const { return m_data.toViewConst(); }
private:
array1d< real64 > m_data;
};
Value / Const Function Arguments
Pass large and non-trivially copyable objects by const reference:
A “large” size can be defined as “more that 16 bytes, “, which is 2 pointers, 2 integer / index, or 2 double,
An object is non-trivially copyable objects when it needs to perform sub-allocation when being copied,
Example: Value / Const Reference function parameters practices
// 16 bytes (string_view are equivalent to 2 constant pointers): PASS BY VALUE
static constexpr string_view str0 = "Hello";
string const str1 = getTimeStr(); // constant string are passed as string_view, beware of lifetime!
void func( string_view param );
arrayView2d< int > myDataView;
// 16 bytes: PASS BY VALUE
stdArray< double, 2 > vec2D;
void func( vec2D param );
// 12 to 16 bytes (globalIndex is 8, integer is 4 to 8): PASS BY VALUE
struct SmallStruct
{
globalIndex id;
integer count;
};
void func( SmallStruct param );
// 16 to 20 bytes (globalIndex is 8, integer is 4 to 8): PASS BY REFERENCE
struct BigStruct
{
globalIndex id;
integer countA;
integer countB;
};
void func( BigStruct const & param );
// Does dynamic allocation: PASS BY REFERENCE
map< int, long > myMap;
stdVector< int > myList { 123 };
void func( map< int, long > const & myMap,
stdVector< int > const & myList );
// LvArray types parameter practices depends on the intent
array2d< int > myConstantData;
array2d< int > myMutableData;,
// passing as non-view means that we will potencially resize the arrays: PASS BY REFERENCE
void arrayResizer( array2d< const int > & myConstantData
array2d< int > & myMutableData );
// passing as view means that we may just affect the data, only if non-const template: PASS BY VALUE
void arrayProcess( arrayView2d< const int > myConstantData
arrayView2d< int > myMutableData );
// 16 bytes superficially... but does dynamic allocation: PASS BY REFERENCE
struct DynAllocStruct
{
string const a;
};
void func( DynAllocStruct const & param );
// Any Group subclass is large: PASS BY REFERENCE
void modify( DomainPartition & domain );
Avoid Undesired Mutability
Enforce const and constexpr when possible
Why?
const and constexpr declaration enables:
Enables compiler optimization, improving code generation with explicit code constraints,
Improves code safety, preventing accidental modification for constant contexts,
Show code intention, making code clearer.
Also, mark methods const if the method is not designed to modify the object state.
Constexpr for Compile-Time Constants
Use constexpr for values known at compile time.
The rule is not absolute: when the impact is not significative, and the code needs is getting really unclear, the rule can be ignored.
Example: Constexpr usage
// Compile-time constants
// A rule of thumb is that oftenly any time a value is constexpr, it can also be static.
static constexpr localIndex numDimensions = 3;
static constexpr real64 tolerance = 1.0e-10;
// Constexpr functions (evaluated at compile time when possible)
constexpr localIndex computeArraySize( localIndex n )
{ return n * n + 2 * n + 1; }
Validation / Precision
Use Tolerance-Based Comparisons
Always consider proper tolerance for floating-point numbers comparisons, taking into account rounding errors, even for extreme values.
Example: Correct float comparison
// BAD - Direct comparison
if( value == 1.0 ) { ... }
if( a == b ) { ... }
// GOOD - Absolute tolerance
real64 const absTol = 1.0e-12;
if( LvArray::math::abs(value) < absTol ) { ... }
// GOOD - Relative tolerance
real64 const relTol = 1.0e-8;
real64 const scale = LvArray::math::max( LvArray::math::abs(a), LvArray::math::abs(b) );
if( LvArray::math::abs(a - b) < relTol * scale ) { ... }
Division Safety, NaN/Inf Values
Always verify that denominators are not zero or near-zero before a division.
In General, we should not make any computation that could result in NaN/Inf values.
Example: Division Safety
// WRONG - unprotected division
real64 const normalizedResidual = residual / initialResidual;
real64 const strainRate = velocityGradient / thickness;
// CORRECT - protected division
real64 computeNormalizedResidual( real64 const residual,
real64 const initialResidual )
{
if( initialResidual > machinePrecision )
return residual / initialResidual;
else
return residual; // or return a flag indicating special case
}
// CORRECT - with error reporting
real64 safeDivide( real64 const numerator,
real64 const denominator,
string const & context )
{
GEOS_ERROR_IF( LvArray::math::abs( denominator ) < machinePrecision,
GEOS_FMT( "Division by zero in {}: denominator = {:.2e}",
context, denominator ) );
return numerator / denominator;
}
Overflow Prevention
Overflow leads to undefined behavior and memory corruption. Validate that operations won’t exceed type / container limits, especially for index calculations.
Performance
Speed Optimization Rules
Hoist Loop Invariants, move computations that don’t change during iterations outside the loop.
When it does not critically affect the code architecture and clarity, fuse multiple related kernels to reduce memory traffic and launch overhead (i.e., statistics kernels process all physics field at once).
Optimize Memory Access for Cache and Coalescing. Access memory sequentially and ensure coalesced access, especially on GPUs.
Minimize Host-Device Transfers. Keep data on the appropriate memory space and minimize transfers.
General Architecture
Avoid Coupling
Minimize coupling between components when possible without compromising memory efficiency or execution speed.
Principles:
Loose coupling: Components should depend on interfaces, not concrete implementations,
No circular dependencies: Consider the existing GEOS dependencies to not make components co-dependent (headers inclusion, packages referencing in
CMakeLists.txt, avoid tightly coupled objects),Dependency injection: Public components should receive their dependencies from external sources. Pass required dependencies using intermediate types instead of direct implementation types, relying on lambda, templates and minimal interfaces (loose coupling, testability),
Performance exceptions: Tight coupling is acceptable when required for performance,
Minimize header inclusions and dependencies.
Example: Reducing coupling
// BAD - Tight coupling to concrete class / implementation
class SolverA
{
void registerSomeMeshData( VTKMesh & mesh );
void solveHypre( HypreInterface * solver, HypreMatrix * matrix );
};
// GOOD - Depends on interface
class SolverB
{
void registerSomeMeshData( MeshLevel & mesh ); // More general interface
void solve( LAInterface * solver, MatrixBase * matrix );
};
// Performance-critical tight coupling
template< typename FIELD_T >
class Kernel
{
// Direct access to specific data layout for performance
void compute( FIELD_T const & data );
};
template<> Kernel::compute( arrayView1d< double > & field )
{ /* template specialization */ }
template<> Kernel::compute( arrayView2d< double > & field );
{ /* template specialization */ }
Avoid Globals and Mutable State
Minimize mutable global states when possible. Prefer passing context explicitly.
Why to avoid globals?
Thread safety: Globals can cause race conditions in parallel code
Testability: Hard to test code that depends on global state
Predictability: Global state makes code behaviour harder to understand
Encapsulation: Violates modularity principles
Example: Avoiding global state
// BAD - Global mutable state
static real64 g_tolerance = 1.0e-6;
void solve()
{
if( residual < g_tolerance ) { ... } // Depends on global
}
// GOOD - Pass as parameter
void solve( real64 tolerance )
{
if( residual < tolerance ) { ... }
}
// GOOD - Member variable
class Solver
{
real64 const m_tolerance;
public:
Solver( real64 tolerance ) : m_tolerance( 1.0e-6 ) {}
void solve()
{
if( residual < m_tolerance ) { ... }
}
};
Acceptable Global Usage:
Library wrappers (MPI wrapper, system-level resources, dependancies interface)
Read-only configuration (immutable after initialization)
Performance counters (for profiling)
Magic values
Avoid magic values:
arbitrary values should not be written more than once, define constants, consider using or extending
PhysicsConstants.hpp/Units.hpp,Prefer to let appear the calculus of constants rather than writing its value directly without explaination (constexpr has no runtime cost).