• Zeno节点系统中的C++最佳实践


    1.经典的多态案例

    IObject 具有一个 eatFood 纯虚函数,而 CatObject 和 DogObject 继承自 IObject,他们实现了 eatFood 这个虚函数,实现了多态。

    • 注意这里解构函数(~IObject)也需要是虚函数,否则以 IObject * 存储的指针在 delete 时只会释放 IObject 里的成员,而不会释放 CatObject 里的成员 string m_catFood。

    • 所以这里的解构函数也是多态的,他根据类型的不同调用不同派生类的解构函数。

    • override 作用:减少告警,派生类的override写错的话,也不会重新创建一个新的虚函数,比较安全

    • eg:my_course/course/15/a.cpp

    #include 
    #include 
    #include 
    
    using namespace std;
    
    struct IObject {
        IObject() = default;
        IObject(IObject const &) = default;
        IObject &operator=(IObject const &) = default;
        virtual ~IObject() = default;
    
        virtual void eatFood() = 0;
    };
    
    struct CatObject : IObject {
        string m_catFood = "someFish";
    
        virtual void eatFood() override {
            cout << "cat is eating " << m_catFood << endl;
            m_catFood = "fishBones";
        }
    
        virtual ~CatObject() override = default;
    };
    
    struct DogObject : IObject {
        string m_dogFood = "someMeat";
    
        virtual void eatFood() override {
            cout << "dog is eating " << m_dogFood << endl;
            m_dogFood = "meatBones";
        }
    
        virtual ~DogObject() override = default;
    };
    
    int main() {
        shared_ptr<CatObject> cat = make_shared<CatObject>();
        shared_ptr<DogObject> dog = make_shared<DogObject>();
    
        cat->eatFood();
        cat->eatFood();
    
        dog->eatFood();
        dog->eatFood();
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 测试:
      在这里插入图片描述

    (1)多态用于设计模式之“模板模式”

    这样之后如果有一个任务是要基于 eatFood 做文章,比如要重复 eatFood 两遍。

    • 就可以封装到一个函数 eatTwice 里,这个函数只需接受他们共同的基类 IObject 作为参数,然后调用 eatFood 这个虚函数来做事
    • 这样只需要写一遍 eatTwice,就可以对猫和狗都适用,实现代码的复用(dont-repeat-yourself),也让函数的作者不必去关注点从猫和狗的其他具体细节,只需把握住他们统一具有的“吃”这个接口
    • 只要参数不涉及生命周期,那么一定要用普通指针
    • eg:my_course/course/15/a.cpp
    #include 
    #include 
    #include 
    
    using namespace std;
    
    struct IObject
    {
        IObject() = default;
        IObject(IObject const &) = default;
        IObject &operator=(IObject const &) = default;
        virtual ~IObject() = default;
    
        virtual void eatFood() = 0;
    };
    
    struct CatObject : IObject
    {
        string m_catFood = "someFish";
    
        virtual void eatFood() override
        {
            cout << "cat is eating " << m_catFood << endl;
            m_catFood = "fishBones";
        }
    
        virtual ~CatObject() override = default;
    };
    
    struct DogObject : IObject
    {
        string m_dogFood = "someMeat";
    
        virtual void eatFood() override
        {
            cout << "dog is eating " << m_dogFood << endl;
            m_dogFood = "meatBones";
        }
    
        virtual ~DogObject() override = default;
    };
    void eatTwice(IObject *obj)
    {
        obj->eatFood();
        obj->eatFood();
    }
    
    int main()
    {
        shared_ptr<CatObject> cat = make_shared<CatObject>();
        shared_ptr<DogObject> dog = make_shared<DogObject>();
    
    	eatTwice(cat.get());
    	eatTwice(dog.get());
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    (2)shared_ptr 如何深拷贝?

    深拷贝中:make_shared(*p1),等价于make_shared(int const&),就是拷贝构造

    C++成员函数 return this或者*this 首先说明:this是指向自身对象的指针,*this是自身对象。
    return *this 返回 的是当前对象的克隆(副本)或者本身(若 返回 类型为A, 则是克隆(实际上是匿名对象), 若 返回 类型为A&, 则是本身 )。
    
    而std::shared_ptr::operator*中element_type& operator*() const noexcept;
    所以上述的等价是对的
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以知道:this等价于obj* const,*this等价于obj & const。上述中说等价于某个拷贝构造构造函数不是很恰当的,但是确实会调用这个拷贝构造函数

    #include 
    #include 
    
    using namespace std;
    
    int main() {
        shared_ptr<int> p1 = make_shared<int>(42);
        shared_ptr<int> p2 = make_shared<int>(*p1);
        *p1 = 233;
        printf("%d\n", *p2);
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    std::unique_ptr<std::string> unique = std::make_unique<std::string>("test");
    std::shared_ptr<std::string> shared = std::move(unique);:
    std::shared_ptr<std::string> shared = std::make_unique<std::string>("test");
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (3)能把拷贝构造函数也作为虚函数?

    现在我们的需求有变,不是去对同一个对象调用两次 eatTwice,而是先把对象复制一份拷贝,然后对对象本身和他的拷贝都调用一次 eatFood 虚函数(用shared_ptr的深拷贝技术)。

    • cat->eatFood()产生副作用,导致newCat->eatFood()中m_catFood为” fishBones”,逻辑不对,而应该是“someFish”才对

    这要怎么个封装法呢?

    • 你可能会想,是不是可以把拷贝构造函数也声明为虚函数,这样就能实现了拷贝的多态?不行,因为 C++ 规定“构造函数不能是虚函数”。
    • 虚函数表是在构造函数中指定的
    • 解决办法1:
    #include 
    #include 
    #include 
    
    using namespace std;
    
    struct IObject
    {
        IObject() = default;
        IObject(IObject const &) = default;
        IObject &operator=(IObject const &) = default;
        virtual ~IObject() = default;
    
        virtual void eatFood() = 0;
    };
    
    struct CatObject : IObject
    {
        string m_catFood = "someFish";
    
        virtual void eatFood() override
        {
            cout << "cat is eating " << m_catFood << endl;
            m_catFood = "fishBones";
        }
    
        virtual ~CatObject() override = default;
    };
    
    struct DogObject : IObject
    {
        string m_dogFood = "someMeat";
    
        virtual void eatFood() override
        {
            cout << "dog is eating " << m_dogFood << endl;
            m_dogFood = "meatBones";
        }
    
        virtual ~DogObject() override = default;
    };
    void eatTwice(IObject *obj)
    {
        obj->eatFood();
        obj->eatFood();
    }
    
    int main()
    {
        shared_ptr<CatObject> cat = make_shared<CatObject>();
        shared_ptr<DogObject> dog = make_shared<DogObject>();
    
        shared_ptr<CatObject> newcat = make_shared<CatObject>(*cat);
        shared_ptr<DogObject> newdog = make_shared<DogObject>(*dog);
    
        cat->eatFood();
        newcat->eatFood();
    
        dog->eatFood();
        newdog->eatFood();
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    解决办法2:模板函数

    • 索性把 eatTwice 声明为模板函数,的确能解决问题,但模板函数不是面向对象的思路,并且如果 cat 和 dog 是在一个 IObject 的指针里就会编译出错,例如右图的 vector(这是游戏引擎中很常见的用法)。
    • 右边get()获取的是*IObject,抽象类是不能实例化的,会出错
      在这里插入图片描述

    解决办法3:正确解法:额外定义一个 clone 作为纯虚函数,然后让猫和狗分别实现他

    • eg:15/b.cpp
    #include 
    #include 
    #include 
    
    using namespace std;
    
    struct IObject {
        IObject() = default;
        IObject(IObject const &) = default;
        IObject &operator=(IObject const &) = default;
        virtual ~IObject() = default;
    
        virtual void eatFood() = 0;
        virtual shared_ptr<IObject> clone() const = 0;
    };
    
    struct CatObject : IObject {
        string m_catFood = "someFish";
    
        virtual void eatFood() override {
            cout << "eating " << m_catFood << endl;
            m_catFood = "fishBones";
        }
    
        virtual shared_ptr<IObject> clone() const override {
            return make_shared<CatObject>(*this);
        }
    
        virtual ~CatObject() override = default;
    };
    
    struct DogObject : IObject {
        string m_dogFood = "someMeat";
    
        virtual void eatFood() override {
            cout << "eating " << m_dogFood << endl;
            m_dogFood = "meatBones";
        }
    
        virtual shared_ptr<IObject> clone() const override {
            return make_shared<DogObject>(*this);
        }
    
        virtual ~DogObject() override = default;
    };
    
    void eatTwice(IObject *obj) {
        shared_ptr<IObject> newObj = obj->clone();
        obj->eatFood();
        newObj->eatFood();
    }
    
    int main() {
        shared_ptr<CatObject> cat = make_shared<CatObject>();
        shared_ptr<DogObject> dog = make_shared<DogObject>();
    
        eatTwice(cat.get());
        eatTwice(dog.get());
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • clone 的调用
      这样一来,我们通用的 eatTwice 函数里只需调用 obj->clone(),就等价于调用了相应的猫或是狗的 make_shared(*obj),这就实现了拷贝的多态。
      在这里插入图片描述
    • 方法1:如何批量定义 clone 函数?
    • 可以定义一个宏 IOBJECT_DEFINE_CLONE,其内容是 clone 的实现。这里我们用 std::decay_t 快速获取了 this 指针所指向的类型,也就是当前所在类的类型。
      Eg:this = CatObject* const,*this是 CatObject& const,
      decay_t
    • 宏的缺点是他不遵守命名空间的规则,宏的名字是全局可见的,不符合 C++ 的高大尚封装思想。
    • 宏:IOBJECT_DEFINE_CLONE
      高大尚 C++ 封装:zeno::IObject::clone()
    • eg:course/15/c.cpp
    #include 
    #include 
    #include 
    #include 
    #include "print.h"//__cxa_demangle打印出来的this指针类型是不正确的,与通过gdb命令查看到的this类型不同
    
    using namespace std;
    
    struct IObject
    {
        IObject() = default;
        IObject(IObject const &) = default;
        IObject &operator=(IObject const &) = default;
        virtual ~IObject() = default;
    
        virtual void eatFood() = 0;
        virtual shared_ptr<IObject> clone() const = 0;
    };
    
    #define IOBJECT_DEFINE_CLONE                                 \
        virtual shared_ptr<IObject> clone() const override       \
        {                                                        \
            SHOW(decltype(*this));                                \
            return make_shared<decay_t<decltype(*this)>>(*this); \
        }
    
    struct CatObject : IObject
    {
        string m_catFood = "someFish";
    
        IOBJECT_DEFINE_CLONE
    
        virtual void eatFood() override
        {
            cout << "eating " << m_catFood << endl;
            m_catFood = "fishBones";
        }
    
        virtual ~CatObject() override = default;
    };
    
    struct DogObject : IObject
    {
        string m_dogFood = "someMeat";
    
        IOBJECT_DEFINE_CLONE
    
        virtual void eatFood() override
        {
            cout << "eating " << m_dogFood << endl;
            m_dogFood = "meatBones";
        }
    
        virtual ~DogObject() override = default;
    };
    
    void eatTwice(IObject *obj)
    {
        shared_ptr<IObject> newObj = obj->clone();
        obj->eatFood();
        newObj->eatFood();
    }
    
    int main()
    {
        shared_ptr<CatObject> cat = make_shared<CatObject>();
        shared_ptr<DogObject> dog = make_shared<DogObject>();
    
        eatTwice(cat.get());
        eatTwice(dog.get());
    
        SHOW(const int &);
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 方法2:如何批量定义 clone 函数?
      另一种方法是定义一个 IObjectClone 模板类。其模板参数是他的派生类 Derived。
      然后在这个 IObjectClone 里实现 clone 即可。那为什么需要派生类作为模板参数?
      因为 shared_ptr 的深拷贝需要知道对象具体的类型。
      注意这里不仅 make_shared 的参数有 Derived,this 指针(原本是 IObjectClone * const 类型)也需要转化成 Derived 的指针才能调用 Derived 的拷贝构造函数 Derived(Derived & const )。
    • eg:course/15/d.cpp
    • ref:const 指针与指向const的指针
      在这里插入图片描述

    5.CRTP

    CRTP (Curiously Recurring Template Pattern / 奇异递归模板模式)

    • 形如 struct Derived : Base {};
      基类模板参数包含派生类型的,这种就是传说中的 CRTP。
      包含派生类型是为了能调用派生类的某些函数(我们这个例子中是拷贝构造函数)。
    • 我们的目的是让基类能调用派生类的函数,其实本来是可以通过虚函数的,但是:
    1. 虚函数是运行时确定的,有一定的性能损失。
    2. 拷贝构造函数无法作为虚函数。
    这就构成了 CRTP 的两大常见用法:
    1. 更高性能地实现多态。
    2. 伺候一些无法定义为虚函数的函数,比如拷贝构造,拷贝赋值等。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    CRTP 的一个注意点:如果派生类是模板类

    • 如果派生类 Derived 是一个模板类,则 CRTP 的那个参数应包含派生类的模板参数,例如:
    template <class T>
    struct Derived : Base<Derived<T>> {};
    
    • 1
    • 2

    在这里插入图片描述

    CRTP 的改进:如果基类还想基于另一个类

    • eg:course/15/d.cpp
      现在我们的需求有变,需要新增一个“超狗(superdog)”类,他继承自普通狗(dog)。
      这时我们可以给 IObjectClone 新增一个模板参数 Base,其默认值为 IObject。
      这样当用户需要的时候就可指定第二个参数 Base,从而控制 IObjectClone 的基类,也就相当于自己继承自那个 Base 类了,不
      指定的话就默认 IObject。
      在这里插入图片描述

    IObject:一切 Zeno 对象的公共基类

    • std:any,随着Iobject拷贝而拷贝,随着Iobject销毁而销毁
      在这里插入图片描述

    IObjectClone:自动实现所有 clone 系列虚函数
    在这里插入图片描述

    • assign 是什么东西?
      assign(IObject *other) 是用于拷贝赋值,把对象就地拷贝到另一个地址的对象去。
      同理还有 move_assign 对应于移动赋值,move_clone 对应于移动构造
      就这样把 C++ 的四大特殊函数变成了多态的虚函数,这就是被小彭老师称为自动虚克隆(auto-vitrual-clone)的大法。

    6.类型擦除

    • 开源的体积数据处理库 OpenVDB 中有许多“网格”的类(可以理解为多维数组),例如:
      openvdb::Vec3fGrid,FloatGrid,Vec3IGrid,IntGrid,PointsDataGrid
      我们并不知道他们之间的继承关系,可能有也可能没有。

    • 但是在 Zeno 中,我们必须有。他们还有一些成员函数,这些函数可能是虚函数,也可能不是。
      如何在不知道 OpenVDB 每个类具体继承关系的情况下,实现我们想要的继承关系,从而实现封装和代码重用?
      简单,只需用一种称为类型擦除(type-erasure)的大法。

    • 类型擦除:还是以猫和狗为例
      例如右边的猫和狗类,假设这两个类是某个第三方库里写死的。居然没有定义一个公用的 Animal 基类并设一个 speak 为虚函数。现在你抱怨也没有用,因为这个库是按 LGPL 协议开源的,你只能链接他,不能修改他的源码,但你的老板却要求你把 speak 变成一个虚函数。
      在这里插入图片描述

    • 你还是可以照常定义一个 Animal 接口,其具有一个纯虚函数 speak。然后定义一个模板类 AnimalWrapper,他的模板参数 Inner 则是用来创建他的一个成员 m_inner。

    • 然后,给 AnimalWrapper 实现 speak 为原封不动去调用 m_inner.speak()。

    • 这样一来,你以后创建猫和狗对象的时候只需绕个弯改成用 new AnimalWrapper 创建就行了,或者索性:

    using WrappedCat = AnimalWrapper<Cat>;
    
    • 1

    在这里插入图片描述

    • 就这样,根本不用修改 Cat 和 Dog 的定义,就能随意地把 speak 封装为多态的虚函数。只要语义上一样,也就是函数名字一样,就可以用这个办法随意转换任意依赖的操作为虚函数。
    • 实际上 std::any 也是一个类型擦除的容器……
      这里我们的 Animal 擦除了 speak 这个成员函数,而 std::any 实际上是擦除了拷贝构造函数和解构函数,std::function 则是擦除 operator() 函数。
    • 参考:Chapter 34. Boost.TypeErasure

    类型擦除利用的是 C++ 模板的惰性实例化

    • 由于 C++ 模板惰性编译的特性,这个擦除掉的表达式会在你实例化 AnimalWrapper 的时候自动对 T 进行编译。这意味着如果你给他一个不具有一个名为 speak 成员函数的类(比如这里的 Phone 类只有 play 函数)就会在实例化的那行出错。
    • 注意:这里的 m_inner.speak() 只是一个例子,其实不一定是成员函数,完全可以是 std::sort(m_inner.begin(), m_inner.end()) 之类的任意表达式,只要语义上通过,就可以实例化。
      在这里插入图片描述
    • Zeno 中对 OpenVDB 的类型擦除
      结合类型擦除技术,自动虚克隆技术。
      VDBGrid 作为所有网格类的基类提供各个操作做为虚函数,VDBGridWrapper 则是那个实现了擦除的包装类。
    • 继承体系:VDBFloatGrid继承至VDBGrid,VDBGrid继承至IObject
    • typename目的是:让她知道GridT::Ptr是个类型

    7.全局变量初始化的妙用

    我们可以定义一个 int 类型全局变量 helper,然后他的右边其实是可以写一个表达式的,这个表达式实际上会在 main 函数之前执行!

    • 全局变量的初始化会在 main 之前执行,这实际上是 C++ 标准的一部分,我们完全可以放心利用这一点来执行任意表达式。

    • eg:course/15/g.cpp
      在这里插入图片描述

    8.逗号表达式的妙用

    那么这里是因为比较巧合,printf 的返回类型正好是 int 类型,所以可以用作初始化的表达式。如果你想放在 main 之前执行的不是 printf 而是别的比较复杂的表达式呢?

    • 可以用逗号表达式的特性,总是会返回后一个值,例如 (x, y) 始终会返回 y,哪怕 x 是 void 也没关系。因此只需要这样写就行:
    • eg:
    static int helper = (任意表达式, 0);
    
    • 1

    在这里插入图片描述
    在这里插入图片描述

    lambda 的妙用

    • []{ xxx; yyy; return zzz; }()
      可以在表达式层面里插入一个语句块,本质上是立即求值的 lambda 表达式(内部是分号级别,外部是逗号级别)。
    • 在函数体内也可以这样:
    [&]{ xxx; yyy; return zzz; }()
    来在语句块内使用外部的局部变量。
    
    • 1
    • 2

    在这里插入图片描述

    9.静态初始化(static-init)大法

    带有构造函数和解构函数的类

    • eg:course/15/f.cpp
      实际上,只需定义一个带有构造函数和解构函数的类(这里的 Helper),然后一个声明该类的全局变量(helper),就可以保证:
    1. 该类的构造函数一定在 main 之前执行
    2. 该类的解构函数一定在 main 之后执行
    
    • 1
    • 2
    • 该技巧可用于在程序退出时删除某些文件之类。类似C语言atexit
      这就是静态初始化(static-init)大法。
      在这里插入图片描述

    静态初始化用于批量注册函数

    • 我们可以定义一个全局的函数表(右图中的 functab),然后利用小彭老师的静态初始化大法,把这些函数在 main 之前就插入到全局的函数表。
    • 这样 main 里面就可以仅通过函数名从 functab 访问到他们,从而 catFunc 和 dogFunc 甚至不需要在头文件里声明(只需要他们的函数签名一样即可放入 function 容器)。
    • eg:course/15/h.cpp
      在这里插入图片描述

    静态初始化的顺序是符号定义的顺序决定的,若在不同文件则顺序可能打乱

    • 你可能已经兴冲冲地把 dogFunc 和 catFunc 挪到另一个文件,然后把 functab 声明为 extern std::map<…> functab;
    • 就是说,如果 functab 所在的 main.o 文件在链接中是处于 cat.o 和 dog.o 后面的话,那么 cat.o 和 dog.o 的静态初始化就会先被调用,这时候 functab 的 map 还没有初始化(map 的构造函数也是静态初始化!)从而会调用未初始化的 map 对象导致奔溃。
      • eg:course/15/i.cpp
        在这里插入图片描述

    函数体内的静态初始化

    • 为了寻找思路,我们把眼光挪开全局的 static 变量,来看看函数的 static 变量吧!
    • 众所周知,函数体内声明为 static 的变量即使函数退出后依然存在。
    • 实际上函数的 static 变量也可以指定初始化表达式,这个表达式会在第一次进入函数时执行。
      注意:是第一次进入的时候执行而不是单纯的在 main 函数之前执行哦!
    • eg:course/15/j.cpp
      在这里插入图片描述

    如果函数体内的 static 变量是一个类呢?

    • 如果函数体内的 static 变量,是一个带有构造函数和解构函数的类,则 C++ 标准保证:
    1. 构造函数会在第一次进入函数的时候调用。
    2. 解构函数依然会在 main 退出的时候调用。
    3. 如果从未进入过函数(构造函数从未调用过)则 main 退出时也不会调用解构函数。
    
    • 1
    • 2
    • 3
    • 并且即使多个线程同时调用了 func,这个变量的初始化依然保证是原子的(C++11 起)。
      这就是函数静态初始化(func-static-init)大法。
    • course/15/k.cpp
      在这里插入图片描述

    函数静态初始化可用于“懒汉单例模式”

    • eg:course/15/l.cpp
      getMyClassInstance() 会在第一次调用时创建 MyClass 对象,并返回指向他的引用。
      根据 C++ 函数静态变量初始化的规则,之后的调用不会再重复创建。
      并且 C++11 也保证了不会多线程的危险,不需要手动写 if 去判断是否已经初始化过,非常方便!
    #include 
    #include 
    
    struct MyClass
    {
        MyClass()
        {
            printf("MyClass initialized\n");
        }
    
        void someFunc()
        {
            printf("MyClass::someFunc called\n");
        }
    
        ~MyClass()
        {
            printf("MyClass destroyed\n");
        }
    };
    
    static MyClass &getMyClassInstance()
    {
        static MyClass inst;
        return inst;
    }
    
    int main()
    {
        std::thread t_thread1(getMyClassInstance);
        std::thread t_thread2(getMyClassInstance);
        std::thread t_thread3(getMyClassInstance);
        getMyClassInstance().someFunc();
        t_thread1.join();
        t_thread2.join();
        t_thread3.join();
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 测试:
      在这里插入图片描述

    函数静态初始化和全局静态初始化的配合

    • 函数静态初始化和全局静态初始化的配合
      如果在全局静态初始化(before_main)里使用了函数静态初始化(Helper)会怎样?
      会让函数静态初始化(Helper)执行得比全局静态初始化(before_main)还早!
      在这里插入图片描述

    用包装,避免因为链接的不确定性打乱了静态初始化的顺序

    • 利用这个发现,我们意识到可以把 functab 用所谓的“懒汉单例模式”包装成一个 getFunctab() 函数,里面的 inst 变量会在第一次进入的时候初始化。因为第一次调用是在 defCat 中,从而保证是在所有 emplace 之前就初始化过,因此不会有 segfault 的问题了!
    • Eg:把catFunc()和static int defCat放到另外一个cpp文件里面
    • course/15/m.cpp
      在这里插入图片描述

    函数表结合工厂模式

    • make_unique<>返回的是一个函数function
    • eg:course/15/n.cpp
      在这里插入图片描述

    Zeno 中定义节点的宏

    • 在 Zeno 中每个节点还额外有一个 Descriptor 的信息,因此遵循以下格式:
    ZENO_DEFNODE(ClassName)({...<descriptor-brace-initializer>...})
    
    • 1
    • _defNodeClassHelper返回的是一个lambda,后面增加个()才是完整的
    • 这里没使用逗号表达式是因为#define会出错
      在这里插入图片描述
    • Descriptor 的定义
    在参数类型已经确定的情况下,例如:
    void func(Descriptor const &desc);func(Descriptor(...));func({...});
    等价(C++11 起)。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    • Zeno 中一切节点的基类
      输入输出全部存储在节点的 inputs 和 outputs 成员变量上。
      inputBounds 表示他连接在哪个节点的哪个端口上,比如 {“PrimitiveCreate”, “prim”} 就表示这个端口连接了 PrimitiveCreate 节点的 prim 输出端口。
      (zany 是 shared_ptr 的缩写)
      在这里插入图片描述

    • eg:一个节点的定义,以 MakeBoxPrimitive 为例
      在这里插入图片描述

    • MaxBoxPrimitive 节点的内部:apply 的定义
      通过 get_input(“name”) 获取端口名 name 上类型为 T 的对象,如果类型不是 T,则出错。
      在这里插入图片描述

    • NumericObject 的定义

    • NumericObject 是基于 std::variant 的。

    • 注意他的 get 成员函数,这和 std::get 相比更安全,例如 value 是 int 类型,但用户却调用了 get。则这里 is_constructible 是 true,不会出错,而是会自动把 int转换成 float 类型。同样地如果输入是 float,却调用了 get 的话,那么就相当于 vec3f(val) 也就是三个分量都是 val 的三维矢量,同样不会出错。

    • 参考:C++ std::is_constructible模板用法及代码示例std::is_constructible
      在这里插入图片描述

    • MaxBoxPrimitive 节点的内部:apply 的定义
      通过 set_output(“name”, std::move(obj)) 来指定名字为 name 的输出端口对象为 obj。
      在这里插入图片描述

    10.模板类设计与类体系设计

    (1)模板类设计:采用一个普通的抽象类作为基类

    基类抽象化方案1:
    模板类的体系设计中,如果基类的代码、数据很多,可能会导致膨胀问题。

    • 一个解决方法是采用一个普通的基类,并在其基础上建立模板化的基类:
    • 这样的写法,可以将通用逻辑(不必泛型化的)抽出到 base 中,避免留在 base_t 中随着泛型实例化而膨胀。
    struct base {
      virtual ~base_t(){}
      
      void operation() { do_sth(); }
      
      protected:
      virtual void do_sth() = 0;
    };
    
    template <class T>
      struct base_t: public base{
        protected:
        virtual void another() = 0;
      };
    
    template <class T, class C=std::list<T>>
      struct vec_style: public base_t<T> {
        protected:
        void do_sth() override {}
        void another() override {}
        
        private:
        C _container{};
      };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    (2)类体系设计:纯虚类如何放入容器里

    基类抽象化方案2:
    顺便也谈谈纯虚类,抽象类的容器化问题。

    • 对于类体系设计,我们鼓励基类纯虚化,但这样的纯虚基类就无法放到 std::vector 等容器中了:
    #include 
    
    namespace {
      struct base {};
    
      template<class T>
        struct base_t : public base {
          virtual ~base_t(){}
          virtual T t() = 0;
        };
    
      template<class T>
        struct A : public base_t<T> {
          A(){}
          A(T const& t_): _t(t_) {}
          ~A(){}
          T _t{};
          virtual T t() override { std::cout << _t << '\n'; return _t; }
        };
    }
    
    std::vector<A<int>> vec; // BAD
    
    int main() {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这里用 declval 是没意义的,应该使用智能指针来装饰抽象基类:

    std::vector<std::shared_ptr<base_t<int>>> vec;
    
    int main(){
      vec.push_back(std::make_shared<A<int>>(1));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (3)运行时多态

    放弃基类抽象化的设计方案,改用所谓的运行时多态 trick 来设计类体系。

    • 其特点是基类不是基类,基类的嵌套类才是基类:Animal::Interface 才是用于类体系的抽象基类,它是纯虚的,但却不影响 std::vector 的有效编译与工作。Animal 使用简单的转接技术将 Animal::Interface 的接口(如 toString())映射出来,这种转接有点像 Pimpl Trick
    #include 
    #include 
    #include 
    #include 
    
    class Animal {
     public:
      struct Interface {
        virtual std::string toString() const = 0;
        virtual ~Interface()                 = default;
      };
      std::shared_ptr<const Interface> _p;
    
     public:
      Animal(Interface* p) : _p(p) { }
      std::string toString() const { return _p->toString(); }
    };
    
    class Bird : public Animal::Interface {
     private:
      std::string _name;
      bool        _canFly;
    
     public:
      Bird(std::string name, bool canFly = true) : _name(name), _canFly(canFly) {}
      std::string toString() const override { return "I am a bird"; }
    };
    
    class Insect : public Animal::Interface {
     private:
      std::string _name;
      int         _numberOfLegs;
    
     public:
      Insect(std::string name, int numberOfLegs)
          : _name(name), _numberOfLegs(numberOfLegs) {}
      std::string toString() const override { return "I am an insect."; }
    };
    
    int main() {
      std::vector<Animal> creatures;
    
      creatures.emplace_back(new Bird("duck", true));
      creatures.emplace_back(new Bird("penguin", false));
      creatures.emplace_back(new Insect("spider", 8));
      creatures.emplace_back(new Insect("centipede", 44));
    
      // now iterate through the creatures and call their toString()
    
      for (int i = 0; i < creatures.size(); i++) {
        std::cout << creatures[i].toString() << '\n';
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
  • 相关阅读:
    C++:stack 定义,用法,作用,注意点
    在 Windows 终端运行已有的 Python 程序
    应用层协议不难理解,其实它们就出现在你熟悉的地方
    java 工程管理系统源码+项目说明+功能描述+前后端分离 + 二次开发
    FastAPI学习-18.Response 返回 XML 格式
    LeetCode HOT 100 —— 48.旋转图像
    maven 项目添加 git-hook 脚本,约束提交内容格式
    Redis 备份恢复(持久化)手册
    工程机械流通行业BI经营分析框架(一)四大关注方向
    乘积小于 K 的子数组
  • 原文地址:https://blog.csdn.net/u011436427/article/details/126494888