Sign in to follow this  
matt77hias

Smart Pointers (shared_ptr and ComPtr)

Recommended Posts

I have multiple classes/structs owning and/or using for instance ID3D11Device and ID3D11DeviceContext. Currently I use ComPtr smart pointers for these structures instead of raw pointers in case a class/struct needs to own them and a reference otherwise. Generally, however, my Renderer class is the real owner. If my Renderer is destructed, owning a ID3D11Device or ID3D11DeviceContext is useless since rendering is finished. Is it therefore better practice to always pass references around and use member variable references in case a class needs to 'own'  a ID3D11Device and/or ID3D11DeviceContext? So ComPtr will only be used in my Renderer? It should even be possible to use the more efficient (since no reference counting is required) unique_ptr with a custom destructor making ComPtr useless.

Share this post


Link to post
Share on other sites

Destructor of a pass will release owned object.
Do you mean an explicit reset or just the default behavior when releasing the stack space (i.e. calling destructor on ComPtr)?

it will receive it as a raw pointer

But now you need to provide guarantee of not passing nullptr. Isn't this a bit odd for ID3D11Device and ID3D11DeviceContext, since you "never" expect them to be nullptr.

Share this post


Link to post
Share on other sites
Happy SDE, on 27 Mar 2017 - 9:08 PM, said: Destructor of a pass will release owned object. Do you mean an explicit reset or just the default behavior when releasing the stack space (i.e. calling destructor on ComPtr)?

Renderer owns a Pass.

Renderer is wrapped by std::unique_ptr.

Pass is a Renderer's member (stack allocation, not by new).

When I need release renderer, I call std::unique_ptr::reset().

Pass will automatically destroyed with it's all owned objects (like input layout) via RAII (stack destructor, +ComPtr::Release() for owned objects).

Device lives longer, than renderer (in current implementation, it is a singleton).

 

it will receive it as a raw pointer

But now you need to provide guarantee of not passing nullptr. Isn't this a bit odd for ID3D11Device and ID3D11DeviceContext, since you "never" expect them to be nullptr.

Usually textures are created once (between resizes), context is created once before Renderer.

If something goes wrong, exception will be thrown and there will be no way of calling Render() in this case.

So I have 146% guarantee, that there will be no nullptr at Render() or similar call. :)

It's called an "invariant" in C++.

Edited by Happy SDE

Share this post


Link to post
Share on other sites
input layout

Is your input layout owned by multiple objects? If not, aren't there a lot of ComPtrs with a ref count of 1 and isn't this an overhead compared to unique_ptr with a custom destructor?

 

 

Device lives longer, than renderer (in current implementation, it is a singleton).

If your Device outlives most of your code, why using the same smart pointers. I mean if objects A, B, C all have a ComPtr to the same data, but without A (e.g. your Device), B and C should not exist since no rendering will be performed anymore. The problem with a ComPtr is that your pointed data is not guarenteed to be destructed after destructing A (without using reset) since B and C could still live due to programmers violating the design?

Edited by matt77hias

Share this post


Link to post
Share on other sites

If IL is owned by multiple Passes, there will be AddRef() twice, like standard rules of c++.

This AddRef() will be called only 2 times.

The other option: have SharedObjects object, that will own the IL, and pass raw IL pointer to clients.

 

IF you pass ComPtr by value in each Render() call, this is a problem.

But if you only store it as a member - no overhead.

shared_ptr is the same as AddRef(): ++ operator is atomic.

 

Situation when more than 1 object owns the same object is quite rare (in my code).

Usually the construction is multi-level:

Device -> Renderer -> Pass.

For GameRenderer, I know what passes are there, and their order.

For SplashScreenRenderer, passes are different, but they are known.

It is static on compilation time.

Customization is available in construction time (in constructors), for example: MSAA/no MSAA version of a shader.

The other customization option have 2 version of a renderer with the same interface:

Renderer::std::unique_ptr<ISsao> m_ssao;

And have 2 implementation of it: NoSsaoPass and SsaoPass.

NoSsaoPass will do nothing.

 

But after all is created, there is no need to destroy any pass/any of it subobject.

There is only need to update buffers/call Render() in some (strict) order.

This is a job of a particular renderer (like SplashScreenRenderer or GameRenderer).

 

When a game is finished, entire renderer is destroyed (with all Passes, and it's subobjects), and new renderer (SpashScreenRenderer will be created).

The device and context will be still alive.

Edited by Happy SDE

Share this post


Link to post
Share on other sites

Option1:

class Renderer{
   Renderer(): m_passA{m_shared.m_sharedIL.Get()}, m_passB{m_shared.m_sharedIL.Get()}{}
   
   SharedObjects m_shared; //Should be before m_passA and m_passB
   PassA         m_passA;
   PassB         m_passB;
}
struct SharedObjects
{
    SharedObjects() {build m_sharedIL, if something went wrong, throw exception}
    ComPtr<IL> m_sharedIL;
}
class PassA
{
    PassA(IL* il) : m_il{il}{}
    IL* m_il;
}
class PassB
{
    PassB(IL* il) : m_il{il}{}
    IL* m_il;
}

Option2:

No SharedObjects class, but PassA and PassB will store ComPtr to the same object;

Edited by Happy SDE

Share this post


Link to post
Share on other sites

Option1:

class Renderer{
   Renderer(): m_a{m_shared.m_sharedIL}, m_b{m_shared.m_sharedIL}{}
   
   SharedObjects m_shared; //Should be before m_a and m_b
   PassA m_a;
   PassB m_b;
}
struct SharedObjects
{
    ComPtr<IL> m_sharedIL;
}
class PassA
{
    PassA(IL* il) : m_il{il}{}
    IL* m_il;
}
class PassB
{
    PassB(IL* il) : m_il{il}{}
    IL* m_il;
}

Option2:

No SharedObjects class, but PassA and PassB will store ComPtr to the same object;

Option 3 : if you use IL& instead of IL* in PassA and PassB, you could use unique_ptr<IL> with a custom deleter?

Share this post


Link to post
Share on other sites

How many D3D objects do you have? (how much bytes are you going to save?)

It feels not like solving a performance issue, but rather a design issue. Since all the pointers are not allowed to be nullptr, which can only be enforced with a reference. Using a ComPtr or SharedPtr seems the wrong tool if the ref count stays one. But because I do not find online examples/tutorials pursuing a similar philosophy, I start wondering whether I missed pitfalls. Move semantics is problematic, but then you could use reference_wrapper.

Share this post


Link to post
Share on other sites

It feels not like solving a performance issue, but rather a design issue. Since all the pointers are not allowed to be nullptr, which can only be enforced with a referenc

Is it better to throw when you try to create an object (texture for example), but failed?

That will solve a problem of having nullptr instead of fully constructed object.

Share this post


Link to post
Share on other sites

 

It feels not like solving a performance issue, but rather a design issue. Since all the pointers are not allowed to be nullptr, which can only be enforced with a referenc

Is it better to throw when you try to create an object (texture for example), but failed?

That will solve a problem of having nullptr instead of fully constructed object.

 

Creating a reference to nullptr will throw something (or just error?), but you could throw something yourself before making a reference and this is only one location in code (as opposed to all the methods excepting a pointer).

Share this post


Link to post
Share on other sites

Creating a reference to nullptr will throw something (or just error?), but you could throw something yourself before making a reference and this is only one location in code (as opposed to all the methods excepting a pointer).
void __cdecl main()
{
    int* pi = nullptr;        //Construction
    std::cout << *pi << '\n'; //Usage
}

In my case there will exception in construction.

In your case there will be "access violation" exception in usage.

I would prefer the first one.

Share this post


Link to post
Share on other sites

 

But does a reference would ever make sense for you?

Big object copy construction avoidance  :wink:

 

For C++11 not really an issue anymore with the move semantics. But here you could use the pointer as well?

I would reformulate and nuance my previous question: Does a pointer to reference conversion would ever make sense for you?

Share this post


Link to post
Share on other sites
For C++11 not really an issue anymore with the move semantics.

[I've updated my answer before you posted this message], so not "Big", but "Expensive".

If you pass std::string by value to a function, there will be "new" + copy. Move will not work here.

If you pass ComPtr<> by value, there will be AddRef/Release().
 

Does a pointer to reference conversion would ever make sense for you?

Nothing comes to my mind right now.

 

Let's take a look at the problem from a different angle:

 

Input:

1. You have COM object (by D3D), for example, texture.

2. You have wrapper - WRL::ComPtr<>, that stores a pointer (you can't do better with your code, because you need to store a pointer).

3. Reference counting is baked inside the texture, and not inside the wrapper (ComPtr).

std::shared_ptr<> will add 2 counters: ref + weak_ref.

4. COM is a C API (dll-bounary restriction)

5. Amount of D3D object usage inside a renderer is 1.000-50.000 (take a look by Alt+F5 in VS with your program).

 

Output:

1. What is it possible to improve here on CPU side?

2. Does it worth to improve it?

Edited by Happy SDE

Share this post


Link to post
Share on other sites

3. Reference counting is baked inside the texture, and not inside the wrapper (ComPtr).

:O Didn't know, thought it was similar to shared_ptr except for the destructor.

 

class PassA {     PassA(IL* il) : m_il{il}{}     IL* m_il; } class PassB {     PassB(IL* il) : m_il{il}{}     IL* m_il; }

I still wonder why you pass a raw pointer if PassA and PassB are going to have the pointer as member? 

 


 

Share this post


Link to post
Share on other sites
matt77hias, on 27 Mar 2017 - 10:30 PM, said: class PassA { PassA(IL* il) : m_il{il}{} IL* m_il; } class PassB { PassB(IL* il) : m_il{il}{} IL* m_il; } I still wonder why you pass a raw pointer if PassA and PassB are going to have the pointer as member?

1. Real owner - is SharedObjects object.

2. It will outlive PassA && PassB => no need them to be owners.

3. For usage, D3D requires from you IL raw pointer, not ComPtr => You will need to call ComPtr<>::Get() in Render() or just store it as raw pointer.

Edited by Happy SDE

Share this post


Link to post
Share on other sites

1. Real owner - is SharedObjects object. 2. It will outlive PassA && PassB => no need them to be owners. 3. For usage, D3D requires from you IL raw pointer, not ComPtr => You will need to call ComPtr<>::Get() in Render() or just store it as raw pointer.
 

 

Ok thank you, then am going to change all my ComPtr usage, since only one object really has to own them whereas the other objects could hold on to a raw pointer. Even though it does not represent ownership anymore, it still works for move semantics (as opposed to references).

Share this post


Link to post
Share on other sites

If you pass std::string by value to a function, there will be "new" + copy. Move will not work here.

Thats not true at all.

void test(std::string value)
{
}

std::string strTest = L"HelloWorld";
test(std::move(strTest)); // calls move-ctor of "value"

This will only involve the move-constructor on the call to test, no copy. Obviously if you don't call std::move it will invoke a copy, and using std::string&& might avoid a move-ctor call in unoptimized builds, but other than that, passing by value is perfectly fine with move-semantics, and is probably prefered for situations where you don't know if the function is called with a temporary or an object to copy. Onyl real downside is that debugging gets slightly more complicated, as now you have to step through the std::string move-ctor before you can enter the function body.

Share this post


Link to post
Share on other sites
Thats not true at all.

With one small caveat: it will destroy initial strTest:

void Test(std::string value)
{
    std::cout << "InTest: " << value << '\n';
}

void __cdecl main()
{
    std::string strTest = "HelloWorld";
    Test(std::move(strTest)); // calls move-ctor of "value"
    std::cout << "After Test: " << strTest << '\n';
}
InTest: HelloWorld
After Test:
Press any key to continue . . .
Edited by Happy SDE

Share this post


Link to post
Share on other sites

Yeah, it will destroy strTest, but after it has been moved, so it will boil down to "delete nullptr" (and also invoke an additional move-ctor compared to passing by std::string&&)- but how is this different to passing by && and moving? Or did you try to make an entirely different point?

Plus, I haven't tested it, but I just hope compilers are smart enough to do something about this minor overhead and compile the underlying assembly to be on par with passing by std::string.

Edited by Juliean

Share this post


Link to post
Share on other sites

We've discussed in this topic "const T&", T*, and "T" mostly related to ComPtr.

 

BTW try to pass ComPtr<ID3D11DeviceContext> as && (instead of const T& or T) :)

And tell me, how much frames will you render :wink:

 

Upd: If you pass it as T, it will call AddRef()/Release() on each call

Edited by Happy SDE

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this