【问题标题】:Is the member field order of a class "stable"?类的成员字段顺序是否“稳定”?
【发布时间】:2014-05-05 19:16:37
【问题描述】:

考虑到 c++(或 c++11),我有一些数据数组,其中包含 2*N 个整数,代表 N 对。对于每个偶数 i=0,2,4,6,...,2*N 它认为 (data[i],data[i+1]) 形成这样的一对。现在我想有一种简单的方法来访问这些对,而无需编写如下循环:

for(int i=0; i<2*N; i+=2) { ... data[i] ... data[i+1] ... }

所以我写了这个:

#include <iostream>

struct Pair {
    int first; int second;
};

int main() {
    int N=5;
    int data[10]= {1,2,4,5,7,8,10,11,13,14};
    Pair *pairs = (Pair *)data;

    for(int i=0; i<N; ++i)
        std::cout << i << ": (" << pairs[i].first << ", " << pairs[i].second << ")" << std::endl;

    return 0;
}

输出:

0: (1, 2)
1: (4, 5)
2: (7, 8)
3: (10, 11)
4: (13, 14)

ideaone:http://ideone.com/DyWUA8

如您所见,我将 int 指针转换为 Pair 指针,这样 c++ 就可以简单地处理我的数据是 int 大小的两倍。而且我知道,因为这就是数组的工作方式,所以数据数组是成对的两个 sizeof(int) 对齐的。但是,我不确定我是否可以假设一个 Pair 恰好是两个 sizeof(int) 以及成员字段 first 和 second 是否按该顺序(或对齐)存储。从技术上讲,在最坏的情况下,我可以想象编译器首先存储 2 个字节,然后存储 4 个字节,然后存储 2 个字节(假设 int 是 4 个字节),并以某种方式管理它。当然,这可能很可笑,但在 c++ 中是否允许这样做?

请注意,我不想将所有数据复制到新数组并手动将其转换为 Pairs。恕我直言,这对于语法糖来说是一项昂贵的操作。

我可以假设 Pair 类的对齐方式吗?结构也一样吗?还有其他方法吗?

根据我在此处阅读的内容 (How is the size of a C++ class determined?),类在内存中的对齐方式取决于 C++ 的编译器,而不是语言。 这是否意味着我注定要复制我的数据使用讨厌的语法?我可以以某种方式强制 c++ 语言中的最小对齐,还是需要编译器开关?

【问题讨论】:

  • 为什么不只存储 5 对数组,而不是 10 个整数数组?
  • 这就是我获取数据的方式。我正在使用 Matlab mex 库,它为我提供了一个 2xN 矩阵,表示对,作为输入参数。
  • 这不是一个安全的转换。如果您使用char[] 作为底层数据缓冲区并正确对齐您想要转换的类型,它可能是。
  • @JohnDibling 您是否建议这样做:ideone.com/nsHp0s,因为类的第一个成员必须始终与对象的开头对齐?

标签: c++ casting memory-alignment


【解决方案1】:

您所做的事情违反了严格的别名规则,因此除了任何可能的大小和对齐问题外,还会导致未定义的行为。

最简洁的解决方案是将数据存储在逻辑对中,而不是作为平面数据存储,如果需要,可以进行一次性转换。不要担心执行数据转换的性能,除非分析显示这是您的执行时间所花费的地方。从长远来看,将数据分组的清晰性几乎肯定会带来正确性。

或者,您可以创建内联函数,抽象出访问平面数组数据的名义“第一”和“第二”属性。

【讨论】:

  • 在这里,我担心性能的原因不是因为分析表明它,而是因为我不喜欢做实际上不需要的事情。这也是为什么我不会遍历所有可能的整数值以找到等于 72 的整数的原因。请注意,复制数据只是为了创建语法糖。
  • @Herbert:但是做任何你想做的事情背后的全部动机似乎是语法糖。
  • 不过没有冒犯,我明白你的意思,它是有效的:速度之前的正确性。但是,我在您的回答中读到的唯一建议是将类功能包装在函数中,这并不相同。例如,这将不允许我以这种方式排序:ideone.com/pyC8Xl,因为数据实际上并没有被 c++ 语言解释为成对数组。
  • @JohnDibling 语法糖,但不以速度为代价。我不明白为什么我不能有一个表示两个最小对齐整数的数据类型并且仍然有快速的代码;)如果 c++ 可以做指针算术,我更愿意让他而不是我来做。
  • @Herbert:你可以——只是不是你想要的方式。您还必须允许自己利用特定于编译器的能力。
【解决方案2】:

结构的对齐至少是其成员的最大对齐,但它可以更大。此外编译器可以在你的成员之间添加填充,所以你的代码是不安全的。

基本上,关于结构布局的唯一保证是:

  1. 第一个成员的偏移量为 0。
  2. 内存中的成员顺序与代码中的相同。

您可以在此定义中使用第一个保证:

struct Pair {
    int p[2];
};

现在,sizeof(Pair) 可能比 2*sizeof(int) 大,但这并不重要。

或者,如果您想要额外的乐趣:

typedef int Pair[2];

指向数组的指针很有趣!

无论如何我的建议是这样做:

int data[10]= {1,2,4,5,7,8,10,11,13,14};

for(int i=0; i<N; ++i)
{
    int *pair = data + 2*i;
    std::cout << i << ": (" << pairs[0] << ", " << pairs[1] << ")" << std::endl;
}

或者,如果您更喜欢额外的乐趣:

typedef int Pair[2];
int data[10]= {1,2,4,5,7,8,10,11,13,14};
Pair *pairs = (Pair*)data;

for(int i=0; i<N; ++i)
{
    std::cout << i << ": (" << pairs[i][0] << ", " << pairs[i][1] << ")" << std::endl;
}

【讨论】:

  • 我是否也知道 sizeof(Pair) == 2*sizeof(int),因为我正在转换数组并且需要索引 1 中的元素也对齐,所以我需要它。跨度>
  • @Herbert:我认为您可以在Pair 的末尾添加填充字节,尽管我不知道为什么编译器会这样做。无论如何,这不应该影响 index==1 的情况。
  • 为什么不呢? pair[0] 会对齐,但pairs[1] 不再对齐了,对吧?
  • 啊,是的!我以为你想要pair[0][1]。我认为您不能 100% 确定不会有额外的填充。
  • 我可能会选择你的后一种解决方案,因为我建议 sizeof(int[2]) 在任何情况下都应该等于 2*sizeof(int)。
【解决方案3】:

这是否意味着我注定要复制我的数据或使用讨厌的语法?

没有

还有其他方法吗?

是的,使用提供您喜欢的语法的包装类。这是一种方法

http://ideone.com/nitI0B

#include <iostream>

struct Pairs {
    int* _data;
    Pairs( int data[] ) : _data(data) {}
    int & first( size_t x ) const { return _data[x*2]; }
    int & second( size_t x ) const { return _data[x*2+1]; }
};

int main() {
    int N=5;
    int data[10]= {1,2,4,5,7,8,10,11,13,14};
    Pairs pairs( data );

    for(int i=0; i<N; ++i)
        std::cout << i << ": (" << pairs.first(i) << ", " << pairs.second(i) << ")" << std::endl;

    return 0;
}

更新

这是一个将 int[2] 包装在结构中(如 C++11 std::array)但允许(实际上是强制)编译器在 int[2] 之后填充的解决方案。编译器不太可能添加任何额外的填充,但标准并不排除它。我还添加了一个随机访问迭代器,以允许将迭代器传递给 std::sort 并将原始数据成对排序。我这样做是为了我的一个教育,可能比它的价值更麻烦。

See this in ideone

// http://stackoverflow.com/questions/23480041/is-the-member-field-order-of-a-class-stable
#include <iostream>
#include <algorithm>
#include <stddef.h>

struct Pair {
    int _data[2]; // _data[0] and _data[1] are consecutive,
                  // and _data[0] is at offset 0 (&Pair == &_data[0])
    int _unused[6]; // simulate the compiler inserted some padding here
    int first() { return _data[0]; }
    int second() { return _data[1]; }
    int & operator[] ( size_t x ) { return _data[x]; }
    friend inline bool operator< ( const Pair & lhs, const Pair & rhs ) {
        return lhs._data[0] < rhs._data[0];
    }
    // it is unlikely that the compiler will add any padding to a struct
    // Pair, so sizeof(Pair) == sizeof(_data)
    // however, the standard doesn't preclude it, so we define our own
    // copy constructor and assignment operator to ensure that nothing
    // extraneous is stored
    Pair( const Pair& other ) {
        _data[0] = other._data[0];
        _data[1] = other._data[1];
    }
    const Pair& operator=( const Pair& other ) {
        _data[0] = other._data[0];
        _data[1] = other._data[1];
        return *this;
    }
};

struct Pairs {
    int* _data;
    size_t _size;
    Pairs( int data[], size_t size ) : _data(data), _size(size) {}
    Pair & operator[] ( size_t x ) const {
        return *reinterpret_cast< Pair * >( _data + 2 * x );
    }
    class rai
        : public std::iterator<std::random_access_iterator_tag, Pair>
    {
        int * _p;
        size_t _size;
        size_t _x;
    public:
        rai() : _p(NULL), _size(0), _x(0) {}
        rai( int* data, size_t size )
            : _p(data), _size(size), _x(0) {}
        friend inline bool operator== (const rai& lhs, const rai& rhs) {
            return lhs._p == rhs._p && lhs._x == rhs._x;
        }
        friend inline bool operator!= (const rai& lhs, const rai& rhs) {
            return lhs._p != rhs._p || lhs._x != rhs._x;
        }
        Pair& operator* () const {
            return *reinterpret_cast< Pair * >( _p + 2 * _x );
        }
        rai& operator+=( ptrdiff_t n ) {
            _x += n;
            if (_x >= _size) { _x = _size = 0; _p = NULL; }
            return *this;
        }
        rai& operator-=( ptrdiff_t n ) {
            if (n > _x) _x = 0;
            else _x -= n;
            return *this;
        }
        friend inline rai operator+ ( rai lhs, const ptrdiff_t n ) {
            return lhs += n;
        }
        friend inline rai operator- ( rai lhs, const ptrdiff_t n ) {
            return lhs -= n;
        }
        friend inline bool operator< ( const rai & lhs, const rai & rhs ) {
            return *lhs < *rhs;
        }
        rai& operator++ () { return *this += 1; }
        rai& operator-- () { return *this -= 1; }
        friend inline ptrdiff_t operator-(const rai& lhs, const rai& rhs) {
            return lhs._p == NULL
                ? (rhs._p == NULL ? 0 : rhs._size - rhs._x)
                : lhs._x - rhs._x;
        }
    };
    inline rai begin() { return rai(_data,_size); }
    static inline const rai end() { return rai(); }
};

int main() {
    int N=5;
    int data[10]= {1,2,7,8,13,14,10,11,4,5};
    Pairs pairs( data, N );

    std::cout << "unsorted" << std::endl;
    for(int i=0; i<N; ++i)
       std::cout << i << ": (" << pairs[i].first() << ", "
                 << pairs[i].second() << ")" << std::endl;

    std::sort(pairs.begin(), pairs.end());

    std::cout << "sorted" << std::endl;
    for(int i=0; i<N; ++i)
        std::cout << i
            << ": (" << pairs[i][0]  // same as pairs[i].first()
            << ", "  << pairs[i][1]  // same as pairs[i].second()
            << ")" << std::endl;

    return 0;
}

【讨论】:

  • 那不允许我对数据使用 std,例如对其进行排序:ideone.com/pyC8Xl,对吧?
  • @Herbert 我们可能需要一个随机访问迭代器...我开始使用它,稍后会再次使用它
  • 使用int[2]s数组不是更容易吗?
  • @Herbert 是的,我想我试过了,但它在 gcc 或 clang 中不起作用,但不记得为什么......我今晚会玩它,这是一个有趣的挑战:)
  • 我接受了您的回答,因为它可以满足我的要求,但是,老实说,我认为这使问题过于复杂;这对于一些简单的事情来说代码太多了;)感谢您的努力,它确实说明了 C++ 如何在手动内存分割和解释方面存在一些限制。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-15
  • 1970-01-01
  • 2013-03-23
  • 2021-02-01
  • 1970-01-01
  • 2010-10-06
  • 2015-01-05
相关资源
最近更新 更多