Skip to main content
GameDev.net gamedev.net
🔒 Locked

Should I be using CComptr , boost::share_ptr, or boost::intrusive_ptr for COM

Started by chadsxe Dec 15, 2009 at 5:20 PM 16 replies 6.4k views
Original Post
chadsxe
chadsxe
Hi, I am trying to figure out the best route as to how to handle COM object. More specific a few DirectX objects. From the looks of things there are three options to be had. CComPtr boost::shared_ptr with custom deleter boost::intrusive_ptr To be honest I can't really figure out what the pro's and con's are of one versus the other. Intrusive seems to remove the ref count all together. Beyond that I am open for some advice. Regards Chad
ApochPiQ
ApochPiQ
You should prefer CComPtr - neither shared_ptr nor intrusive_ptr is designed to work with COM. Although they may appear to work, they won't do reference counting correctly, as mentioned here.
chadsxe
chadsxe
Quote:
Original post by ApochPiQ
You should prefer CComPtr - neither shared_ptr nor intrusive_ptr is designed to work with COM. Although they may appear to work, they won't do reference counting correctly, as mentioned here.


It says shared_ptr can work with a custom deleter no?

Regards

Chad

Codeka
Codeka
Quote:
Original post by chadsxe
It says shared_ptr can work with a custom deleter no?
It will "sort of" work. Shared pointers will not incrememnt/decrement the ref-count of the COM pointer (assuming you've set it up correctly, it'll increment the ref-count once when you first create a shared_ptr, then when all copies of that shared_ptr are destroyed, the ref-count will be decremented once).

You should generally prefer CComPtr, though. That's what it was designed for, and there's not much point (in my opinion) trying to shoe-horn shared_ptr into something it's not just for the sake of "consistency" or some other aesthetic reason.
chadsxe
chadsxe
Quote:
Original post by Codeka there's not much point (in my opinion) trying to shoe-horn shared_ptr into something it's not just for the sake of "consistency" or some other aesthetic reason.
I guess you got a point.

Regards

Chad

cache_hit
cache_hit
Actually intrusive_ptr works fine with com objects. Indeed, this is *exactly* the point of intrusive_ptr. To allow integration with objects with their own defined reference counting model.

Here's some code I use for simplifying the use of COM objects. Feel free to use it

//This template is only enabled if T is a subclass of IUnknown (i.e. a com pointer)template<typename T>void intrusive_ptr_add_ref(	T* t,	typename boost::enable_if<boost::is_base_of<::IUnknown, T> >::type* dummy=0){	t->AddRef();}//This template is also only enabled if T is a subclass of IUnknown (i.e. a com pointer)template<typename T>void intrusive_ptr_release(	T* t,	typename boost::enable_if<boost::is_base_of<::IUnknown, T> >::type* dummy=0){	t->Release();}namespace com{	//My proposed alternate of CComPtr.  This version is unimplemented	//so that we can explicitly designate which types the class should	//exist for (namely, those types that derive from IUnknown)	template<class T, class Enable=void>	class ptr;	template<class T>	class ptr<		T,  		typename boost::enable_if<boost::is_base_of<::IUnknown, T> >::type	>	{	public:		typedef boost::intrusive_ptr<T>	type; 	private:		//This prevents the common error of writing something like		//com::ptr<IDirect3D9> var;		//You must actually write		//com::ptr<IDirect3D9>::type var;		ptr();		~ptr();	};	//The rest of what follows is used for prodiving a simplified	//way of calling API functions that expect an Ixxxx** and returns	//a newly allocated COM pointer with a ref count of 1.  The normal	//approach is to use CComPtr::operator&, but this is dangerous.	template<typename T>	typename ptr<T>::type take_ptr(T* t)	{		return ptr<T>::type(t, false);	}	template<typename T>	class recv_ptr_t	{	public:		recv_ptr_t(typename ptr<T>::type& r) : auto_(r), raw_(NULL) {}		~recv_ptr_t() {auto_ = take_ptr(raw_);}		operator T**() { return &raw_; }		operator void**() { return reinterpret_cast<void**>(&raw_); }	private:		T* raw_;		typename ptr<T>::type& auto_;	};	template<class T> 	recv_ptr_t<typename T::element_type> recv_ptr(T& rptr)	{		return recv_ptr_t<typename T::element_type>(rptr);	}	template<typename Dest, typename Src>	ptr<Dest> query_interface(Src p, const IID& iid = __uuidof(Dest))	{		void* pv = NULL;		p->QueryInterface(iid, &pv);		Dest* p2 = static_cast<Dest*>(pv);		return ptr<Dest>(p2, false);	}}


Here's a code snippet that uses the above stuff:

com::ptr<IDirect3D9>::type d3d(::Direct3DCreate9(DX_SDK_VERSION), false);com::ptr<IDirect3DDevice9>::type device;d3d->CreateDevice(   D3DADAPTER_DEFAULT,    D3DDEVTYPE_HAL,    NULL,   D3DCREATE_HARDWARE_VERTEXPROCESSING,   &present_params,   com::take_ptr(device));
ApochPiQ
ApochPiQ
Yes, but the point is, you have to do extra work to make intrusive_ptr work correctly with COM's reference counting system. Can you at all justify doing that over using the CComPtr class which is already guaranteed to do the correct thing?
cache_hit
cache_hit
Quote:
Original post by chadsxe
To be honest I can't really figure out what the pro's and con's are of one versus the other. Intrusive seems to remove the ref count all together. Beyond that I am open for some advice.


See the previous post for a solution using intrusive_ptr, but I figured I would also respond to this to explain exactly what intrusive_ptr is for and how it works.

shared_ptr manages its own internal reference count. This is great most of the time, but lots of other objects manage their own reference counts and you don't always have the ability to change the way this operates. COM is a great example, if you don't use their internal reference counting scheme your program will leak memory, and calling delete on an IUnknown* is incorrect.

The way intrusive_ptr works is by having the programmer tell intrusive_ptr how to find the *actual* reference counting functions to use. Instead of incrementing its own internal reference count, it just calls this function.

Naturally, if it's going to invoke a function for you, you have to wonder if you're decreasing your application's performance. You already have add ref and release functions, why should you waste efficiency *first* calling some wrapper function, and *then* calling the real function? It is for this reason that intrusive_ptr requires you to implement your wrapper functions as free functions (i.e. global functions). This way they can be inlined and completely eliminate any overhead of the intrusive_ptr internals calling your AddRef or release function. Specifically, an intrusive_ptr is going to try to write:

intrusive_ptr_add_ref(internal_ptr_);


and

intrusive_ptr_release(internal_ptr_);


So your global function must be defined accordingly. For the case of COM, there are an unbounded number of possible types. In fact, anything that derives from IUnknown is a COM object. Using some slightly more advanced techniques, you can define a templated intrusive_ptr_xxx function that is located as a match for an invocation if and only if its argument is IUnknown*, or a pointer to a subclass of IUnknown. So that's where the enable_if stuff comes from in the previous code sample.

If you don't understand how it works it's probably fine. The important thing is that you can just "use it" and it should work.
cache_hit
cache_hit
Quote:
Original post by ApochPiQ
Yes, but the point is, you have to do extra work to make intrusive_ptr work correctly with COM's reference counting system. Can you at all justify doing that over using the CComPtr class which is already guaranteed to do the correct thing?


Absolutely.

a) CComPtr is part of ATL. I don't want to bring in an entire library when I only need one class from it. Especially if I don't actually need that class.

b) overloaded operator& is DANGEROUS, DANGEROUS, DANGEROUS. In fact, due to overloaded operator&, it's not guaranteed to do the correct thing in all circumstances.

c) despite the fact that the code to make it work does not look simple, using it actually is very simple.
iMalc
iMalc
Quote:
Original post by ApochPiQ
Yes, but the point is, you have to do extra work to make intrusive_ptr work correctly with COM's reference counting system. Can you at all justify doing that over using the CComPtr class which is already guaranteed to do the correct thing?
Indeed. "Can" does not equal "should".
CComPtr does many other nice things for you such as making QueryInterface calls easier, etc.

cache_hit:
(Edited)
a) I think you can apply the same logic of this one to boost. One is probably more likely to already be using ATL than one is to be using boost.

b) Not overloading operator & is very inconvenient (3x in capitals [smile]). CAdapt is provided as a means of removing that behaviour.

The only reason to use an intrusive_ptr would be for job security! [razz]
cache_hit
cache_hit
Quote:
Original post by iMalc
Quote:
Original post by ApochPiQ
Yes, but the point is, you have to do extra work to make intrusive_ptr work correctly with COM's reference counting system. Can you at all justify doing that over using the CComPtr class which is already guaranteed to do the correct thing?
Indeed. "Can" does not equal "should".


"Should" equals "should" though. Can you really justify not only creating a dependency on an ENTIRE LIBRARY for a single class, but also littering your codebase with a horrendously dangerous function call, when there is an alternative that has 100% of the benefits of CComPtr, with none of the negatives?
iMalc
iMalc
Oh, I didn't see your QueryInterface adapter there - nice touch.

Well each to his own then I guess.
cache_hit
cache_hit
Honestly if it weren't for operator& I'd use it. I can stomach making a dependency on ATL. But what happens when you store CComPtr in a container? I don't even think you can actually, because STL, and for that matter, pretty much anything depends on operator& behaving a certain way. operator& doesn't even return the same type, so if you have a vector > then there's no way to operate on this generically, and for that matter it might not even compile. If it does compile, you're in for a world of hurt, and that's just one container.

Relying on anything that uses operator& should be banned by virtually all reasonable coding standards IMO. This is the type of bug that if you do ever run into, it will take you days, if not weeks to figure out
MJP
MJP
I prefer CComPtr, if I have the option. You just have to watch out for the assignment operator...a lot of noobies use that when they really want Attach (assignment operator will call AddRef, Attach won't).
ApochPiQ
ApochPiQ
Quote:
Original post by cache_hit
Quote:
Original post by ApochPiQ
Can you at all justify doing that over using the CComPtr class which is already guaranteed to do the correct thing?


Absolutely.



Fair enough [smile]

I must admit that my last experience with COM and C++ was in a shop stuck in the mid '90s, writing MFC and ATL code all over the place; in that context I often saw people doing (unsafe) manual reference counting on their COM pointers, and lots of bugs as a result. From that perspective, using CComPtr was "the right thing to do" as far as writing mostly correct code. Worse, that shop was behind the curve on VS releases for a long time, and still used VC++6.0, which obviously couldn't cope with most of boost anyways, so the applicability of boost to the project was a moot point.

Since then I've been careful to only touch COM in managed languages, which is a far better way to go about things [smile]


So I guess my instincts are out of date on that issue. Should I ever find myself having to do COM work in C++ again (God forbid!) I'll definitely be looking for something like cache_hit's code.
Cygon
Cygon
As far as I know, the latest answer to COM pointers in ATL are _com_ptr_t (and _bstr_t, _variant_t). This is also the pointer that's used for the ISomeInterfacePtr typedefs when you #import a COM DLL (#import is a Microsoft extension to VC++).

They work well in STL containers and you even get a clean solution for QueryInterface:

_COM_SMARTPTR_TYPEDEF(ISomething, IID_ISomething);_COM_SMARTPTR_TYPEDEF(IOther, IID_IOther);    // Create a new instance using CoCreateInstance()ISomethingPtr something = ISomethingPtr(CLSID_Something);// QueryInterface to another IIDtry {  IOtherPtr other = something;}catch(const _com_error &error) {  cerr << "QueryInterface failed with HRESULT = " << error.Error();}

Or assign new instances easily:

template<typename Class>inline CComObject<Class> *com_new() {  CComObject<Class> *pInstance;  CheckError(CComObject<Class>::CreateInstance(&pInstance));  return pInstance;}// Create an instance of a local ATL classISomethingPtr something = com_new<CSomeAtlCoClass>();// Take out the pointer without Release()ingISomething *raw = something.Detach();// Assign a new pointer without AddRef()ingsomething.Attach(new ManuallyImplementedCoClass());

Earlier DirectX SDKs also declared _com_ptr_ts for you if you #included before d3d9.h, but I believe this is no longer the case.

Personally, I'd stay away from CComPtr, CComBSTR and their companions. Compare the code behind CComPtr and _com_ptr_t and the choice should be rather clear.
Professional C++ and .NET developer trying to break into indie game development.
Follow my progress: http://blog.nuclex-games.com/ or Twitter - Topics: Ogre3D, Blender, game architecture tips & code snippets.
cache_hit
cache_hit
I'll have to look into the solution about _com_ptr_t, _bstr_t, etc. To be honest I'm not very familiar with them. I suppose if I had to remove the library dependency I could copy them out in theory.

Still though, I think I would miss the ability to write things like:

d3d->CreateDevice(..., take_ptr(device));

The only alternatives are either an overloaded address-of operator or an additional line of code to first store it in a raw pointer.
iMalc
iMalc
Quote:
Original post by Cygon
As far as I know, the latest answer to COM pointers in ATL are _com_ptr_t (and _bstr_t, _variant_t). This is also the pointer that's used for the ISomeInterfacePtr typedefs when you #import a COM DLL (#import is a Microsoft extension to VC++).

They work well in STL containers and you even get a clean solution for QueryInterface:
Ah actually I'm all too familiar with those as well. The problem is that even the VS2008 implementation of them misses some consts in it. For example the less-than operator uses the one parameter version and although it takes that parameter by const reference, the method itself isn't const. This causes much difficulty when trying to store a std::set of these etc.

The address-of operator of a CComPtr can be worked around by wrapping them in a CAdapt.
Wrapping an ISomethingPtr pointer in something to correct its bugs is a lot more work.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.