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.
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