std::array是否与旧的C数组兼容?

38

一个 std::array<T,N> v 和一个 T u[N] 的底层位表示是否相同?

换句话说,从一个数组复制 N*sizeof(T) 字节到另一个数组是安全的吗?(无论是通过 reinterpret_cast 还是 memcpy。)

编辑:

为了澄清,重点在于 相同的位表示reinterpret_cast

例如,假设我有这两个类,它们都是一些平凡可复制的类型 T,对于某个 N

struct VecNew {
    std::array<T,N> v;
};

struct VecOld {
    T v[N];
};

还有遗留功能

T foo(const VecOld& x);

如果这些表示相同,那么该调用是安全的且避免了复制:
VecNew x;
foo(reinterpret_cast<const VecOld&>(x));

你是使用 data/&array_name[0] 还是直接使用 "array" 的名称进行复制? - NathanOliver
5
不能使用reinterpret_cast,因为会违反严格别名规则。 - Dan
1
嗯...原始问题是关于复制的,而新问题则涉及到reinterpret_cast。这有些不同... - Barry
@Barry,最初的问题是“std::array是否与旧的C数组位兼容?”,现在仍然是这样。其他所有内容都是由此引起的。 - shinjin
1
你似乎正在尝试通过用新的构造替换旧的构造来现代化遗留的C++代码,是吗? - Laurent LA RIZZA
1
然后有人通过添加新字段更改了“VecNew”,并享受调试。不,谢谢。 - Slava
5个回答

20

这并不直接回答你的问题,但你应该简单地使用std::copy

T c[N];
std::array<T, N> cpp;

// from C to C++
std::copy(std::begin(c), std::end(c), std::begin(cpp));

// from C++ to C
std::copy(std::begin(cpp), std::end(cpp), std::begin(c));

如果T是一个可以平凡复制的类型,那么它会编译成memcpy。如果不是,则进行逐个元素的复制赋值并且是正确的。无论哪种方式,这都能做到正确的事情,并且非常易读。不需要手动字节算术运算。

7
Nitpick:std::copy 并不总是编译成 memcpy,这是一个实现细节。例如,VC++ 在字节复制时使用 memmove - Mgetz
10
我犹豫不决。这是一个很好的答案……适用于另一个问题! - underscore_d
看起来只有当两个参数是相同的数组类型时(只有testtest3编译成memmove),它才执行快速复制。https://godbolt.org/g/SGdWwp - Bad_ptr

16

std::array 提供了 data() 方法,可用于与适当大小的c风格数组进行复制:

const size_t size = 123;
int carray[size];
std::array<int,size> array;

memcpy( carray, array.data(), sizeof(int) * size );
memcpy( array.data(), carray, sizeof(int) * size );

文档所述。

该容器是一个聚合类型,其语义与持有C风格数组T[N]作为其唯一非静态数据成员的结构相同。

因此,内存占用量似乎与C风格数组兼容,尽管不清楚为什么要在存在没有任何开销的正确方法时使用reinterpret_cast "hack"。


5
我想要澄清的正是“看起来是这样”的部分。 - shinjin
你没有回答为什么。 - Slava

11

我认为可以(但标准并不保证)。

根据[array]/2:

数组是一个聚合体([dcl.init.aggr]),可以用最多N个可转换为T的元素进行列表初始化。

而[dcl.init.aggr]则定义了:

聚合体是一个数组或者一个类(第[class]条),它:

  • 没有用户自定义、显式或继承构造函数([class.ctor]);

  • 没有私有或受保护的非静态数据成员(第[class.access]条);

  • 没有虚函数([class.virtual]);

  • 没有虚拟、私有或受保护的基类([class.mi])。

由此可见,“可以用列表初始化”只有在类开头没有其他成员且没有虚表时才可能实现。

data()的规定如下:

constexpr T* data() noexcept;

返回值:这样的指针,使得[data(), data() + size())是一个有效范围,且data() == addressof(front())

标准基本上想表达“它返回一个数组”,但同时为其他实现留下了余地。

唯一可能的其他实现是一个带有单独元素的结构体,在这种情况下,您可能会遇到别名问题。但在我看来,这种方法除了增加复杂性外没有任何好处。将数组展开成结构体是得不偿失的。

因此,将std::array不实现成数组是没有意义的

但是仍然存在一个漏洞。


我不同意可能会出现别名问题。你对这个说法的理由是什么? - Brian Bi
结构体和数组在严格别名方面是不兼容的类型。 - rustyx
我认为你对严格别名规则的解释不正确。如果那是这样的话,那么数组类型也将与其元素类型不兼容,这显然是荒谬的。 - Brian Bi
他对严格别名的主张并不暗示你所声称的意思。 - alexchandel
1
@Brian,这不是RustyX所说的。数组从来都不兼容具有相同数量和类型成员的“struct”。然而,即使您对指向数组与指向其元素的指针的兼容性的旁推也很快会变得非常真实!请参阅ecatmur的答案,了解正在起草的P0137R1中的乐趣。如果您愿意并且有能力,请提交国家机构评论以表达怀疑。 - underscore_d

9
data()方法的要求是返回一个指向T*的指针,使得:

[data(),data() + size())是有效的范围,并且data() == addressof(front())

这意味着您可以通过data()指针顺序访问每个元素,因此如果T是平凡可复制的,则确实可以使用memcpysizeof(T) * size()字节从/到数组T[size()]中进行复制,因为这相当于逐个复制每个元素。
但是,您不能使用reinterpret_cast,因为那会违反严格别名规则,因为data()不需要实际上由数组支持 - 而且,即使您保证std::array包含一个数组,自C++17以来,您也不能(即使使用reinterpret_cast)将指向数组的指针强制转换为其第一个成员的指针(必须使用std::launder)。

5
“自C++17起,即使使用reinterpret_cast,你也不能将指向数组的指针强制转换为指向其第一个成员的指针(必须使用std::launder)”,听起来很有趣:委员会疯狂暴走了!请提供更多信息。与此同时,我会准备一些爆米花。” - Cheers and hth. - Alf
2
@Cheersandhth.-Alf:“指向数组的指针不能转换为/从其第一个元素的指针”:请参见http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0137r1.html。 - ecatmur
3
关于链接,那是一整块文字。有几百公里长。能不能大致指出一个不到203米的区域? - Cheers and hth. - Alf
2
看起来这是关于让编译器供应商控制标准管理的问题,因此,该编译器的缺陷和愚蠢行为变得标准化了。哦,好吧。 - Cheers and hth. - Alf
4
这句话的意思是:这不是关于危险,而是关于优化;许多科学代码(咳嗽 SPEC 咳嗽)可以在编译器假定不同大小的数组和指针即使元素类型相同时也不会别名的情况下得到有效加速。编译器作者认为这种加速效果对于编写科学、Fortran风格代码的客户来说非常值得(公平地说)。这种加速效果被认为是值得的,尽管可能会导致编写更多面向系统或对象的代码的用户产生混乱和破坏。 - ecatmur
显示剩余13条评论

7

array不太关心你在其上实例化的基础类型。

如果要使用memcpyreinterpret_cast进行复制并获得有用的结果,您实例化的类型必须是平凡可复制的。将这些项存储在array中不影响memcpy(等等)仅适用于平凡可复制类型的要求。

array需要成为一个连续容器和聚合体,这基本上意味着元素的存储必须是一个数组。标准显示为:

T elems[N]; // exposition only

它随后有一条注释,至少暗示着它必须是一个数组(§[array.overview]/4):
[注:成员变量elems仅用于说明array是一个类集合。 名称elems不是array的接口的一部分。 ——注解]
[强调添加]
请注意,真正不需要的只是特定的名称elems

2
这个新草案已经摆脱了那部分。现在我们只需要知道它是一个聚合体,可以用NT(但+1)进行列表初始化。 - Barry
@Barry:我并不确定这真的改变了什么。一时之间,我没有看到满足其要求(连续储存器,聚合体)的方法,除了只有一个数据成员,而这个数据成员是一个数组。我想,如果你能保证元素之间没有填充(padding),你可以创建具有离散元素的可变参数模板,但这仅限于因为元素仍然像数组一样可以寻址。 - Jerry Coffin
1
@JerryCoffin 噢,我并不是说std::array不肯定是对原始数组的包装器。我只是想说现在关于该描述的措辞完全不同了(不确定这种变化的意义是什么,只是想指出)。 - Barry
如果存储是以正确的顺序离散成员,则初始化(但不包括其他部分)可以工作。 - Jerry Coffin
我的意思是,data功能只保证在那里有一个有效的数组。初始化意味着在该数组之前没有任何内容。因此,存在一个数组,并且在它之前没有任何内容,因此作为结构体的第一个成员,可以将其指针重新解释为指向结构体的指针,反之亦然(这是关于类成员的部分结束时的情况)。 - Cheers and hth. - Alf
显示剩余2条评论

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