我可以在 class 定义中放置 "non-static blocks" 代码吗?

Can I place "non-static blocks" of code in class definitions?

C++中有非静态块吗?

如果不是,如何优雅地模拟?

我想替换类似的东西:-

class C{
    public: void ini(){/* some code */}
};
class D{
    std::vector<C*> regis; //will ini(); later
    public: C field1; 
    public: C field2;  
    public: C field3;             //whenever I add a new field, I have to ... #1
    public: D(){
        regis.push_back(&field1);
        regis.push_back(&field2);
        regis.push_back(&field3); //#1 ... also add a line here
    }
    public: void ini(){
        for(auto ele:regis){
            ele->ini();
        }
    }
};

与 :-

class D{
    std::vector<C*> regis;                        
    public: C field1;{regis.push_back(&field1);}//less error-prone (because it is on-site)
    public: C field2;{regis.push_back(&field2);}
    public: C field3;{regis.push_back(&field3);}
    public: D(){    }  //<-- empty
    public: void ini(){
        for(auto ele:regis){
            ele->ini();
        }
    }
};

我在 C++ 中发现了很多与 static-block 相关的问题,但没有找到任何关于 non-static-block 的问题.

为了方便回答,这里是a full code

可以使用 X-MACRO (wiki link) 来完成,但我正在努力避免它。

编辑

在实际情况下,fieldX 可以具有从某个 C 派生的任何类型。

我考虑另一个糟糕的解决方法:-

class D{
    std::vector<C*> regis;     
    char f(C& c){   regis.push_back(&c); return 42;}                 
    public: C field1; char dummyWaste1=f(field1);
    public: C field2; char dummyWaste2=f(field2);
    public: C field3; char dummyWaste3=f(field3);

Edit2(赏金原因)

skypjack 的回答很有用,但我很想知道更多替代方案。
最后的 objective 是模拟通用的非静态块,具有更多的多样性。
换句话说,如果新的解决方案可以解决这个问题就好了:-

class D{
    int field1=5;
    { do something very custom; /* may access field1 which must = 5 */}
    //^ have to be executed after "field1=5;" but before "field2=7"
    int field2=7;
    int field3=8;
    { do something very custom ; /* e.g. "field1=field2+field3" */}
    //^ have to be executed after "field3=8;"
};

不会为每个块浪费 1 char(或更多 - 用于对齐)。

how to emulate it elegantly?

可以直接初始化regis

std::vector<C*> regis = { &field1, &field2, &field3 };

也就是定义你的class为:

class D{
public:
    C field1;
    C field2;
    C field3;

    void ini(){
        for(auto ele:regis){
            ele->ini();
        }
    }

private:
    std::vector<C*> regis = { &field1, &field2, &field3 };
};

否则,如果您可以向 C 添加构造函数,则还原逻辑并将其自身添加到向量中:

#include<vector>

struct C {
    C(std::vector<C*> &vec) {
        vec.push_back(this);
        // ...
    }

    void ini() {}
};

class D{
    std::vector<C*> regis{};

public:
    C field1 = regis;
    C field2 = regis;
    C field3 = regis;

    void ini(){
        for(auto ele:regis){
            ele->ini();
        }
    }
};

int main() { D d{}; d.ini(); }

------ 编辑 ------

根据评论中的要求:

C is a holy class for me. Is it possible to not hack C?

这是一个可能的替代方案,不需要您修改 C:

#include<vector>

struct C {
    void ini() {}
};

struct Wrapper {
    Wrapper(std::vector<C*> &vec) {
        vec.push_back(*this);
        // ...
    }

    operator C *() { return &c; }

private:
    C c;
};

class D{
    std::vector<C*> regis{};

public:
    Wrapper field1{regis};
    Wrapper field2{regis};
    Wrapper field3{regis};

    void ini(){
        for(auto ele:regis){
            ele->ini();
        }
    }
};

int main() { D d{}; d.ini(); }

看到这个问题,让我想到了自己的"Object with properties"概念。我认为它更实用而不是优雅,但我敢于展示它:

为了建立 FieldObject 之间的依赖关系,我在 Field 中引入了对 Object 的反向引用。以增加一个成员为代价,这有一定的便利性。

class字段-存储字段所需的基础class:

class Object; // forward reference

// super class of all object fields
class Field {
  // variables:
  private:
    // parent object
    Object &_obj;
  // methods:
  protected:
    // constructor.
    Field(Object &obj);
    // destructor.
    ~Field();
    // disabled:
    Field(const Field&) = delete;
    Field operator=(const Field&) = delete;
}

对应的class对象-字段容器:

#include <vector>

// super class of objects which may contain fields
class Object {
  // friends:
  friend class Field;
  // variables:
  private:
    // table of all registered fields
    std::vector<Field*> _pFields;
  // methods:
  protected:
    // constructor.
    Object() = default;
    // destructor.
    virtual ~Object() = default;
    // disabled:
    Object(const Object&) = delete;
    Object& operator=(const Object&) = delete;
};

class Field 的实现必须 "know" 两者 classes:

#include <algorithm>

// implementation of class Field

Field::Field(Object &obj): _obj(obj)
{
  _obj._pFields.push_back(this);
}

Field::~Field()
{
  _obj.erase(
    std::find(
      _obj._pField.begin(), _obj._pField.end(), this));
}

字段实例模板:

// a class template for derived fields
template <typename VALUE>
class FieldT: public Field {
  // variables:
  private:
    // the value
    VALUE _value;
  // methods:
  public:
    // constructor.
    FieldT(Object &obj, const VALUE &value):
      Field(obj), _value(value)
    { }
    // copy constructor.
    FieldT(Object &obj, const FieldT &field):
      Field(obj), _value(field._value)
    { }
    // disabled:
    FieldT(const FieldT&) = delete;
    FieldT& operator=(const FieldT&) = delete;
};

我认为的优缺点:

优点:

  1. 对象永远不会注册非字段。

  2. 您可能永远不会忘记显式构造字段,因为在这种情况下 C++ 编译器会提醒您。 (字段不是默认可构造的。)

  3. 您可以在派生的复制构造函数中决定 Object 是复制字段还是用常量(或替代)值初始化它。

缺点:

  1. 它需要在每个派生的 Object 中明确编写代码,这可能被认为是乏味的。 (我个人喜欢一定程度的显式代码,以便于维护。)

  2. 并非万无一失。 例如。使用 new 创建的字段是 "begging" 内存泄漏。 (虽然......我从来没有考虑过简单地删除 operator new()。我很快就会检查这个。)

所以,这在现实生活中是怎样的:

// storage of a 2d vector
struct Vec2f {
  float x, y;
  Vec2f(float x, float y): x(x), y(y) { }
};

// storage of a 2d circle
class Circle: public Object {
  // fields:
  public:
    // center
    FieldT<Vec2f> center;
    // radius
    FieldT<float> radius;
 // methods:
 public:
   // constructor.
   explicit Circle(float x, float y, float r):
     Object(),
     center(*this, Vec2f(x, y)),
     radius(*this, r)
   { }
   // copy constructor.
   Circle(const Circle &circle):
     Object(),
     center(*this, center),
     radius(*this, radius)
  { }
};

注:

您可能想知道为什么字段是 public – 这是故意的。在我的现实生活中,这些字段有更多,即 getter 和 setter 以及(可选)信号槽来安装监护人和修改通知程序。我的目的是将我从对象中访问方法的繁琐编写中解放出来。

在现实生活中,这个概念具有更大的潜力:

  • 对于可以写入和读取 XML 文件或其他格式的通用 I/O classes 已经足够了。

  • 可以在通用 GUI classes 的第二个概念中获得,以通用方式简化用户界面的创建。

行政额外数据的价值:

一些额外的字节对我来说似乎足够值得,如果它们能保证大量安全的开发时间。当然,也有我考虑每一位的反例,例如加载几何图形进行视觉模拟时的顶点数据。 (对于顶点数据示例,我从未考虑过访问每个顶点的 属性 概念。相反,我使用这些属性一次性访问顶点数组。)

关于附加施工细节的附加要求:

class D{ int field1=5; { do something very custom; } //^ have to be executed after "field1=5;" but before "field2=7" int field2=7; int field3=8; { do something very custom ; }//have to be executed after "field3=8;" };

我会选择一个与上述概念无关的简单解决方案:

class D {
  int field1 = 5;
  struct InitBefore {
    InitBefore() {
      // do something very custom;
    }
  } initBefore;
  int field2 = 7;
  int field3 = 8;
  struct InitAfter {
    InitAfter() {
      // do something very custom;
    }
  } initAfter;
  // the rest...
};

很遗憾,这不满足额外要求:

without wasting 1 char for each block.

(原因可参考SO: sizeof empty structure is 0 in C and 1 in C++ why?。)

也许,可以利用应用于适当成员的构造函数的序列运算符和静态方法来解决这个问题。这对我来说甚至太难看了,我不敢为此草拟代码。

可能是,可以通过在构造函数中添加 "very custom code" 的各个字段创建一个嵌入式的私有派生 class 来解决。由于字段实际上有数据,因此应该避免空 struct/class 问题。

实际上,我从不关心非零 structs/classes 因为我只在极少数情况下才需要这个技巧本身(预计只有少数几个实例)。

由于(在撰写本文时)距离赏金结束还有 7 天,我会记住这一点...

是这样的吗?

class D
{
public:
    D()
    {   
        objects.emplace("field1", std::make_unique<C>());
        //{ do something very custom; }
        //^ have to be executed after "field1=5;" but before "field2=7"
        objects.emplace("field2", std::make_unique<C>());
        objects.emplace("field3", std::make_unique<C>());
        //{ do something very custom; }//have to be executed after "field3=8;"

        // if you want you can store the pointers in the vector. but since you store everything in the map you maybe wanna drop the vector 
        for (const auto& o : objects)
        {
            regis.emplace_back(o.second.get());
        }
    }

private:
    void ini()
    {
        for (auto ele : regis){
            ele->ini();
        }

        /* or use the map to iterate 
        for (const auto& o : objects)
        {
            o.second->ini();
        }       
        */

        // to acces a specific field:
        //objects["field1"]->any_obj_from_c;
    }
    std::vector<C*> regis; //will ini(); later
    std::map<std::string, std::unique_ptr<C>> objects;
};

也许您正在寻找由 lambda 初始化的虚拟字段捕获 this 声明后立即调用的指针:

#include <iostream>

struct C { };

class D{
    int field1=5;
    int dummy1{[&]{ std::cout << field1 << std::endl; return 0;}()};
    int field2=7;
    int field3=8;
    int dummy2{[&]{ std::cout << (field1 + field2 + field3) << std::endl; return 0;}()};
};

int main() {
    D d;
}

输出:

5  
20

[live demo]

我会反过来并使用类型 C 的 "real" 对象初始化向量,让成员 field0 .. field2 类型为 "reference to C",即 C&,并用向量的各个元素初始化它们。这种"turn around"的优点是各种C元素在vector中一个接一个的放置,这是最紧凑的方式,没有填充,同时仍然提供"named"数据成员(即 field0..field2)用于访问元素而不暴露向量成员本身。

自定义 "static" 代码可以用逗号表达式来完成,这样您就可以放置几乎任意的代码。对于逗号表达式中不允许的代码,例如变量的声明,仍然可以调用成员函数或使用 lambda 表达式。因此,对于在各个字段之间执行的 "static" 代码,我们不需要任何虚拟成员。

我们唯一需要虚拟成员的地方是在最后一个 field_x 之后执行的代码;所以开销是单个 char 值,这里的 "static" 代码再次用逗号表达式解决。

请参阅以下代码,其中演示了该方法。请注意,您实际上不必触摸 class C;引入成员 functions/data C::setXC::printint x 仅用于演示目的:

class C{
public:
    void ini(){/* some code */}

    // the following methods and data members are actually not necessary; they have been introduced just for demonstration purpose:
    void setX(int _x) { x = _x;  };
    void print() { cout << x << endl; }
    int x;
};

class D{
protected:
    std::vector<C> regis = std::vector<C>(3); //will ini(); later
public:
    C &field0 = (regis[0].setX(5),printAllRegis("at field0:"),regis[0]);
    C &field1 = (regis[1].setX(7),printAllRegis("at field1:"),regis[1]);
    C &field2 = (regis[2].setX(regis[0].x + regis[1].x),printAllRegis("at field2:"),regis[2]);
    char dummy = (cout << "after field2: ", field2.print(), '0');
    D(){ }

    void ini(){
        for(auto ele:regis){ ele.ini(); }
    }

    void printAllRegis(string header) {
        int i=0;
        cout << header << endl;
        for(auto ele:regis){ cout << "  field" << i++ << ":"; ele.print(); }
    }
};

int main() {

    D d;
    /* Output:
     at field0:
       field0:5
       field1:0
       field2:0
     at field1:
       field0:5
       field1:7
       field2:0
     at field2:
       field0:5
       field1:7
       field2:12
     after field2: 12
     */

    return 0;
}

这在这里可能有点矫枉过正,但仍然有替代方案:不是维护 D.h 和 D.cpp 而是编写小应用程序,它将在基础上生成 D.h 和 D.cpp D.txt,并保持 D.txt。因此,当您需要向 D 添加新字段(或其他一些更改)时,只需向 D.txt 和 运行 生成器添加一行。

我认为反过来创建它可能更聪明。与其为了迭代目的使用额外的指针向量人为地增强结构,不如使用命名字段增强向量。

您为什么要这样做的一个强有力的指标是您的字段的布局和命名方案听起来已经很像数组了。想想看,你的名字只是掩盖了你很可能访问带有索引的连续字段。

class C {
public:
    int value; // For code example
    void ini() { value = 42; };
};

class D {
    std::array<C, 4 /*or w/e you need*/ > storage;
public:
    // Reference initializers are compulsory, so less chance of forgetting & bugs
    int field_index = 0;
    C& field1 = storage.at(field_index++);
    C& field2 = storage.at(field_index++);
    void ini(){
        for(auto& ele: storage) {
            ele.ini();
        }
    }
    // We can even do some templating due to std::array
    template<size_t I> C& field() { 
        return std::get<I>(storage); 
    }
};

现在您可以通过增加数组的大小计数来添加额外存储的 Cs,并通过简单地添加额外的成员行来命名任何字段。当然,如果您需要成员重载或想要一个平面层次结构,您也可以将数组存储为成员而不是从它继承。

额外加分:由于所有访问索引都是常量表达式,因此任何好的编译器都可能内联实际访问,无论是按索引还是按字段完成。

int main() {
     D d; d.ini();
     int s = d.field1.value + d.field2.value;
     // alternatively d.field<0>().value + d.field<1>().value;
     printf("%i", s); // Compiler can deduce to printf("%i", 84);
}

例如,上面的代码基本上可以用 clang 和 gcc 编译成 printf("%i", 84)。你可以用你自己的编译器检查这个或者查看 link 到 https://godbolt.org/g/50VjyY.


编辑:我意识到也许初始化语义是列出多个连续字段的初始推理。但是,您通常直接对字段进行的任何初始化也可以在数组初始化程序中完成。假设一些构造函数

// Replace this (from your structure)
D::D(/*some arguments*/) : field1(args), field2(otherargs) {}

// with this
D::D(/*same arguments*/) : storage{C{args}, C{otherargs}} {}

编辑2:虽然它可能无法准确回答如何很好地表达上述数据表示的根本问题,但应该注意的是,确实有一种方法可以执行任意以您最初可能希望的方式编码,但您必须非常小心。

class D{
    std::vector<C*> regis;                        
public:
    C field1 = (regis.push_back(&field1), C{});
    C field2 = (regis.push_back(&field2), C{});
    C field3 = (regis.push_back(&field3), C{});
    D(){    }  //<-- empty
    void ini(){
        for(auto ele:regis){
            ele->ini();
        }
    }
};

在这里您需要确认 any 在您以后可能添加的任何其他构造函数的初始化列表中的初始化将删除相应指针的 push_back!这可能会引入一个难以发现的错误并导致不明显的副作用。出于这个原因,我永远不会建议在默认成员初始值设定项中使用逗号运算符 ,

通过正确使用标准,理论上你可以使用以前成员的值

12.6.2.5

Initialization shall proceed in the following order:

...

[...] nonstatic data members shall be initialized in the order they were declared in the class definition.

请注意,这也适用于使用数组进行的其他初始化,并且不限于您的原始方法。