🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Checking if a type is defined

Started by
16 comments, last by Juliean 3 years, 1 month ago

Hello,

so I've got this compile-time constant (an extension to <type_traits>) for checking if a given type is defined at a specific point in time:

template<typename T>
concept IsDefined = std::is_object_v<T> && !std::is_pointer_v<T> && (sizeof(T) > 0);

This is based off an answer on stack-overflow and in and off itself works (I use concepts, but it doesn't matter if its constexpr bool or SFINEA-class like in the original answer).

Now my problem, which I just noticed, is this case:

class Foo;

void test()
{
	static_assert(!sys::IsDefined<Foo>);
}

class Foo {};

void test2()
{
	static_assert(sys::IsDefined<Foo>);
}

This will fail the second assertion, due to the compiler just generating the specialization the first time it encounters it used and will then memorize whether the type was defined at the point, instead of evaluating IsDefined at that specific point in time.

Is there anything slick/clever I can do to have the compiler actually do what I intend it to? Or do I have to put IS_DEFINED in a macro instead (that way it would always evaluate at the site where its used).

Advertisement

If you don't mind telling - why do you want to test if a type is forward-declared? Shaping metaprogramming logic based on this seems like a bad idea; likewise, if you simply want to test if the type exists, then you shouldn't really need to tell a forward declaration from the definition itself.

That being said - that's neither here nor there.

First, we need to modify the test a bit.

Your current check cannot work. Let's snag another version of the test from the same stackoverflow thread. The struct-based approach provides a “blanket” false-result for all classes and only returns true if the type satisfies the requirements that the specialization imposes.

The second problem is a bit trickier. I think it has to do with static_assert, which is only evaluated once the entire module/translation unit has been processed. Hence the effect is what you observe: the code above or below the assert is meaningless and all asserts of the same type coalesce into a single value across the unit. In this case a definition for Foo exists (even if it's at the end of the unit), so that's what the compiler sees.

We need to use a bit of trickery to get around that, which is why we're adding one more template parameter to the struct. This will allow us to tell asserts that are placed in different parts of the code apart from each other.

template <class, size_t, class = void>
struct IsDefined2: std::false_type // <- default to false
{
};


template <class T, size_t _>
struct IsDefined2 < T, _,
		   ::std::enable_if_t < std::is_object<T>::value&&
		   !std::is_pointer<T>::value&&
	   (sizeof(T) > 0)
	   >
	   > : std::true_type // <- specialize true for classes that meet the criteria
	   {
	   };

Next, let's make things a bit more meaningful. Effectively, what we want to do is get the following tests to return the correct values:

if sfinae then not defined
if defined then not sfinae

template<typename T, size_t _>
constexpr bool is_sfinae = !IsDefined2<T, _>::value;

template<typename T, size_t _>
constexpr bool is_defined = IsDefined2<T, _>::value;


class Foo2;

void test_2()
{
	static_assert(is_sfinae<Foo2, 0>);
	static_assert(!is_defined<Foo2, 0>);
}

class Foo2 { };

void test2_2()
{
	static_assert(is_defined<Foo2, 1>);
	static_assert(!is_sfinae<Foo2, 1>);
}

And voila.

So far so good, but this does not really provide a very pretty solution what with the hard-coded specializations.

Macros to the rescue. Enhance!

// requires T to be a SFINAE class
#define assert_class_not_defined(T)	static_assert(is_sfinae<T, __LINE__>)
// requires T to be defined
#define assert_class_defined(T)		static_assert(is_defined<T, __LINE__>)

class Foo3;
void test_3()	{ assert_class_not_defined(Foo3); }

class Foo3 { };
void test2_3()	{ assert_class_defined(Foo3); }

Of course VS' intellisense can't handle this and I get red squigglies under the SFINAE test. But the code compiles.

Not sure what compiler you're on or how portable this is, but hopefully this helps!

irreversible said:
If you don't mind telling - why do you want to test if a type is forward-declared? Shaping metaprogramming logic based on this seems like a bad idea; likewise, if you simply want to test if the type exists, then you shouldn't really need to tell a forward declaration from the definition itself.

Sure! I have multiple examples:

  1. In my type-system, objects can optionally be refcounted, as long as they specify an AddRef/Release-function:
template<typename T>
concept RefCountable = requires(const T& t) { t.AddRef();  t.Release(); };

template<typename Type>
struct RefCounter
{
	static_assert(sys::IsDefined<Type>, "ObjectType must be defined to use Refcounter.");

	static constexpr bool IsRefCountable = RefCountable<Type>;
	
	STATIC_CLASS(RefCounter);

	static void AddRef(const Type* pObject) noexcept;
	static void Release(const Type* pObject);
};
	
template<typename Type>
void RefCounter<Type>::AddRef(const Type* pObject) noexcept
{
	if constexpr (RefCountable<Type>)
	{
		if (pObject)
			pObject->AddRef();
	}
}

Now the problem is that if I ever happen to try to use this ref-counter from a forward-declared type, it would be specialized to not ref-count at all. The full example of how this happens is very complicated and has something to do with exporting symbols from a DLL, but trust me, it absolutely can happen and did happen - and cost me a very very long time trying to figure out why the F the refcounter was broken under specific circumstances.
Now its a separate discussion whether or not this is a good solution for (optional) recounting, but its not something that I'll want to change at that point in time - so the assert here is invaluable.

2. In the case above, the problem I mentioned doesn't really matter since static_assert will prevent compilation from happening as soon as its ever used with a forward-declared class (and if the inverse case happens, I'll just get another compile-error. Now the other case that I came across these days is more complicated. Lets say I have a type-trait:

template<typename Type>
constexpr bool GeneratesBytecode = false;

Lets just keep it simple and say that its supposed to be specialized for objects that have some custom requirements for my bytecode-compiler. Now the idea was to specialize it for certain base-classes, and have them available for all children:

template<typename Derived> requires std::is_base_of_v<BaseCommandData, Derived>
constexpr bool GeneratesBytecode<Derived> = true;

Now the place where this specialization is defined, BaseCommandData is only forward-declared (and kind of has to be), but at places where the specialization is to be used, it is defined (at the piece of code that receives the elements that are derived of BaseCommandData). However, at a place where this specialization is visible, without the definition of BaseCommandData being visible, I get an error complaining that BaseCommandData is undefined when using is_base_of_v. The truth is, in this case, I don't really care: As long as BaseCommandData is not defined I know for a fact that the other class is not derived of BaseCommandData if the definition is not visible. And so, I modified the specialization:

template<typename Derived> requires (sys::IsDefined<Derived> && std::is_base_of_v<BaseCommandData, Derived>)
constexpr bool GeneratesBytecode<Derived> = true;

_______________________________________________________________________

I hope this kind of makes sense. The thing is, for Case 2 I'm already using another solution entirely so its not that pressing. Also, while I think your solution is interesting, but isn't using LINE not enough to differentiate? What if I happen to use it in the exact same source-line in two different files (I recon its probably very unlikely that this happens, but still possible). Maybe I could try also including __FILE__ or something?

You can't use __FILE__ directly as a template argument because const char*/const wchar_t* needs external linkage. Of course, since you'd already be using macros, you could try and hide the gory details, but this won't work if you want to use the test anywhere other than in global scope (on second thought - it might be possible in C++20; I'm too lazy to check).

Instead, unless you're writing a library or need your code to be very portable, you could use __COUNTER__. It's non-standard, but quite widely supported.

Or, as an alternative, while you cannot pass the file name directly, you can pass its hash. I haven't really needed a constexpr hash function myself so I can't recommend a good implementation (I'm sure boost has something, although std::hash seems to be runtime only). For something as simple as this, however, you could simply grab some cheap function off the web. If you're still paranoid about collisions, you could expand it to 64 bits ?.

Using the function from stackoverflow:

unsigned constexpr const_hash(char const* input)
{
	return *input ?
		   static_cast<unsigned int>(*input) + 33 * const_hash(input + 1) :
		   5381;
}

template<HASH32 hash, size_t line>
class fileline { public: static constexpr uint32_t value = 1; };

#define assert_fileline() static_assert(fileline<const_hash(__FILE__), __LINE__>::value == 1)

assert_fileline();

As for the examples you gave - from the description I can understand the second one better (the one you already solved). The first one smells of code stink a bit, tho - especially the part about certain objects being specialized to not be refcounted. My (possibly naive) approach might be to rethink the entire architecture and split the refcounted base from the non-refcounted one. But then again - I'm probably simply not understanding the problem, so I'll digress.

irreversible said:
You can't use __FILE__ directly as a template argument because const char*/const wchar_t* needs external linkage. Of course, since you'd already be using macros, you could try and hide the gory details, but this won't work if you want to use the test anywhere other than in global scope (on second thought - it might be possible in C++20; I'm too lazy to check). Instead, unless you're writing a library or need your code to be very portable, you could use __COUNTER__. It's non-standard, but quite widely supported.

Ah sorry, I forgot to mention the compiler in your last reply. I'm currently just using MSVC. I'm technically writing a library and want to support other platforms, But I've got tons of code that I already know is non-portable, so I'm quite ok with using something that is even widely supported.

irreversible said:
As for the examples you gave - from the description I can understand the second one better (the one you already solved). The first one smells of code stink a bit, tho - especially the part about certain objects being specialized to not be refcounted. My (possibly naive) approach might be to rethink the entire architecture and split the refcounted base from the non-refcounted one. But then again - I'm probably simply not understanding the problem, so I'll digress.

Well, I'll admit that the separation between refcounts is perhaps a bit smelly and perhaps a premature optimization. It does really pay off for my bytecode-compiler, where handling a non-refcounted type is just infinitely more simple and efficient than a refcounted one. Aside from performance, it would probably be better to just make a shared “Object” base-class with virtual functions, or something.

The context for where all of this is happening is in my type-system (which is reponsible for being able to serialize and show fields of different types in an editor/runtime-environment, kind of what Unity can do with its [SerializeFields]). One potential category of type is “object”, where my originally intend was to be able to register support for objects without having to inherit a base-class. And also not force a type to not use ref-counting where not required (this also applies to other operations like checking if an object is valid via a custom function, for handle-like types etc…). I don't think separating refcounted/non-ref counted here is a good option, because the idea is to provide an interface that allows me to write generic code for handling “objects” without having to worry about the details (and something like ref-counting will be handled internally if its required). I think, as I said if i really were to clean things up I'd probably go with a base-class for objects; I already said f-it and went with a base-class for “asset” types because my last approach didn't pan out that well. Though I'm actually kind of happy with how the type-system and objects are working out, so I don't see a need to change things drastically amongst much more pressing work to be done.

Yeah, sorry, I also digressed quite a bit. But thanks for the help so far!

Juliean said:
Well, I'll admit that the separation between refcounts is perhaps a bit smelly and perhaps a premature optimization. It does really pay off for my bytecode-compiler, where handling a non-refcounted type is just infinitely more simple and efficient than a refcounted one. Aside from performance, it would probably be better to just make a shared “Object” base-class with virtual functions, or something.

Yeah - that's what it looks like to me. A job for an interface.

I'm not sure how all the refcounting really ties into this or what exactly you mean by bytecode (serialized data? interpreted code?), but I've given reflection enough thought to have figured out at least five difficulty levels and a quadrillion different ways of accomplishing it. And in C++ none of them are easy really.

Juliean said:
One potential category of type is “object”, where my originally intend was to be able to register support for objects without having to inherit a base-class.

This sounds ambitious. I'm fairly fresh off of rewriting my serialization wrapper myself. I opted for inheriting from a common base, which gathers registered members and reads/writes them by offset and size. Having a common base allows me to process serialized objects hierarchically and super automatically with manual management necessary only for types that are too messy or require post-read initialization. I do need to manually register my types and serialized class members, though, but I also use them for storing/reading human-readable settings/properties where this kind of manual approach pays off (since I can group them in a meaningful way).

I'm not really sure if your “type-system” is functionally related to serialization (it seems to be, among other things), but if I had to go about writing a “next level” reflection layer for an editor (e.g. one without a base class), I'd probably skip all other middleware solutions and go with an offline tool that parses the raw C++ source code at compile time and generates a metadata representation from that. As far as I know UE does its property management this way (I don't use it myself, so this is is from skimming the docs), presumably also Unity and a bunch of other frameworks. Not that I myself could write a parser for modern C++, that is…

Juliean said:
Though I'm actually kind of happy with how the type-system and objects are working out, so I don't see a need to change things drastically amongst much more pressing work to be done.

I can identify with this. I it works, then it doesn't really matter how ugly it is on the inside ?. Right? RIGHT?

(no, really - there's always a better architecture which you think of when you finish the current one)

I guess the type you want to ref-count is known at the time when you put it into your function is it? If so, you can drive another route and instead test if the type has or doesn't has the needed functions right?

SFINAE about type inheritance is very complicated and slow but there are also meta programming tricks you can use to determine presence of a certain function. So instead of looking for inheritance, you could look for that functions you need and if they exist, go the ref-count way and otherwise put a warning into console?

We have this in our engine base module

#define TEMPLATE_META_HAS_TYPE_MEMBER(Name) \
template<typename T> struct Has##Name \
{ \
    typedef char YesType; \
    typedef int NoType; \
    \
    struct Fallback { int Name; }; \
    struct Derived : T, Fallback { }; \
    \
    template<typename U, U> struct Has##Name##Internal; \
    \
    template <typename C> static NoType Has##Name##InternalTest(Has##Name##Internal<int Fallback::*, &C:: Name>*); \
    template <typename C> static YesType Has##Name##InternalTest(...); \
    \
    TEMPLATE_META_VALUE(sizeof(Has##Name##InternalTest<Derived>(0)) == sizeof(YesType)); \
}; \
template <typename T> struct Has##Name<T&> { TEMPLATE_META_VALUE(false); }

And are using it for our type system like so

namespace SE
{
    namespace TypeTraits
    {
        namespace Composition
        {
            TEMPLATE_META_HAS_TYPE_MEMBER(GetAllocator);
            TEMPLATE_META_HAS_TYPE_MEMBER(GetType);
        }
    }
}

So asking a type if it offers type information is quite easy this way, even if it isn't safe for detecting a certain function signature, but we also have a solution for this as well.

So what you can do about your ref-counting could as well be

template<typename T> inline ... RefCountOrNot(T &myType)
{
    if(HasMyRefCountFunction<T>::Value)
        DoRefCounting(myType);
    else
        DoSomethingElse(myType);
}

Pretty much the only thing you can safely with your IsDefined is static_assert(IsDefined<X>). In this case it doesn't matter that each static_assert is only evaluated once, because there are only two possible outcomes:

  1. The first instance of static_assert(IsDefined<X>) fails, so compilation stops. Subsequent instances are irrelevant because the code does not compile. At worst you'll get spurious error messages in addition to the correct error message.
  2. The first instance of static_assert(IsDefined<X>) succeeds, so all subsequent instances also succeed. This is correct, because there is no way to undefine X after it has already been defined.

Your example with static_assert(!IsDefined<X>) is contrived and should never appear in real code. Code can either require X to be defined or can not require X to be defined, but code can never require X to not be defined.

@a light breeze Yep, that example is contrived, I made it to make a presentable example of my problem. Its just to highlight what will happen in production code if I have for example an “if constexpr(IsDefined)" which appears at different points in code when the first instance has been evaluated with an undefined type. As you correctly say, for the case where I static_assert it doesn't matter if the specialization gets set to “undefined” since that will mean that I have to include the type at that point before I can resume compilation (which is what I want).

Shaarigan said:
I guess the type you want to ref-count is known at the time when you put it into your function is it? If so, you can drive another route and instead test if the type has or doesn't has the needed functions right?

Yeah, thats the part that I already solved though. The problem occurs when a forward-declared type is passed to the SFINEA-evaluation-mechanism, which will make all further uses of the check will appear to report that the type doesn't have the required function (even though it actually has).

Shaarigan said:
SFINAE about type inheritance is very complicated and slow but there are also meta programming tricks you can use to determine presence of a certain function. So instead of looking for inheritance, you could look for that functions you need and if they exist, go the ref-count way and otherwise put a warning into console?

Fortunately I have access to concepts which makes this mindnumbindly easy. I'm glad I'm not stuck with an old version of c++ or have to maintain c-compatibility personally :P

irreversible said:
I'm not sure how all the refcounting really ties into this or what exactly you mean by bytecode (serialized data? interpreted code?), but I've given reflection enough thought to have figured out at least five difficulty levels and a quadrillion different ways of accomplishing it. And in C++ none of them are easy really.

I have a visual scripting-language which used to have a horrible “each node is a class”-model of execution. I'm in the process of writing a stack-based bytecode-compiler/interpreter for that. The type-system is the driving force behind the visual-scripting (every value-pin represents a value that is registered with the type-system). If an object-type is refcounted, every time it is returned from a node I need to assign it to a local variable and manage the lifetime of that, requiring additional instructions and in general complicating the code-gen. If a type is not ref-counted, I can just push the value on the stack and forget about it until it is consumed (in most cases).

irreversible said:
This sounds ambitious. I'm fairly fresh off of rewriting my serialization wrapper myself. I opted for inheriting from a common base, which gathers registered members and reads/writes them by offset and size. Having a common base allows me to process serialized objects hierarchically and super automatically with manual management necessary only for types that are too messy or require post-read initialization. I do need to manually register my types and serialized class members, though, but I also use them for storing/reading human-readable settings/properties where this kind of manual approach pays off (since I can group them in a meaningful way). I'm not really sure if your “type-system” is functionally related to serialization (it seems to be, among other things), but if I had to go about writing a “next level” reflection layer for an editor (e.g. one without a base class), I'd probably skip all other middleware solutions and go with an offline tool that parses the raw C++ source code at compile time and generates a metadata representation from that. As far as I know UE does its property management this way (I don't use it myself, so this is is from skimming the docs), presumably also Unity and a bunch of other frameworks. Not that I myself could write a parser for modern C++, that is…

Yeah, the type-system itself is fairly ambitious. But its at the core of my engine and is really what enabled me to be productive and write good tooling in the end.
I've worked with unreal, and while their pre-parser was pretty cool, it also resulted in a bunch of problems and made things really slow (in terms of compilation and IntelliSense). I also can't be bothered to deal with having to integrate some tooling into the IDE/compiler, and parse c++-files etc… though I would actually like to have it for the part where I have to register a types properties:

EventSystem::Declaration EventSystem::GenerateDeclaration(void) noexcept
{
	return 
	{
		L"Event",
		{
			{ L"EnableBackgroundCompilation", true, core::bindAttribute(&EventSystem::m_enableBackgroundCompilation, &EventSystem::OnBackgroundCompilationChanged) },
			{ L"FullBackgroundCompilation", true, core::bindAttribute(&EventSystem::m_fullBackgroundCompilation) },
			{ L"OpenMessagesOnBackgroundCompilation", true, core::bindAttribute(&EventSystem::m_openMessagesOnBackgroundCompilation, &EventSystem::OnOpenBackgroundCompilationChanged) }
		}
	};
}

That would be pretty neat to automate, but thats also not really part of the layer of the type-system that I was talking about in this post so far. This is more of a consumer of the type-system, where each call to “bindAttribute” will reference a member which is of a type known to the type-system (EventSystem itself has nothing to do with the type-system.
One last comment about the shared base-class/interface: That would also not be the be-all and end-all anyways, since “objects” is just one category in the type-system. I also have primitives (int/float/bool), enums and structs, which I definately don't want to put under one shared base-class (I'm not a fan of the “everything is an object”-paradigm and also the current system has a pretty minimal overhead for dealing with primitive types). I hope this is all somewhat understandable, I know that this is a complex system which one could write an entire online-documentation about (I did write a blog-post which unfortunately is horribly formatted know)

irreversible said:
I can identify with this. I it works, then it doesn't really matter how ugly it is on the inside ?. Right? RIGHT? (no, really - there's always a better architecture which you think of when you finish the current one)

Oh, I do refactor things a lot, perhaps way too much. This is by far not the first iteration of the type-system, and it will probably not be the last eigther :D

Juliean said:
That would be pretty neat to automate, but thats also not really part of the layer of the type-system that I was talking about in this post so far.

The way I ended up doing this is by having a CSearchable base class, which can be coupled with a CSerializble class.

The former handles properties and human-readable I/O via some markup language or ini file, forms the basis of a cvar system, which allows me to “address” variables via “links”, e.g. something like SetFloat("[Engine:World:Camera:Fov]", 90.f) and contains information about all serializable members. It also provides OnSave() and OnLoad() functions, which allow me to handle types that are too bothersome to generalize or need more tender love and care.

The latter simplifies serializing to/fraw raw binary data members which do not fall in the purview of CSearchable.

If you inherit from CSearchable, you need to do just one thing (have the CSEARCHABLE() macro present, which performs all of the initialization voodoo and allows you to provide an alias for that class) and then you get access to a whole bunch of goodies. All in all, a class that contains serializable members might look something like this:

class CWorldBase
	: public CSearchable
{
	public:
		CSEARCHABLE("WorldBase");	
		
		float fov = 70.f;
		
		bool OnLoad(
			const CPropertiesFile& propsFile) {
			// could get the fov value from here, but it's been done automatically already into
			// 'fov', becase of the derived class
			_Unref(propsFile); 
			// the camera in this example needs post-initialization			
			GetWorldCamera()->SetFov(fov);
			}
};

class CWorldImpl
	: public CWorldBase
{
	public:
		// this needs to be here due to a quirk in my code. The bottom line here is that C++
		// doesn't give you the name of a class without initializing it, which has made my
		// life infinitely more complicated, especially in templated classes.
		using _ThisType								= CWorldImpl;


		// the class will be addressable with the name "World"
		CSEARCHABLE("World");

		int32_t cat = 420;
		int32_t dog	= 69;

		
		// takes the existing 'cat' and 'dog' variable and groups them in 'Animals', etc. In
		// an XML-style properties file this class might end up looking something like:
		//	...
		//		<World>
		//			<Attributes>
		//				<Animals>
		//					<Attrib name="cat", value="420">
		//					<Attrib name="dog", value="69">
		//				</Animals>
		//				<Camera>
		//					<Attrib name="fov", value="70.0">
		//				</Camera>
		//			</Attributes>
		//			<Properties>
		//					<WorldSize value="0">
		//			</Properties>
		//			<Kennels count="666">
		//				<Kennel>
		//					...
		//				</Kennel>
		//				...
		//			</Kennels>
		//			<Scratchy>
		//				...
		//			</Scratchy>
		//		</World>
		//	...
		CSEARCHABLE_ATTRIBS(Animals, cat, dog); 
		// expose a base class member the same way, but in a different group
		CSEARCHABLE_ATTRIBS(Camera, fov); 
		
		// see below, but this expands to something like:
		// ISearchableProperty& PROP_WorldSize = *CreateProperty<int32_t>("WorldSize");
		CSEARCHABLE_PROPERTY(int32_t, WorldSize);
		
		
		// expose an array of CKennel's, which also derive from CSearchable and are therefore
		// automatically read and written by the framework provided by CSearchable. This only		
		// works for storage classes, which I'm handling specifically. I think I'm only suppoting 		
		// a vector and my own hashmap class for now, for instance.	
		CSEARCHABLE_CHILD(::std::vector<CKennel>, Kennels);
		
		// same for objects not in containers	
		CSEARCHABLE_CHILD(CCatTower, Scratchy);		
};

This is to say - I'm using the searchable to expose three types of “variables”:

  • attribs are registered once per class and non-intrusively “linked to” regular data members. The take up no space in the class, can be addressed, but are not thread-safe, etc. That is, attributes are registered once per class (not instance) and stored as a linked list of static collections. As far as efficiency goes, this is not too far off from compile-time.
  • properties support thread-safe and type-safe read-writes (e.g. they include a lock and you can't write to their data fields directly, but have to use something like SetInt() or an interpretative accessor like SetFromString(), which becomes especially useful in an editor environment). Internally, I'm also assuming the name of a property uses Hungarian notation to encode a bunch of different naming schemes. For instance, the above “WorldSize” expands to a member named PROP_WorldSize (this demarcates it from attributes), shows up as “WorldSize” in markup, can be addressed as “WorldSize” or “world_size” in the cvar framework and is split into “World Size” in an editor text field. Properties are allocated using new.
  • children are classes that are assumed to inherit from CSearchable and form a hierarchy, which I specifically setup for a system-wide property search function. Each child is allocated via new.

The distinction between properties and attribs not only lays the groundwork for a future cvar framework, but also a provides thread-safe proxy for communication between the editor, automation and world-driven inputs (from a script, event system or what not).

All of this took me a bunch of effort to write up, but I'm quite happy with the result as it as kills a whole lot of birds with one stone and takes care of a huge problem I've been tackling: properly saving and loading a state which I keep adding to during development. Furthermore, from my perspective it minimizes issues caused by negligence by almost the same amount as marking individual fields as UPROPERTY. It's still not too difficult to forget to add an attribute to CSEARCHABLE_ATTRIBS(), but the upshot is that once you do add it there, there's no additional steps and the binding happens automagically.

Juliean said:
Oh, I do refactor things a lot, perhaps way too much. This is by far not the first iteration of the type-system, and it will probably not be the last eigther :D

/me sends a deeply understanding look.

This topic is closed to new replies.

Advertisement