一个变量是否是大小为1的数组?

15
考虑一下:
int main(int, char **) {
  int variable = 21;
  int array[1] = {21};
  using ArrayOf1Int = int[1];
  (*reinterpret_cast<ArrayOf1Int *>(&variable))[0] = 42;
  *reinterpret_cast<int *>(&array) = 42;
  return 0;
}

我刚刚违反了严格别名规则吗?

或者,就像这个评论引导我提出这个问题:一个变量是大小为1的数组吗

请注意,我将此标记为语言律师问题。因此,我不关心-fno-strict-aliasing或编译器特定的行为,而是关心标准中所说的内容。同时,我认为了解这个问题在C++03、C++11、C++14和更新版本中是否以及如何改变也很有趣。


1
数组的内容按定义是连续排列的。因此,大小为1的数组与单个变量无法区分。布局相同。当然,引用数组会得到指向数组第一个元素的指针,而引用非数组对象会得到该对象的lvalue。 - Sam Varshavchik
1
@FirstStep 但是数组不是其基本类型的“序列”(非官方术语)吗?因此,我访问的实际对象的动态类型应该在两种情况下都与指针的类型匹配。 - Daniel Jour
decltype 和这有什么关系?你能把问题整理一下,只涉及数组吗? - Kerrek SB
@KerrekSB decltype与实际问题无关(如果这让您产生了误导,我很抱歉),而是与我的失败有关,即未正确指定类型(“指向1个int数组的指针”)。 - Daniel Jour
1
使用别名:using A = T[1]; - Kerrek SB
显示剩余9条评论
4个回答

9

显然,如果一个对象是大小为一的数组的变量,你可以用一个对象初始化大小为一的数组的引用:

int variable{21};
int (&array)[1] = variable; // illegal

然而,初始化是非法的。相关条款为第4条[conv](标准转换),其中第1段规定如下:
标准转换是具有内置含义的隐式转换。第4条列举了这些转换的完整集合。
这个条款太长了,在此不引用,但它对于将对象转换为任意大小的数组的引用没有任何说明。同样,在reinterpret_cast(5.2.10 [expr.reinterpret.cast])中的部分没有明确说明涉及数组的任何行为,但在第1段中明确排除了这种情况:
......可以使用reinterpret_cast显式执行的转换如下所示。不能使用reinterpret_cast显式执行其他转换。
我认为没有明确说明对象不是一个对象数组,但有足够的省略使案例成为隐含的。标准关于对象和数组的保证是指向对象的指针的行为就像它们指向长度为1的数组一样(5.7 [expr.add]第4段):
对于这些运算符,指向非数组对象的指针与其元素类型相同的长度为1的数组的第一个元素的指针的行为相同。
这个声明的存在也意味着数组对象和非数组对象是不同的实体:如果它们被认为是相同的,那么这个声明本来就不需要。
关于标准的先前(或未来)版本:虽然不同条款中的确切措辞可能已经改变,但总体情况并没有改变:对象和数组始终是不同的实体,到目前为止,我不知道有意图改变这一点。

关于“reinterpret_cast无法显式执行其他转换。”,该列表从未完整,因为它仅包括引用结构和其第一个成员之间的单向转换,而在类成员部分中,它是双向的(我记得措辞是“反之亦然”)。但是,每当提到这个遗漏时,都会遭到激烈的否认和争论。无论如何,在这个答案中,我认为它与问题的意图无关:我无法想象OP认为标准允许对例如int变量进行索引,这就是问题所在。 - Cheers and hth. - Alf
5
我认为这忽略了问题的关键:当然,使用变量和使用大小为1的数组的语法是不同的,但OP想要探讨的是:除了语法之外,变量是否具有与数组相同的属性。 - M.M

7

reinterpret_cast 只有在 C++11 及以上版本才能表现得可预测,所以在 C++11 之前,两行代码都不能保证具有定义的行为。本文将假定使用 C++11 或更高版本。

第一行

(*reinterpret_cast<decltype(&array)>(&variable))[0] = 42;

在这行代码中,解除引用 reinterpret_cast 会产生一个 glvalue,但不会通过该 glvalue 访问 int 对象。当访问 int 对象时,指向数组的 glvalue 已经被衰减为指向该对象的指针(即 int*)。
然而,可以“构造”一个看起来可能包含严格别名违规的情况,如下所示:
struct S {
    int a[1];
};
int variable = 42;
S s = reinterpret_cast<S&>(variable);

这并不违反严格别名规则,因为您可以通过聚合体或联合类型的子对象访问对象。 (自C++98以来一直存在此规则。)

第二行

*reinterpret_cast<decltype(&variable)>(&array) = 42;

reinterpret_cast保证给出数组的第一个子对象的指针,这是一个int对象,所以通过int指针对其进行赋值是定义明确的。


关于“glvalue引用数组已经衰减为指针”的那一点确实帮了很多,我认为非常准确。 - Daniel Jour
你确定使用 S 的例子是合法的吗?复制操作不需要对一个与被访问对象类型不同的值进行左值到右值转换吗? - Kerrek SB
@KerrekSB lvalue-to-rvalue转换是否受到比严格别名更严格的规则限制?我在[conv.lval]中没有看到任何规则。 - Brian Bi
2
@Brian:我认为这并不是“更严格”,只是现有规则似乎适用:您正在通过类型为S的glvalue访问类型为int的对象。这是被禁止的。那么,您为什么认为这是可以的呢?请注意,variable不是类型为S的对象的子对象。它已经是一个完整的对象了。 - Kerrek SB

6

最近的一个草案如下:

§[expr.unary.op]/3:

一元 & 运算符的结果是其运算对象的指针。 [...] 对于指针算术 (5.7) 和比较 (5.9, 5.10) 来说,以这种方式获取其地址并不属于数组元素的对象被视为属于具有类型 T 的一个元素的数组。

在这里我们处理的所有类型都是指针,但是我们(最终)会对它们进行解引用。因此,这可能不足以定义行为(但这是一个接近的判断)。

至于版本之间的变化:这个措辞在 N4296 中出现(即 C++14 和 C++17 之间的草案),但不在 N4140 或 N3337(基本上是 C++14 和 C++11)中。

C11 标准对 fscanf_sfwscanf_s 也有类似的措辞(§K.3.5.3.2/4):

这些参数中的第一个与 fscanf 相同。该参数紧随在参数列表中,其类型为 rsize_t,并给出了配对的第一个参数所指向的数组中的元素数。如果第一个参数指向标量对象,则将其视为一个元素的数组。


1

一个包含1个整数的数组与一个整数不兼容。

这意味着:

struct A {
  int x;
};
struct B {
  int y[1];
};
A a={0};
std::cout << ((B*)&a).y[0];

is not defined behavior. See [basic.types]/11 for the definition of layout-compatible.

A::xB::y不是来自于[basic.types]/10相同类型 -- 其中一个属于[basic.types]/10.2(标量类型),另一个属于[basic.types]/10.4(字面值数组)。它们不是布局兼容的枚举类型。它们不是类类型,因此[class.name]/20-21不适用。

因此,[class.name]/20(公共初始序列)不认为xy是公共初始序列。

我不知道有哪个编译器不会使AB实际上完全相同,但标准规定上述重新解释是非法的,因此编译器可以自由地假设它永远不会被执行。如果您依赖它,这可能会导致优化器或其他严格别名的利用者引起意外行为。

我个人认为,说明一个数组T[N]与N个相邻的T序列具有相同的布局兼容性是一个好主意。这将允许许多有用的技术,例如:

struct pixel {
  union {
    struct {
      char r, g, b, a;
    } e;
    std::array<char,4> pel;
  };
};

在这里,pixel.pel[0]被保证对应于pixel.e.r。但据我所知,这是不合法的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接