Weezyg
Weezyg

Reputation: 25

How to create an object creation function that will be called by the name associated with it?

I have a class inheritance hierarchy: Figure -> Circle, Point, Line, Rectangle.

I need to create a function to create a Circle graphical object from a given hierarchy, by the name associated with it. The function should return unique_ptr to the object. The parameters of the function are the name of the object and its characteristics (x, y, radius). When adding new classes to the hierarchy, the function should not be changed.

Tell me, how can I implement this function? I do not understand

For example ?? :

unique_ptr<Figure> CreateFigure(const std::string& name) {
     if (name == "circle")
        return make_unique<Circle>();
     if (name == "line")
        return make_unique<Line>()

Upvotes: 2

Views: 539

Answers (3)

Ted Lyngmo
Ted Lyngmo

Reputation: 118017

Since C++ (up to at least C++20) doesn't have reflection, you could create a std::unordered_map with the names of the Figures as keys that maps to functions that creates the actual objects.

The parameters of the function are the name of the object and its characteristics. When adding new classes to the hierarchy, the function should not be changed.

I interpret this as the parameters needed to define each Figure are only known at run-time, supplied by the user or read from a file, so I put the creation in a Creator class that creates objects by reading the names and parameter values from a stream. See the create_from_stream function below.

It can read from a file or any other istream supplying the correct input. Example:

Circle 10 15 5
Rectangle 5 5 640 400

When adding a new class you only need to put it in the unordered_map (named fnmap below) to make it available for runtime creation.

Here's an outline for C++11:

#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>

// An abstract base class defining the interface for all derived classes
struct Figure {
    virtual ~Figure() = default;
    virtual const std::string& heading() const = 0;

    // read the parameters from an istream
    virtual std::istream& read_params(std::istream&) = 0;
    virtual void paint() const = 0;
};

// a proxy for the derived class' read_params function
std::istream& operator>>(std::istream& is, Figure& f) {
    return f.read_params(is);
}

struct Circle : public Figure {
    const std::string& heading() const override {
        static const std::string head = "<x> <y> <radius>";
        return head;
    }
    std::istream& read_params(std::istream& is) override {
        return is >> x >> y >> radius;
    }
    void paint() const override {
        std::cout << "circle {" << x << ',' << y << ',' << radius << "}\n";
    }
    int x, y, radius;
};

struct Rectangle : public Figure {
    const std::string& heading() const override {
        static const std::string head = "<x> <y> <width> <height>";
        return head;
    }
    std::istream& read_params(std::istream& is) override {
        return is >> x >> y >> w >> h;
    }
    void paint() const override {
        std::cout << "Rectangle {" << x << ',' << y << ',' << w << ',' << h << "}\n";
    }
    int x, y, w, h;
};

class Creator {
public:
    static void menu() {
        static const std::vector<std::string> options = makeopts();
        std::cout << "Figures and their parameters:\n";
        for(auto& s : options) std::cout << s << '\n';
    }

    // A function that uses a map of Figure names mapped to lambdas creating
    // objects, reading the names and parameters from a stream.
    static std::unique_ptr<Figure> create_from_stream(std::istream& is) {
        std::string figname;
        if(is >> figname) {
            try {
                // lookup the creation function and call it
                // throws out_of_range if the Figure isn't found.
                auto fig = fnmap.at(figname)();

                // dereference the unique_ptr and use the operator>> overload
                // to read parameters
                if(is >> *fig) return fig;
                // failed to read parameters
                is.clear();
                is.ignore(); // skip one char or the rest of the line:
                // is.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
                throw std::runtime_error("erroneous parameters for " + figname);
            } catch(const std::out_of_range&) {
                throw std::runtime_error("don't know how to create a " + figname);
            }
        }
        return nullptr; // failed to read Figure name
    }

private:
    // a function to create menu options
    static std::vector<std::string> makeopts() {
        std::vector<std::string> rv;
        rv.reserve(fnmap.size());
        for(const auto& p : fnmap) {
            rv.emplace_back(p.first + ' ' + p.second()->heading());
        }
        return rv;
    }

    static const std::unordered_map<std::string,
                                    std::function<std::unique_ptr<Figure>()>>
        fnmap;
};

const std::unordered_map<std::string, std::function<std::unique_ptr<Figure>()>>
    Creator::fnmap{
        {"Circle", [] { return std::unique_ptr<Circle>(new Circle); }},
        {"Rectangle", [] { return std::unique_ptr<Rectangle>(new Rectangle); }}
    };

int main() {
    // let the user create Figures
    while(true) {
        try {
            Creator::menu();
            std::cout << "\nEnter name and parameters of a Figure to create: ";
            auto fig = Creator::create_from_stream(std::cin);
            if(!fig) break; // probably EOF, abort
            std::cout << "Painting: ";
            fig->paint();
            std::cout << '\n';
        } catch(const std::runtime_error& ex) {
            std::cerr << "Error: " << ex.what() << std::endl;
        }
    }
    std::cout << "Bye bye\n";
}

Upvotes: 1

StefanKssmr
StefanKssmr

Reputation: 1226

Based on the suggestions in the comments you can have a create function with variadic template arguments. In the code below, if the given arguments fit with constructor of the required class, it is constructed, otherwise an empty std::unique_ptr is returned. A drawback of this solution is that you have to update the create class every time you add a new class.

In order to avoid that, there exist approaches like self registering classes, but they have other drawbacks. For instance it is rather difficult to used different constructors, or you may run into problems when you have multiple compilation units.This article may be helpful.

Here is a possible 'solution' (in quotes since it does not solve your original problem):

#include <iostream>
#include <memory>
#include <string>
#include <type_traits>

namespace detail
{

template <class Type, class ... Args>
inline
std::enable_if_t<std::is_constructible<Type,Args...>::value, Type*>
make_new_if_constructible_impl (Args&&... args)
{
    return new Type (std::forward<Args>(args)...);
}

template <class Type, class ... Args>
inline
std::enable_if_t<!std::is_constructible<Type,Args...>::value, Type*>
make_new_if_constructible_impl (Args&&...)
{
    return nullptr;
}
} // namespace detail

template <class Type, class ... Args>
inline
Type*
make_new_if_constructible (Args&&...args)
{
    return detail::make_new_if_constructible_impl<Type>(std::forward<Args>(args)...);
}

struct Figure
{
};

struct Circle : Figure
{
    Circle (double r) {std::cout << "created circle with radius " << r << std::endl;};
};

struct Rectangle : Figure
{
    Rectangle (double h, double w) {std::cout << "created rectangle " << h << 'x' << w << std::endl;};
};

template <class ...Args>
std::unique_ptr<Figure> create(const std::string name, Args&&... args)
{
    if ("Circle" == name)
        return std::unique_ptr<Figure>(make_new_if_constructible<Circle>(std::forward<Args>(args)...));
    if ("Rectangle" == name)
        return std::unique_ptr<Figure>(make_new_if_constructible<Rectangle>(std::forward<Args>(args)...));
    else
        return std::unique_ptr<Figure>(nullptr);
}

int main()
{
    auto circle = create("Circle",10);
    std::cout << std::boolalpha << !!circle <<std::endl;
    auto rectangle = create("Rectangle",5,10);
    std::cout << std::boolalpha << !!rectangle <<std::endl;
    auto nocircle = create("Circle",5,10); 
    std::cout << std::boolalpha << !!nocircle <<std::endl;
}

Here is the console output:

created circle with radius 10
true
created rectangle 5x10
true
false

As you can see, no Circle is created by the last create call since no matching constructor is found. On the other hand, the first two create calls were successful.

Here's a live demo.

UPDATE std::enable_if_t is c++14, not c++11 as tagged in the question. If someone wants this to work with c++11, use typename std::enable_if<...>::type instead.

Upvotes: 1

A M
A M

Reputation: 15265

The standard approach to solve your problem is to use an Abstract Factory design pattern.

Based on a "key". like a name (for example "Circle") or an id, like the integer "3", the required class will be created.

So, factory has always a "create"-method, and a container stores all "create"-methods. To store all methods, we often use a std::map.

The problem is always, that the constructors used in class hierachies, may have different number of parameters. That is unfortunately not so easy to implement, because the factory "wants" to store functions with the same signature. But this can of course be solved with variadic templates.

See the solution below:

#include <iostream>
#include <map>
#include <utility>
#include <any>


// Some demo classes ----------------------------------------------------------------------------------
struct Base {
    Base(int d) : data(d) {};
    virtual ~Base() { std::cout << "Destructor Base\n"; }
    virtual void print() { std::cout << "Print Base\n"; }
    int data{};
};
struct Child1 : public Base {
    Child1(int d, std::string s) : Base(d) { std::cout << "Constructor Child1 " << d << " " << s << "\n"; }
    virtual ~Child1() { std::cout << "Destructor Child1\n"; }
    virtual void print() { std::cout << "Print Child1: " << data << "\n"; }
};
struct Child2 : public Base {
    Child2(int d, char c, long l) : Base(d) { std::cout << "Constructor Child2 " << d << " " << c << " " << l << "\n"; }
    virtual ~Child2() { std::cout << "Destructor Child2\n"; }
    virtual void print() { std::cout << "Print Child2: " << data << "\n"; }
};
struct Child3 : public Base {
    Child3(int d, long l, char c, std::string s) : Base(d) { std::cout << "Constructor Child3 " << d << " " << l << " " << c << " " << s << "\n"; }
    virtual ~Child3() { std::cout << "Destructor Child3\n"; }
    virtual void print() { std::cout << "Print Child3: " << data << "\n"; }
};



using UPTRB = std::unique_ptr<Base>;


template <class Child, typename ...Args>
UPTRB createClass(Args...args) { return std::make_unique<Child>(args...); }

// The Factory ----------------------------------------------------------------------------------------
template <class Key, class Object>
class Factory
{
    std::map<Key, std::any> selector;
public:
    Factory() : selector() {}
    Factory(std::initializer_list<std::pair<const Key, std::any>> il) : selector(il) {}

    template<typename Function>
    void add(Key key, Function&& someFunction) { selector[key] = std::any(someFunction); };

    template <typename ... Args>
    Object create(Key key, Args ... args) {
        if (selector.find(key) != selector.end()) {
            return std::any_cast<std::add_pointer_t<Object(Args ...)>>(selector[key])(args...);
        }
        else return nullptr;
    }
};

int main()
{
    Factory<int, UPTRB> factory{
        {1, createClass<Child1, int, std::string>},
        {2, createClass<Child2, int, char, long>}
    };
    factory.add(3, createClass<Child3, int, long, char, std::string>);


    // Some test values
    std::string s1(" Hello1 "); std::string s3(" Hello3 ");
    int i = 1;  const int ci = 1;   int& ri = i;    const int& cri = i;   int&& rri = 1;

    UPTRB b1 = factory.create(1, 1, s1);
    UPTRB b2 = factory.create(2, 2, '2', 2L);
    UPTRB b3 = factory.create(3, 3, 3L, '3', s3);

    b1->print();
    b2->print();
    b3->print();
    b1 = factory.create(2, 4, '4', 4L);
    b1->print();
    return 0;
}

Here the general creation function is:

template <class Child, typename ...Args>
UPTRB createClass(Args...args) { return std::make_unique<Child>(args...); }

Then there is the factory, that stores all the creation functions.

Upvotes: 2

Related Questions