重载内部没有数组的[]操作符

Overloading the [] operator with no array internally

本文关键字:操作符 数组 内部 重载      更新时间:2023-10-16

我看到了以下代码:

class SomeClass
{
public:
    int mA;
    int mB;
    int mC;
    int mD;
    int operator[] (const int index) const
    {
        return ((int*)(this))[index];
    }
};

这是如何工作的?我知道这个关键字是指向这个类的指针,但没有属性知道有多少变量…我们如何安全地访问"this"指针的[index] ?

不推荐这种类型的东西,因为它不受c++标准的保证。然而,大多数编译器确实显式地定义了它们的内存布局行为(通常基于每个体系结构),并提供#pragma来操纵打包行为(例如MSVC中的#pragma pack)。如果您了解/利用这些特性,您可以使它在大多数给定的编译器/体系结构上工作。然而,它将可移植!对于每个新的编译器,您都需要重新测试和调整,这是一项代价高昂的维护任务。一般来说,我们更喜欢易于移植。

如果你真的想这样做,你可以添加一个static_assert来验证编译器的行为。

int operator[] (const int index) const
{
    static_assert(sizeof(SomeClass) == 4 * sizeof(mA), "Padding not supported");
    return ((int*)(this))[index];
}

因为标准不允许对成员重新排序,所以从逻辑上我们可以推断,如果SomeClass的大小是16,那么这段代码将按预期工作。有了assert,如果有人在不同的编译器上构建,并且它试图填充它(从而把我们搞砸了),我们至少会得到通知。

然而,我们可以符合标准并实现数组槽的名称。您可以考虑这样的模式:

class SomeClass
{
    enum Index {
        indexA,
        indexB,
        indexC,
        indexD,
        indexCount;
    };
    int mData[indexCount];
public:
    int operator[] (const int index) const
    {
        return mData[index];
    }
    int& A() { return mData[indexA]; }
    int& B() { return mData[indexB]; }
    int& C() { return mData[indexC]; }
    int& D() { return mData[indexD]; }
};

这提供了类似的功能,但由c++标准保证。

这个'工作'只是因为它是未定义的行为。编译器为mB选择的内存位置恰好与&mA + 1相同,mC的地址是&mA + 2&mB + 1,以此类推。

然而,类确实是"标准布局"(所有数据成员都有相同的访问说明符,并且没有使用继承[n3337§9 p7]),这在传统上确实产生了成员变量的这种组织。但这并不能保证。"标准布局"可能意味着其他东西,比如成员之间的一些填充量[n3337§9.2 p14],但我怀疑有没有任何平台真正做到了这一点。

规范确实说,在一个对象之后获取地址是合法的,所以计算&mA + 1是合法的。[n3337§5.7 p5]规范还规定,无论指针是如何计算的,具有正确类型和值的指针都指向对象。

如果一个T类型的对象位于地址A,则一个cv T*类型的指针(其值为地址A)被称为指向该对象,而不管该值是如何获得的。[注:例如,数组末尾后1的地址(5.7)将被认为指向可能位于该地址的数组元素类型的不相关对象。]本;[n3337§3.9.2 p3]

在审查了§5.9和§5.10之后,我认为以下可能在技术上是合法的,尽管它可能有未指定的行为:

if (&mA + 1 == &mB && &mB + 1 == &mC && &mC + 1 == &mD) {
    return ((int*)(this))[index];
}

即使强制转换也是合法的,因为规范中说标准布局类可以强制转换为指向其第一个成员的指针。[n3337§9.2 p20]