Jump to content
  • Advertisement
Sign in to follow this  
Juliean

Wrong template instantiation with SFINAE

This topic is 915 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello,

 

so I've got this template code for handling optional reference counting in my type system. There is an issue with a specific type where it calls the wrong version of the template, but I'll first show as much code of it as I can. (BTW, I'm on Visual Studio 2015).

struct refCountHelper
{
    template<typename ObjectType>
    static auto AddRef(ObjectType* pObject) -> decltype(AddRef(pObject, 0))
    {
        return AddRef(pObject, 0);
    }

private:

    template<typename ObjectType>
    static auto AddRef(ObjectType* pObject, int) -> decltype(pObject->AddRef(), void())
    {
        if(pObject)
            pObject->AddRef();
    }

    template<typename ObjectType>
    static void AddRef(ObjectType* pObject, long)
    {
    }
};

This is the class that handles the reference-counting. If the object being passed in has an AddRef-method, it calls it, otherwise it chooses an empty method (which hopefully gets compiled out in release).

 

The class in question which gets used by this and produces the issue is my asset-class:

class ACCLIMATE_API BaseAsset
{
public:

    DECLARE_REF_COUNTER // see below

};

template<typename Type>
class Asset final :
    public BaseAsset
{
};

#define DECLARE_REF_COUNTER \
        void AddRef(void) \
        { \
            m_referenceCounter++; \
        } \
        void Release(void) \
        { \
            m_referenceCounter--; \
            if(!m_referenceCounter) \
                delete this; \
        } \
        int m_referenceCounter = 0;

So the BaseAsset-class has a reference-counter, and the Asset<Type> class is then used as an actual representation of an asset of a certain type.

 

Now I can do ie:

refCountHelper::AddRef<asset::Asset<ai::BehaviourTree>>(&asset);

To increase the reference count of an asset. This is obviously used by other template code, like a wrapper for storing an arbitrary object for the script system:

template<typename ObjectType>
class ObjectContainer :
	public Object
{
public:

	// TODO: get rid of const-cast
	ObjectContainer(const ObjectType& object) : Object(Type()),
		m_pObject(&const_cast<ObjectType&>(object))
	{
		refCountHelper::AddRef<ObjectType>(m_pObject);
	}

private:

    ObjectType* m_pObject;
}

I hope you can still follow the code and get what it does.

 

Because now we get to the issue. The code works fine, except for one specific instance of an asset-type. Look at this test-code:

using TestAsset = asset::Asset<ai::BehaviourTree>;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    TestAsset asset(L"Testing");

    core::refCountHelper::AddRef<TestAsset>(&asset); // 1 - works

    core::ObjectContainer<TestAsset> object2(asset); // 2 - doesn't - essentially calls the empty "static void AddRef(ObjectType* pObject, long)" in the refCounterHelper-class
}

This is what is happening:

In the line "1" where I call the reference counting directly, it calls the correct template method and increases the ref-counter.

In the line "2", where the ObjectCointainer<TestAsset>-constructor calls the referencing counting itself (implementation above), it calls the empty AddRef-helper method instead.

 

What the heck? I checked the callstack, the template type is passed correctly. Also its really just that I added another layer of indirection, the actual refCounterHelper::AddRef-calls are identically, yet when I call it via the ObjectContainer it somehow fails to choose the correct method for calling AddRef on the Asset<>-class.

 

Now here's the deal:

- This only happens with this specific type of asset. Asset<Texture>, Asset<Script>, ... I pretty much checked all of them and this very instance is the only one that fails.

- The test code is executed in a separate exe, but it also happens inside of the actual DLL where all the other code is located.

- It also only happens if I actually include the definition of the class in question. If I remove the header and forward declare it instead:

namespace acl
{
	namespace ai
	{
		class BehaviourTree;
	}
}

It works as expected.

- It also works if I make a similar class inside the test-project, where I have a templated class whose constructor calls the refCounterHelper.

 

So even though this also happens inside the same (DLL)-project, I suspect it has something to do with the goddamn DLL-**** again. Anyways, here is what the header of the class that is not working looks like:

#pragma once
#include "Types.h"
#include "..\Asset\ExportAsset.h"
#include "..\Core\Dll.h"
#include "..\System\Pointer.h"
#include "..\System\ClassHelper.h"

namespace acl
{
    namespace ai
    {
        class BehaviourNode;
        class Blackboard;

        class ACCLIMATE_API BehaviourTree
        {
            using NodeVector = std::vector<sys::Pointer<BehaviourNode>>;
        public:
            NON_COPYABLE(BehaviourTree); // deletes assignment-operator & copy-ctor

            BehaviourTree(const Blackboard* pBlackboard);
            BehaviourTree(const Blackboard* pBlackboard, NodeId uid);
            ~BehaviourTree(void);
        };

        EXPORT_ASSET_OBJECT(BehaviourTree) // exports some important template-declarations outside of the DLL, see below

    }
}

#define EXPORT_ASSET_OBJECT(Type) EXPORT_OBJECT(acl::asset::Asset<Type>) \
                                EXPORT_OBJECT(Type)

#define EXPORT_OBJECT(Type) \
        static_assert(acl::core::isObject<Type>::value, "Type is not an object"); \
        EXPORT_TEMPLATE_CLASS(acl::core::ObjectContainer<Type>); 

// exporting code for template symbols
#ifdef ACCLIMATE_EXPORT
#define ACCLIMATE_API __declspec (dllexport)
#define EXPORT_TEMPLATE template
#else
#define ACCLIMATE_API __declspec (dllimport)
#define EXPORT_TEMPLATE extern template
#endif

#define EXPORT_TEMPLATE_CLASS(...) \
    __pragma(warning(disable : 4251)) \
    EXPORT_TEMPLATE class ACCLIMATE_API __VA_ARGS__; \
    __pragma(warning(default : 4251))

So I am already exporting some of the symbols of the class in question (core::ObjectContainer<ai::BehaviourTree>), which is needed for the type-system to work. I just cannot see how this is any different here though, since this is how I declare all my other asset data classes which are working, too:

#pragma once
#include "ExecutionUnit.h"
#include "Trigger.h"
#include "CustomTrigger.h"
#include "Stack.h"
#include "..\Asset\Asset.h"
#include "..\System\Pointer.h"

namespace acl
{
    namespace event
    {

        class ACCLIMATE_API Instance
        {
            Instance(const std::wstring& stName, core::TypeId type);
            Instance(const std::wstring& stName, core::TypeId type, unsigned int startUid, unsigned int staticUid);
            ~Instance(void);
            Instance(const Instance& instance);

            void operator=(const Instance&) = delete;
        };

        EXPORT_ASSET_OBJECT(Instance);

    }
}

_____________________________________________________________________________________

 

So this is it. I want to apologize for the huge amount of explanation and code, I just cannot really get a working test example, since the error is as you can see very specific.

I also hope I managed to explain what the code does and what the actual problem is. I'm actually not sure if anyone even has any idea what the issue could be, but I'd like to see if someone has at least something I could try. What could be the potential problem? I just cannot see why it would just choose the wrong implementation for this specific combination of Asset<ai::BehaviourTree>, in case that it can see the actual implementation of the header.

 

I'm pretty lost at this point, its just one of those f**** up issue I just don't have any idea anymore and probably has eighter a really complicated, or stupidly simple solution... so, got anything? Thanks!
 

 

 

 

Share this post


Link to post
Share on other sites
Advertisement

It looks like it revolves around your decltype for the returns.  When I comment them out, it works fine.  I'm guess it has to do with trying to set the return type to void using the decltype (since the function doesn't have a return call).  So it is defaulting to the version that already has void as a return type.  long and int are effectively the same type in VS, so I don't think that would stop anything.

 

[edit]

And thinking about it more (without further testing), void() might be making a return type of a function pointer with a return type of void, not the actual void type.

class Test
{
public:
  void AddRef(){}
};
 
struct refCountHelper
{
public:
  template <typename ObjectType>
  static auto AddRef(ObjectType* pObject)/* -> decltype(AddRef(pObject, 0))*/
  {
    return AddRef<ObjectType>(pObject, 0);
  }
 
private:
  template <typename ObjectType>
  static auto AddRef(ObjectType* pObject, int)/* -> decltype(pObject->AddRef(), void())*/
  {
    if (pObject)
      pObject->AddRef();
  }
 
  template<typename ObjectType>
  static void AddRef(ObjectType* pObject, long)
  {
  }
};
 
int main(int argc, char* argv[])
{
Test* test = new Test();
 
refCountHelper::AddRef(test);
 
return 0;
}
Edited by Rattrap

Share this post


Link to post
Share on other sites

Thanks for the input,

 

however unfortunately removing the decltype doesn't work quite that way. I can remove it from the public AddRef-method, which is cool, Visual Studio 2013 didn't implement this yet unless I'm mistaken.

 

But for the private AddRef-methods, I do need it, since this template can be called with both classes with AddRef and without AddRef:

class RefCounted
{
    void AddRef() {};
};

class NonRefCounted
{
};

RefCounted refCounted;
NonRefCounted nonRefCounted;
refCounterHelper::AddRef(&refCounted); // works eigther way
refCounterHelper::AddRef(&nonRefCounted); // doesn't work unless I check for SFINAE with the decltype

If I leave out the decltype, then it always tries to call the first method (which calls pObject->AddRef()) and thus gives an compilation error when I call it with an object without AddRef (which is a valid use-case, should have probably put more emphasis on it).

 

Also regarding long and int, although they are essentially the same it is enough for the compiler to make a difference here (otherwise there would be ambiguity-errors when compiling). 0 still seems to prefer int over long, I also tried long long but it doesn't make a difference.

 

Also keep in mind that the code is generally working, it only fails when called via the ObjectContainer-wrapper for asset::Asset<ai::BehaviourTree>. Its really, really weird.

using TestAsset = asset::Asset<ai::BehaviourTree>; // fails when using in ObjectContainer
// using TestAsset = asset::Asset<event::Instance>; // always works regardless

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    TestAsset asset(L"Testing");

    core::refCountHelper::AddRef<TestAsset>(&asset); // always works for all types

    core::ObjectContainer<TestAsset> object2(asset); // doesn't work for asset::Asset<ai::BehaviourTree>, if AI/BehaviourTree.h is included
}

So thanks for the suggestions, but its still not solved unfortunately :/

Share this post


Link to post
Share on other sites

I see what your saying about the non-reffed version.  I tried stripping it down some (replaced ai:BehaviorTree with an empty class Test2 since I don't have all your headers),  Both calls ended up calling the first version of the function, which would seem to be the correct behavior, since it should only be the wrapper Asset getting manipulated.  Now something I notice (and am guessing this is just a bad cut and paste or just not a complete code), is that

TestAsset asset(L"Testing");

shouldn't work in your posted code, since in your example I don't see any kind of constructor of Asset that would pass along the parameter into the ai:BehaviorTree constructor.

Edited by Rattrap

Share this post


Link to post
Share on other sites

This might suggest the problem is hiding in Asset, since I think what your displaying is incomplete (no constructor, not internal storage, etc).

Share this post


Link to post
Share on other sites

Now something I notice (and am guessing this is just a bad cut and paste or just not a complete code), is that

TestAsset asset(L"Testing");

shouldn't work in your posted code, since in your example I don't see any kind of constructor of Asset that would pass along the parameter into the ai:BehaviorTree constructor.

 

 


This might suggest the problem is hiding in Asset, since I think what your displaying is incomplete (no constructor, not internal storage, etc).

 

Oh yeah, that is actually a copy/paste error, I decided to leave out most of the code regarding the asset-class that I felt is irrelevant to the problem, since the class is actually quite huge and has some more dependencies.

 

In regard to the constructor, currently asset::Asset<Type> only stores a Type*, which is initialized at a later stage, so there is nothing it has to pass on, the name that you see in my code is actually a parameter of the BaseAsset-class itself.

 

I don't see how this could cause this kind of issue though. I suspect it has something to do with the DLL-related code of exporting template instantiations, though I wonder why it would fail for calling the same code inside the actual DLL itself then.

Thats kind of the problem with this issue, I cannot really show it exactly in a reliable way without sharing my complete engine code, so I was mainly hoping that people have just some ideas what to try or look out for (like you already did smile.png ), so keep em coming...

Edited by Juliean

Share this post


Link to post
Share on other sites

This one is a long shot, but have you tried throwing a breakpoint on the non-ref AddRef and check to see if anything looks weird about the template parameter or the function parameters when it gets called?

 

And I definately get not being able to share everything, I'm just trying to fill in the gaps where I can :)

Share this post


Link to post
Share on other sites

This one is a long shot, but have you tried throwing a breakpoint on the non-ref AddRef and check to see if anything looks weird about the template parameter or the function parameters when it gets called?

 

In debug mode (absoluely no optimizations), everything looks quite like it should, yes. In developement mode (only basic optimizations so I can run at something faster than 3 FPS but still debug normally), the call to refCounterHelper::AddRef actually has an "event::Instance"-template parameter when called from ObjectContainer<asset::Asset<ai::BehaviourTree>>. I belive this is due to COMDAT-folding, where all empty AddRef-implementations probably get removed except for one, which just happens to be of type "event::Instance" (which doesn't have ref-counting anyways).

 

 

 


And I definately get not being able to share everything, I'm just trying to fill in the gaps where I can smile.png

 

Sure, I do appreciate it smile.png

Edited by Juliean

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!