在C++中,可变长数组成员是否有效?

62

在C99中,您可以这样声明一个结构体的柔性数组成员:

struct blah
{
    int foo[];
};

然而,当我们在工作中有人尝试使用clang编译C++代码时,那个语法就不能用了。(使用MSVC时可以)我们不得不将其转换为:

struct blah
{
    int foo[0];
};

在查阅了C++标准后,我发现完全没有提到可变成员数组;我一直认为[0]是无效的声明,但显然对于可变成员数组它是有效的。可变成员数组在C++中是否有效?如果有效,正确的声明是[]还是[0]


4
你可以使用一个std::vector<int>成员变量,这样你就能够关注更有趣的事情了,除非这是一个布局问题。 - fredoverflow
3
那个flexible-array-member标签有点...孤单。不过可能只是我的感觉。 - Marcus Borkenhagen
5
有时需要使用既可在C中使用也可在C++中使用的结构(系统API是其中一个非常常见的例子)。 - Michael Burr
3
@FredOverflow,通常情况下我会这样做,但在这种情况下,blah需要一个连续的分配,并且foo的大小是可变的。关于为什么我们首先需要它,这当然是一个很好的设计问题,但我无法在这里讨论。 - MSN
5
根据C99标准,那个具体的结构是非法的,因为它声明:“作为一个特例,带有多个命名成员的结构的最后一个元素可以具有不完整的数组类型; 这称为“灵活数组成员”。 ”因此,在C99中,blah需要一个额外的成员,在foo之前才能有效。 - HelloGoodbye
显示剩余3条评论
10个回答

36

C++不支持在结构体末尾使用C99灵活数组成员,无论是使用空索引符号还是0索引符号(除了一些特定于厂商的扩展):

struct blah
{
    int count;
    int foo[];  // not valid C++
};

struct blah
{
    int count;
    int foo[0]; // also not valid C++
};
据我所知,C++0x 也不会添加这个功能。 不过,如果你将数组大小调整为 1 个元素:
struct blah
{
    int count;
    int foo[1];
};

这段代码可以编译并且运行良好,但从技术上讲它是未定义的行为。您可以使用一个表达式来分配适当的内存,这个表达式不太可能出现一位误差:

struct blah* p = (struct blah*) malloc( offsetof(struct blah, foo[desired_number_of_elements]);
if (p) {
    p->count = desired_number_of_elements;

    // initialize your p->foo[] array however appropriate - it has `count`
    // elements (indexable from 0 to count-1)
}

因此,它可以在C90、C99和C++之间进行移植,并且与C99的灵活数组成员一样有效。

Raymond Chen对此进行了很好的阐述:为什么有些结构以大小为1的数组结尾?

注意:在Raymond Chen的文章中,有一个打印错误/bug在初始化“灵活”数组的示例中。应该这样写:

for (DWORD Index = 0; Index < NumberOfGroups; Index++) { // note: used '<' , not '='
  TokenGroups->Groups[Index] = ...;
}

13
即使你分配了超额的内存,你仍然无法有效地访问超出数组边界一个元素的成员。这种行为是未定义的;C++实现可以根据构造的实际类型添加边界检查。 - CB Bailey
2
@Charles - 我认为你对此不正确(即使是学究式的),否则以下内容将是未定义行为:int* p = malloc(sizeof(int)*4); p[3] = 0; - Michael Burr
3
我认为Charles说“一个”元素的原因不是因为他认为分配数组是不可能的,而是因为在你特定的结构体blah中,数组的长度为1。他的说法是,由于p->foo是类型为blah[1]的,所以p->foo[1]是未定义行为。但是,尽管p->foo[1]超出了对象foo的范围,但它并没有超出使用malloc分配的char数组的范围,因此它是在一个对象内部。通过char *进行适当的强制转换,至少可以进行读取访问。不过,我无法记住标准法律术语的具体规定。 - Steve Jessop
13
好的,我猜我得自食其言。WG14声称这是未定义行为(缺陷报告51: http://www.open-std.org/Jtc1/sc22/wg14/www/docs/dr_051.html)。然而,我认为1) WG14在DR51中建议的“更安全的习惯用语”完全荒谬, 2) 在我所使用的所有重要平台上,该未定义行为都能够按预期运行,3) 其他选择(也不属于未定义行为)更为不便和/或更容易出错(因此更可能导致明显的错误) - 所以我可能会继续使用它。但现在至少我会知道我正在违反规定... - Michael Burr
5
是的,我并不是说这不是一种有用的技术,只是它不是严格符合要求的。不幸的是,更安全的表达方式也存在另一个未定义行为的问题。你只能对真正存在的对象执行指针运算,而一个POD对象只有在分配了足够对齐和大小的内存后才开始存在。如果某个声明了非常大数组的东西,而你没有为其分配足够的空间,它就无法开始存在。 - CB Bailey
显示剩余5条评论

36

C++在1998年首次被标准化,因此它比C中灵活数组成员的添加要早(这是在C99中新增的)。在2003年有一个对C++的勘误,但那并没有添加任何相关的新功能。下一版的C++(C++2b)仍在开发中,似乎仍未添加灵活数组成员。


16
请你更新这个回答?看起来C++11没有添加灵活数组成员(§9.2/9),而且C++14似乎也是一样的。 - Adam Rosenfield
8
C++17也没有这些功能,但我认为它们仍在研究中,所以可能会出现在C++2a中? - Daniel H
3
有什么办法可以让GCC g++给出一个警告?使用-std=c++xx -Wall -Wextra好像不够,感觉很可怕。:-( - Ciro Santilli OurBigBook.com
4
如果我记得正确的话,使用-Wpedantic或者-pedantic会触发警告。 - Werner Henze
3
尽管有一篇论文存在,但C++20仍然没有任何改变。链接如下:https://thephd.github.io/vendor/future_cxx/papers/d1039.html - Flamefire
仍然在GNU++23中收到警告。@CiroSantilliOurBigBook.com我使用了“-Wall -Wextra -pedantic -pg -g3 -Os -flto -save-temps -std=gnu++23 -fdiagnosics-color=always”。 - HackerDaGreat57

5

如果您的应用程序仅需要几个已知的大小,那么您可以使用模板有效地实现一个灵活的数组。

template <typename BASE, typename T, unsigned SZ>
struct Flex : public BASE {
    T flex_[SZ];
};

我不理解你关于“将应用程序限制为仅需要几个已知的尺寸”的评论,我不清楚在使用这种方法时为什么会考虑到这一点。你能进一步解释一下吗? - Mark Ch
@MarkCh:模板必须在编译时实例化,因此flex_的大小将在编译时固定。但是,您可以使用任意多个大小,每个大小将表示不同的类型。 - jxh
2
固定和灵活对我来说似乎相互矛盾。 - dtech

4

如果你只想要

struct blah { int foo[]; };

那么你根本不需要结构体,而只需处理一个malloc/new分配的int数组。

如果你有一些成员在开头:

struct blah { char a,b; /*int foo[]; //not valid in C++*/ };

然后在C++中,我想你可以用一个foo成员函数来替换foo

struct blah { alignas(int) char a; char b; 
    int *foo(void) { return reinterpret_cast<int*>(&this[1]); } };

使用示例:

#include <stdlib.h>
struct blah { 
    alignas(int) char a;
    char b;
    ////////
    int *foo(void) { return reinterpret_cast<int*>(&this[1]); }
};
int main()
{
    blah *b = (blah*)malloc(sizeof(blah)+10*sizeof(int));
    if(!b) return 1;
    b->foo()[1]=1;
}

这种在结构体后将内存强制转换的类型不存在严格别名问题,因为该内存是动态的(没有声明类型)。

@AndrewHenle 将 int 替换为 double,包括 alignas 和强制类型转换/返回值。一般来说,在 C 或 C++ 中,只要(1)有足够的空间和(2)结构体具有相等或更大的对齐方式,就可以在 malloced/aligned_alloced 结构体之后放置数组,以便 "just after" (&this[1]) 足够对齐。 - Petr Skocik
我只是担心使用alignas()来对齐一种不太严格对齐的类型可能会强制将一个更严格对齐的struct元素转换为无效的对齐方式。用最严格的元素类型替换alignas()并不是一个好的解决方案,因为包含其他structs的复杂structs可能会因添加了更严格对齐的类型而导致其中一个子struct发生改变。 - Andrew Henle
1
我听说过一些(在我看来很有能力的)人争论说,弹性数组成员甚至是不需要的,只要有空间,就没有问题索引超过最后一个大小为1的数组。是的,不对。这是未定义行为并且可能会给你带来麻烦 - Andrew Henle
1
@AndrewHenle 是的,这就是为什么我说我不会打赌的原因。 :) 至于alignas不适当地减少对齐,这是不可能发生的(def. with _Alignas,我假设C++的行为相同)。试图这样做是一种约束违规(=编译器错误)。 - Petr Skocik
1
有了这个,我会说你的方法在最终的1大小数组“struct hack”上是一个重大的改进。根据您对“alignas()”的描述,我没有看到任何UB被调用的方式,这是我最初的担忧。 - Andrew Henle
显示剩余4条评论

4
第二个不包含元素,而是指向blah之后。因此,如果您有这样的结构:
struct something
{
  int a, b;
  int c[0];
};

你可以像这样做:

你可以像这样做:

struct something *val = (struct something *)malloc(sizeof(struct something) + 5 * sizeof(int));
val->a = 1;
val->b = 2;
val->c[0] = 3;

在这种情况下,c将作为一个具有5个int的数组,但数组中的数据将位于something结构体之后。
我正在开发的产品使用这个大小可变的字符串:
struct String
{
  unsigned int allocated;
  unsigned int size;
  char data[0];
};

由于支持的架构,这将消耗8个字节加上已分配
当然,所有这些都是C语言,但例如g++会毫不犹豫地接受它。

这很有趣。我想你不可能将此结构作为值传递给函数,对吧?因为那样可能只会传递 sizeof(String),而不会考虑你为 data 分配的大小。但只要你将其作为引用或指针传递,它应该可以工作,对吗? - filipe
好的,你可以通过它,但它只会传递字符串中的数据。此外,根据编译器的不同,它可能会生成一些警告。 - terminus
5
这不是真的。在C和C++中,T[0]既不是有效的类型说明符,也不是合法的语法。你应该使用T[] - Johannes Schaub - litb
1
@Johannes 在 C99 中是有效的,请参见 http://www.open-std.org/jtc1/sc22/wg14/www/newinc9x.htm。 - terminus
5
当然,所有这些都是C语言,但是例如g++却毫不费力地接受了它。对于g++来说很好。根据标准,这仍然是未定义行为,因此应该避免使用。@terminus 那个链接只提到了C99中存在柔性数组成员(但从未提到C ++);它并不支持您关于使用[0]声明它们的论点,这是无效的语法。 - underscore_d
6
在任何版本的C语言中,零长度数组都是无效的。您不能拥有零长度的可变长度数组(VLA)。这是GCC非标准扩展。这段代码在标准的C和C++中都无法编译。 - Lundin

4

是的!“设计”部分很好地解释了为什么C++到目前为止没有正式允许灵活数组成员:如果FAM结构体不是物理上位于周围类的末尾,代码将会悄悄地出错(必须是最后一个成员,不能是基类,...)https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1039r0.html#design - undefined
@SusanneOberhauser 我猜最初不支持C++的原因是因为它具有继承,而继承需要对基类的布局做出假设才能工作。但是在C++11中,有一种无法继承的final类,所以一个好的解决方案是只允许在final类中使用继承? - undefined
1
在C语言中,一个具有可变数组成员的结构体不能嵌套在另一个结构体中,即使是在最后位置也不行。因此,将结构体定义为最终类解决了其中一个挑战,但并没有解决另一个挑战(结构体作为成员的情况)。 - undefined
1
@SusanneOberhauser 我认为可以采用与C语言中指定的相同限制,即不嵌入其他结构并且只能作为最后一个成员。最具破坏性的变化将出现在分配这种对象上(以及我们在其生命周期中如何跟踪其大小),无论是在动态分配还是静态分配的情况下。这将对实现者提出挑战,在C语言中这些挑战是无关紧要的,因为在那里分配(和跟踪大小)是手动的。 - undefined

3
灵活数组目前还不是C++标准的一部分。这就是为什么 int foo[] 或者 int foo[0] 可能无法编译。虽然有一个提案正在讨论中,但它尚未被最新版本的C++(C++2b)接受。
然而,几乎所有现代编译器都通过编译器扩展来支持它。

问题在于,如果您在最高警告级别(-Wall --pedantic)下使用此扩展,可能会导致警告。

一种解决方法是使用一个只有一个元素的数组并进行越界访问。虽然这种解决方案在规范(dcl.arrayexpr.add)中被视为未定义行为,但大多数编译器将生成有效的代码,甚至clang -fsanitize=undefined也可以正常工作:
#include <new>
#include <type_traits>

struct A {
    int a[1];
};

int main()
{
    using storage_type = std::aligned_storage_t<1024, alignof(A)>;
    static storage_type memory;
    
    A *ptr_a = new (&memory) A;

    ptr_a->a[2] = 42;
    
    return ptr_a->a[2];
}

demo

可以翻译为:

{{链接1:演示}}



1
我曾遇到同样的问题,即声明一个灵活的数组成员,可以从C++代码中使用。通过查看 glibc 头文件,我发现有一些可变数组成员的用法,例如在声明为以下内容的 struct inotify 中(省略了注释和一些无关的成员):
struct inotify_event
{
  //Some members
  char name __flexarr;
};

__flexarr宏的定义如下:

/* Support for flexible arrays.
   Headers that should use flexible arrays only if they're "real"
   (e.g. only if they won't affect sizeof()) should test
   #if __glibc_c99_flexarr_available.  */
#if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L
# define __flexarr  []
# define __glibc_c99_flexarr_available 1
#elif __GNUC_PREREQ (2,97)
/* GCC 2.97 supports C99 flexible array members as an extension,
   even when in C89 mode or compiling C++ (any version).  */
# define __flexarr  []
# define __glibc_c99_flexarr_available 1
#elif defined __GNUC__
/* Pre-2.97 GCC did not support C99 flexible arrays but did have
   an equivalent extension with slightly different notation.  */
# define __flexarr  [0]
# define __glibc_c99_flexarr_available 1
#else
/* Some other non-C99 compiler.  Approximate with [1].  */
# define __flexarr  [1]
# define __glibc_c99_flexarr_available 0
#endif

我不熟悉MSVC编译器,但是可能需要根据MSVC版本添加一个条件宏。


0

标准C++不支持灵活数组成员,然而clang文档表示:

“除了这里列出的语言扩展之外,Clang旨在支持广泛的GCC扩展。”

C++的gcc文档表示:

“GNU编译器为C++语言提供这些扩展(您还可以在C++程序中使用大多数C语言扩展)。”

而C的gcc文档则支持零长度数组。

https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html


-3
更好的解决方案是将其声明为指针:
struct blah
{
    int* foo;
};

或者更好的方式是将其声明为std::vector:
struct blah
{
    std::vector<int> foo;
};

3
这两个都不容易序列化,这正是灵活数组成员的全部意义所在。 - doron
4
不,int[0]并不会创建一个指针。请参考terminus的回答。 - kriss
1
@Zac Howland:没问题。我会称之为地址,它的重要性在于它还允许在结构体中定义一个内存对齐的零长度成员。即使使用C++,在处理硬件感知的低级程序时也有用处。我同意doron关于序列化的观点。 - kriss
真遗憾它不在标准中,而是特定于供应商的扩展 :-( - kriss
@ZacHowland:向量中的元素保证彼此相邻,但保证与“struct blah”的其他成员不相邻。另一方面,灵活数组元素与“struct blah”的其他成员是相邻的。 - Ben Voigt
显示剩余2条评论

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