【问题标题】:How to create an object creation function that will be called by the name associated with it?如何创建将由与其关联的名称调用的对象创建函数?
【发布时间】:2020-05-20 04:42:32
【问题描述】:

我有一个类继承层次结构:图 -> 圆、点、线、矩形。

我需要创建一个函数来从给定的层次结构中创建一个圆形图形对象,并使用与之关联的名称。该函数应将 unique_ptr 返回给对象。该函数的参数是对象的名称及其特征(x、y、半径)。向层次结构中添加新类时,不应更改功能。

告诉我,我该如何实现这个功能?我不明白

例如?? :

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

【问题讨论】:

  • C++ 没有真正的反射,所以虽然有多种包装方式,但在某种程度上你必须有这样的代码if (name == "Circle") return new Circle(...) else if (name == "Rectangle") ...。对于不同的参数编号和类型,您可以使用variadic template,这就是std::make_unique 的工作原理
  • 所以你需要有一个可以调用的函数,比如CreateFigure("Circle", ...);CreateFigure("Line", ...);,分别返回std::unique_ptr&lt;Circle&gt;std::unique_ptr&lt;Line&gt;
  • @john 我更新了问题,示例代码是这样的吗?
  • @Tas 我更新了问题,示例代码是这样的吗?

标签: c++ oop c++11


【解决方案1】:

解决问题的标准方法是使用抽象工厂设计模式。

基于“键”。如名称(例如“Circle”)或 id,如整数“3”,将创建所需的类。

因此,工厂始终有一个“创建”方法,而一个容器存储了所有“创建”方法。为了存储所有方法,我们经常使用std::map

问题始终是,类层次结构中使用的构造函数可能具有不同数量的参数。不幸的是,实现起来并不容易,因为工厂“想要”存储具有相同签名的函数。但这当然可以通过可变参数模板来解决。

请看下面的解决方案:

#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;
}

这里一般的创建函数是:

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

然后是存储所有创建函数的工厂。

【讨论】:

  • 不错的解决方案。不过,您可以在 createClass 中使用 std::forward
  • 请注意,您必须传递确切的类型,传递 const char* 而不是 std::string 将使 std::any_cast 失败。
  • @Jarod42:是的,这是正确的。但是,这是纯演示代码。需要适应现实生活。也可以使用 std::forward 甚至更多。 . .
  • @ArminMontigny,什么可以代替 std :: any ?
【解决方案2】:

根据 cmets 中的建议,您可以使用带有可变模板参数的 create 函数。在下面的代码中,如果给定的参数适合所需类的构造函数,则构造它,否则返回一个空的std::unique_ptr。此解决方案的一个缺点是每次添加新类时都必须更新创建类。

为了避免这种情况,存在诸如自注册类之类的方法,但它们还有其他缺点。例如,使用不同的构造函数相当困难,或者当您有多个编译单元时可能会遇到问题。这个article 可能会有所帮助。

这是一个可能的“解决方案”(用引号括起来,因为它不能解决您原来的问题):

#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;
}

这是控制台输出:

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

如您所见,最后一次create 调用没有创建Circle,因为没有找到匹配的构造函数。另一方面,前两个create 调用是成功的。

这是live demo

UPDATE std::enable_if_t 是 c++14,而不是问题中标记的 c++11。如果有人希望它与 c++11 一起使用,请改用 typename std::enable_if&lt;...&gt;::type

【讨论】:

    【解决方案3】:

    由于 C++(至少 C++20)没有反射,您可以使用 Figures 的名称创建一个 std::unordered_map 作为映射到创建实际对象的函数的键。

    函数的参数是对象的名称及其特征。向层次结构中添加新类时,不应更改功能。

    我将此解释为定义每个 Figure 所需的参数仅在运行时知道,由用户提供或从文件中读取,因此我将创建放在 Creator 类中,该类通过读取来创建对象流中的名称和参数值。请参阅下面的create_from_stream 函数。

    它可以从提供正确输入的文件或任何其他istream 中读取。示例:

    Circle 10 15 5
    Rectangle 5 5 640 400
    

    添加新类时,您只需将其放在unordered_map(下面命名为fnmap)中即可用于运行时创建。

    这是 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";
    }
    

    【讨论】:

      猜你喜欢
      • 2018-10-24
      • 2015-04-03
      • 1970-01-01
      • 1970-01-01
      • 2021-10-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多