[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 <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);

image.png

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容