How to detect if a class has member variables?
Asked Answered
H

2

8

Problem

I would like to detect if a class has member variables and fail a static assert if they do. Something like:

struct b {
    int a;
}
static_assert(!has_member_variables<b>, "Class should not contain members"). // Error.

struct c {
    virtual void a() {}
    void other() {}
}
static_assert(!has_member_variables<c>, "Class should not contain members"). // Fine.

struct d : c {
}
static_assert(!has_member_variables<d>, "Class should not contain members"). // Fine.

struct e : b {
}
static_assert(!has_member_variables<e>, "Class should not contain members"). // Error.

struct f : c {
    char z;
}
static_assert(!has_member_variables<f>, "Class should not contain members"). // Error.

Is there a way to achieve this with SFINAE template? This class may have inheritance or even multiple inheritance with virtual functions (no members in the base classes though).

Motivation

I have a pretty simple setup as follows:

class iFuncRtn {
    virtual Status runFunc(Data &data) = 0;
};

template <TRoutine, TSpecialDataType>
class FuncRoutineDataHelper : public iFuncRtn {
    Status runFunc(Data &data) {
        static_assert(!has_member_variables<TRoutine>, "Routines shouldnt have data members!");
        // Prepare special data for routine
        TSpecialDataType sData(data);
        runFuncImpl(sData);
}

class SpecificRtn : 
    public FuncRoutineDataHelper<SpecificRtn, MySpecialData> {
    virtual Status runFuncImpl(MySpecialData &sData) {
        // Calculate based on input 
        sData.setValue(someCalculation);
    }
};

The FunctionalityRoutines are managed and run on a per tick basis. They are customized and can perform a wide variety of tasks such as contacting other devices etc. The data that is passed in can be manipulated by the routine and is guaranteed to be passed in on each tick execution until the functionality is finished. The right type of data is passed in based on the DataHelper class. I wan't to discourage future people from mistakenly adding data to the functionality routines as it is very unlikely to do what they expect. To force this, I was hoping to find a way with static assert.

Heredity answered 6/9, 2018 at 15:32 Comment(20)
So if a base class has data members and not the derived class, it's ok?Pissed
You can use std::is_empty if the base classes are also expected to be empty. Otherwise, there is no way in current C++.Stoke
@Pissed None of any of the inheritance tree should have member variables.Heredity
@Brian std::is_empty will detect my virtual members so I can't use this. The Motivation part should have made this clear.Heredity
Without reflection, there is no way to figure this out if you can't rely on the size...Stephainestephan
@YSC Those test cases should help. The motivation is simple enough. I have classes where if a person derives from them and puts data in them they are 99.9% chance doing something wrong. I wan't to put a static assert to stop that. I have also expanded the motivation. Hopefully it clears things up.Heredity
Is SpecificFunctionalityRoutine suoposed to inherit FunctionalityRoutine? Should your system verify that SpecificFunctionalityRoutine is empty?Steroid
I haven't yet had enough coffee yet this morning to think this through completely and figure if it'll get where you want to, but my thought process is this. By definition, the template states there is a mapping between a TRoutine and a TSpecialDataType. and part of the contract you want is that TRoutine can't have any data members. Therefore TRoutine only encapsulates a routine. Can you rework this where instead of a class, the first template parameter is a std::function<> type? That'd enforce what you want without question.Hampshire
@Hampshire std::function can easily pack state (so can a data-less class, but only if you're a bit mischievous).Steroid
Why are you using a C++ class to store what you want to be a Status(*)(Data&)? That is the C++ signature of some stateless callable with a Data& which returns a Status.Maighdiln
@YakkAdamNevraumont, I don't want the classes to store anything. They are simply a grouping of functionalities.Heredity
Why not instead make it safe to add members/state? Otherwise just write a comment and move onTweeter
@LightnessRacesinOrbit It is perfectly safe to add a member, it is just not likely that if the person does, they have read the design and understand what the functionality routines are about. Adding a comment is a policy based protection, it is the weakest form of insurance. If i can add a static_assert then it will be a much stronger assurance for people to do the right thing. Enforcement of coding policy through static_assert is one of its massive advantages.Heredity
Yeah but not at the cost of spending weeks trying to make it work and having complicated code. Sometimes just write a comment telling people how your class design works (or include it in your architecture documentation) then it's up to them to either do it properly or not. Life's too short to try forcing C++ to be a different language.Tweeter
@LightnessRacesinOrbit I am not spending weeks, and my hope was the code would not be complex. At the moment a comment is serving the purpose and it is in the architecture documentation, but for my own development i wanted to see if this code was possible. This is more about the motivation though ...Heredity
@LightnessRacesinOrbit static_assert and template type assertion are part of c++? I don't get your meaning about making c++ a different language ...Heredity
Don't get me wrong, I applaud the investigation.Tweeter
I meant making C++ a different language by adding constraints to how C++ programmers can write [some] classes in it. Which is effectively what you're trying to do, no? I wouldn't take it too seriously though as a throwaway comment :)Tweeter
@LightnessRacesinOrbit For me, being able to force your intended design on other internal system coders through compile time assertions is a hugely powerful feature that metaprograming and static_assert provides. It means you can turn hard to find run-time errors due to design mis-use into obvious compile time failures.Heredity
Yes, it can be very useful. I use static_assert sometimes. But I also pick my battles! And I tend only to use it for diagnostic purposes (as in, "this check is here because you can't do what you're trying to do, with my design, and this assert makes it easier to spot that") not social purposes (as in, "this check is here because I don't want you to do what you're trying to do as it may -- but may not -- cause you problems later). Certainly YMMV and I have no problem with your question per se.Tweeter
C
9

You can solve this by depending on the compiler doing empty base class optimizations, by checking if a class derived from your T has the same size as an empty class with virtual functions:

template<typename T, typename... BaseClasses>
class IsEmpty
{
    // sanity check; see the updated demo below
    static_assert(IsDerivedFrom<T, BaseClasses...>::value);

    struct NonDerived : BaseClasses... { virtual ~NonDerived() = default; };
    struct Derived : T { virtual ~Derived() = default; };

public:
    inline static constexpr bool value = (sizeof(NonDerived) == sizeof(Derived));
};

This should work with both single and multiple inheritance. However, when using multiple inheritance, it's necessary to list all base classes, like that:

static_assert(IsEmpty<Derived, Base1, Base2, Base3>::value);

Obviously, this solution rules out final classes.

Here's the updated demo.

Here's the original demo. (doesn't work with multiple inheritance)

Caesura answered 6/9, 2018 at 16:18 Comment(8)
It's a good solution, but it breaks if the class has multiple vtable pointers.Barnes
Hmmm. Will this still work with virtual inheritance? (Can't test right now).Loadstone
@JesperJuhl I reckon it won't, since virtual inheritance adds size overhead.Barnes
@JesperJuhl: unfortunately, it doesn't :( I try to avoid it and didn't think about this case.Caesura
@Caesura then I suggest your answer also mention that caveat.Loadstone
@JesperJuhl I've updated the code to make it sort-of multiple inheritance friendly.Caesura
@Caesura my comment was about virtual inheritance, not multiple inheritance.Loadstone
@JesperJuhl Modifying example to add virtual inhertance doesn't break it. But if you add a tangle of inheritance, it breaks (ie Base2:Base and change all inheritance to virtual). If you then upgrade NonDerived to inherit everything virtually, it works again.Maighdiln
P
4

You will have to mark the classes in some way or another. Pick a way you are comfortable with, a property or some kind of type integer member with an enum. Whoever makes sub-classes will have to follow your convention to make it work.

All other answers here will be some variant of this.

Any answer that uses a sizeof could not guarantee this will work between platforms, compilers, or even classes on the same platform and compiler, due to easily being able to fit a new member inside the default class member alignment, where the sizes of sizeof could easily end up the same for a sub-class.


Background:

As stated in your code and question, all of that is just plain and basic C ad C++ code, and is resolved entirely at compile time. The compiler will tell you if a member exists or not. After its compiled it's a mash of efficient, nameless, machine code with no hints or help for that kind of thing by itself.

Any name you use for a function or data member effectively disappears, as you know it and see it there, after compile and there is no way to lookup any member by name. Each data member is known only by its numerical offset from the top of the class or struct.

Systems like .Net, Java, and others are designed for reflection, which is the ability to remember class members by name, where you can find them at runtime when you program is running.

Templates in C++, unless mixed mode C++ on something like .Net, are also all resolved at compile time, and the names will also all be gone, so the templates by themselves buy you nothing.

Languages like Objective-C also are written to not fail necessarily if certain types of special members are missing, similar to what you are asking, but under the covers its using a lot of supporting code and runtime management to keep track independently, where the actual function itself and its code are still unware and rely on other code to tell them if a member exists or to not fail on null member.


In pure C or C++ you will need to just make your own system, and be literal about tracking dynamically what does what. You could make enums, or lists or dictionaries of name strings. This is what is normally done, you just have to leave hints for yourself. A class cannot be compiled in a way that gives implicit visibility to future sub-classes by definition, without using some form if RTTI.

Its common to put a type member on a class for this very reason, which could be a simple enum. I would not count on sizes or anything that might be platform dependent.

Protocol answered 6/9, 2018 at 16:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.