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;
}
这样之后如果有一个任务是要基于 eatFood 做文章,比如要重复 eatFood 两遍。
#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;
}
深拷贝中: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;
所以上述的等价是对的
可以知道: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;
}
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");
现在我们的需求有变,不是去对同一个对象调用两次 eatTwice,而是先把对象复制一份拷贝,然后对对象本身和他的拷贝都调用一次 eatFood 虚函数(用shared_ptr的深拷贝技术)。
这要怎么个封装法呢?
#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;
}
解决办法2:模板函数
解决办法3:正确解法:额外定义一个 clone 作为纯虚函数,然后让猫和狗分别实现他
#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;
}
#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;
}
CRTP (Curiously Recurring Template Pattern / 奇异递归模板模式)
1. 虚函数是运行时确定的,有一定的性能损失。
2. 拷贝构造函数无法作为虚函数。
这就构成了 CRTP 的两大常见用法:
1. 更高性能地实现多态。
2. 伺候一些无法定义为虚函数的函数,比如拷贝构造,拷贝赋值等。
CRTP 的一个注意点:如果派生类是模板类
template <class T>
struct Derived : Base<Derived<T>> {};
CRTP 的改进:如果基类还想基于另一个类
IObject:一切 Zeno 对象的公共基类
IObjectClone:自动实现所有 clone 系列虚函数
开源的体积数据处理库 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>;
类型擦除利用的是 C++ 模板的惰性实例化
我们可以定义一个 int 类型全局变量 helper,然后他的右边其实是可以写一个表达式的,这个表达式实际上会在 main 函数之前执行!
全局变量的初始化会在 main 之前执行,这实际上是 C++ 标准的一部分,我们完全可以放心利用这一点来执行任意表达式。
eg:course/15/g.cpp
那么这里是因为比较巧合,printf 的返回类型正好是 int 类型,所以可以用作初始化的表达式。如果你想放在 main 之前执行的不是 printf 而是别的比较复杂的表达式呢?
static int helper = (任意表达式, 0);
lambda 的妙用
[&]{ xxx; yyy; return zzz; }()
来在语句块内使用外部的局部变量。
带有构造函数和解构函数的类
1. 该类的构造函数一定在 main 之前执行
2. 该类的解构函数一定在 main 之后执行
静态初始化用于批量注册函数
静态初始化的顺序是符号定义的顺序决定的,若在不同文件则顺序可能打乱
函数体内的静态初始化
如果函数体内的 static 变量是一个类呢?
1. 构造函数会在第一次进入函数的时候调用。
2. 解构函数依然会在 main 退出的时候调用。
3. 如果从未进入过函数(构造函数从未调用过)则 main 退出时也不会调用解构函数。
函数静态初始化可用于“懒汉单例模式”
#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;
}
函数静态初始化和全局静态初始化的配合
用包装,避免因为链接的不确定性打乱了静态初始化的顺序
函数表结合工厂模式
Zeno 中定义节点的宏
ZENO_DEFNODE(ClassName)({...<descriptor-brace-initializer>...})
在参数类型已经确定的情况下,例如:
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
NumericObject 的定义
NumericObject 是基于 std::variant 的。
注意他的 get 成员函数,这和 std::get 相比更安全,例如 value 是 int 类型,但用户却调用了 get。则这里 is_constructible 是 true,不会出错,而是会自动把 int转换成 float 类型。同样地如果输入是 float,却调用了 get 的话,那么就相当于 vec3f(val) 也就是三个分量都是 val 的三维矢量,同样不会出错。
MaxBoxPrimitive 节点的内部:apply 的定义
通过 set_output(“name”, std::move(obj)) 来指定名字为 name 的输出端口对象为 obj。
基类抽象化方案1:
模板类的体系设计中,如果基类的代码、数据很多,可能会导致膨胀问题。
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:
顺便也谈谈纯虚类,抽象类的容器化问题。
#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() {
}
这里用 declval 是没意义的,应该使用智能指针来装饰抽象基类:
std::vector<std::shared_ptr<base_t<int>>> vec;
int main(){
vec.push_back(std::make_shared<A<int>>(1));
}
放弃基类抽象化的设计方案,改用所谓的运行时多态 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';
}
}