没有合适的资源?快使用搜索试试~ 我知道了~
引言 我有一个显示屏模块: 模块上有一个128*64的单色显示屏,一个单片机(B)控制它显示的内容。单片机的I²C总线通过四边上的排针排母连接到其他单片机(A)上,A给B发送指令,B绘图。 B可以向屏幕逐字节发送显示数据,但是不能读取,所以程序中必须设置显存。一帧需要1024字节,但是单片机B只有512字节内存,其中只有256字节可以分配为显存。解决这个问题的方法是在B的程序中把显示屏分成4个区域,保存所有要绘制的图形的信息,每次在256字节中绘制1/4屏,分批绘制、发送。 简而言之,我需要维护多个类型的数据。稍微具体点,我要把它们放在一个类似于数组的结构中,然后遍历数组,绘制每一个元素。
资源详情
资源评论
资源推荐
详解详解C++值多态中的传统多态与类型擦除值多态中的传统多态与类型擦除
引言引言
我有一个显示屏模块:
模块上有一个128*64的单色显示屏,一个单片机(B)控制它显示的内容。单片机的I²C总线通过四边上的排针排母连接到其他单片机(A)上,A给B发送指令,B绘图。
B可以向屏幕逐字节发送显示数据,但是不能读取,所以程序中必须设置显存。一帧需要1024字节,但是单片机B只有512字节内存,其中只有256字节可以分配为显存。解决这个问题的方法是在B的程
序中把显示屏分成4个区域,保存所有要绘制的图形的信息,每次在256字节中绘制1/4屏,分批绘制、发送。
简而言之,我需要维护多个类型的数据。稍微具体点,我要把它们放在一个类似于数组的结构中,然后遍历数组,绘制每一个元素。
不同的图形,用相同的方式来对待,这是继承与多态的最佳实践。我可以设计一个Shape类,定义virtual void draw() const = 0; ,每收到一个指令就new一个Line、Rectangle等类型的对象出来,放
入std::vector<Shape*>中,在遍历中对每个Shape*指针调用->draw()。
但是对不起,今天我跟new杠上了。单片机程序注重运行时效率,除了初始化以外,没事最好别瞎new。每个指令new一下,清屏指令一起delete,恐怕不大合适吧!
我需要值多态,一种不需要指针或引用,通过对象本身就可以表现出的多态。
背景背景
我得先介绍一点知识,一些刚上完C++入门课程的新手不可能了解的,却是深入C++底层和体会C++设计思想所必需的知识,正因为有了这些知识我才能想出“值多态”然后把它实现出来。如果你对这些知
识了如指掌,或是已经迫不及待地想知道我是怎么实现值多态的,可以直接拉到下面实现一节。
多态多态
多态,是指为不同类型的实体提供统一的接口,或用相同的符号来代表多种不同的类型。C++里有很多种多态:
先说编译期多态。非模板函数重载是一种多态,用相同的名字调用的函数可能是不同的,取决于参数类型。如果你需要一个函数名字能够多处理一种类型,你就得多写一个重载,这样的多态是封闭式多
态。好在新的重载不用和原有的函数写在一起。
模板是一种开放式多态——适配一种新的类型是对那个新的类型提要求,而模板是不改动的。相比于后文中的运行时多态,C++鼓励模板,“STL”的“T”就足以说明这一点。瞧,标准库的算法都是模板函
数,而不是像《设计模式》中那样让各种迭代器继承自Iterator<T>基类。
模板多态的弊端在于模板参数T类型的对象必须是即取即用的,函数返回以后就没了,不能持久地维护。如果需要,那得使用类型擦除。
运行时多态大致可以分为继承一套和类型擦除一套,它们都是开放式多态。继承、虚函数这些东西,又称OOP,我在本文标题中称之为“传统多态”,我认为是没有异议的。面向对象编程语言的四个特
点,抽象、封装、继承、多态,大家都熟记于心(有时候少了抽象),以致于有些人说到多态就是虚函数。的确,很多程序中广泛使用继承,但既然function/bind已经“救赎”了,那就要学它们、用它们,
还要学它们的设计和思想,在合理范围内取代继承这一套工具,因为它们的确有很多问题——“蝙蝠是鸟也是兽,水上飞机能飞也能游”,多重继承、虚继承、各种overhead……连Lippman都看不下去了:
继承的另一个主要问题,也是本文主要针对的问题,是多态需要一层间接,即指针或引用。仍然以迭代器为例,如果begin方法返回一个指向新new出来的Iterator<T>对象的指针,客户在使用完迭代器后还得
记得把它delete掉,或者用std::lock_guard一般的RAII类来负责迭代器的delete工作,总之需要多操一份心。
因此在现代C++中,基于类型擦除的多态逐渐占据了上风。类型擦除是用一个类来包装多种具有相似接口的对象,在功能上属于多态包装器,如std::function就是一个多态函数包装器,原计划在C++20中标
准化的polymorphic_value是一个多态值包装器——与我的意图很接近。后面会详细讨论这些。
私以为,这两种运行时多态,只有语义上的不同。
虚函数的实现虚函数的实现
《深度探索C++对象模型》中最吸引人的部分莫过于虚函数的实现了。尽管C++标准对于虚函数的实现方法没有作出任何规定和假设,但是用指向虚函数表(vtable)的指针来实现多态是这个小圈子里心
照不宣的秘密。
假设有两个类:
class Base
{
public:
Base(int i) : i(i) { }
virtual ~Base() { }
virtual void func() const {
std::cout << "Base: " << i << std::endl;
}
private:
int i;
};
class Derived : public Base
{
public:
Derived(int i, int j)
: Base(i), j(j) { }
virtual ~Derived() { }
virtual void func() const override {
std::cout << "Derived: " << j << std::endl;
}
private:
int j;
};
这两个类的实例在内存中的布局可能是这样:
如果你把一个Derived实例的指针赋给Base*的变量,然后调用func(),程序会把这个指针指向的对象当作Base的实例,解引用它的第二格,在vtable中下标为2的位置找到func的函数指针,然后把this指针传入调
用它。虽然被当成Base实例,但该对象的vtable实际指向的是Derived类的vtable,因此被调用的函数是Derived::func,基于继承的多态就是这样实现的。
而如果你把一个Derived实例赋给Base变量,只有i会被拷贝,vtable会初始化成Base的vtable,j则被丢掉了。调用它的func,Base::func会执行,而且很可能是直接而非通过函数指针调用的。
这种实现可以推及到继承树(强调“树”,即单继承)的情况。至于多重继承中的指针偏移和虚继承中的子对象指针,过于复杂,我就不介绍了。
vtable指针不拷贝是虚函数指针语义的罪魁祸首,不过这也是不得已而为之的,拷贝vtable指针会引来更大的麻烦:如果Base实例中有Derived虚函数表指针,调用func就会访问该对象的第三格,但第三格是
无效的内存空间。相比之下,把维护指针的任务交给程序员是更好的选择。
类型擦除类型擦除
不拷贝vtable就不能实现值语义,拷贝vtable又会有访问的问题,那么是什么原因导致了这个问题呢?是因为Base和Derived实例的大小不同。实现了类型擦除的类也使用了与vtable相同或类似的多态实
现,而作为一个而非多个类,类型擦除类的大小是确定的,因此可以拷贝vtable或其类似物,也就可以实现值语义。C++想方设法让类类型表现得像内置类型一样,这是类型擦除更深刻的意义。
类型擦除,顾名思义,就是把对象的类型擦除掉,让你在不知道它的类型的情况下对它执行一些操作。举个例子,std::function有一个带约束的模板构造函数,你可以用它来包装任何参数类型匹配的可调用
对象,在构造函数结束后,不光是你,std::function也不知道它包装的是什么类型的实例,但是operator()就可以调用那个可调用对象。我在一篇文章中剖析过std::function的实现,当然它还有很多种实现方法,
其他类型擦除类的实现也都大同小异,它们都包含两个要素:可能带约束的模板构造函数,以及函数指针,无论是可见的(直接维护)还是不可见的(使用继承)。
为了获得更真切的感受,我们来写一个最简单的类型擦除:
class MyFunction
{
private:
class FunctorWrapper
{
public:
virtual ~FunctorWrapper() = default;
virtual FunctorWrapper* clone() const = 0;
virtual void call() const = 0;
};
template<typename T>
class ConcreteWrapper : public FunctorWrapper
{
public:
ConcreteWrapper(const T& functor)
: functor(functor) { }
virtual ~ConcreteWrapper() override = default;
virtual ConcreteWrapper* clone() const
{
return new ConcreteWrapper(*this);
}
virtual void call() const override
{
functor();
}
private:
T functor;
};
public:
MyFunction() = default;
template<typename T>
MyFunction(T&& functor)
: ptr(new ConcreteWrapper<T>(functor)) { }
MyFunction(const MyFunction& other)
: ptr(other.ptr->clone()) { }
MyFunction& operator=(const MyFunction& other)
{
if (this != &other)
{
delete ptr;
ptr = other.ptr->clone();
}
return *this;
}
MyFunction(MyFunction&& other) noexcept
: ptr(std::exchange(other.ptr, nullptr)) { }
MyFunction& operator=(MyFunction&& other) noexcept
{
if (this != &other)
{
delete ptr;
ptr = std::exchange(other.ptr, nullptr);
}
return *this;
}
~MyFunction()
{
delete ptr;
}
void operator()() const
{
if (ptr)
ptr->call();
}
FunctorWrapper* ptr = nullptr;
};
MyFunction类中维护一个FunctorWrapper指针,它指向一个ConcreteWrapper<T>实例,调用虚函数来实现多态。虚函数有析构、clone和call三个,它们分别用于MyFunction的析构、拷贝和函数调用。
类型擦除类的实现中总会保留一点类型信息。MyFunction类中关于T的类型信息表现在FunctorWrapper的vtable中,本质上是函数指针。类型擦除类也可以跳过继承的工具,直接使用函数指针实现多态。无论
使用哪种实现,类型擦除类总是可以被拷贝或移动或两者兼有,多态性可以由对象本身体现。
不是每一滴牛奶都叫特仑苏,也不是每一个类的实例都能被MyFunction包装。MyFunction对T的要求是可以拷贝、可以用operator()() const调用,这些称为类型T的“affordance”。说到affordance,普通的模板函
数也对模板类型有affordance,比如std::sort要求迭代器可以随机存取,否则编译器会给你一堆冗长的错误信息。C++20引入了concept和requires子句,对编译器和程序员都是有好处的。
每个类型擦除类的affordance都在写成的时候确定下来。affordance被要求的方式不是继承某个基类,而只看你这个类是否有相应的方法,就像Python那样,只要函数接口匹配上就可以了。这种类型识别
方式称为“duck typing”,来源于“duck test”,意思是“If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck”。
类型擦除类要求的affordance通常都是一元的,也就是成员函数的参数中不含T,比如对于包装整数的类,你可以要求T + 42,但是无法要求T + U,一个类型擦除类的实例是不知道另一个属于同一个类但是
构造自不同类型对象的实例的信息的。我觉得这条规则有一个例外,operator==是可以想办法支持的。
MyFunction类虽然实现了值多态,但还是使用了new和delete语句。如果可调用对象只是一个简单的函数指针,是否有必要在堆上开辟空间?
SBO
小的对象保存在类实例中,大的对象交给堆并在实例中维护指针,这种技巧称为小缓冲优化(Small Buffer Optimization, SBO)。大多数类型擦除类都应该使用SBO以节省内存并提升效率,问题在于
SBO与继承不共存,维护每个实例中的一个vtable或几个函数指针是件挺麻烦的事,还会拖慢编译速度。
但是在内存和性能面前,这点工作量能叫事吗?
class MyFunction
{
private:
static constexpr std::size_t size = 16;
static_assert(size >= sizeof(void*), "");
struct Data
{
Data() = default;
char dont_use[size];
} data;
template<typename T>
static void functorConstruct(Data& dst, T&& src)
{
using U = typename std::decay<T>::type;
if (sizeof(U) <= size)
new ((U*)&dst) U(std::forward<U>(src));
else
*(U**)&dst = new U(std::forward<U>(src));
}
template<typename T>
static void functorDestructor(Data& data)
{
using U = typename std::decay<T>::type;
if (sizeof(U) <= size)
((U*)&data)->~U();
else
delete *(U**)&data;
}
template<typename T>
static void functorCopyCtor(Data& dst, const Data& src)
{
using U = typename std::decay<T>::type;
if (sizeof(U) <= size)
new ((U*)&dst) U(*(const U*)&src);
else
*(U**)&dst = new U(**(const U**)&src);
}
template<typename T>
static void functorMoveCtor(Data& dst, Data& src)
{
using U = typename std::decay<T>::type;
if (sizeof(U) <= size)
new ((U*)&dst) U(*(const U*)&src);
else
*(U**)&dst = std::exchange(*(U**)&src, nullptr);
}
template<typename T>
static void functorInvoke(const Data& data)
{
using U = typename std::decay<T>::type;
if (sizeof(U) <= size)
(*(U*)&data)();
else
(**(U**)&data)();
}
template<typename T>
static void (*const vtables[4])();
void (*const* vtable)() = nullptr;
public:
MyFunction() = default;
template<typename T>
MyFunction(T&& obj)
: vtable(vtables<T>)
{
functorConstruct(data, std::forward<T>(obj));
}
MyFunction(const MyFunction& other)
: vtable(other.vtable)
{
if (vtable)
((void (*)(Data&, const Data&))vtable[1])(this->data, other.data);
}
MyFunction& operator=(const MyFunction& other)
{
this->~MyFunction();
vtable = other.vtable;
new (this) MyFunction(other);
return *this;
}
MyFunction(MyFunction&& other) noexcept
: vtable(std::exchange(other.vtable, nullptr))
{
if (vtable)
((void (*)(Data&, Data&))vtable[2])(this->data, other.data);
}
MyFunction& operator=(MyFunction&& other) noexcept
{
this->~MyFunction();
new (this) MyFunction(std::move(other));
return *this;
}
~MyFunction()
{
if (vtable)
((void (*)(Data&))vtable[0])(data);
}
void operator()() const
{
if (vtable)
((void (*)(const Data&))vtable[3])(this->data);
}
};
template<typename T>
void (*const MyFunction::vtables[4])() =
剩余12页未读,继续阅读
weixin_38705762
- 粉丝: 6
- 资源: 905
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功
评论0