Reputation: 25
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
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 Figure
s 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
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
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