【问题标题】:C++ non-virtual class member variables memory layout?C++非虚拟类成员变量内存布局?
【发布时间】:2017-06-17 14:55:42
【问题描述】:

我有一个非虚拟类模板A,如下所示,我执行以下操作

#include <iostream>
// my class template
template<typename T>
class A
{
    public:
    T x;
    T y;
    T z;
    // bunch of other non-virtual member functions including constructors, etc
    // and obviously no user-defined destructor
    // ...
};

int main()
{
    //now I do the following
    A<double> a;
    a.x = 1.0; // not important this
    a.y = 2.0;
    a.z = 3.0;

    // now the concerned thing 
    double* ap = (double*)&a;
    double* xp = &(a.x);

    // can I correctly and meaningfully do the following?     
    double new_az = ap[2]; // guaranteed to be same as a.z (for any z) ? ** look here **
    double new_z = xp[2]; // guaranteed to be same as a.z (for any z) ? ** look here **

    std::cout<<new_az<<std::endl;
    std::cout<<new_z<<std::endl;
    return 0;
}

那么,如果我使用原始点指向对象A 或成员变量a.x,是否可以保证,我将正确获取其他变量?

【问题讨论】:

  • 你为什么要这样做,而不是仅仅使用std::array
  • @CodyGray 虽然我这里的演示课很简单。我的实际课程更复杂。而且我必须将其成员变量传递给库中的函数(这不是我的,并且出于某些明显的原因使用指针接口)。这就是为什么我立刻想到了这两个选项。
  • 没有这样的保证。该标准规定xyz 将具有increasing addresses,但不排除在它们之间插入私有或受保护成员。
  • 编译器也允许在成员之间添加填充以优化内存访问模式。例如,如果您创建一个struct A { char x; int y },则很可能在xy 之间会有一些未使用的字节,这只是因为在对齐时访问int 会更快。
  • @yeputons 如果情况只是 T 类型的 x、y、z 怎么办?在函数参数中将函数与此类类链接时会发生什么?

标签: c++ class data-structures memory-layout


【解决方案1】:

正如许多用户指出的那样,不能保证结构的内存布局与适当的数组相同。而通过索引访问成员的“思想上正确”的方式将是创建一些丑陋的operator [],其中包含switch

但是,实际上,您的方法通常没有问题,并且建议的解决方案在生成的代码和运行时性能方面较差。

我可以建议 2 个其他解决方案。

  1. 保留您的解决方案,但在编译时中验证您的结构布局是否对应于一个数组。在您的具体情况下,将STATIC_ASSERT(sizeof(a) == sizeof(double)*3);
  2. 把你的模板类改成数组,把x,y,zvariables转换成数组元素的访问函数。

我的意思是:

#include <iostream>
// my class template
template<typename T>
class A
{
public:
    T m_Array[3];

    T& x() { return m_Array[0]; }
    const T& x() const { return m_Array[0]; }

    // repeat for y,z
    // ...
};

如果您也将数组的长度(即表示的向量的维度)作为模板参数,您可以在每个访问函数中放置一个“STATIC_ASSERT”以确保成员的实际存在。

【讨论】:

  • 感谢大家提供有用的信息和建议性的替代方案。正如你们大多数人所建议的那样,我已经修改了我的类模板以使用一个简单的数组,就像 valdo 在这个答案中一样。这是完成我所要求的最有效和正确的方法。
【解决方案2】:

不,没有保证,不是你做的方式。例如,如果 T 是 int8_t,则它在您指定 1 字节打包时才起作用。

最简单且正确的方法是在模板类中添加一个运算符 [],例如:

T& operator[](size_t i)
{
  switch(i)
  {
  case 0: return x;
  case 1: return y;
  case 2: return z:
  }
  throw std::out_of_range(__FUNCTION__);
}

const T& operator[](size_t i) const
{
  return (*const_cast<A*>(this))[i];  // not everyone likes to do this.
}

但这并不是真正有效的。一种更有效的方法是将向量(或点)坐标放在一个数组中,并使用 x()、y()、z() 成员函数来访问它们。然后,您的示例将适用于所有情况,前提是您在类中实现了 T* 运算符。

operator T*() { return &values[0]; }
operator const T*()const  { return &values[0]; }

【讨论】:

    【解决方案3】:

    如果你真的想做这样的事情:

    template <typename T>
    class FieldIteratable
    {
      using Data = std::array<T, 5/*magic number*/>;
      Data data_;
      public:
      const Data & data() { return data_; }
      T& a1 = data_[0]; // or some macro
      char padding1[3]; // you can choose what field is iteratable
      T& a2 = data_[1];
      char padding2[3]; // class can contain other fields can be
      T& a3 = data_[2];
      char padding3[3];
      T& a4 = data_[3];
      char padding4[3];
      T& a5 = data_[4];
    
    
    };
    
    
    
    int main() {
    
      FieldIteratable<int> fi;
    
      int* a = &fi.a1;
      *a++ = 0;
      *a++ = 1;
      *a++ = 2;
      *a++ = 3;
      *a++ = 4;
    
      std::cout << fi.a1 << std::endl;
      std::cout << fi.a2 << std::endl;
      std::cout << fi.a3 << std::endl;
      std::cout << fi.a4 << std::endl;
      std::cout << fi.a5 << std::endl;
    
      for(auto i :fi.data())
        std::cout << i << std::endl;
    
      return 0;
    }
    

    【讨论】:

    • 它使类型不可复制和不可移动。还有这个的内存占用。
    • 为什么要手动插入填充?只是为了证明它即使在那里也能工作?
    • @aschepler 是的。
    • @Sopel 类型可以复制和移动。只需使用 data_ 即可。至于内存 - 它可能会通过优化处理
    • 好吧,可以复制数据,但不能复制 FieldIterable。我怀疑它是否可以优化。
    猜你喜欢
    • 2020-09-30
    • 2015-09-01
    • 2011-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-18
    • 1970-01-01
    • 2012-09-27
    相关资源
    最近更新 更多