ComPtr源码分析

news2025/1/18 16:56:36

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.coloradmin.cn/o/997634.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BUUCTF easyre 1

使用die工具进行文件信息的查看 可以看到是64位程序 使用IDA64打开 f5 反汇编 得到flag

超详细Python第三方库的安装,多图,逐步骤

Python第三方库的安装 前言1. PyCharm中安装模块2. PyCharm终端中命令安装3. 命令行安装4. 命令补充 总结 前言 Python有丰富的第三方库&#xff0c;在Python编程中&#xff0c;经常需要安装第三方库&#xff0c;本文详细介绍了第三方模块/软件包的安装。 提示&#xff1a;大家…

手写Spring:第17章-通过三级缓存解决循环依赖

文章目录 一、目标&#xff1a;通过三级缓存解决循环依赖二、设计&#xff1a;通过三级缓存解决循环依赖2.1 通过三级缓存解决循环依赖2.2 尝试使用一级缓存解决循环依赖 三、实现&#xff1a;通过三级缓存解决循环依赖3.1 工程结构3.2 通过三级缓存解决循环依赖类图3.3 设置三…

Kafka3.0.0版本——消费者(手动提交offset)

目录 一、消费者&#xff08;手动提交 offset&#xff09;的概述1.1、手动提交offset的两种方式1.2、手动提交offset两种方式的区别1.3、手动提交offset的图解 二、消费者&#xff08;手动提交 offset&#xff09;的代码示例2.1、手动提交 offset&#xff08;采用同步提交的方式…

JavaScript-----jQuery

目录 前言&#xff1a; 1. jQuery介绍 2. 工厂函数 - $() jQuery通过选择器获取元素&#xff0c;$("选择器") 过滤选择器&#xff0c;需要结合其他选择器使用。 3.操作元素内容 4. 操作标签属性 5. 操作标签样式 6. 元素的创建,添加,删除 7.数据与对象遍历…

torch.cuda.is_available() 解决方

本人使用的显卡如下&#xff0c;打开任务管理器查看 Anaconda下载哪个版本都可以 使用命令conda create -n pytorch python3.6创建一个名为pytorch的环境&#xff0c;解释器使用3.6的 使用命令conda activate pytorch进入该环境 进入pytorch官网&#xff0c;选择下列选项 复…

九 动手学深度学习v2 ——卷积神经网络之AlexNet

文章目录 AlexNetVGG AlexNet AlexNet新引入dropout、ReLU、maxpooling和数据增强。 VGG VGG神经网络连接 图7.2.1的几个VGG块&#xff08;在vgg_block函数中定义&#xff09;。其中有超参数变量conv_arch。该变量指定了每个VGG块里卷积层个数和输出通道数。全连接模块则与Ale…

超详细最新PyCharm+Python环境安装,多图,逐步骤

PyCharmPython环境安装 前言一、pycharm下载安装1. 安装地址2. 安装详细步骤 二、Python下载安装1. 安装地址2. 安装详细步骤3. 环境变量忘记添加4. python安装成功测试 三. PyCharm上配置Python总结推荐文章 前言 文章会详细介绍PyCharmPython详细安装步骤&#xff0c;接下来…

【数据分享】2012-2023年全国范围的逐月NPP/VIIRS夜间灯光数据

在之前的文章中我们分享了2012-2022年全球范围逐年NPP/VIIRS夜间灯光数据&#xff01;以及2012-2022年中国范围的逐年NPP/VIIRS夜间灯光数据&#xff08;均可查看之前的文章获悉详情&#xff09;很多小伙伴询问有没有逐月的夜间灯光数据。 本次我们分享的是2012-2023年中国范围…

Flask 快速上手教程 — 了解与基本使用

Flask 快速上手教程 — 了解与基本使用 这篇博客是我刚接触 flask&#xff0c;研究文档时的一些记录与体会&#xff0c;希望对各位刚接触 flask 的朋友有所帮助。 且在此篇后&#xff0c;我还会另写一篇关于纯后端的 flask 教程&#xff0c;介绍一下如何使用 flask 创建一个较…

Oracle for Windows安装和配置——2.1.Oracle for Windows安装

​2.1.1. 准备Oracle软件 1)下载或拷贝安装软件 下载地址:otn.oracle.com或my oracle support。下载文件列表。具体如图2.1.1-1所示。图2.1.1-1 下载文件列表 --说明: 1)通过otn.oracle.com站点,可以免费下载用于安装的Oracle软件,但通常只能下载到Oracle各大版本的base…

切面(增强)的优先级

Component Aspect Order(value 10)//为增强类指定一个优先级的值,值越小,优先级越高,优先级越高的前置先执行,后置后执行,类似洋葱 为增强类指定一个优先级的值,值越小,优先级越高,优先级越高的前置先执行,后置后执行,类似洋葱 首先会执行前置通知,再执行目标方法,按照顺序和优…

并查集介绍和常用模板

并查集介绍和常用模板 前言&#xff1a; 并查集&#xff08;Union-find set 也叫Disjoint Sets&#xff09;是图论里面一种用来判断节点之间是否连通的数据结构&#xff0c;学会使用它可以处理一些跟节点连通性的问题。它有两个很重要的方法&#xff1a; Find(x)&#xff1a;…

2023软件设计师上半年真题解析(上午+下午)

上午试题 1.系统总线 计算机中&#xff0c;系统总线用于&#xff08;&#xff09;连接。 A&#xff0e;接口和外设 B&#xff0e;运算器,控制器和寄存器C&#xff0e;主存及外设部件 D&#xff0e;DMA控制器和中断控制器 总线可以划分为数据总线、地址总线和控制总线。系统总…

自然语言处理应用(一):情感分析

情感分析 随着在线社交媒体和评论平台的快速发展&#xff0c;大量评论的数据被记录下来。这些数据具有支持决策过程的巨大潜力。 情感分析&#xff08;sentiment analysis&#xff09;研究人们在文本中 &#xff08;如产品评论、博客评论和论坛讨论等&#xff09;“隐藏”的情…

面向OLAP的列式存储DBMS-16-[ClickHouse]python操作ClickHouse

clickhouse查询表容量方法 1 clickhouse常用命令 #clickhouse-client进入客户端 pda1:)show databases; pda1:)create database test; pda1:)use system; pda1:)show tables; pda1:) exit; 其余的就是常规的一些sql语句。 2 python操作clickhouse 2.1 clickhouse-driver(9…

Windows下python,psycopg2使用sm3连接HGDB

瀚高数据库 目录 环境 文档用途 详细信息 环境 系统平台&#xff1a;N/A 版本&#xff1a;4.5 文档用途 本文介绍在HGDB使用sm3认证时&#xff0c;python使用psycopg2连接HGDB的方法。 详细信息 Python连接HGDB可以使用psycopg2、Django&#xff0c;Django是依赖psycopg2的&a…

微信小程序登录问题(思路简略笔记)

配置问题 这是小程序登录问题&#xff0c;必要的两个配置。 流程思路 1. 微信小程序端&#xff0c;会返回一个code。 2. 查看需要返回给微信小程序端的数据。 3. 既然需要返回三个数据&#xff0c;先看openid如何拿到 WX-Login https://api.weixin.qq.com/sns/jscode2ses…

C高级 Shell脚本

一、作业&#xff1a;写一个shell脚本&#xff0c;将以下内容放到脚本中 在家目录下创建目录文件&#xff0c;dir在dir下创建dir1和dir2把当前目录下的所有文件拷贝到dir1中&#xff0c;把当前目录下的所有脚本文件拷贝到dir2中把dir2打包并压缩为dir2.tar.xz再把dir2.tar.xz移…

2023-9-10 集合-Nim游戏

题目链接&#xff1a;集合-Nim游戏 #include <iostream> #include <cstring> #include <algorithm> #include <unordered_set>using namespace std;const int N 110, M 10010;int n, m; int s[N], f[M];int sg(int x) {if(f[x] ! -1) return f[x];//…