在C++中定义许多组合对象的最佳方法

Best way to define many composed objects in C++?

本文关键字:对象 最佳 方法 组合 许多 C++ 定义      更新时间:2023-10-16

我是素食主义者,所以假设我们有蔬菜:

class Vegetable {}; // base class for vegetables
class Tomato : public Vegetable {};
class Potato : public Vegetable {};
class Carrot : public Vegetable {};
class Broccoli : public Vegetable {};

假设我们想和他们一起做饭:

class Meal {}; // base class for meals
class Soup : public Meal {
    ...
    Soup(Vegetable *veg1, Vegetable *veg2) : veg1(veg1), veg2(veg2) {};
};
class Salad : public Meal {
    ...
    Salad(Vegetable *veg1, Vegetable *veg2, Vegetable *veg3) : veg1(veg1), veg2(veg2), veg3(veg3) {};
};
class VeggieBurger : public Meal {
    ...
    VeggieBurger(Vegetable *veg) : veg(veg) {};
};

现在我们想在食谱中定义具有不同蔬菜组合的不同膳食:

std::vector<Meal *> cookbook;
cookbook.push_back(new Soup(new Tomato, new Potato));
cookbook.push_back(new Soup(new Potato, new Broccoli));
cookbook.push_back(new Salad(new Tomato, new Carrot, new Broccoli));
cookbook.push_back(new Salad(new Tomato, new Potato, new Tomato));
cookbook.push_back(new Salad(new Broccoli, new Potato, new Carrot));
cookbook.push_back(new VeggieBurger(new Potato));
// many more meals...

因此,我们在堆上创建了许多小对象,这些对象通过构造函数参数组合在一起,并在运行时推送到 std::vector 上。显然,这种设计的缺点是,我们必须自己管理内存,删除膳食析构函数中的蔬菜对象,并在超出范围时删除我们的食谱。

因此,一个可能的设计选择是使用智能指针来减轻对我们的膳食和蔬菜进行内存管理的负担。

但我想知道是否可以在编译时编写食谱,也许使用某种模板魔法?说明书不一定是 std::vector,但我们仍然希望能够迭代它,获取 Meal 对象并在组合餐上调用成员函数。有没有更好的方法?

我发现代码生成对我来说正成为一个非常有用的工具。也许写一个简短的程序,甚至只是一个脚本,然后为你生成样板代码。

当您添加到食谱时,您都会运行程序/脚本,该程序/脚本将为每顿饭生成标题,并为食谱本身生成标头,甚至可能是一些来源。

与这一代人的深入程度取决于你。您可以编辑代码生成器的源代码以添加新的膳食,您可以进行一些简单的基于文本的分析,或者,如果真的值得花时间和精力进行维护,请将代码生成器转换为编辑器(以代码生成作为其输出(。

至少有一个非常知名的 AAA 游戏引擎实际上为任何标记为与本机代码接口的脚本生成C++头文件。从那里,源文件中的宏实现样板方法。其余方法由开发人员实现。

更新:C++11 实际上支持可变参数模板参数。我对 C++11 没有任何经验,所以我不确定可变参数模板参数是否会支持我们正在查看的内容。

好吧,使用 C++11 您可以执行以下操作:

std::vector<Meal *> cookbook = {
    new Soup(new Tomato(), new Potato()),
    new Soup(new Potato(), new Broccoli()),
    new Salad(new Tomato(), new Potato(), new Tomato()),
    // etc
};

当然,这最终仍然会在运行时而不是编译时运行运算符 new 和构造函数,但至少更紧凑一些。

编辑

不幸的是,C++11 没有提供一种方法来创建静态存储持续时间的未命名对象并获取其地址以用于此类构造。 您需要为此类对象命名,如下所示:

static Tomato tomatoes[] = {
    { /* first tomato initializer */ },
    { /* second */ },
    /* more */
}
static Potato potatoes[] = { ...
static Soup soups[] = { 
    { &tomatoes[0], &potatoes[0] },
    ...
static Salad salads[] = {
    { &tomatoes[4], &potatoes[2], &tomatoes[5] },
    ...
std::vector<Meal *> cookbook = {
    &soups[0], &soups[1], &soups[2], ...
    &salads[0], &salads[1], ...

这非常容易出错,但如果您遵循 Sion Sheevok 的答案,则对于生成哪种C++代码是一个不错的选择。

好吧,如果在编译时都知道,那么这里有一个坏主意:

template <class... Types>
struct cookbook {
    std::tuple<Types...> data;
    template<int i, bool safe>
    struct safe {
        static const Meal* get(const std::tuple<Types...>& data) 
        {return std::get<i, Types...>(data);}
    };
    template<int i, false>
    struct safe {
        static const Meal* get(const std::tuple<Types...>& data) 
        {return NULL;}
    };
    class iterator {
    protected:
        friend cookbook;
        cookbook* parent;
        int index;
    public: 
        iterator(cookbook* p, int i) : parent(p), index(i) {}
        iterator& operator++() {++index; return *this;}
        iterator& operator+=(int i) {index += i; return *this;}
        const Meal& operator*() const {
             switch (i) {
             case 0: return p->safe<0, Types...>::get(data);
             case 1: return p->safe<1, Types...>::get(data);
             case 2: return p->safe<2, Types...>::get(data);
             case 3: return p->safe<3, Types...>::get(data);
             case 3: return p->safe<4, Types...>::get(data);
             case 3: return p->safe<5, Types...>::get(data);
             }
        }
        bool operator==(const iterator& r) {
            return parent==r.parent && index==r.index;
        }
        bool operator!=(const iterator& r) {
            return index!=r.index || parent!=r.parent;
        }
    };
    iterator begin() {return iterator(this, 0);}
    iterator end() {return iterator(this, tuple_size<Types...>::value+1);}
};

这实际上创建了一个说明书对象,其中(有效地(每个Meal类型都有一个成员,以及一个硬编码迭代器来抓取每个成员。

我怀疑这实际上会编译,因为我从未尝试过这样的事情,也没有带有可变参数模板的编译器。 另外,我只实现了其中的一小部分。 你可以用

 template<class RecipieType, class....Types>
 struct Recipie : RecipieType {
      //same as above

这会让你

 #define soup1types Tomoato,Potato
 #define soup2types Potato,Broccoli
 #define salad1types Tomato,Carrot,Broccoli
 #define cooktypes Recipie<Soup,soup1types>
                   Recipie<Soup,soup2types>
                   Recipie<Salad,salad1types>
 cookbook<cooktypes> book; //bam. recipies exist.

此外,当人们看到这个时,他们可能会讨厌你(和我(。

即使使用模板,您也需要某种类型擦除才能将生成的实例化放在某个地方。你可以有一个Meal基类,它通过模板参数获取其成分。例如:

template <typename... T>
class Salad: public Meal {
};
std::vector<std::unique_ptr<Meal>> cookbook;
cookbook.push_back(std::unique_ptr<Meal>(new Salad<Tomato>()));
cookbook.push_back(std::unique_ptr<Meal>(new Salad<Tomato, Carrot>()));
cookbook.push_back(std::unique_ptr<Meal>(new Salad<Tomato, Tomato, Carrot>()));

当然,为了在某种形状或形式中有用,您仍然需要提供一些访问器来检索各种成分。为此,您需要还原原始Salad实例化 - 除了它丢失了。您可以在Meal中创建一个虚拟函数,该函数以Salad实现并组装各种成分。