索引结构是否合法?

jco*_*der 103 c c++ struct

无论代码有多糟糕,并且假设对齐等在编译器/平台上不是问题,这种未定义或破坏的行为是什么?

如果我有这样的结构: -

struct data
{
    int a, b, c;
};

struct data thing;
Run Code Online (Sandbox Code Playgroud)

它是合法的访问a,bc作为(&thing.a)[0],(&thing.a)[1](&thing.a)[2]

在每种情况下,在我尝试过的每个编译器和平台上,我尝试过的每个设置都"有效".我只是担心编译器可能没有意识到bthing [1]是相同的东西,并且'b'的存储可能被放入寄存器中,而东西[1]从内存中读取错误的值(例如).在每种情况下,我都尝试过它做了正确的事情.(我当然意识到这一点并不多见)

这不是我的代码; 它是我必须使用的代码,我对这是代码还是破坏代码感兴趣,因为不同影响我更改它的优先级:)

标记C和C++.我最感兴趣的是C++,但如果它不同,我也会感兴趣.

Whi*_*TiM 72

这是非法的1.这是C++中的Undefined行为.

你是采用数组方式获取成员,但这是C++标准所说的(强调我的):

[dcl.array/1]: ...数组类型的对象包含一个连续分配的非空的N个子对象,类型为T ...

但是,对于成员来说,没有这样的连续要求:

[class.mem/17]: ...;实现对齐要求可能导致两个相邻成员不能立即分配 ...

虽然上面两个引号应该足以暗示为什么索引到struct你所做的不是C++标准定义的行为,让我们选择一个例子:看表达式(&thing.a)[2]- 关于下标运算符:

[expr.post//expr.sub/1]: 后缀表达式后跟方括号中的表达式是后缀表达式.其中一个表达式应为类型为"T的数组"的glvalue或类型为"指向T的指针"的prvalue,另一个应为未编组枚举或整数类型的prvalue.结果是"T"型.类型"T"应该是完全定义的对象类型.66 表达式E1[E2](根据定义)相同((E1)+(E2))

深入研究上述引文的粗体文字:关于向指针类型添加整数类型(请注意此处的重点)..

[expr.add/4]:当一个具有整数类型的表达式被添加到指针或从指针中减去时,结果具有指针操作数的类型.如果表达式P指向元件x[i]阵列对象x 与n个元素,表述P + JJ + P(其中,J具有值j)指向(可能-假设的)元件x[i + j] ,如果0 ? i + j ? n; 否则,行为未定义....

注意if子句的数组要求; 否则否则在上述报价.该表达式显然不符合if子句的条件; 因此,未定义的行为.(&thing.a)[2]


在旁注:虽然我已经在各种编译器上广泛地试验了代码及其变体,但是它们没有在这里引入任何填充,(它的工作原理); 从维护的角度来看,代码非常脆弱.在执行此操作之前,您仍应断言实现已连续分配成员.并保持入境:-).但它仍然是未定义的行为......

其他答案提供了一些可行的解决方法(具有已定义的行为).



正如在评论中正确指出的那样,[basic.lval/8]在我之前的编辑中不适用.谢谢@ 2501和@MM

1:请参阅@ Barry对此问题的回答,这是唯一一个可以thing.a通过此parttern 访问结构成员的法律案例.

  • 严格的alising与此无关.int类型包含在聚合类型中,此类型可以别名为int.` - 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员), (4认同)
  • 严格别名与此无关.填充不是对象存储值的一部分.此答案也未能解决最常见的情况:没有填充时会发生什么.建议实际删除这个答案. (4认同)

250*_*501 48

在C中,即使没有填充,这也是未定义的行为.

导致未定义行为的事情是越界访问1.当你有一个标量(结构中的成员a,b,c)并尝试将它用作数组2来访问下一个假设元素时,就会导致未定义的行为,即使碰巧有另一个相同类型的对象在那个地址.

但是,您可以使用struct对象的地址并计算特定成员的偏移量:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );
Run Code Online (Sandbox Code Playgroud)

这必须单独为每个成员完成,但可以放入类似于数组访问的函数中.


1(引用自:ISO/IEC 9899:201x 6.5.6加法运算符8)
如果结果指向数组对象的最后一个元素之后的一个,则不应将其用作被计算的一元*运算符的操作数.

2(引用自:ISO/IEC 9899:201x 6.5.6加法运算符7)
出于这些运算符的目的,指向不是数组元素的对象的指针与指向第一个元素的指针的行为相同长度为一的数组,以对象的类型作为其元素类型.

  • 请注意,这仅适用于类是标准布局类型的情况.如果不是,它仍然是UB. (3认同)

Sla*_*ica 43

在C++中如果你真的需要它 - 创建operator []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a
Run Code Online (Sandbox Code Playgroud)

它不仅保证工作,而且使用更简单,您不需要编写不可读的表达式 (&thing.a)[0]

注意:假设您已经拥有包含字段的结构,并且需要通过索引添加访问权限,则会给出此答案.如果速度是一个问题,你可以改变结构,这可能更有效:

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};
Run Code Online (Sandbox Code Playgroud)

此解决方案将改变结构的大小,因此您也可以使用方法:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};
Run Code Online (Sandbox Code Playgroud)

  • @Lundin如果你关心这种结构的速度,那么数据应该首先组织成一个数组,而不是单独的字段. (6认同)
  • @Lundin你的意思是不可读和未定义的行为?不用了,谢谢. (2认同)
  • 这些引用至少会使事物的大小翻倍.做`thing.a()`. (2认同)

Sto*_*ica 14

对于c ++:如果需要在不知道其名称的情况下访问成员,则可以使用指向成员变量的指针.

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;
Run Code Online (Sandbox Code Playgroud)

  • 你也可以通过制作arr constexpr来改进.这将在数​​据部分中创建单个固定查找表,而不是在运行中创建它. (3认同)
  • 假设有效?我假设相反.**看看生成的代码. (2认同)

Pet*_*des 10

在ISO C99/C11中,基于联合的类型惩罚是合法的,因此您可以使用它而不是将指针索引到非数组(请参阅其他各种答案).

ISO C++不允许基于联合的类型惩罚. GNU C++作为扩展,我认为其他一些不支持GNU扩展的编译器通常支持union type-punning.但这并不能帮助您编写严格的可移植代码.

使用当前版本的gcc和clang,使用a switch(idx)来选择成员编写C++成员函数将优化编译时常量索引,但会为运行时索引生成可怕的分支asm.这没有什么本质上的错误switch(); 这只是当前编译器中的遗漏优化错误.他们可以有效地编译Slava的switch()函数.


解决方案/解决方法是以另一种方式执行:为类/结构提供一个数组成员,并编写访问器函数以将名称附加到特定元素.

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};
Run Code Online (Sandbox Code Playgroud)

我们可以在Godbolt编译器资源管理器上查看不同用例的asm输出.这些是完整的x86-64 System V功能,省略了尾部RET指令,以更好地显示内联时获得的内容.ARM/MIPS /无论什么都是类似的.

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]
Run Code Online (Sandbox Code Playgroud)

相比之下,@ Slava使用switch()for C++ 的答案使得asm像运行变量索引一样.(以前的Godbolt链接中的代码).

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret
Run Code Online (Sandbox Code Playgroud)

与C(或GNU C++)基于联合的类型惩罚版本相比,这显然是可怕的:

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]
Run Code Online (Sandbox Code Playgroud)


Bar*_*rry 9

在C++中,这主要是未定义的行为(取决于哪个索引).

来自[expr.unary.op]:

出于指针运算(5.7)和比较(5.9,5.10)的目的,不是以这种方式获取地址的数组元素的对象被认为属于具有一个类型元素的数组T.

因此,表达式&thing.a被认为是指一个数组int.

来自[expr.sub]:

表达式E1[E2](根据定义)相同*((E1)+(E2))

从[expr.add]:

当向指针添加或从指针中减去具有整数类型的表达式时,结果具有指针操作数的类型.如果表达式P指向具有元素x[i]的数组对象xn元素,则表达式P + JJ + P(其中J具有值j)指向(可能是假设的)元素x[i + j]if 0 <= i + j <= n; 否则,行为未定义.

(&thing.a)[0]是完美的形式,因为&thing.a被认为是一个大小为1的数组,我们正在采取第一个索引.这是一个允许的索引.

(&thing.a)[2]违反了先决条件0 <= i + j <= n,因为我们有i == 0,j == 2,n == 1.简单地构造指针&thing.a + 2是未定义的行为.

(&thing.a)[1]是一个有趣的案例.它实际上并没有违反[expr.add]中的任何内容.我们被允许一个指针超过数组的末尾 - 这就是.在这里,我们转向[basic.compound]中的注释:

指针类型的值是指向或超过对象末尾的指针,表示对象53占用的内存中的第一个字节的地址(1.7)或者在对象占用的存储结束后的内存中的第一个字节, 分别.[注意:超过对象末尾的指针(5.7)不被视为指向可能位于该地址的对象类型的无关对象.

因此,获取指针&thing.a + 1是定义的行为,但取消引用它是未定义的,因为它没有指向任何东西.


Yak*_*ont 8

这是未定义的行为.

C++中有许多规则试图让编译器有一些理解你正在做什么的希望,因此它可以推理并优化它.

有关于别名(通过两种不同的指针类型访问数据),数组边界等的规则.

当你有一个变量时x,它不是数组成员的事实意味着编译器可以假设没有[]基于数组的访问可以修改它.因此,每次使用它时都不必不断地从内存中重新加载数据; 只有有人可以从名称中修改它.

因此(&thing.a)[1]编译器可以假设不参考thing.b.它可以使用这个事实来重新排序读取和写入thing.b,使您希望它执行的操作无效,而不会使您实际告诉它执行的操作失效.

一个典型的例子是抛弃const.

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';
Run Code Online (Sandbox Code Playgroud)

在这里你通常得到一个编译器说7然后2!= 7,然后是两个相同的指针; 尽管指的ptr是事实x.x当你要求的值时,编译器会采用一个恒定值的事实来不打扰它x.

但是当你拿到地址时x,就强迫它存在.然后你抛弃const,并修改它.所以内存中的实际位置x已被修改,编译器在阅读时可以自由地实际读取它x!

编译器可能足够聪明,可以弄清楚如何避免跟随ptr读取*ptr,但通常它们不是.ptr = ptr+argc-1如果优化器变得比你更聪明,请随意使用或者使用某些混乱.

您可以提供operator[]获得正确项目的自定义.

int& operator[](std::size_t);
int const& operator[](std::size_t) const;
Run Code Online (Sandbox Code Playgroud)

两者都有用.


Chr*_*les 6

这是一种使用代理类按名称访问成员数组中元素的方法.这是非常C++,除了语法偏好之外,与ref-returns访问器函数没有任何好处.这会使->运算符重载作为成员访问元素,因此为了被接受,人们既不喜欢访问器(d.a() = 5;)的语法,也不能容忍使用->非指针对象.我希望这也可能会使不熟悉代码的读者感到困惑,所以这可能比你想投入生产的东西更加巧妙.

Data在该代码结构还包括用于将标操作过载,以访问其内部索引元素ar阵列构件,以及beginend功能,迭代.此外,所有这些都是非const和const版本的重载,我觉得需要包括完整性.

Data->用于通过名称访问的元件(像这样:my_data->b = 5;),一个Proxy返回对象.然后,因为这个Proxyrvalue不是一个指针,它自己的->运算符是自动链调用的,它返回一个指向自身的指针.这样,Proxy对象被实例化并且在初始表达式的评估期间保持有效.

一个的敷设渠道Proxy对象填充其3个基准构件a,bc根据在构造传递一个指针,其被假定为指向包含至少3个值,其类型被给出为模板参数的缓冲器T.因此,不是使用作为Data类成员的命名引用,而是通过在访问点填充引用来节省内存(但不幸的是,使用->而不是.运算符).

为了测试编译器的优化器如何消除使用引入的所有间接Proxy,下面的代码包括2个版本的main().该#if 1版本使用->[]运算符,#if 0版本执行等效的过程集,但只能通过直接访问Data::ar.

Nci()函数生成用于初始化数组元素的运行时整数值,这可防止优化器直接将常量值插入每个std::cout <<调用中.

对于gcc 6.2,使用-O3,两个版本main()生成相同的程序集(在第一个比较之间#if 1#if 0之前切换main()):https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif
Run Code Online (Sandbox Code Playgroud)