文章目录

  • 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_CLONEvirtual void eatFood() override{cout << "eating " << m_catFood << endl;m_catFood = "fishBones";}virtual ~CatObject() override = default;
};struct DogObject : IObject
{string m_dogFood = "someMeat";IOBJECT_DEFINE_CLONEvirtual 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的指针

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

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

  • 就这样,根本不用修改 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);


lambda 的妙用

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

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 中一切节点的基类
    输入输出全部存储在节点的 inputs 和 outputs 成员变量上。
    inputBounds 表示他连接在哪个节点的哪个端口上,比如 {“PrimitiveCreate”, “prim”} 就表示这个端口连接了 PrimitiveCreate 节点的 prim 输出端口。
    (zany 是 shared_ptr 的缩写)

  • eg:一个节点的定义,以 MakeBoxPrimitive 为例

  • MaxBoxPrimitive 节点的内部:apply 的定义
    通过 get_input<T>(“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.模板类设计与类体系设计

  • 参考:理解 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; // BADint 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

Zeno节点系统中的C++最佳实践相关推荐

  1. 分布式电源选址定容 在改进的IEEE33节点系统中分布式电源选择最佳接入点和接入容量

    分布式电源选址定容 软件:Matlab 介绍:在改进的IEEE33节点系统中分布式电源选择最佳接入点和接入容量,以网损和电压越限惩罚为目标进行粒子群优化,能得出最佳接入点和接入容量,接入前后电压变化, ...

  2. Mac,obs桌面音频禁用,无法录制系统声音,问题修复最佳实践!

    支持: MacOS 10.15及更高版本,Intel & M1可原生运行 问题: obs仅能录制麦克风,不能录制系统声音:obs设置显示"桌面音频禁用" 解决: 利用Loo ...

  3. #今日论文推荐# 爱丁堡大学等首篇《移动无线网络中的深度学习》综述论文,67页pdf涵盖570篇文献阐述深度学习在移动无线网络中的应用最佳实践

    #今日论文推荐# 爱丁堡大学等首篇<移动无线网络中的深度学习>综述论文,67页pdf涵盖570篇文献阐述深度学习在移动无线网络中的应用最佳实践 移动设备的迅速普及以及移动应用和服务的日益普 ...

  4. Go在迅雷P2P连通系统中的性能优化实践-朱文

    目 录 1. 如何构建压测环境 2. 如何分析性能瓶颈 3. 如何做性能优化 语言层面 设计层面 4. 总结 主要内容 我是来自迅雷的后台开发架构师,今天很高兴给大家分享一下我在迅雷连通系统中的性能优 ...

  5. 基于蒙特卡洛概率潮流计算 在IEEE33节点系统中,由于风光出力的不确定性,利用蒙特卡洛生成风速和光照强度得到出力

    基于蒙特卡洛概率潮流计算 在IEEE33节点系统中,由于风光出力的不确定性,利用蒙特卡洛生成风速和光照强度得到出力,可得到每个节点的电压和支路功率变化,网损和光照强度. ID:795064451977 ...

  6. 智能安防系统中的人工智能应用实践思考

    [toc] <64. 智能安防系统中的人工智能应用实践思考> 引言 智能安防系统是人工智能在安防领域的应用之一.随着人工智能技术的不断发展,智能安防系统逐渐成为人们生活中不可或缺的一部分. ...

  7. .NET Core 2.1中的HttpClientFactory最佳实践

    ASP.NET Core 2.1中出现一个新的HttpClientFactory功能, 它有助于解决开发人员在使用HttpClient实例从其应用程序发出外部Web请求时可能遇到的一些常见问题. 介绍 ...

  8. [专栏精选]Unity中的Git最佳实践

    本文节选自洪流学堂公众号技术专栏<大话Unity2018>,未经允许不可转载. 洪流学堂公众号回复专栏,查看更多专栏文章. 小新:"我昨天尝试了一下使用Git来管理Unity项目 ...

  9. 为什么java需要静态类_java – 为什么OOP中静态类的最佳实践有所不同?

    我目前正在阅读有关 Java最佳实践的内容,我发现根据 this book,我们必须支持非静态的静态类.我记得在C#最佳实践中,我们必须根据Dennis Doomen的C#3.0,4.0和5.0编码指 ...

最新文章

  1. intellij idea cpu占用率太大太满 运行速度太慢解决方案
  2. 配置方法_CISCO防火墙端口映射配置方法
  3. Docker技术入门与实战 第二版-学习笔记-9-Docker Compose 项目-2-Compose 命令说明
  4. dojo.publish 和 dojo.subscribe
  5. 统计学习方法 学习笔记(五):支持向量机(下)
  6. 520送你一份WebStorm的主题包,附带使用图文教程
  7. 【docker】【pycharm】pycharm配置docker远程连接
  8. 《FPGA全程进阶---实战演练》第十一章 VGA五彩缤纷
  9. NIKKEI Programming Contest 2019 翻车记
  10. DataGridView:DataGridView控件清空绑定的数据
  11. 算法设计与分析 (知识点总结)
  12. ADMM算法框架(未完待续 持续更新)
  13. 高精度地图领域常见术语解释
  14. iOS从零开始,使用Swift:下一步去哪里
  15. 洛谷P3537 [POI2012]SZA-Cloakroom(背包)
  16. 跟同事关系再好,这3种话宁烂肚里也别张嘴,莫让福运悄悄离开你
  17. Qt qss 九宫格
  18. 《关于我同学拿我照片网恋这件事所引发的学习之旅》(1)
  19. 最强量子计算机争霸:谷歌和IBM,谁是真正赢家?
  20. SAP中采购订单历史分类标识与实际业务描述

热门文章

  1. colgroup的学习
  2. HTML5 <colgroup> 标签、HTML5 <caption> 标签
  3. 数字图像处理与Python实现-图像滤波-Frangi滤波器
  4. AES加密报错:Illegal key size or default parameters或希望支持32位密钥
  5. 户籍不在本市并已申请基本养老保险或基本医疗保险关系转移手续销户提取业务办理指南(试行)...
  6. 萧乾升:4.17黄金白银TD实时最新策略,名师在线解套
  7. 精选| 2020年5月R新包推荐(第42期)
  8. Django shell测试
  9. win10 只有一种语言且只有一种输入法的情况下,如何才能在打开新的窗口中默认英文输入
  10. 【mycat】mycat水平分表