The rainflow library provides a modern C++ wrapper (rainflow.hpp) that
encapsulates the C implementation in a type-safe, object-oriented interface.
This wrapper uses templates, RAII patterns, and STL containers to provide
a more idiomatic C++ experience while maintaining full access to the
high-performance C core.
The library uses a two-layer architecture:
- Layer 1: C Core (
rainflow.c/rainflow.h) - Pure C99 implementation
- Manual memory management
- Function-based API
- Maximum portability
- Layer 2: C++ Wrapper (
rainflow.hpp) - Template classes
- Automatic resource management (RAII)
- STL container integration
- Exception-safe operations
The C++ wrapper is header-only and adds minimal overhead - it's a thin wrapper that delegates to the C functions.
// C types live in the C namespace (aliased as RF)
namespace RF = rainflow_C; // RFC_CPP_NAMESPACE
// The C++ wrapper class (global scope)
template< class T = std::vector<RF::rfc_value_tuple_s> >
class RainflowT
{
// Nested type aliases
typedef RF::rfc_value_t rfc_value_t;
typedef RF::rfc_counts_t rfc_counts_t;
typedef std::vector<rfc_value_t> rfc_value_v;
typedef std::vector<rfc_counts_t> rfc_counts_v;
typedef T rfc_tp_storage;
// ...
};
// Default instantiation
typedef RainflowT<> Rainflow;The nested types are accessed through the class, e.g. Rainflow::rfc_counts_v,
Rainflow::RFC_RES_REPEATED. There is no namespace Rainflow; Rainflow
is a typedef. RF aliases the C namespace rainflow_C, not the C++ class.
#include "rainflow.hpp"
#include <iostream>
int main()
{
// Create rainflow counter
Rainflow::Rainflow rf;
// Initialize: 100 classes, width 1.0, offset 0.0, hysteresis 1.0
if (!rf.init(100, 1.0, 0.0, 1.0))
{
std::cerr << "Initialization failed" << std::endl;
return 1;
}
// Configure Wöhler curve
if (!rf.wl_init_modified(1000.0, 1e7, 5.0, 5.0))
{
std::cerr << "Wöhler initialization failed" << std::endl;
return 1;
}
// Prepare data
std::vector<double> data = {0.0, 10.0, 0.0, 20.0, 0.0, 30.0, 0.0};
// Feed data
if (!rf.feed(data.data(), data.size()))
{
std::cerr << "Counting failed" << std::endl;
return 1;
}
// Finalize
if (!rf.finalize(Rainflow::RFC_RES_REPEATED))
{
std::cerr << "Finalization failed" << std::endl;
return 1;
}
// Get damage
double damage;
if (rf.damage(&damage))
{
std::cout << "Total damage: " << damage << std::endl;
}
// Automatic cleanup (RAII)
return 0;
}The C++ wrapper uses RAII (Resource Acquisition Is Initialization) to automatically manage resources:
{
Rainflow::Rainflow rf;
rf.init(100, 1.0);
// Use rf...
} // Automatic deinit() called by destructorNo need to call deinit() manually - the destructor handles cleanup.
The Rainflow class is non-copyable but movable:
Rainflow::Rainflow rf1;
rf1.init(100, 1.0);
// Error: Cannot copy
// Rainflow::Rainflow rf2 = rf1; // Compile error
// OK: Can move
Rainflow::Rainflow rf2 = std::move(rf1); // rf1 is now invalid
// OK: Can return by value (move)
auto create_rf() -> Rainflow::Rainflow
{
Rainflow::Rainflow rf;
rf.init(100, 1.0);
return rf; // Move construction
}This prevents accidental resource duplication while allowing efficient transfer of ownership.
The Rainflow class is templated on the turning point storage type:
template<typename TStorage = rfc_tp_storage>
class Rainflow { ... };Default Storage: std::vector<rfc_value_tuple_s>
You can provide custom storage if needed:
// Custom allocator for vector
using CustomAlloc = std::allocator<Rainflow::rfc_value_tuple_s>;
using CustomStorage = std::vector<Rainflow::rfc_value_tuple_s, CustomAlloc>;
Rainflow::Rainflow<CustomStorage> rf;Requirements for TStorage:
- Must support
push_back(rfc_value_tuple_s) - Must support
operator[](size_t) - Must support
size()andclear() - Must support range-based iteration
The C++ wrapper provides methods that work directly with STL containers:
Rainflow::Rainflow rf;
rf.init(100, 1.0);
// Feed from vector
std::vector<double> data = {...};
rf.feed(data.data(), data.size());
// Get range pairs into vectors
Rainflow::rfc_counts_v counts;
Rainflow::rfc_value_v amplitudes;
rf.rp_get(counts, amplitudes);
// Iterate over results
for (size_t i = 0; i < counts.size(); i++)
{
if (counts[i] > 0)
{
std::cout << "Range: " << amplitudes[i] * 2
<< ", Count: " << counts[i] << std::endl;
}
}
// Access turning points (stored in std::vector)
const auto& tp_storage = rf.tp_storage();
for (const auto& tp : tp_storage)
{
std::cout << "TP: pos=" << tp.pos
<< ", value=" << tp.value
<< ", damage=" << tp.damage << std::endl;
}The wrapper provides direct access to the turning point container:
Rainflow::Rainflow rf;
rf.init(100, 1.0);
// Feed data
std::vector<double> data = {...};
rf.feed(data.data(), data.size());
// Access turning points
const Rainflow::rfc_tp_storage& tps = rf.tp_storage();
std::cout << "Number of turning points: " << tps.size() << std::endl;
// Iterate
for (const auto& tp : tps)
{
std::cout << "Position: " << tp.pos << std::endl;
std::cout << "Value: " << tp.value << std::endl;
std::cout << "Damage: " << tp.damage << std::endl;
std::cout << "Adjacent position: " << tp.adj_pos << std::endl;
}Most methods return bool to indicate success/failure:
Rainflow::Rainflow rf;
if (!rf.init(100, 1.0))
{
// Check error code
int error = rf.error_get();
switch (error)
{
case Rainflow::RFC_ERROR_MEMORY:
std::cerr << "Out of memory" << std::endl;
break;
case Rainflow::RFC_ERROR_INVARG:
std::cerr << "Invalid arguments" << std::endl;
break;
default:
std::cerr << "Error: " << error << std::endl;
}
return 1;
}Available error codes (from C layer):
enum rfc_error_e
{
RFC_ERROR_UNEXP, // Unexpected error
RFC_ERROR_NOERROR = 0, // No error
RFC_ERROR_INVARG, // Invalid arguments
RFC_ERROR_UNSUPPORTED, // Feature not supported
RFC_ERROR_MEMORY, // Memory allocation failed
RFC_ERROR_TP, // Turning point processing error
RFC_ERROR_AT, // Amplitude transformation error
RFC_ERROR_DH_BAD_STREAM, // Bad damage history stream
RFC_ERROR_DH, // Damage history error
RFC_ERROR_LUT, // Lookup table error
RFC_ERROR_DATA_OUT_OF_RANGE, // Data exceeds class range
RFC_ERROR_DATA_INCONSISTENT, // Processed data is inconsistent (internal error)
};You can access the underlying C context for advanced operations:
Rainflow::Rainflow rf;
rf.init(100, 1.0);
// Get reference to C context
rfc_ctx_s& ctx = rf.ctx_get();
// Modify C context directly
ctx.counting_method = RF::RFC_COUNTING_METHOD_ASTM;
ctx.internal.flags |= RF::RFC_FLAGS_COUNT_LC;
// Use C functions directly if needed
RFC_feed(&ctx, data, data_len);This provides full access to the C API when needed.
const Rainflow::Rainflow& rf_const = rf;
// Get const reference
const rfc_ctx_s& ctx = rf_const.ctx_get();
// Read-only access
int class_count = ctx.class_count;Rainflow::Rainflow rf;
rf.init(100, 1.0);
rf.wl_init_modified(1000.0, 1e7, 5.0, 5.0);
// Feed data and count
rf.feed(data.data(), data_len);
rf.finalize(Rainflow::RFC_RES_REPEATED);
// Get total damage
double damage;
rf.damage(&damage);
std::cout << "Total damage: " << damage << std::endl;
// Calculate damage from range pairs
Rainflow::rfc_counts_v counts;
Rainflow::rfc_value_v amplitudes;
rf.rp_get(counts, amplitudes);
double damage_from_rp;
rf.damage_from_rp(damage_from_rp, counts, amplitudes,
Rainflow::RFC_RP_DAMAGE_CALC_METHOD_DEFAULT);
std::cout << "Damage from RP: " << damage_from_rp << std::endl;Rainflow::Rainflow rf;
rf.init(100, 1.0);
rf.wl_init_modified(1000.0, 1e7, 5.0, 5.0);
// Enable damage history
rf.dh_init(Rainflow::RFC_SD_TRANSIENT_23c, nullptr, data_len, false);
// Feed data
rf.feed(data.data(), data_len);
rf.finalize(Rainflow::RFC_RES_REPEATED);
// Get damage history
const double* dh_data;
size_t dh_count;
rf.dh_get(&dh_data, &dh_count);
// Copy to vector for easier handling
std::vector<double> damage_history(dh_data, dh_data + dh_count);
// Plot or analyze damage over time
for (size_t i = 0; i < damage_history.size(); i++)
{
std::cout << "Sample " << i << ": " << damage_history[i] << std::endl;
}Rainflow::Rainflow rf;
rf.init(100, 1.0);
rf.feed(data.data(), data_len);
rf.finalize(Rainflow::RFC_RES_REPEATED);
// Get rainflow matrix
Rainflow::rfc_rfm_item_v rfm;
rf.rfm_get(rfm);
// Create 2D matrix
unsigned class_count;
rf.class_count(&class_count);
std::vector<std::vector<double>> matrix(
class_count,
std::vector<double>(class_count, 0.0)
);
for (const auto& item : rfm)
{
matrix[item.from][item.to] = item.counts / RF::RFC_FULL_CYCLE_INCREMENT;
}
// Access matrix elements
for (unsigned i = 0; i < class_count; i++)
{
for (unsigned j = 0; j < class_count; j++)
{
if (matrix[i][j] > 0)
{
std::cout << "(" << i << "," << j << "): "
<< matrix[i][j] << std::endl;
}
}
}You can derive from the Rainflow class to add custom functionality:
class MyRainflow : public Rainflow::Rainflow<>
{
public:
// Add custom methods
void print_summary() const
{
double dmg;
if (const_cast<MyRainflow*>(this)->damage(&dmg))
{
std::cout << "Damage: " << dmg << std::endl;
}
const auto& tps = tp_storage();
std::cout << "Turning points: " << tps.size() << std::endl;
}
// Custom initialization
bool init_for_steel()
{
if (!init(100, 10.0, 0.0, 10.0))
return false;
// Steel S-N curve parameters
return wl_init_modified(500.0, 1e6, 5.0, 7.0);
}
};
// Usage
MyRainflow rf;
rf.init_for_steel();
rf.feed(data.data(), data_len);
rf.finalize(Rainflow::RFC_RES_REPEATED);
rf.print_summary();| Feature | C API | C++ Wrapper |
|---|---|---|
| Memory Management | Manual | Automatic (RAII) |
| Resource Cleanup | RFC_deinit() | Destructor |
| Error Handling | Return codes | Return codes |
| Type Safety | Limited | Strong typing |
| Container Support | Arrays/pointers | STL vectors |
| Namespacing | Prefix (RFC_) | Namespace |
| Object Orientation | No | Yes |
| Performance | Baseline | Same (zero overhead) |
C Code:
rfc_ctx_s ctx;
double* data = malloc(1000 * sizeof(double));
if (!RFC_init(&ctx, 100, 1.0, 0.0, 1.0, RFC_FLAGS_DEFAULT))
{
free(data);
return 1;
}
if (!RFC_feed(&ctx, data, 1000))
{
RFC_deinit(&ctx);
free(data);
return 1;
}
double damage;
RFC_damage(&ctx, &damage);
RFC_deinit(&ctx);
free(data);C++ Code:
Rainflow::Rainflow rf;
std::vector<double> data(1000);
if (!rf.init(100, 1.0, 0.0, 1.0))
return 1;
if (!rf.feed(data.data(), data.size()))
return 1;
double damage;
rf.damage(&damage);
// Automatic cleanupUse RAII: Let the destructor handle cleanup
// Good { Rainflow::Rainflow rf; // ... } // Automatic cleanup // Avoid manual deinit unless necessary rf.deinit(); // Usually not needed
Check Return Values: Always verify operations succeeded
if (!rf.init(100, 1.0)) { std::cerr << "Error: " << rf.error_get() << std::endl; return 1; }
Use STL Containers: Leverage vector integration
std::vector<double> data = load_measurement(); rf.feed(data.data(), data.size());Const Correctness: Use const references where appropriate
const Rainflow::rfc_tp_storage& tps = rf.tp_storage();Move Semantics: Transfer ownership efficiently
auto create() -> Rainflow::Rainflow { Rainflow::Rainflow rf; rf.init(100, 1.0); return rf; // Move, not copy }
The C++ wrapper follows the "zero-overhead" principle:
- No virtual functions (unless you add them)
- Inline functions for wrapper methods
- Direct delegation to C functions
- Same memory layout as C struct
Use C API When:
- Interfacing with C-only codebases
- Maximum portability required
- Embedded systems with no C++ support
Use C++ Wrapper When:
- Modern C++ codebase (C++11 or later)
- Want RAII and automatic cleanup
- Using STL containers
- Prefer object-oriented design
- features.rst - Complete feature list
- examples.rst - More code examples
- delegates.rst - Custom behavior via delegates
- minimal_build.rst - Minimal C build options