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.
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;
}
We tried elision, in that makeScheme
returns a Table
and Scheme
has an attribute of type Table
. 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 of makeScheme
and the constructor list of Scheme()
. Still, The mere construction of t
in both Table
and Scheme
triggered two instances of Table
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 of Scheme
. Then this constructor could instantiate the table attribute in place without any copies or moves. Your consumer would call
Scheme s = Factory::makeSchemeXY
instead of Scheme s = Factory::makeSchemeXY()
, which may look surprising and thus may violate the principle of least surprise.
#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:
template <typename FactoryFunction>
Scheme(FactoryFunction const& f):t(f()) {}
//
// or optionally template-free:
// Scheme( Table(*f)(void) ):t(f()){}
};
struct Factory{
Factory()=delete;
static Table makeScheme(){ Table t; t.A=2; return t; }
};
struct App{
Scheme x = Factory::makeScheme;
};
int main(){
App a;
}
Output
~Table