How to pass big data from a factory to a constructor with neither dynamic memory nor unnecessary copies?

133 views Asked by At

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

  1. 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.

  2. 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).

2

There are 2 answers

2
joergbrech On BEST ANSWER

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

Live Code

2
GRamon On

I was able to refine joergbrech's solution to one that circumvents the principle of surprise:

#include<iostream>
#include<functional>

class Table;
class Factory;
class Scheme;

class Table{
    friend class Factory;
    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( std::function<Table(void)> f ):t(f()) {}
};

struct Factory{
    Factory()=delete;
    //
    static std::function<Table(void)> makeScheme(){ return Factory::createScheme; }
    //
    static Table createScheme(){ Table t; t.A=2; return t; }
};

struct App{
    Scheme x = Factory::makeScheme();
};

int main(){
    App a;
}

To further improve, one would wrap std::function<Table(void)> into a class that can only be constructed from factory, and make Scheme`s sole constructor demand an instance of said class. This would assert that scheme cannot be constructed any other way.