【问题标题】:Can I place "non-static blocks" of code in class definitions?我可以在类定义中放置“非静态块”代码吗?
【发布时间】:2017-09-24 18:03:40
【问题描述】:

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 的回答非常有用,但我很想找到更多替代方案。
最终目标是模拟具有更多多样性的通用非静态块。
换句话说,如果新的解决方案能解决这个问题就好了:-

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(或更多 - 用于对齐)。

【问题讨论】:

  • 你真的想要一个指针向量吗?通过说明内存所有权,引用向量不是更好吗?
  • @Theolodis 是的,我可以,但是这样做仍然没有解决问题。
  • 为什么需要各个字段?
  • @Mat 每个字段都是一个数据库。我正在尝试做一个数据库中心。 stackoverflow.com/a/43557157 我还想按名称访问这些字段以提高可读性。前任。 field1,2,3真实姓名为DatabaseHenToCage1to1DatabaseChickToFood1to1
  • @javaLover 这就是我写评论的原因......顺便说一句,只要它们具有相同的类型,为什么需要将它们作为单独的变量?难道你不能只做一个字段的初始化向量:std::vector&lt;C&gt; regis = { C(), C(), C() }; 编辑:或者让它成为一个地图,从而保持名称可读......

标签: c++ initialization c++14 non-static


【解决方案1】:

如何优雅地模仿它?

你可以直接初始化regis

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

也就是说,将你的类定义为:

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(); }

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

根据 cmets 的要求:

C 对我来说是一个神圣的职业。是否可以不破解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(); }

【讨论】:

  • 我必须将field3 添加到可能很远的行中。换句话说,std::vector&lt;C*&gt; regis={...}C field3; 相去甚远,因此问题仍然存在。
  • @javaLover 远吗?你的意思是视觉上很远?如果有帮助,请将其放在其他字段的正上方。
  • 是的,它在视觉上很远。在糟糕的日子里,我肯定会忘记它。它发生了。
  • @javaLover 添加了另一个可能对您有帮助的解决方案。
  • @javaLover Wrapperoperator C *(),并且您可以在使用 C * 的任何时候使用它。如果需要,您还可以添加对C 的引用的演员表。话虽这么说,你不能吃蛋糕,所以......
【解决方案2】:

也许您正在寻找一个由 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]

【讨论】:

  • 我还是会浪费 1 个字符,但它比我的更漂亮。谢谢。
  • @javaLover 理论上可以很好地避免浪费额外的内存,你可以像this
【解决方案3】:

我会把它转过来,用C 类型的“真实”对象初始化向量,让成员field0 .. field2 的类型为“对C 的引用”,即C&amp;,然后用向量的各个元素。这种“转身”的好处是各种C-元素在vector中一个接一个地放置,这是最紧凑的方式并且没有填充,同时仍然提供“命名”数据成员(即field0..field2 ) 用于访问元素而不暴露向量成员本身。

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

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

请参阅以下代码,该代码演示了该方法。请注意,您实际上不必触摸类C;成员函数/数据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;
}

【讨论】:

  • 令我惊讶的是,这个 sn-p 是compilableC&amp; c=(....,.....,....); 中的类元组括号语法叫什么?
  • 这是内置的逗号运算符(参见en.cppreference.com/w/cpp/language/operator_other);它的语法是E1,E2,其中E1E2 是表达式,E1E2 被评估之前被评估; (E1,E2) 的结果总共是E2 的评估结果。并且它是标准化的(很长一段时间,我想回到 C89/90 或更长的时间)。
  • 很漂亮。因为这是一个迟到的答案,这让我怀疑人们为什么不建议它。 ....因为逗号运算符不流行(?)。
  • 对,我认为它不受欢迎;如果 ohne 在简单的表达之前想要一些副作用,这很方便。
【解决方案4】:

看到这个问题,它让我想起了我自己的“具有属性的对象”概念。我认为它更实用而不是优雅,但我敢于呈现它:

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

class Field – 存储字段所需的基类:

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

对应的类Object——字段容器:

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

Field 类的实现必须“知道”这两个类:

#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 创建的字段正在“乞求”内存泄漏。 (虽然...我从未考虑过简单地删除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)
  { }
};

注意:

您可能想知道为什么这些字段是公开的——这是故意的。在我的现实生活中,这些字段有更多,即 getter 和 setter 以及(可选)信号槽来安装监护人和修改通知器。我的目的是让我摆脱在对象中编写访问方法的繁琐工作。

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

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

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

管理额外数据的成本:

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

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

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

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

不会为每个块浪费 1 个字符。

(原因可以在SO: sizeof empty structure is 0 in C and 1 in C++ why?找到。)

也许,它可以利用应用于适当成员的构造函数的序列运算符和静态方法来解决。这对我来说甚至看起来如此丑陋,以至于我不敢为此编写代码。

可能是,可以解决创建一个嵌入的、私有派生类的相应字段,其中可以在构造函数中添加“非常自定义的代码”。由于字段实际上有数据,因此应该防止空结构/类问题。

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

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

【讨论】:

    【解决方案5】:

    这样的?

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

    【讨论】:

    • 性能很差,但有点创意。我当然不会使用它。谢谢。
    【解决方案6】:

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

    【讨论】:

      【解决方案7】:

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

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

      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)。您可以使用自己的编译器进行检查,或查看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

      初始化应按以下顺序进行:

      ...

      [...] 非静态数据成员应按照它们在类定义中声明的顺序进行初始化。

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

      【讨论】:

      • 谢谢。 .....我正在尝试检查我的理解...... C&amp; field2 = storage.at(1); 中的数字 1std::array&lt;C, 4&gt; storage; 的数字 4 -> 必须由编码人员手动计算。 ....这可能是一个缺点。
      • 数组的大小,恕我直言,我认为与计算成员数量没有什么不同。手动索引计数可以通过引入计数器变量index 并使用index++ 而不是直接使用12 进行索引来补救。可能应该注意的是,由于数据布局更加清晰,因此内存(和性能)可能大大优于向量实现。
      • 我希望第二次编辑包含更直接适用于您的直接意图的信息。小心使用它,并注意微小变化可能带来的严重副作用。
      • 我同意index++ 是个好主意。 ....这是一个很好的参考!不幸的是,Stephan Lechner 在你之前已经提到过(....,....)。谢谢。
      • 我添加了一个模板方法,只有 std::array 才有可能。
      猜你喜欢
      • 1970-01-01
      • 2019-06-13
      • 2014-08-09
      • 1970-01-01
      • 1970-01-01
      • 2017-01-03
      • 2014-02-14
      • 2021-07-19
      • 2015-03-08
      相关资源
      最近更新 更多