【问题标题】:Automatic generation of member based operations?自动生成基于成员的操作?
【发布时间】:2026-01-04 09:40:01
【问题描述】:

是否有任何内置的 C++ 机制能够自动生成代码以在类的每个成员上调用函数或运算符?

例如,如果我们有数百种类型需要使用一组特定的函数或运算符。在每个类型中,每种类型都需要为其所有成员调用相同的函数或运算符(仅此而已)。

对于一些具体的例子,我们可以说这些类型有函数Save()Load(),或者流操作符<<>>。当TYPE::Save(stream s)被调用时,我们如何自动执行A.Save(s) + B.Save(s) + C.Save(s)?如果代码只是简单地将执行传递给所有成员,有没有办法自动化这个过程?

这里是一些将自动生成的代码示例(只是Save() 函数)。 编辑注意:本例将所有 3 个成员显示为同一类型,但该解决方案应该适用于任何类型组合的成员,只要它们都共享相关的函数/运算符

class TYPE
{
protected:
    XTYPE A, B, C;
public:
    void Save(stream s)
    {
        A.Save( s );
        B.Save( s );
        C.Save( s );
    }
};

我的一个旧引擎使用疯狂的循环宏来实现这一点。它使使用宏构建类型变得更加容易(当有数百个类使用它们时),并且生成的类不难理解,但是宏本身很混乱且过于复杂。我希望有更多的东西......内置到语言中。

【问题讨论】:

  • 不是开箱即用的,我认为您正在寻找的东西将被称为编译时反射之类的东西。搜索那个词我发现了这个:veselink1.github.io/blog/cpp/metaprogramming/2019/06/08/…也许它适合你。
  • 在 C++ 中,这通常通过代码生成来解决,但实际上基于宏的解决方案也是可能的。
  • 传统智慧是,每当您认为要“迭代多个变量”时,都应该将它们改为数组(或其他适当的容器)。然后一个简单的循环就足够了。当然有时这可能不合适,但有时最简单的解决方案是最好的。
  • 您可以使用参数包。 struct TYPE_BASE { XTYPE A, B,C; }; template<typename Base, auto...Members> struct ImplementSpecialMembers : Base { void Save(stream s) { ((this->*Members).Save(s),...); }; class TYPE : public ImplementSpecialMembers<TYPE_BASE, &TYPE_BASE::A, &TYPE_BASE::B, &TYPE_BASE::C> {};
  • 可以使用数组或列表代替普通成员。但我们自动生成的函数只是次要任务,与类型没有任何具体关系。或者换句话说,我们不想(完全)重新设计我们所有预先存在的类型,只是为了自动化流操作。

标签: c++ class member


【解决方案1】:

正如 P Kramer 所说,您可能会使用一些反射技巧。另一种选择是尝试减少您必须编写的代码量。一个技巧是创建一个接受 parameter pack 的 lambda,并使用 fold expression 对包中的每个项目应用一些操作,如下所示:

void Save(stream s)
{
    [&](auto&&... members) {
        (members.Save(s), ...);
    } (A, B, C);
}

即使ABC 具有不同的类型,只要它们都具有Save() 成员函数,上述方法也有效。

您也可以使用常规模板函数而不是 lambda 来执行此操作,但以上是编写它的最短方法。它仍然需要您列出要应用该函数的成员变量的所有名称,您可以考虑将其放入宏中:

#define MEMBERS A, B, C

class TYPE
{
protected:
    XTYPE MEMBERS;
public:
    void Save(stream s)
    {
        [&](auto&&... members) {
            (members.Save(s), ...);
        } (MEMBERS);
    }
};

受文森特回答的启发,您还可以添加一个函数,该函数返回一个 std::tuple of references 到成员变量,然后可以使用 std::apply 和上面的 lambda 进行迭代:

class TYPE
{
protected:
    XTYPE A, B, C;

    auto Members()
    {
        return std::forward_as_tuple(A, B, C);
    }
public:
    void Save(stream s)
    {
        std::apply([&](auto&&... members) {
            (members.Save(s), ...);
        }, Members());
    }
};

灵活性稍差,但代码更紧凑的是创建一个成员函数,将函数作为参数,并将该函数应用于所有成员:

class TYPE
{
protected:
    XTYPE A, B, C;

    void ApplyToMembers(auto&& func)
    {
        [&](auto&&... members) {
            (func(members), ...);
        } (A, B, C);
    }
public:
    void Save(stream s)
    {
        ApplyToMembers([&](auto&& member){ member.Save(s); });
    }
};

【讨论】:

    【解决方案2】:

    这是一种真正面向对象的方法。您可以使用称为迭代器模式的设计模式。它基本上为TYPE 提供了一个名为“Iterator”的接口,该接口实现了hasNext()next()。然后将XTYPE 放入vector<XTYPE> 并在每个上执行。

    class Iterator {
      virtual boolean hasNext() const = 0;
      virtual XTYPE *next() = 0;
    };
    
    class TYPE: public Iterator
    {
    private:
        size_t pos;
    protected:
        vector<XTYPE> XTYPES;
    
    public:
        void Save(stream s)
        {
            for (auto &XT : XTYPES ) {
                XT.Save(s);
            }
        }
        virtual boolean hasNext() const {
            if (pos => XTYPES.size() ) {
              return false;
            }
        };
        virtual XTYPE *next() {
            return XTYPES[pos++];
        }
    
    };
    

    基本上,您必须以一种或另一种方式确保您要调用的类型对您要调用的组有某种方式。您还可以保持ABC 不变,并在迭代器类中创建一个指向每个XTYPE 的指针向量。像这样:

    class Iterator {
      virtual boolean hasNext() const = 0;
      virtual XTYPE *next() = 0;
    protected:
      int pos;
      vector<XTYPE*> XTYPES_ITER;
    };
    
    class TYPE: public Iterator
    {
    protected:
        XTYPE A, B, C;
    public:
        Type() {
            XTYPES_ITER = {&A, &B, &C};
        }
        void Save(stream s)
        {
            for (auto XT : XTYPES_ITER ) {
                XT->Save(s);
            }
        }
    };
    

    【讨论】:

    • 如果XTYPE变量的数量是固定的,std::array会更好。对于第二个例子,你不需要class Iterator,你可以有一个成员函数auto XTYPES() { return std::array&lt;XTYPES*&gt;{&amp;A, &amp;B, &amp;C}; },然后调用for (auto XT: XTYPES()) XT-&gt;Save(s);。这避免了必须为每个 TYPE 对象存储一个指针向量,编译器应该能够进行合理的优化工作。
    • @G.Sliepen 谢谢,这肯定更有效率。作为一个新程序员,我只是太专注于这种模式。