5

I am building a custom BDD class to store different types of data (e.g., long, char*, double, …) for my program.

In order to store the data, I need a struct for each table, like this:

struct MYSTRUCT0
{
    char variable0[10];
    char variable1[70];
};
struct MYSTRUCT1
{
    long variable0;
    long variable1;
    char variable2[6];
    double variable3;
};

But it's much work each time I need a new table, because I need to write a function to save each table in a file, to read it, etc. Worse, it's not really object-oriented.

So my question is, is there a way to "browse" the struct to simplify my code? Something like this:

for(int v=0; v<arraysize; v++)
for(int i=0; i<MYSTRUC0.length; i++)
{
    if (MYSTRUCT.getvar(i).type == long)
         DoSomethingForLong(myarray(v).getval(i));

    if (MYSTRUCT.getvar(i).type == char*)
         DoSomethingForCharPtr(myarray(v).getval(i));
}

I know it's possible for code like this to work directly in C++. I just use it to illustrate what I mean.

Katie
  • 51
  • 7
Entretoize
  • 2,124
  • 3
  • 23
  • 44
  • if you can use c++17, take a look at [`std::variant`](http://en.cppreference.com/w/cpp/utility/variant). Also, see [this related question](https://stackoverflow.com/questions/44196608/how-can-i-simplify-this-redundant-c-code) – pergy Oct 13 '17 at 12:20
  • Is only related to compiler ? I'm using VS2017 so it should work... – Entretoize Oct 13 '17 at 12:23
  • Maybe you could use `struct` wrapping around `union` with a flag in `struct` to indicate which `union` member is in use like [this answer](https://stackoverflow.com/a/11035544/1981061)? – Griffin Oct 13 '17 at 12:25
  • You can do it using this monstrosity: http://www.boost.org/doc/libs/1_65_1/libs/fusion/doc/html/fusion/adapted/adapt_struct.html – John Zwinck Oct 13 '17 at 12:44
  • A C++ solution would probably **be** object oriented. If you had a virtual function `DoSomething()` in each struct, the processing code could be a lot simpler. And easier to maintain. – Bo Persson Oct 13 '17 at 12:46
  • Griffin I think you solution is simple, I will just use char* instead of char[] to avoid memory waste. Can you post as an answer ? – Entretoize Oct 13 '17 at 12:51
  • It sounds like you want reflection, which is not in C++ yet, but there are libraries that support it, like https://github.com/apolukhin/magic_get – mnistic Oct 13 '17 at 12:57
  • @Entretoize I will post an answer with an example. – Griffin Oct 13 '17 at 13:23

2 Answers2

1

Below code is just an example of how you can make your own "variable-type-aware" struct that maybe what you want:

#include <vector>

enum MyTypes
{
    LONG,
    CHARPTR,
    DOUBLE,
} myTypes;

struct MyStruct
{
    MyStruct(long longVar)
    {
        variable.longVar = longVar;
        whichType = LONG;
    }

    MyStruct(char* charPtr)
    {
        variable.charPtr = charPtr;
        whichType = CHARPTR;
    }

    MyStruct(double var)
    {
        variable.var = var;
        whichType = DOUBLE;
    }

    ~MyStruct()
    {
    }

    MyTypes whichType;
    union {
        long longVar;
        char* charPtr;
        double var;
    } variable;
};

void DoSomethingForLong(MyStruct* doubleStruct)
{
    /*Do something specific to long*/
};

void DoSomethingForCharPtr(MyStruct* doubleStruct)
{
    /*Do something specific to char pointer*/
};

void DoSomethingForDouble(MyStruct* doubleStruct)
{
    /*Do something specific to double*/
};

int main()
{
    std::vector<MyStruct*> myVec;

    // add a struct with long variable
    long longVar = 2000000000;
    MyStruct* myLongStruct = new MyStruct(longVar);
    myVec.push_back(myLongStruct);

    // add a struct with char pointer
    char* charArray = new char[1000];
    MyStruct* myCharPtrStruct = new MyStruct(charArray);
    myVec.push_back(myCharPtrStruct);

    // add a struct with double variable
    double doubleVar = 200.200;
    MyStruct* myDoubleStruct = new MyStruct(doubleVar);
    myVec.push_back(myDoubleStruct);

    for (int i = 0; i < myVec.size(); ++i)
    {
        MyStruct* tempStruct = myVec[i];
        if (tempStruct->whichType == LONG)
        {
            DoSomethingForLong(tempStruct);
        }
        else if (tempStruct->whichType == CHARPTR)
        {
            DoSomethingForCharPtr(tempStruct);
        }
        else if (tempStruct->whichType == DOUBLE)
        {
            DoSomethingForDouble(tempStruct);
        }
    }

    if (myLongStruct)
    {
        delete myLongStruct;
        myLongStruct = nullptr;
    }

    if (myCharPtrStruct)
    {
        if (charArray)
        {
            delete[] charArray;
            charArray = nullptr;
        }

        delete myCharPtrStruct;
        myCharPtrStruct = nullptr;
    }

    if (myDoubleStruct)
    {
        delete myDoubleStruct;
        myDoubleStruct  = nullptr;
    }
}
Griffin
  • 716
  • 7
  • 25
1

If you go to the trouble of adding a member function that can export your data members as a tuple, then we can use some template meta programming to make this work.

Live Demo (C++14)

First, the alteration:

struct MYSTRUCT0
{
    char variable0[10];
    char variable1[70];
    std::tuple<char(&)[10], char(&)[70]> GetData()
    {
        return std::tie(variable0, variable1);
    }
};
struct MYSTRUCT1
{
    long variable0;
    long variable1;
    char variable2[6];
    double variable3;
    std::tuple<long&, long&, char(&)[6], double&> GetData()
    {
        return std::tie(variable0, variable1, variable2, variable3);
    }
};

std::tie will put references to these members into a tuple.

The nice thing about a tuple is that it encodes all the types into a list that we can take advantage of. (You could probably write macro(s) to create these structs for you.)

From here the strategy is to write a function that can process any tuple.

Since we access elements of a tuple with a call to std::get<i> where i is some index, we need a way to get indices for these elements, so we introduce a level of indirection to create them using a std::index_sequence:

template<class... T>
void ProcessData(const std::tuple<T...>& data){
    std::cout << "Processing " << sizeof...(T) << " data elements...\n";
    detail::ProcessDataImpl(data, std::make_index_sequence<sizeof...(T)>{});
}

The definition of detail::ProcessDataImpl is going to use a technique called simple pack expansion. It's a trick where we take advantage of array initialization to call a function for each element in a parameter pack. It looks a little weird, but bear with me:

template<class... T, size_t... I>
void ProcessDataImpl(const std::tuple<T...>& data, std::index_sequence<I...>){
    using swallow = int[];
    (void)swallow{0, (void(ProcessElement(std::get<I>(data))), 0)...};
}

This will call a function called ProcessElement for each element in the tuple. We use the comma operator and void casting to ensure that the function doesn't really do anything, and all our operations are solely for their side effects (calling our ProcessElement function).

Our ProcessElement function will use yet another level of indirection to pass on the argument for processing for more complicated types like our character arrays. Otherwise we can overload it for the types that we need:

template<class T>
struct ProcessElementImpl
{
    static void apply(const T& element)
    {
        static_assert(sizeof(T) == 0, "No specialization created for T");
    }
};

template<size_t N>
struct ProcessElementImpl<char[N]>
{
    static void apply(const char(&arr)[N])
    {
        std::cout << "Process char array of size " << N << std::endl;
    }
};

template<class T>
void ProcessElement(const T& element)
{
    ProcessElementImpl<T>::apply(element);
}

void ProcessElement(long _element)
{
    std::cout << "Process a long\n";
}

void ProcessElement(double _element)
{
    std::cout << "Process a double\n";
}

Notice that we overloaded for long and double, but we passed it along to ProcessElementImpl for our character array. This is required because we cannot partially specialize a template function, and we want to process arbitrarily-sized arrays.

The base class template also contains a static_assert so that we're forced to write a specialization for exporting a data type.

Finally we can call it like so:

int main()
{
    MYSTRUCT0 struct0;
    ProcessData(struct0.GetData());
    MYSTRUCT1 struct1;
    ProcessData(struct1.GetData());
    return 0;
}

Output:

Processing 2 data elements...
Process char array of size 10
Process char array of size 70
Processing 4 data elements...
Process a long
Process a long
Process char array of size 6
Process a double
Community
  • 1
  • 1
AndyG
  • 39,700
  • 8
  • 109
  • 143