Runge-Kutta schemes consist of an algorithm, implemented in Scheme
, and a piece of data, called Table
(Butcher tableau).
For the construction of a scheme, we want consumers to use the syntax
Scheme s = Factory::makeSchemeXY();
where XY identifies the particular table used in scheme. (and there shall be no other way to construct schemes).
The implementation is achieved below with an expression template design pattern, in that effectively the table is an expression template.
Since the table can be huge, the example below uses a shared_ptr so as to avoid that unnecessary copies of the table are created and destroyed.
Question
What we really want is for a table to be constructed in a factory function, and then stored as an attribute of scheme. How can we do this without the use of dynamic allocation?
The question is relevant to us because we foresee some embedded targets where we won't be able to use dynamic allocation. Also, we hope it to be unnecessary because after all we just want the table as one attribute to be constructed and live.
#include<iostream>
#include<memory>
class Table;
class Factory;
class Scheme;
class Table{
friend class Factory;
friend class Scheme;
double A=0;
Table(){}
public:
~Table(){std::cout<<"~Table\n";} // only one table is used, hence avoid instantiation of copy
};
class Scheme{
std::shared_ptr<Table> pt;
friend class Factory;
Scheme()=delete;
void operator=(Scheme const&)=delete;
public:
Scheme(std::shared_ptr<Table> pt):pt(pt){}
};
struct Factory{
Factory()=delete;
static std::shared_ptr<Table> makeSchemeXY(){
std::shared_ptr<Table> p( new Table ); // heap allocation won't work on some platforms
p->A = 42; // the table should be constructed here, not in the scheme. That would become too messy.
return p; // can't we use std::move, or elision, or something here?
}
};
struct App{
Scheme x = Factory::makeScheme(); // <--- highly preferred consumer syntax
};
int main(){
App a;
}
What we tried
We tried elision, in that
makeScheme
returns aTable
andScheme
has an attribute of typeTable
. However, in the example above, this triggers two calls of~Table
, meaning an unnecessary copy was created.We tried
std::move(t)
in both the return clause ofmakeScheme
and the constructor list ofScheme()
. Still, The mere construction oft
in bothTable
andScheme
triggered two instances ofTable
to live.
Below is a code with un/commented variations for elision and move.
The current un/commenting is compilable but triggers to instantiations of Table
.
#include<iostream>
class Table;
class Factory;
class Scheme;
class Table{
friend class Factory;
friend class Scheme;
double A=0;
Table(){}
public:
~Table(){std::cout<<"~Table\n";}
};
class Scheme{
friend class Factory;
Scheme()=delete;
void operator=(Scheme const&)=delete;
//
Table t;
public:
Scheme(Table t):t(t){} // hope for elision
//Scheme(Table&& t):t(std::move(t)){} // using move
};
struct Factory{
Factory()=delete;
static Table makeScheme(){ Table t; t.A=2; return t; } // hope for elision
//static Table makeScheme(){ Table t; t.A=2; return std::move(t); }
//static Table&& makeScheme(){ Table t; t.A=2; return std::move(t); } // pardon my futile attempt
};
struct App{
Scheme x = Factory::makeScheme();
};
int main(){
App a;
}
Remark: While Table
should be const
, the employed BLAS interface does not support const types (hence we need Table non-const in Scheme, despite it won't change).
If you are willing to deviate from your calling syntax slighlty, you could pass the factory function for
Table
to the constructor ofScheme
. Then this constructor could instantiate the table attribute in place without any copies or moves. Your consumer would callinstead of
Scheme s = Factory::makeSchemeXY()
, which may look surprising and thus may violate the principle of least surprise.Output
Live Code