Some Thoughts About Modern C++ in Scientific Programming
Perfect forwarding (Modern C++)
In the refactoring project I’m currently working, I prototype many implementations and lately I’ve been testing the use of perfect forwarding (Modern C++ feature) in our scientific programming environment. The reason is that some of the legacy code I started from used functions with different signatures.
As the name perfect forwarding suggests, it forwards to the right function based on the type or value categories (argument). Like overload in Object-Oriented (using virtual keyword) but here uses forward reference.
Universal or Forward Reference
To understand what a forward reference is I start with template function declaration (function argument is a template parameter)
template<typename T>
void myFunc(T&& aType) {...}
‘T&&’ is the forward reference declaration (Universal reference).
Like rvalue reference but the behavior is different. It uses Automatic Template Argument Type Deduction to deduce the template parameter. Since C++17 committee has adopted “forward reference” as standard name, another name is also used and its Universal reference. The latter express what it does, rvalue reference bind only to rvalue, but Universal(forward) reference binds to anything, the name universal expresses that fact.
A particular property of the forward reference they preserve the lvalue/rvalue-ness and const-ness of the argument that is passed to functions. This means in the case of lvalue is passed as an argument then the template parameter is deduced to be a lvalue reference and if rvalue is passed as argument an unadorned plain type is deduced. It’s important to note that’s the only case where a template parameter is deduced as lvalue reference. This allows a single function template to accept both lvalue and rvalue parameters. When calling a function that supports different signature it will forward the call according to the overload resolution function.
I have already mentioned at the beginning, our legacy code support functions with different signatures. A possible implementation is to wrap all functions called (different signatures) in one function and forward arguments, that’s the main idea behind perfect forwarding.
I use variadic template to perfect forward all arguments to the right functions. Below the function that forwards to the right function according to the arguments value categories. I’m using the new C++20 features functions template with keyword auto.
/** C++20 template function with universal/forward reference*/
void fwdSrcAlgo(auto&&... aFwdAlgo)
{
// done at compile time!!!
if constexpr (sizeof...(aFwdAlgo) > 0)
{
// calling corresponding function according
// to value categorie of each argument
TreatmentSrcTerms(std::forward<decltype(aFwdAlgo)>(aFwdAlgo)...);
}
}
Below is an example to show how it works. The first function takes arguments as lvalue reference. The second one takes some arguments with forward reference.
template<typename StdVec>
void TreatmentSrcTerms( StdVec& aS, const StdVec& Q, const StdVec &A,
const StdVec &n, double dx, unsigned aNbSections)
{
std::cout << "Testing perfect forwarding lvalue\n";
...
// compute derivative by central second-order scheme (Taylor expansion)
FiniteDifferenceDerivative w_dxH2nd{ std::string{ "H-Field" }, false };
w_dxH2nd.setDerivativeOrder(2); // 2nd order
auto w_dxHvarray = w_dxH2nd.d1x(w_Hfield); // derivative of a scalar field
...
}
/** State variables ... possible implementation*/
template<typename StdVec>
void TreatmentSrcTerms( StdVec& aS, StdVec &&Q, StdVec &&A, const StdVec &n,
double dx, unsigned aNbSections)
{
static_assert( std::size(Q) == aNbSections);
// check if it is a range (C++20 concept)
if constexpr (std::ranges::range<decltype(Q)>)
{
// if push_back is supported can i do this?
if ( std::is_same_v<StdVec::value_type, double>)
{
// Can freely modify the original (forward reference)
Q.push_back(1.3); //b.c. condition at right far end
}
}
std::cout << "Testing perfect forwarding rvalue\n";
}
Below the main function that do all the work.
// one-dimensional scalar field
scalarField1D w_scal1DA{grid1Dptr,string{"Scalar field A-var"}, w_initValues};
scalarField1D w_scal1DQ{grid1Dptr,string{"Scalar field Q-var"}, // set to zero
std::vector<double>(w_scal1DA.values().size())};
// move semantic version (perfect forwarding)
// explicit operator conversion from scalar field (signature based on std vector)
fwdSrcAlgo(w_vecByRef, static_cast<stdvec>(w_scal1DQ), // explicit operator (prvalue)
static_cast<stdvec>(w_scal1DA), // ditto
stdvec(EMCNEILNbSections::value),// temporary initialization
dx, w_scal1DA.values().size()); // value case
“fwdSrcAlgo” is wrapper function that call or forward to physics algo “TreatmentSrcTerms”. Algo function will be called according to their value categories.
Some explanation
Use of explicit operator conversion (cast field as vector) for the 2nd and 3rd args, which return a vector (prvalue: pure reading value). The fourth argument is const reference (lvalue) but we are passing a temporary (prvalue). Reminder: forward reference bind to anything. In the wrapper function we use std::forward to deduce the 2nd and 3rd args as rvalue reference and 4th arg (lvalue reference) can bind to this argument keeping its value-ness and const-ness. It forwards to 2nd function.
Leave a Reply
Want to join the discussion?Feel free to contribute!