[c/c++]trivial/POD类型和standard layout

什么是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 
#include 

int main()
{
    using namespace std;

    cout << is_trivially_default_constructible::value << std::endl;
    cout << is_trivially_copy_constructible::value << std::endl;
    cout << is_trivially_move_constructible::value << std::endl;
    cout << is_trivially_copy_assignable::value << std::endl;
    cout << is_trivially_move_assignable::value << std::endl;
    cout << is_trivially_destructible::value << std::endl;
}

追根溯源

一般而言,在C++库的底层,一个对象的生命周期都会经历以下几个步骤:

#include 


template 
void life_of_an_object
{
    std::allocator 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 
#include 

template 
void myDestroy(Iterator first, Iterator last)
{
    using value_type = typename std::iterator_traits::value_type;

    while (first != last) {
        first->~value_type();
        ++first;
    }
}

template void myDestroy(Foo*, Foo*);
template void myDestroy(std::set::iterator, std::set::iterator);
template void myDestroy(std::list::iterator, std::list::iterator);

image.png

好家伙,就嗯在链表,二叉树上面便利了一遍,这样的遍历,其实是毫无用处毫无意义的吧,对吧?只是遍历了一遍,不做任何事。

你可能感兴趣的:([c/c++]trivial/POD类型和standard layout)