什么是trivial/POD类型?
C++20标准之前,POD类型指符合C的平凡旧数据结构(Plain Old Data),即类似C中结构体的平凡的、不具备特殊操作的数据结构,可以用于元数据交换的数据类型,直接以二进制和C库兼容的数据类型。
设立此概念的初衷是为了描述那些 和 C 中结构体的概念相似的类型
。
但是,这个概念是太过于抽象和难以精确、严禁描述的。通过标准中对 POD 定义的变动,甚至在 C++20 中 std::is_pod 被弃用等种种变化可以看出,这是一个很难刻画的概念。
C++20标准后规则上,POD类型拆分为以下两个定义
C++20标准将POD类型的概念拆分为两个基本概念的合集,即平凡的(trivial)和标准布局(standard layout)。
C++20标准之前,有std::is_pod
可以判对象是否是POD类型, 但在C++20之后std::is_pod
被弃用,建议使用两个新的判断条件
std::is_trivial && std::is_standard_layout
去代替。
1)平凡的
一个平凡的类或者结构体应包含以下定义
有平凡的缺省构造函数,可用这样的默认语法:(SomeConstructor() = default;)
有平凡的copy与move构造函数,可用默认语法.
有平凡的copy与move运算符,可用默认语法.
有平凡的destructor,不能是虚函数.
不包含虚函数和虚基类
2)标准布局的
所有非静态成员有相同的访问权限(public protected privete)
派生类中有非静态成员,且只有一个仅包含静态成员的基类。
基类有非静态成员,派生类中没有非静态成员
类中的第一个非静态成员的类型与其基类不同
没有虚函数与虚基类
所有非静态数据成员均符合标准布局类型,基类也符合标准布局
POD类型的体现
POD类型可以直接使用memcpy和memset来操作,而不损失功能
POD 只是可以安全使用 memcpy 的充分非必要条件。其实只要这个类型是 TriviallyCopyable 的,那就能安全地使用 memcpy 去拷贝它。而 POD 是相比 TriviallyCopyable 更加严格的限制。
下面我们看看为什么会有POD类型的概念
首先,众所周知,C++ 的类里头,有六个最为特殊的成员函数:
- 默认构造函数,即 T::T( )
- 拷贝构造函数,即 T::T( (const) (volatile) T&)
- 拷贝赋值运算符,即 T::operator=( (const) (volatile) T&)
- 析构函数,即 T::~T()移动构造函数,即 T::T( (const) (volatile) T&&)
- 移动赋值运算符,即 T::operator=( (const) (volatile) T&&)
不严谨地来说,只要这个类的以上对应的成员函数,不做什么”额外“的动作,那么这个成员函数就是 Trivial (平凡) 的。举一些例子吧。
struct Foo
{
int x;
};
Foo 六个成员函数全部都是平凡的,因为:默认构造函数不做任何初始化动作(连 .x 初始化为 0 也不会做)拷贝/移动构造函数只是老老实实地依次拷贝/移动各个成员拷贝/移动赋值函数只是老老实实地依次拷贝/移动赋值各个成员析构函数什么也没做.
struct Foo
{
int x;
Foo() = default;
Foo(const Foo &) = default;
Foo(Foo &&) = default;
Foo& operator=(const Foo &) = default;
Foo& operator=(Foo &&) = default;
~Foo() = default;
};
同样,2的六个成员函数全部是trivial的
struct Foo
{
int x;
Foo() {}
Foo(const Foo & src) : x(src.x) {}
Foo(Foo && src) : x(std::move(src.x)) {}
Foo& operator=(const Foo &) {}
Foo& operator=(Foo &&) {}
~Foo() {}
};
抱歉,这里的六个构造函数全都不是trivial的,因为哪怕就是空的函数体,或者是做了和默认构造一样的操作,也是做了特殊操作,那么我们默认这改变了默认行为,所以是非trivial的。
struct Goo
{
Goo() = default;
Goo(const Goo&) { std::cout << 2333 << std::endl;} // 我不平凡!
Goo(Goo &&) = default;
Goo& operator=(const Goo&) = default;
Goo& operator=(Goo &&) = default;
};
struct Foo : Goo
{
Foo() = default;
Foo(const Foo &) = default;
Foo(Foo &&) = default;
Foo& operator=(const Foo &) = default;
Foo& operator=(Foo &&) = default;
~Foo() = default;
};
同样,4的拷贝构造函数也是非trivial,Foo的拷贝构造也是非trival,因为Foo一定会调用Goo的非trivial拷贝构造函数
struct Goo
{
Goo() = default;
Goo(const Goo&) = default;
Goo(Goo &&) = default;
Goo& operator=(const Goo&) = default;
Goo& operator=(Goo &&) = default;
virtual void f() {}
};
struct Foo : Goo
{
Foo() = default;
Foo(const Foo &) = default;
Foo(Foo &&) = default;
Foo& operator=(const Foo &) = default;
Foo& operator=(Foo &&) = default;
~Foo() = default;
};
由于有虚函数,除了析构函数外,其他五个ctor都会对虚指针做一些额外的工作,所以也不满足trivial的概念。
到此为止,差不多就能够理解什么是trivial 平凡的类了
条件比较繁琐,可以用以下函数来做检测:
#include <type_traits>
#include <iostream>
int main()
{
using namespace std;
cout << is_trivially_default_constructible<Foo>::value << std::endl;
cout << is_trivially_copy_constructible<Foo>::value << std::endl;
cout << is_trivially_move_constructible<Foo>::value << std::endl;
cout << is_trivially_copy_assignable<Foo>::value << std::endl;
cout << is_trivially_move_assignable<Foo>::value << std::endl;
cout << is_trivially_destructible<Foo>::value << std::endl;
}
追根溯源
一般而言,在C++库的底层,一个对象的生命周期都会经历以下几个步骤:
#include <memory>
template <typename T>
void life_of_an_object
{
std::allocator<T> alloc;
// 1. 通过 allocator 抑或是 malloc 等其他方式分配出空间
T * p = alloc.allocate(1);
// 2. 通过 placement new,(在需要的时候) 动态地构造对象
new (p) T(); // 这里是默认构造,也可能是带参数的构造方式如 new (p) T(构造参数...);
// 3. 通过显式调用析构函数,(在需要的时候) 动态地销毁对象
p->~T();
// 4. 通过分配函数的对应的解分配手段,解分配空间
alloc.deallocate(p, 1);
}
如果 T 类型是平凡默认构造的,则意味着步骤 2 其实是不需要的——反正 T 类型在默认构造的时候,什么也没做,没有清零内部空间什么的。步骤 2 不做不会对程序的正确性构成任何影响。
如果 T 类型是平凡析构的,则意味着步骤 3 其实是不需要的——反正 T 类型在析构的时候,什么也没做,不用释放内存,不用 close 文件,不用释放 socket…… 。步骤 3 不做同样也不会对程序的正确性构成任何影响。
那我们在模板库中,就可以为对应的成员函数为 Trivial 的类型,做单独的特化,从而提高性能。
诚然,对于这种最简单、最显而易见的情况,哪个编译器做不了优化,哪个是辣鸡。
但是情况要是复杂一些呢?如果被析构的是一段区间呢?
struct Foo
{
~Foo() = default;
};
#include <list>
#include <set>
template <typename Iterator>
void myDestroy(Iterator first, Iterator last)
{
using value_type = typename std::iterator_traits<Iterator>::value_type;
while (first != last) {
first->~value_type();
++first;
}
}
template void myDestroy(Foo*, Foo*);
template void myDestroy(std::set<Foo>::iterator, std::set<Foo>::iterator);
template void myDestroy(std::list<Foo>::iterator, std::list<Foo>::iterator);
好家伙,就嗯在链表,二叉树上面便利了一遍,这样的遍历,其实是毫无用处毫无意义的吧,对吧?只是遍历了一遍,不做任何事。