从C++的POD谈起,到memcpy/memmove的安全性

C++中的POD类型(Plain Old Data)谈起,到memcpy的安全性

​ 最近在写代码时恰巧看了下GMP的源码,发现了里面对委托函数的参数类型规定为指针,引用或是pod,其中pod的判断是std::is_pod,那么什么是POD类型呢?

​ 我们知道C++在一定程度上可以说是C的超集,在C中struct不过是一堆数据的合集,但是在c++中,类的概念出现了,它不仅仅是简单的把数据放在一起,还有很多其他的机制,比如构造函数,析构函数等。在C++中如果一个类和c中的结构体相同,这样的类被称作pod(plain old data),可以通过std::is_pod检查一个类是否是pod类。

​ 在c++,把pod这个要求拆分成了一些子要求的组合。具体来说:

is_pod可以拆分为is_trivialis_standard_layout

  • 其中is_trivial又可以拆分成is_trivially_copyableis_trivially_default_constructibletrivial主要是对构造函数和析构函数的要求

  • is_standrad_layout主要是对子对象内存布局的要求

std::is_trivial

std::is_trivially_copyable

  • 没有非平凡的拷贝构造/赋值函数(这也不需要虚函数或虚基)
  • 没有非平凡的移动构造/赋值函数
  • 有一个普通的析构函数

std::is_trivially_default_constructible

​ 这个要求就是一个类要有默认的构造函数,默认的构造函数可以不做任何事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//not default ctor
class A{
public:
A(){};
};

int main()
{
auto x = is_trivial<A>(); // false
cout<<x<<endl;
return 0;
}
//default ctor
class A{
public:
A() = default;//默认
};

int main()
{
auto x = is_trivial<A>(); // true
cout<<x<<endl;
return 0;
}

小结

到此我们可以看出什么是trivally的类型呢?

我们常说的的BigSix(dtor,default ctor,copy ctor,move ctor,copy assginment operator, move assginment operator)中:

  • 默认构造函数是必须是默认生成的。

  • 剩下的五个是trivial函数即可。

trival函数的定义

  • 首先我们要明确,trival函数只可以是BigSix之一,也就是说不是bigsix的函数没有trival不trival之分。

C++标案里有定义到:

Default constructor, §12.1/4:

A default constructor is trivial if it is not user-provided and if:

  • its class has no virtual functions (10.3) and no virtual base classes (10.1), and
  • no non-static data member of its class has a brace-or-equal-initializer, and
  • all the direct base classes of its class have trivial default constructors, and
  • for all the non-static data members of its class that are of class type (or array thereof), each such class has a trivial default constructor.

Otherwise, the default constructor is non-trivial.

Copy/move constructors, §12.8/12:

A copy/move constructor for class X is trivial if it is not user-provided, its parameter-type-list is equivalent to the parameter-type-list of an implicit declaration, and if

  • class X has no virtual functions (10.3) and no virtual base classes (10.1), and
  • class X has no non-static data members of volatile-qualified type, and
  • the constructor selected to copy/move each direct base class subobject is trivial, and
  • for each non-static data member of X that is of class type (or array thereof), the constructor selected to copy/move that member is trivial;

otherwise the copy/move constructor is non-trivial.

Copy/move assignment operator, §12.8/26:

A copy/move assignment operator for class X is trivial if it is not user-provided, its parameter-type-list is equivalent to the parameter-type-list of an implicit declaration, and if

  • class X has no virtual functions (10.3) and no virtual base classes (10.1), and
  • class X has no non-static data members of volatile-qualified type, and
  • the assignment operator selected to copy/move each direct base class
  • for each non-static data member of X that is of class type (or array thereof), the assignment operator selected to copy/move that member is trivial;

otherwise the copy/move assignment operator is non-trivial.

Destructor, §12.4/5:

A destructor is trivial if it is not user-provided and if:

  • the destructor is not virtual,
  • all of the direct base classes of its class have trivial destructors, and
  • for all of the non-static data members of its class that are of class type (or array thereof), each such class has a trivial destructor.

Otherwise, the destructor is non-trivial

不妨这里给出一个不太严谨但是差不多够用的定义:

  • 只可以是六种类型的函数:default ctor、copy ctor、dtor、copy assignment operator、move ctor和move assignment operator
  • 函数所在的类里没有虚函数或虚基类
  • 如果函数所在类继承于一个基类,该基类相关的函数也必须是trivial函数
  • 函数所在的类的类成员,也需要是trivial类

trival函数的影响

trivial函数会影响以下方面(出自C++ Concurrency In Action(Second Edition) P361):

  • 对于principle of three里的三个函数: 只有copy ctor、copy assignment operator和dtor都为trivial函数的类对应的对象,才可以用memcpy或memmove来进行对象的复制
  • 用于constexpr函数的Literal Types,需要有trivial copy ctor、trivial copy assignment operator和trivial dtor函数
  • 如果一个类,其default ctor、copy assignment operator、copy ctor和dtor都为trivial函数,则该class can be used in a union with a user-defined ctor and dtor
  • 如果一个类,定义了trivial copy assignment operator,则该类可以用于std::atomic<>从而提供a value of that type with atomic operations

std::is_standard_layout

​ 这个要求其实是对内存布局的一个要求,在c中的结构体很简单,没有各种引用,继承,不同访问权限的数据成员的类,虚函数等等东西。

standard layout类应该具有以下特点:

  • 没有虚函数或者虚基类
  • 所有的非static的data members都是相同的访问级别
  • 所有的非static的data members都是standard layout类的对象
  • 如果有基类,基类也是standard layout
  • has no base classes of the same type as the first non-static data member.(没有与其第一个非静态数据成员类型相同的基类)
  • 满足以下两个条件之一:
    • 要么在最派生类中没有非静态数据成员且至多有一个具有非静态数据成员的基类,
    • 要么没有具有非静态数据成员的基类
    • 总结下就是: 总之在继承链上的所有类型只有一个地方可以出现非静态数据。不然版本编译器可能会给出不同的优化策略,内存布局也会不同。

如果不满足上面任何一种则不是standard_layout。

如何判断一个类的对象是否可以用memcpy来复制

根据前面的介绍,应该是需要该类为standard layout类,而查阅资料后发现,其实只要is_trivially_copyable即可,即可,即使不满足standard layout,也可以使用memcpy,比如下面的类:

1
2
3
4
5
struct A {
int x;
private:
int y;
};

image-20230226225012028

这位老哥提出“可以将构成对象的底层字节复制到 char 或 unsigned char 数组中。如果将 char 或 unsigned char 数组的内容复制回对象,则对象随后应保持其原始值”。这就是trivally copyable的意义。

至于std::is_standard_layout,则一般用于保证C++与其他语言(比如C语言)的内存分布一致。

std::is_standard_layou is useful for communicating with other languages (for creating language bindings to native C++ libraries e.g.), and that’s why a standard-layout class has the same memory layout of the equivalent C struct or union.

写在最后

​ 最近在写代码时恰巧看了下GMP的源码,发现了里面对委托函数的参数类型规定为指针,引用或是pod,其中pod的判断是std::is_pod,那么什么是POD类型呢?

为什么GMP里规定了POD呢?主要是因为GMP也会在unlua语言使用,因此is_standard_layout是必须的,同时is_trivally也是必须的,因为作者希望委托函数的参数T也可以是int/float这种基本类型,trival type。

综合起来这其实就是一个POD类型,因此作者做了这样一个判断。