ComPtr源码分析

ComPtr源码分析

ComPtr是微软提供的用来管理COM组件的智能指针。DirectX的API是由一系列的COM组件来管理的,形如ID3D12DeviceIDXGISwapChain等的接口类最终都继承自IUnknown接口类,这个接口类包含AddRefRelease两个方法,分别用来增加和减少内部的引用计数。当引用计数为0时,内存才会真正释放。在实际使用中,我们肯定是不希望,对这类指针,都去人工地调用这两个方法来维护引用计数。这样做的心智负担太大,如果忘记释放某个接口类指针,就会造成内存泄漏;而如果忘记增加引用计数,则可能会在错误的时机提前释放了内存,导致运行时错误。

ComPtr就是为了解决上述问题而存在的。我们来看下面这个例子:

#include <wrl.h>
#include <iostream>using Microsoft::WRL::ComPtr;
using namespace std;class A
{
public:unsigned long ref = 0;void AddRef(){ref++;cout << "incr ref, cur ref " << ref << endl;}unsigned long Release(){ref--;cout << "decr ref, cur ref " << ref << endl;if(ref == 0){cout << "release!" << endl;}return ref;}
};int main()
{A* p = new A;ComPtr<A> p1 = p;ComPtr<A> p2 = p;return 0;
}

这里我们实现了一个类A,它包含AddRefRelease两个方法,分别用来增减引用计数ref。当ref为0时,打印一个release的log。例子的运行结果如下:

ComPtr源码分析1

可以看到,使用ComPtr之后,我们无需对A类指针p进行计数管理,ComPtr会帮我们维护好p的引用计数。当p1和p2离开作用域时,会对p的引用计数减一,当为0时触发真正的release,这里就是打印一句log。

在了解了ComPtr的基本用途之后,我们来欣赏一下ComPtr的源码。它的实现位于Windows SDK的client.h文件中。ComPtr类的数据成员只有一个原始指针,因此不会有额外的空间开销。它首先对原始指针的AddRefRelease方法进行了封装,后面的方法调用都围绕着这两个封装方法展开:

template <typename T>
class ComPtr
{
public:typedef T InterfaceType;protected:InterfaceType *ptr_;template<class U> friend class ComPtr;void InternalAddRef() const throw(){if (ptr_ != nullptr){ptr_->AddRef();}}unsigned long InternalRelease() throw(){unsigned long ref = 0;T* temp = ptr_;if (temp != nullptr){ptr_ = nullptr;ref = temp->Release();}return ref;}
}

可以看到InternalRelease函数会将持有的原始指针置为空,并调用原始指针的Release函数返回当前的引用计数。

ComPtr提供了若干类型的构造函数:

ComPtr() throw() : ptr_(nullptr)
{
}ComPtr(decltype(__nullptr)) throw() : ptr_(nullptr)
{
}template<class U>
ComPtr(_In_opt_ U *other) throw() : ptr_(other)
{InternalAddRef();
}ComPtr(const ComPtr& other) throw() : ptr_(other.ptr_)
{InternalAddRef();
}// copy constructor that allows to instantiate class when U* is convertible to T*
template<class U>
ComPtr(const ComPtr<U> &other, typename Details::EnableIf<Details::IsConvertible<U*, T*>::value, void *>::type * = 0) throw() :ptr_(other.ptr_)
{InternalAddRef();
}ComPtr(_Inout_ ComPtr &&other) throw() : ptr_(nullptr)
{if (this != reinterpret_cast<ComPtr*>(&reinterpret_cast<unsigned char&>(other))){Swap(other);}
}// Move constructor that allows instantiation of a class when U* is convertible to T*
template<class U>
ComPtr(_Inout_ ComPtr<U>&& other, typename Details::EnableIf<Details::IsConvertible<U*, T*>::value, void *>::type * = 0) throw() :ptr_(other.ptr_)
{other.ptr_ = nullptr;
}

如果构造函数传入的参数中包含原始指针,那么这里会调用InternalAddRef来增加原始指针的引用计数。如果传入的参数为类型U的ComPtr,那么还需要判断U类型的指针是否能成功转换为T类型指针,如果不能编译期就要报错,要做到这个就需要借助模板的力量:

typename Details::EnableIf<Details::IsConvertible<U*, T*>::value, void *>::type * = 0

如果U*可以转换到T*,那么IsConvertible的value成员值为true,进而EnableIf的type类型就可以推导为void *,编译可以正常通过;反之则type类型将不存在,那么编译就会报错,通过这个手段就可以在编译期把问题抛出来。比如以下代码:

#include <wrl.h>
#include <iostream>using Microsoft::WRL::ComPtr;
using namespace std;class A
{
public:unsigned long ref = 0;void AddRef(){ref++;cout << "incr ref, cur ref " << ref << endl;}unsigned long Release(){ref--;cout << "decr ref, cur ref " << ref << endl;if(ref == 0){cout << "release!" << endl;}return ref;}
};class B
{
public:unsigned long ref = 0;void AddRef(){ref++;cout << "incr ref, cur ref " << ref << endl;}unsigned long Release(){ref--;cout << "decr ref, cur ref " << ref << endl;if(ref == 0){cout << "release!" << endl;}return ref;}
};int main()
{A* p = new A;ComPtr<A> p1 = p;ComPtr<B> p2 = p1;return 0;
}

由于A和B类型没啥关系,所以指针也是不能互相转换的,那么编译期就会报错:

ComPtr源码分析2

那什么样的A和B类型指针可以互相转换呢?看下面这个例子:

#include <wrl.h>
#include <iostream>using Microsoft::WRL::ComPtr;
using namespace std;class A
{
public:unsigned long ref = 0;void AddRef(){ref++;cout << "incr ref, cur ref " << ref << endl;}unsigned long Release(){ref--;cout << "decr ref, cur ref " << ref << endl;if(ref == 0){cout << "release!" << endl;}return ref;}
};class B : public A
{
};int main()
{B* p = new B;ComPtr<B> p1 = p;ComPtr<A> p2 = p1;return 0;
}

这里B类型继承A类型,那么B类型的指针就可以安全地转换为A类型的指针,编译就能顺利通过了。

对于参数为右值引用的构造函数,根据语义,需要把传入ComPtr的原始指针进行转移。既然只是转移,就不需要对原始指针的引用计数进行增减。注意到构造函数实现里有一句:

this != reinterpret_cast<ComPtr*>(&reinterpret_cast<unsigned char&>(other))

这句代码的作用其实就是判断传入的other对象是否就是当前的this对象。不直接使用取地址操作符来判断this != &other的原因是因为ComPtr重载了取地址操作符,只能转而使用这种很trick的手段。

与构造函数类似,ComPtr也提供了与之对应的赋值操作符重载的函数。内部实现基本上都是新创建一个对象,然后与当前的this对象进行交换,这里就不展开了。

我们刚刚说过,ComPtr提供了取地址操作符的重载函数,但它又提供了一个名为GetAddressOf的函数,那么它们的区别是什么呢?关于这一点,MSDN上特别做了说明:

This method differs from ComPtr::GetAddressOf in that this method releases a reference to the interface pointer. Use ComPtr::GetAddressOf when you require the address of the interface pointer but don’t want to release that interface.

也就是说,调用取地址操作符时会触发一次Release操作,而GetAddressOf是不会的。我们从源码上也能看出端倪:

Details::ComPtrRef<ComPtr<T>> operator&() throw()
{return Details::ComPtrRef<ComPtr<T>>(this);
}const Details::ComPtrRef<const ComPtr<T>> operator&() const throw()
{return Details::ComPtrRef<const ComPtr<T>>(this);
}

而ComPtrRef类中有个类型转换函数:

operator InterfaceType**() throw()
{return this->ptr_->ReleaseAndGetAddressOf();
}

当转换为原始类型的二级指针时,会触发ComPtr的ReleaseAndGetAddressOf函数,这个函数的定义如下:

T** ReleaseAndGetAddressOf() throw()
{InternalRelease();return &ptr_;
}

它和GetAddressOf函数的实现就多了一句Release:

T** GetAddressOf() throw()
{return &ptr_;
}

那么区别就非常明显了。最后我们写个例子来验证一下:

#include <wrl.h>
#include <iostream>using Microsoft::WRL::ComPtr;
using namespace std;class A
{
public:unsigned long ref = 0;void AddRef(){ref++;}unsigned long Release(){ref--;return ref;}
};void f(A** pp)
{}int main()
{A* p = new A;ComPtr<A> p1 = p;ComPtr<A> p2 = p;f(&p1);f(p2.GetAddressOf());cout << boolalpha;cout << "p1 nullptr " << ( p1.Get() == nullptr ) << endl;cout << "p2 nullptr " << ( p2.Get() == nullptr ) << endl;return 0;
}

运行结果如下:

ComPtr源码分析3

如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊

Reference

[1] ComPtr Class

[2] DirectX11–ComPtr智能指针

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/127921.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Qt6中使用Qt Charts

官方文档&#xff1a;Qt Charts 6.5.2 如果你是使用 CMake 构建的&#xff0c;则应在 CMakeLists.txt 中添加如下两行代码&#xff1a; find_package(Qt6 REQUIRED COMPONENTS Charts)target_link_libraries(mytarget PRIVATE Qt6::Charts) 其中 mytarget 为你的项目名称。一共…

aardio语言的通用数据表维护

import win.ui; /*DSG{{*/ var winform win.form(text"通用数据表维护";right617;bottom427;bgcolor15780518) winform.add( buttonAdd{cls"button";text"增加空行";left469;top40;right564;bottom80;flat1;z2}; buttonDel{cls"button&quo…

应用爆炸式增长,看F5如何做好网络安全防护

近年来&#xff0c;应用的数量呈现爆炸式增长。出行、支付、订单&#xff0c;开会&#xff0c;数字化的形式都在取代传统的消费&#xff0c;业务开展、工作内容都在发生着巨大的变化。随着数字化进程的加速&#xff0c;安全风险、安全问题暴露得越来越多。作为拥有强大安全基因…

【雷达原理】雷达信号级建模与仿真

目录 前言一、LFMCW信号概述1.1 优点1.2 缺点 二、LFMCW信号模型2.1 发射信号模型2.2 接收信号模型2.3 信号混频 三、MATLAB仿真3.1 仿真结果3.2 代码 四、参考文献 前言 雷达信号形式多种多样&#xff0c;按照雷达的体制进行分类&#xff0c;有脉冲雷达和连续波雷达。脉冲雷达…

Nacos docker实现nacos高可用集群项目

目录 Nacos是什么&#xff1f; Nacos在公司里的运用是什么&#xff1f; 使用docker构建nacos容器高可用集群 实验规划图&#xff1a;​编辑 1、拉取nacos镜像 2、创建docker网桥&#xff08;实现集群内的机器的互联互通&#xff08;所有的nacos和mysql&#xff09;&#x…

pytorch代码实现之空间通道重组卷积SCConv

空间通道重组卷积SCConv 空间通道重组卷积SCConv&#xff0c;全称Spatial and Channel Reconstruction Convolution&#xff0c;CPR2023年提出&#xff0c;可以即插即用&#xff0c;能够在减少参数的同时提升性能的模块。其核心思想是希望能够实现减少特征冗余从而提高算法的效…

WebDAV之π-Disk派盘 + 天悦日记

天悦日记是一款清爽简约的日记记录工具,通过天悦日记app随时随地快速写日记,更有智能数据统计分析报表,多端同步多种备份,本地备份和基于WebDAV协议的云端备份。跨平台使用,支持多设备、多平台无差别使用。天悦日记将每一天经历都清晰记录在手机,一目了然知道曾经的经历,…

Linux初探 - 概念上的理解和常见指令的使用

目录 Linux背景 Linux发展史 GNU 应用场景 发行版本 从概念上认识Linux 操作系统的概念 用户的概念 路径与目录 Linux下的文件 时间戳的概念 常规权限 特殊权限 Shell的概念 常用指令 ls tree stat clear pwd echo cd touch mkdir rmdir rm cp mv …

uboot顶层Makefile前期所做工作说明四

一. uboot顶层 Makefile文件 uboot 顶层 Makefile&#xff0c;就是 uboot源码工程的根目录下的 Makefile文件。 本文继续对 uboot顶层 Makefile的前期准备工作进行介绍。续上一篇文章内容的学习&#xff0c;如下&#xff1a; uboot顶层Makefile前期所做工作说明三_凌肖战的博…

DAMO-YOLO训练自己的数据集,使用onnxruntime推理部署

DAMO-YOLO训练自己的数据集&#xff0c;使用onnxruntime推理部署 DAMO-YOLO 是阿里达摩院智能计算实验室开发的一种兼顾速度与精度的目标检测算法&#xff0c;在高精度的同时&#xff0c;保持了很高的推理速度。 DAMO-YOLO 是在 YOLO 框架基础上引入了一系列新技术&#xff0…

Java的环境配置

目录 window系统安装java下载JDK配置环境变量JAVA_HOME 设置PATH设置CLASSPATH 设置测试JDK是否安装成功 Linux&#xff0c;UNIX&#xff0c;Solaris&#xff0c;FreeBSD环境变量设置流行JAVA开发工具使用 Eclipse 运行第一个 Java 程序 window系统安装java 下载JDK 首先我们…

爬虫进阶-反爬破解5(selenium的优势和点击操作+chrome的远程调试能力+通过Chrome隔离实现一台电脑登陆多个账号)

目录 一、selenium的优势和点击操作 二、chrome的远程调试能力 三、通过Chrome隔离实现一台电脑登陆多个账号 一、selenium的优势和点击操作 1.环境搭建 工具&#xff1a;Chrome浏览器chromedriverselenium win用户&#xff1a;chromedriver.exe放在python.exe旁边 MacO…

Unity汉化一个插件 制作插件汉化工具

我是编程一个菜鸟&#xff0c;英语又不好&#xff0c;有的插件非常牛&#xff01;我想学一学&#xff0c;页面全是英文&#xff0c;完全不知所措&#xff0c;我该怎么办啊...尝试在Unity中汉化一个插件 效果&#xff1a; 思路&#xff1a; 如何在Unity中把一个自己喜欢的插件…

新装Ubuntu系统的一些配置

背景&#xff1a; 最近办公要在Ubuntu系统上进行&#xff0c;于是自己安装了一个Ubuntu22.04系统&#xff0c;记录下新系统做的一些基本配置。 环境 &#xff1a; 系统&#xff1a;Ubuntu-22.04内核&#xff1a;6.2.0-26-generic架构&#xff1a;x86_64 一、 配置root密码 新…

Centos7 完全断网离线环境下安装MySQL 8.0.33 图文教程

Centos7 完全断网离线环境安装MySQL 8.0.33 图文教程 1.1前言1.2 下载离线安装包1.3 将下载好的离线安装包上传到Centos 7 服务器1.3.1 方式一:联网环境下可利用rz命令进行文件上传1.3.2 方式二:断网环境下使用 XFtp 等软件工具进行上传1.4 解压安装包1.5 执行安装脚本1.6 重…

Linux TCP和UDP协议

目录 TCP协议TCP协议的面向连接1.三次握手2.四次挥手 TCP协议的可靠性1.TCP状态转移——TIME_WAIT 状态TIME_WAIT 状态存在的意义&#xff1a;&#xff08;1&#xff09;可靠的终止TCP连接。&#xff08;2&#xff09;让迟来的TCP报文有足够的时间被识别并被丢弃。 2.应答确认、…

信息安全技术概论-李剑-持续更新

图片和细节来源于 用户 xiejava1018 一.概述 随着计算机网络技术的发展&#xff0c;与时代的变化&#xff0c;计算机病毒也经历了从早期的破坏为主到勒索钱财敲诈经济为主&#xff0c;破坏方式也多种多样&#xff0c;由早期的破坏网络到破坏硬件设备等等 &#xff0c;这也…

类和对象:构造函数,析构函数与拷贝构造函数

1.类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。 空类中真的什么都没有吗&#xff1f;并不是&#xff0c;任何类在什么都不写时&#xff0c;编译器会自动生成以下6个默认成员函数。 默认成员函数&#xff1a;用户没有显式实现&#xff0c;编译器…

Python之线程Thread(一)

一、什么是线程 线程(Thread)特点: 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;…

elasticsearch的索引库操作

索引库就类似数据库表&#xff0c;mapping映射就类似表的结构。我们要向es中存储数据&#xff0c;必须先创建“库”和“表”。 mapping映射属性 mapping是对索引库中文档的约束&#xff0c;常见的mapping属性包括&#xff1a; type&#xff1a;字段数据类型&#xff0c;常见的…