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

C++ duck typing with optional members

Started by Hodgman Aug 25, 2015 at 7:00 AM 7 replies 5.8k views
Original Post
Hodgman
Hodgman
So duck-typing using templates is easy. You have a function that takes a const T& argument, and then you can access it's members by name without knowing anything else about what type of object T actually is.

But what if you want to try and access a member, and only use it if it actually exists?

I thought this was simultaneously really neat, and horrible, hence the coding horror section biggrin.png
#define DECLARE_FIND_MEMBER(NAME)                                                     \
namespace FindMember {                                                                \
template<class T, class R=void> class NAME##_ {                                       \
    struct Fallback { int NAME; };                                                    \
    struct Derived : T, Fallback { };                                                 \
    template<class U, U> struct Check;                                                \
    template<class U> static sizeone& Dummy(Check<int Fallback::*, &U::NAME> *);      \
    template<class U> static sizetwo& Dummy(...);                                     \
    template<bool Exists> struct Helper        { typedef decltype(&T       ::NAME) Type; static Type Pointer() { return &T       ::NAME; } }; \
    template<>            struct Helper<false> { typedef decltype(&Fallback::NAME) Type; static Type Pointer() { return &Fallback::NAME; } }; \
public:                                                                               \
    enum { exists = sizeof(Dummy<Derived>(0)) == 2 };                                 \
    typedef typename Helper<exists>::Type Type;                                       \
    static Type Pointer() { return Helper<exists>::Pointer(); }                       \
};                                                                                    \
template<class R, class E>                                                            \
R* operator->*( E& object, const FindMember::NAME##_<E,R>& ) {                        \
    return internal::TryGetMember<R>(object, FindMember::NAME##_<E>::Pointer()); }    \
template<class E, class R> bool NAME(E& entity, R& result)                            \
{                                                                                     \
    result = internal::TryGetMember<StripPointer<R>::Type>(                           \
        entity, FindMember::NAME##_<E>::Pointer());                                   \
    return FindMember::NAME##_<E>::exists;                                            \
}}                                                                                    //

template<class T, class Y> struct IsSameType       { const static bool value = false; };
template<class T         > struct IsSameType<T, T> { const static bool value = true; };
template<class T> struct StripRef           { typedef T Type; };
template<class T> struct StripRef<      T&> { typedef T Type; };
template<class T> struct StripRef<const T&> { typedef T Type; };
template<class T> struct StripPointer           { typedef T Type; };//todo - all the combinations...
template<class T> struct StripPointer<      T*> { typedef T Type; };
template<class T> struct StripPointer<const T*> { typedef T Type; };

namespace internal {
	template<bool condition> struct TryGetMember_
	{
		template<class Type, class Entity, class MemberPtr>
		static Type* GetPointer( Entity& view, MemberPtr ptr )
		{
			return &((view).*(ptr));
		}
	};
	template<> struct TryGetMember_<false> { 
		template<class T> static T* GetPointer(...){ return nullptr; }
	};

	template<class ExpectedType, class T, class MemberPtr>
	ExpectedType* TryGetMember( T& entity, MemberPtr ptr )
	{
		const bool correctType = IsSameType<ExpectedType, T>::value;
		return TryGetMember_<correctType>::GetPointer<ExpectedType>( entity, ptr );
	}
}

#define TRY_GET_MEMBER( entity, member, type )                                                           \
    internal::TryGetMember<type>( entity, FindMember::member##_<StripRef<decltype(entity)>>::Pointer() ) //

#define TRY_GET_MEMBER_ALT( ptr, object, member )                                 \
    ((ptr) = internal::TryGetMember<StripPointer<decltype(ptr)>::Type>(           \
        object, FindMember::member##_<StripRef<decltype(object)>>::Pointer() ))   //
Example usage -- I have a scene that contains multiple "DrawItem" structs for each object, which describe how to draw that object in different ways. When doing shadow mapping, or deferred rendering, or forward rendering, the renderer needs to get different sub-sets of these DrawItems.
DECLARE_FIND_MEMBER( zpass );
DECLARE_FIND_MEMBER( opaque );
DECLARE_FIND_MEMBER( alpha );
DECLARE_FIND_MEMBER( gbuffer );

class TestModule : public SceneModule<TestModule>
{
public:
    template<class SceneView> void Collect( SceneView& view )
    {
        //Macro syntax
        std::vector<DrawItem*>* zpass   = TRY_GET_MEMBER( view, zpass, std::vector<DrawItem*> );
        std::vector<DrawItem*>* opaque  = TRY_GET_MEMBER( view, opaque, std::vector<DrawItem*> );
        std::vector<std::pair<float,DrawItem*>>* alpha = TRY_GET_MEMBER( view, alpha, std::vector<std::pair<float,DrawItem*>> );
        std::vector<DrawItem*>* gbuffer = TRY_GET_MEMBER( view, gbuffer, std::vector<DrawItem*> );

        //Alternate macro syntax:
        std::vector<DrawItem*>* zpass = 0;
        TRY_GET_MEMBER_ALT( zpass, view, zpass );

        //Alternate syntax that doesn't use a macro
        std::vector<DrawItem*>* zpass = view->*FindMember::zpass_<SceneView, std::vector<DrawItem*>>();   

        //Another macroless syntax:
        std::vector<DrawItem*>* zpass = 0;
        FindMember::zpass(view, zpass);   


        //n.b. these if statements might be removed at compile-time because in some cases it's provable that they're always testing nullptr
        if( opaque )
            CollectOpaque( *opaque );
        if( alpha )
            CollectAlpha( *alpha );
        if( zpass )
            CollectDepthOnly( *zpass );
        if( gbuffer )
            CollectGBuffer( *gbuffer );
}

.....
};

struct ShadowMapView
{
	std::vector<DrawItem*> zpass;
};
struct ForwardRenderView
{
	std::vector<DrawItem*> zpass;
	std::vector<DrawItem*> opaque;
	std::vector<std::pair<float,DrawItem*>> alpha;
};
struct DeferredRenderView
{
	std::vector<DrawItem*> gbuffer;
	std::vector<std::pair<float,DrawItem*>> alpha;
};

void test()
{
	TestModule m;
	DeferredRenderView v;
	m.Collect(v); // calls m.CollectGBuffer(v.gbuffer) and m.CollectAlpha(v.alpha)
}
Still debating whether I should make use of this in my engine biggrin.png
Madhed
Madhed

My god... it's beautiful and terrifying.

aeroKittens
aeroKittens

Awesome!!!

[attachment=28865:awesome.png]

can't help being grumpy... Just need to let some steam out, so my head doesn't explode... 
blutzeit
blutzeit

Nice!!!

Some ideas: Put the FindMember classes inside a namespace, e.g. FindMember::Foo instead of FindMember_Foo. Instead of macro TRY_GET_MEMBER, overload operator ->* on these FindMember classes.

std::vector<DrawItem*>* zpass = view->*FindMember::zpass<std::vector<DrawItem*>();
openwar  - the real-time tactical war-game platform
Hodgman
Hodgman

Some ideas: Put the FindMember classes inside a namespace, e.g. FindMember::Foo instead of FindMember_Foo. Instead of macro TRY_GET_MEMBER, overload operator ->* on these FindMember classes.


std::vector<DrawItem*>* zpass = view->*FindMember::zpass<std::vector<DrawItem*>();

I like it. Added support for this alternate syntax to the OP and used the namespace idea smile.png

[edit] Also wrote another macroless syntax which is quite clean now!

bool found = FindMember::MemberName(object, outputPointer);

Sik_the_hedgehog
Sik_the_hedgehog

Is there something templates can't do? (make toast?)

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.
Chris_F
Chris_F




But what if you want to try and access a member, and only use it if it actually exists?

Then you would use Objective C instead.

fastcall22
fastcall22
Shhhh. Objective-C was supposed to die out three decades ago.
Hodgman
Hodgman

But what if you want to try and access a member, and only use it if it actually exists?


Then you would use Objective C instead.

Everyone knows that Objective C is a myth. What's next, unicorns? laugh.png

Topic Locked

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

Sign in to reply to this topic.