无论如何'糟糕'代码是,并假设对齐等在编译器/平台上不是问题,这是未定义或损坏的行为吗?
如果我有这样的结构: -
struct data
{
int a, b, c;
};
struct data thing;
是合法来a
,b
和c
作为(&thing.a)[0]
,(&thing.a)[1]
和{{1} }?
在每种情况下,在我尝试过的每个编译器和平台上,我尝试过的每个设置都可以使用#39;。我只是担心编译器可能没有意识到 b 和 thing [1] 是相同的东西并存储到' b'可能会被放入寄存器中,而事物[1]从内存中读取错误的值(例如)。在每种情况下,我都尝试过它做了正确的事情。 (我当然意识到这并不能证明太多)
这不是我的代码;它必须与我合作的代码,我对这是糟糕代码还是破解代码感兴趣,因为不同影响我更改它的优先级很多:)
标记C和C ++。我最感兴趣的是C ++,但如果它不同,我也感兴趣,只是为了兴趣。
答案 0 :(得分:69)
这是非法的 1 。这是C ++中未定义的行为。
你是以阵列方式接纳成员,但这是C ++标准所说的(强调我的):
[dcl.array/1]: ...数组类型的对象包含连续分配的非空N组 类型为T的子对象
但是,对于成员来说,没有这样的连续的要求:
[class.mem/17]: ...;实施对齐要求可能导致两个相邻 成员不能在彼此之后立即分配 ......
虽然以上两个引号应足以暗示为什么索引到struct
并不是C ++标准定义的行为,让我们选择一个示例:查看表达式(&thing.a)[2]
- 关于下标运算符:
<强> [expr.post//expr.sub/1] 强> 后缀表达式后跟方括号中的表达式是a 后缀表达。其中一个表达式应该是类型的glvalue “T的数组”或“T指针”类型的prvalue和另一个 是无范围枚举或整数类型的prvalue。结果是 类型“T”。类型“T”应是完全定义的对象类型.66 表达式
相同(根据定义)E1[E2]
与((E1)+(E2))
深入研究上述引文的粗体文字:关于向指针类型添加整数类型(请注意此处的重点)..
[expr.add/4]:当具有整数类型的表达式添加到或减去 指针,结果具有指针操作数的类型。 如果 表达式
P
指向 数组 对象x[i]
的元素x
使用n个元素,表达式P + J
和J + P
(J
所在的位置) 值j
)指向(可能是假设的)元素x[i + j]
如果0 ≤ i + j ≤ n
; 否则,行为未定义。 ...
请注意 if 子句的数组要求;否则上面引用的否则。表达式(&thing.a)[2]
显然不符合 if 子句的条件;因此,未定义的行为。
旁注:虽然我已经在各种编译器上广泛地试验了代码及其变体,但是它们没有在这里引入任何填充,(它工作);从维护的角度来看,代码非常脆弱。在执行此操作之前,您仍应断言实现已连续分配成员。并保持入境:-)。但它仍然是未定义的行为......
其他答案提供了一些可行的解决方法(具有已定义的行为)。
正如评论中正确指出的那样, [basic.lval/8] ,这在我之前的编辑中不适用。谢谢@ 2501和@ M.M。
1 :请参阅@ Barry对此问题的回答,其中只有一个法律案例,您可以通过此parttern访问结构的thing.a
成员。
答案 1 :(得分:49)
没有。在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 );
必须单独为每个成员完成此操作,但可以将其放入类似于数组访问的函数中。
1 (引自:ISO / IEC 9899:201x 6.5.6添加剂操作员8)
如果结果指向一个超过数组对象的最后一个元素的那个,那么
不得用作被评估的一元*运算符的操作数。
2 (引自:ISO / IEC 9899:201x 6.5.6添加剂操作员7)
出于这些运算符的目的,指向不是元素的对象的指针
数组的行为与指向长度为1的数组的第一个元素的指针相同
对象的类型作为其元素类型。
答案 2 :(得分:42)
在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
它不仅可以保证工作,而且使用更简单,您不需要编写不可读的表达式(&thing.a)[0]
注意:假设您已经拥有包含字段的结构,并且需要通过索引添加访问权限,则会给出此答案。如果速度是一个问题,你可以改变结构,这可能更有效:
struct data
{
int array[3];
int &a = array[0];
int &b = array[1];
int &c = array[2];
};
此解决方案将改变结构的大小,因此您也可以使用方法:
struct data
{
int array[3];
int &a() { return array[0]; }
int &b() { return array[1]; }
int &c() { return array[2]; }
};
答案 3 :(得分: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;
答案 4 :(得分:10)
在ISO C99 / C11中,基于联合的类型惩罚是合法的,因此您可以使用它而不是将指针索引到非数组(请参阅其他各种答案)。
ISO C ++不允许基于联合的类型惩罚。 GNU C++ does, as an extension,我认为其他一些不支持GNU扩展的编译器确实支持联合类型惩罚。但这并不能帮助您编写严格的可移植代码。
使用当前版本的gcc和clang,使用switch(idx)
编写C ++成员函数来选择成员将优化编译时常量索引,但会为运行时索引生成可怕的分支asm。 switch()
对此没有任何内在错误;这只是当前编译器中的遗漏优化错误。他们可以编译Slava&#39; 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]; }
};
我们可以在Godbolt compiler explorer上查看不同用例的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]
相比之下,@ Slava使用switch()
对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
与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]
答案 5 :(得分: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';
这里你通常得到一个编译器说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;
两者都有用。
答案 6 :(得分:8)
在C ++中,这是主要是未定义的行为(取决于哪个索引)。
来自[expr.unary.op]:
出于指针的目的 arithmetic(5.7)和comparison(5.9,5.10),一个不是数据元素的对象,其地址被接受 这种方式被认为属于具有
T
类型的一个元素的数组。
因此,表达式&thing.a
被认为是指一个int
的数组。
来自[expr.sub]:
表达式
相同(根据定义)E1[E2]
与*((E1)+(E2))
来自[expr.add]:
当向指针添加或从指针中减去具有整数类型的表达式时,结果具有指针操作数的类型。如果表达式
P
指向具有x[i]
元素的数组对象x
的元素n
,则表达式P + J
和J + P
(其中{{ 1}}具有值J
)指向(可能是假设的)元素j
ifx[i + j]
;否则,行为未定义。
0 <= i + j <= n
格式完美,因为(&thing.a)[0]
被认为是大小为1的数组,我们正在接受第一个索引。这是一个允许的索引。
&thing.a
违反了(&thing.a)[2]
的前提条件,因为我们有0 <= i + j <= n
,i == 0
,j == 2
。简单地构造指针n == 1
是未定义的行为。
&thing.a + 2
是一个有趣的案例。它实际上并没有违反[expr.add]中的任何内容。我们被允许一个指针超过数组的末尾 - 这就是。在这里,我们转到[basic.compound]中的注释:
作为指向或超过对象末尾的指针的指针类型的值表示该地址 内存中的第一个字节(1.7)由object53占用,或者在存储结束后占用内存中的第一个字节 分别被对象占用。 [注意:不考虑超过对象末尾的指针(5.7) 指向可能位于该地址的对象类型的不相关对象。
因此,取指针(&thing.a)[1]
是定义的行为,但解除引用它是未定义的,因为它没有指向任何东西。
答案 7 :(得分:6)
这是一种使用代理类按名称访问成员数组中元素的方法。这是非常C ++,除了语法偏好之外,与ref-returns访问器函数没有任何好处。这会使d.a() = 5;
运算符重载以作为成员访问元素,因此为了被接受,人们需要既不喜欢访问器的语法(->
),又要容忍Data
使用非-pointer对象。我希望这也可能会使不熟悉代码的读者感到困惑,所以这可能比你想要投入生产的东西更加巧妙。
此代码中的ar
结构还包括下标运算符的重载,访问其begin
数组成员内的索引元素,以及end
和Data
函数,用于迭代。此外,所有这些都超载了非const和const版本,我认为需要将其包含在内以便完整。
当使用->
&#39; s my_data->b = 5;
按名称访问元素时(如下所示:Proxy
),将返回Proxy
个对象。然后,因为此->
rvalue不是指针,所以它自己的Proxy
运算符被自动链调用,它返回一个指向自身的指针。这样,Proxy
对象被实例化,并在评估初始表达式时保持有效。
a
对象的构造根据构造函数中传递的指针填充其3个引用成员b
,c
和T
,该指针被假定为指向缓冲区包含至少3个值,其类型作为模板参数Data
。因此,不是使用作为->
类成员的命名引用,而是通过在访问点填充引用来节省内存(但不幸的是,使用.
而不是Proxy
运算符)
为了测试编译器的优化器如何消除使用main()
引入的所有间接,下面的代码包括#if 1
的2个版本。 ->
版本使用[]
和#if 0
运算符,Data::ar
版本执行等效的过程集,但只能直接访问Nci()
。
std::cout
函数生成用于初始化数组元素的运行时整数值,这会阻止优化器直接将常量值插入到每个<<
main()
调用中。
对于gcc 6.2,使用-O3,两个版本的#if 1
生成相同的程序集(在第一个#if 0
之前在main()
和#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
之间切换:{ {3}}
{{1}}
答案 8 :(得分:2)
如果读取值足够,效率不是问题,或者您相信编译器能够很好地优化,或者struct只是3个字节,那么您可以安全地执行此操作:
char index_data(const struct data *d, size_t index) {
assert(sizeof(*d) == offsetoff(*d, c)+1);
assert(index < sizeof(*d));
char buf[sizeof(*d)];
memcpy(buf, d, sizeof(*d));
return buf[index];
}
对于仅C ++版本,您可能希望使用static_assert
来验证struct data
是否具有标准布局,并且可能会在无效索引上抛出异常。
答案 9 :(得分:1)
这是违法的,但有一个解决方法:
struct data {
union {
struct {
int a;
int b;
int c;
};
int v[3];
};
};
现在你可以索引v: