0
点赞
收藏
分享

微信扫一扫

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


文章目录

  • 1.经典的多态案例
  • (1)多态用于设计模式之“模板模式”
  • (2)shared_ptr 如何深拷贝?
  • (3)能把拷贝构造函数也作为虚函数?
  • 5.CRTP
  • 6.类型擦除
  • 7.全局变量初始化的妙用
  • 8.逗号表达式的妙用
  • 9.静态初始化(static-init)大法
  • 10.模板类设计与类体系设计
  • (1)模板类设计:采用一个普通的抽象类作为基类
  • (2)类体系设计:纯虚类如何放入容器里
  • (3)运行时多态

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 <memory>
#include <string>
#include <iostream>

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)多态用于设计模式之“模板模式”

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

  • 就可以封装到一个函数 eatTwice 里,这个函数只需接受他们共同的基类 IObject 作为参数,然后调用 eatFood 这个虚函数来做事
  • 这样只需要写一遍 eatTwice,就可以对猫和狗都适用,实现代码的复用(dont-repeat-yourself),也让函数的作者不必去关注点从猫和狗的其他具体细节,只需把握住他们统一具有的“吃”这个接口
  • 只要参数不涉及生命周期,那么一定要用普通指针
  • eg:my_course/course/15/a.cpp

#include <memory>
#include <string>
#include <iostream>

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;
}

(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;
所以上述的等价是对的

  • ref:c++ 返回*this的成员函数,std::shared_ptr::operator*
  • unique_ptr可以转换为shared_ptr,反之不行

#include <memory>
#include <cstdio>

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;
}

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

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

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

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

这要怎么个封装法呢?

  • 你可能会想,是不是可以把拷贝构造函数也声明为虚函数,这样就能实现了拷贝的多态?不行,因为 C++ 规定“构造函数不能是虚函数”。
  • 虚函数表是在构造函数中指定的
  • 解决办法1:

#include <memory>
#include <string>
#include <iostream>

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;
}

解决办法2:模板函数

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

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

  • eg:15/b.cpp

#include <memory>
#include <string>
#include <iostream>

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;
}

  • clone 的调用
    这样一来,我们通用的 eatTwice 函数里只需调用 obj->clone(),就等价于调用了相应的猫或是狗的 make_shared(*obj),这就实现了拷贝的多态。
  • 方法1:如何批量定义 clone 函数?
  • 可以定义一个宏 IOBJECT_DEFINE_CLONE,其内容是 clone 的实现。这里我们用 std::decay_t<decltype(*this)> 快速获取了 this 指针所指向的类型,也就是当前所在类的类型。
    Eg:this = const CatObject*,*this是const CatObject&,
    decay_t<decltype(*this)是CatObject,去掉const&
  • 宏的缺点是他不遵守命名空间的规则,宏的名字是全局可见的,不符合 C++ 的高大尚封装思想。
  • 宏:IOBJECT_DEFINE_CLONE
    高大尚 C++ 封装:zeno::IObject::clone()
  • eg:course/15/c.cpp

#include <memory>
#include <string>
#include <iostream>
#include <type_traits>
#include "print.h"

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;
}

  • 方法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的指针
  • Zeno节点系统中的C++最佳实践_c++


5.CRTP

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

  • 形如 struct Derived : Base {};
    基类模板参数包含派生类型的,这种就是传说中的 CRTP。
    包含派生类型是为了能调用派生类的某些函数(我们这个例子中是拷贝构造函数)。
  • 我们的目的是让基类能调用派生类的函数,其实本来是可以通过虚函数的,但是:

1. 虚函数是运行时确定的,有一定的性能损失。
2. 拷贝构造函数无法作为虚函数。
这就构成了 CRTP 的两大常见用法:
1. 更高性能地实现多态。
2. 伺候一些无法定义为虚函数的函数,比如拷贝构造,拷贝赋值等。

  • 参考:Fluent C++:奇异递归模板模式(CRTP)

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

  • 如果派生类 Derived 是一个模板类,则 CRTP 的那个参数应包含派生类的模板参数,例如:

template <class T>
struct Derived : Base<Derived<T>> {};

Zeno节点系统中的C++最佳实践_开发语言_02

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

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

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

  • std:any,随着Iobject拷贝而拷贝,随着Iobject销毁而销毁

IObjectClone:自动实现所有 clone 系列虚函数

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

  • 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 变成一个虚函数。
  • Zeno节点系统中的C++最佳实践_开发语言_04

  • 你还是可以照常定义一个 Animal 接口,其具有一个纯虚函数 speak。然后定义一个模板类 AnimalWrapper,他的模板参数 Inner 则是用来创建他的一个成员 m_inner。
  • 然后,给 AnimalWrapper 实现 speak 为原封不动去调用 m_inner.speak()。
  • 这样一来,你以后创建猫和狗对象的时候只需绕个弯改成用 new AnimalWrapper 创建就行了,或者索性:

using WrappedCat = AnimalWrapper<Cat>;

Zeno节点系统中的C++最佳实践_#include_05

  • 就这样,根本不用修改 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
  • Zeno节点系统中的C++最佳实践_c++_06

8.逗号表达式的妙用

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

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

static int helper = (任意表达式, 0);

Zeno节点系统中的C++最佳实践_虚函数_07


Zeno节点系统中的C++最佳实践_开发语言_08

lambda 的妙用

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

[&]{ xxx; yyy; return zzz; }()
来在语句块内使用外部的局部变量。

Zeno节点系统中的C++最佳实践_虚函数_09

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

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

  • eg:course/15/f.cpp
    实际上,只需定义一个带有构造函数和解构函数的类(这里的 Helper),然后一个声明该类的全局变量(helper),就可以保证:

1. 该类的构造函数一定在 main 之前执行
2. 该类的解构函数一定在 main 之后执行

  • 该技巧可用于在程序退出时删除某些文件之类。类似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 退出时也不会调用解构函数。

  • 并且即使多个线程同时调用了 func,这个变量的初始化依然保证是原子的(C++11 起)。
    这就是函数静态初始化(func-static-init)大法。
  • course/15/k.cpp

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

  • eg:course/15/l.cpp
    getMyClassInstance() 会在第一次调用时创建 MyClass 对象,并返回指向他的引用。
    根据 C++ 函数静态变量初始化的规则,之后的调用不会再重复创建。
    并且 C++11 也保证了不会多线程的危险,不需要手动写 if 去判断是否已经初始化过,非常方便!

#include <cstdio>
#include <thread>

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;
}

  • 测试:

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

  • 函数静态初始化和全局静态初始化的配合
    如果在全局静态初始化(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>...})

  • _defNodeClassHelper返回的是一个lambda,后面增加个()才是完整的
  • 这里没使用逗号表达式是因为#define会出错
  • Descriptor 的定义

在参数类型已经确定的情况下,例如:
void func(Descriptor const &desc);
则 func(Descriptor(...));
与 func({...});
等价(C++11 起)。

Zeno节点系统中的C++最佳实践_虚函数_10

  • Zeno 中一切节点的基类
    输入输出全部存储在节点的 inputs 和 outputs 成员变量上。
    inputBounds 表示他连接在哪个节点的哪个端口上,比如 {“PrimitiveCreate”, “prim”} 就表示这个端口连接了 PrimitiveCreate 节点的 prim 输出端口。
    (zany 是 shared_ptr 的缩写)
  • Zeno节点系统中的C++最佳实践_#include_11

  • eg:一个节点的定义,以 MakeBoxPrimitive 为例
  • Zeno节点系统中的C++最佳实践_虚函数_12

  • MaxBoxPrimitive 节点的内部:apply 的定义
    通过 get_input<T>(“name”) 获取端口名 name 上类型为 T 的对象,如果类型不是 T,则出错。
  • Zeno节点系统中的C++最佳实践_开发语言_13

  • 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
  • Zeno节点系统中的C++最佳实践_c++_14

  • MaxBoxPrimitive 节点的内部:apply 的定义
    通过 set_output(“name”, std::move(obj)) 来指定名字为 name 的输出端口对象为 obj。
  • Zeno节点系统中的C++最佳实践_c++_15

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

  • 参考:理解 std::declval 和 decltype

(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{};
  };

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

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

  • 对于类体系设计,我们鼓励基类纯虚化,但这样的纯虚基类就无法放到 std::vector 等容器中了:

#include <iostream>

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() {
}

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

std::vector<std::shared_ptr<base_t<int>>> vec;

int main(){
  vec.push_back(std::make_shared<A<int>>(1));
}

(3)运行时多态

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

  • 其特点是基类不是基类,基类的嵌套类才是基类:Animal::Interface 才是用于类体系的抽象基类,它是纯虚的,但却不影响 std::vector 的有效编译与工作。Animal 使用简单的转接技术将 Animal::Interface 的接口(如 toString())映射出来,这种转接有点像 Pimpl Trick

#include <iostream>
#include <memory>
#include <string>
#include <vector>

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';
  }
}

  • 参考:小彭老师 - Zeno节点系统中的C++最佳实践 - 20220410 - OSDT Meetup


举报

相关推荐

0 条评论