C++11 - Need static or dynamic polymorphism to reduce the number of similar functions

phoffric
phoffric used Ask the Experts™
on
In my program I have many dynamically allocated C-style struct arrays that are connected via pointers. I have to do a deep copy to an external buffer. I have a corresponding number of write function for each struct array (e.g., writeA(), ... , writeZ().

Here is a little example that I made (it compiles, builds, and runs w/o crashing; and a spot check in gdb shows some good results). Notice that I copied the writeB() function to writeE() function, and then made the changes to types, data member, and function name. We all know that copy and paste is usually a bad thing; and the remedy is often dynamic polymorphism or templates.

Here are my goals: I would appreciate a simpler approach than copy/pasting which is tedious and error prone when making changes to the pasting. I would like the function to have as small number of args as possible. If I have a source and destination pointer, and then also have to refer to a source and destination data member, that is four arguments - would like to be able to reduce that to three arguments since the data member name is the same for both pointers.

I am leaning towards templates and would like to have one template function, write(), that deduces the type. Uh oh... If we do that, then in this template write(), we have a recursive write, but now with a different data type - ouch. The different data member (a pointer) also gives me some grief in trying to be elegant (i.e., minimum number of arguments to the template write(). However, if templates cannot do the job (and given Boost and modern C++, I would think templates can make a great meal in the kitchen - when attached to a robot). Let's focus on writeB() and writeE(), which are copies of each other with some tidbits changed.
// g++ -g -std=c++11 write.cpp 

#include "structs.h"

#include <cstring>
#include <cstdint>

void writeA(A_s* myA);
void writeB(B_s* myB, size_t nElem);
void writeE(E_s* myE, size_t nElem);
void writeP(P_s* myP, size_t nElem) {}

void createDummyRoot(A_s* ptrA);


void doSomething(void* ptr) {}

// GLOBAL VARIABLES
uint8_t* buf = new uint8_t[1024000];
uint8_t* curBuf = buf;

int main()
{
  A_s* ptrA = new A_s;
  createDummyRoot(ptrA);
  
  writeA(ptrA);
}


void writeA(A_s* objPtr)
{
  memcpy(curBuf,  objPtr, sizeof(A_s));
  curBuf += sizeof(A_s);
  writeB( objPtr->ptrB,  objPtr->nB);
}


void writeB(B_s* objPtr, size_t nElem)
{
  memcpy(curBuf,  objPtr, sizeof(B_s)*nElem);
  B_s* dstPtr = reinterpret_cast<B_s*>(curBuf);
  
  for( int ii=0; ii < nElem; ++ii )
  {
    B_s& src = objPtr[ii];
    B_s& dst = dstPtr[ii];
    doSomething(dst.ptrE);
    writeE(src.ptrE, src.nE);
  }
  curBuf += sizeof(B_s)*nElem;
}


void writeE(E_s* objPtr, size_t nElem)
{
  memcpy(curBuf,  objPtr, sizeof(E_s)*nElem);
  E_s* dstPtr = reinterpret_cast<E_s*>(curBuf);
  
  for( int ii=0; ii < nElem; ++ii )
  {
    E_s& src = objPtr[ii];
    E_s& dst = dstPtr[ii];
    doSomething(dst.ptrP);
    writeP(src.ptrP, src.nP);
  }
  curBuf += sizeof(E_s)*nElem;
}


void createDummyRoot(A_s* ptrA)
{
  *ptrA = {2, new B_s[2]};
  
  auto pB = ptrA->ptrB;
  for(size_t ib=0; ib < ptrA->nB; ++ib) 
  {
     pB[ib] = {ib*99, ib+2, new E_s[ib+2]}; // fill in B_s array
     
     auto pE = pB[ib].ptrE;
     for(size_t ie=0; ie < pB[ib].nE; ++ie) 
     {
        pE[ie] = {10*ie+11, 2*ie+2, new P_s[2*ie+2]}; // fill in E_s array
        
        auto pP = pE[ie].ptrP;
        for(size_t ip=0; ip < pE->nP; ++ip)
        {
           pP[ip].p = 1 + 10*ib + 100*ie + 1000*ip;
        }
     }
  }
}

Open in new window

In this simple struct example, there is only one pointer per struct. In reality, there could be one, two, or more pointers. Some of the types they point to can even be the same type. The number of elements in general vary wildly among the structs.
#ifndef STRUCTS_H
#define STRUCTS_H

#include <cstddef>

// has plain old C-style structs.
// structs B,C,D,E are dynamically allocated arrays

struct A_s;
struct B_s;
struct E_s;
struct P_s;

struct A_s
{
   size_t nB; // the number of elements of B_s array
   B_s* ptrB;
};

struct B_s
{
   size_t b;
   size_t nE;
   E_s* ptrE;
};

struct E_s
{
   size_t e;
   size_t nP;
   P_s* ptrP;
};

struct P_s
{
   int p;
};

#endif

Open in new window

Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®
evilrixSenior Software Engineer (Avast)

Commented:
Any chance of a little example code that accurately represents your description? I'm happy to try and help once I'm clearer on the data structures involved. It's a little tricky to visualise it just from a description and I don't want to attempt to code up a possible solution that ends up going completely in the wrong direction. :)
evilrixSenior Software Engineer (Avast)

Commented:
I just need some small example code that accurately represents your problem. It doesn't need to be the actual code, as long as it's simple and is an accurate representation of the problem domain. Maybe we can prototype something based on that, which you can the extrapolate to solve your actual problem. I often find working on a simplified example is better, when prototyping a solution, as it stops you getting caught up in unnecessary weeds.

Author

Commented:
Looking at the OP, I can't even understand it. Will work something up.
Acronis in Gartner 2019 MQ for datacenter backup

It is an honor to be featured in Gartner 2019 Magic Quadrant for Datacenter Backup and Recovery Solutions. Gartner’s MQ sets a high standard and earning a place on their grid is a great affirmation that Acronis is delivering on our mission to protect all data, apps, and systems.

evilrixSenior Software Engineer (Avast)

Commented:
Hahaha. No worries. Just so you know it's just about be time for me now, if I'll look back here first thing tomorrow. Night, night.

Author

Commented:
I added a little example to the OP. Now, about that recursion -- If using templates, I would hope that it would not be necessary to identify at the top level all the different types in the entire tree structure. The idea is to keep the real programming as simple as possible once I get this write function generalized. I suppose, if the three differences in the code (i.e., the data type, the write function names, and the data member pointer name) make templates impossible to create a single write with only a few arguments, then what other suggestions do you have?

BTW, in this simple example, I show one pointer per struct. In fact, there can be a bunch of pointers in each struct, and those pointers can be of the same or different data types. There is no circularity in my real-life use cases.
Mihai BarbosTrying to tame bits. They're nasty.

Commented:
Well, it looks to me like writeB and writeE are identical (only the data type is different) so they are definitely good candidates for a template.
But indeed, it's hard to understand what's the problem here...

Author

Commented:
There are three differences, with writeB and writeE being just one of them. The problem is to see if there is a single templated write that can handle both writeB and writeE.
Mihai BarbosTrying to tame bits. They're nasty.

Commented:
Maybe I'm wrong, but if you try to define B_s and E_s like:
struct B_s
{
   size_t n;
   size_t nOther;
   E_s* ptrOther;
};

struct E_s
{
   size_t n;
   size_t nOther;
   P_s* ptrOther;
};

Open in new window

I think that writeB and writeE would be exactly the same.

Author

Commented:
Well the code in the OP is different for writeB and writeE.

Author

Commented:
I think the first two lines can be templated as follows:
template <typename T>
T* doCopy(T* objPtr, size_t nElem)
{
  memcpy(curBuf,  objPtr, sizeof(T)*nElem);
  T* dstPtr = reinterpret_cast<T*>(curBuf);
  return dstPtr;
}

Open in new window


But the loop with the [ii] subscripts is giving me trouble. I wonder if I need pointers to class member functions in the template to handle this.
Mihai BarbosTrying to tame bits. They're nasty.

Commented:
You mean the writeE and writeP calls ?
Mihai BarbosTrying to tame bits. They're nasty.

Commented:
Is this what you want ?
template <typename T>
void doWrite(T* objPtr, size_t nElem)
{
	memcpy(curBuf,  objPtr, sizeof(T) * nElem);
	T* dstPtr = reinterpret_cast<T*>(curBuf);

	for( int ii=0; ii < nElem; ++ii )	{
		T& src = objPtr[ii];
		T& dst = dstPtr[ii];
		doSomething(dst.ptrOther);
		doWrite(src.ptrOther, src.nOther);
	}
	curBuf += sizeof(T) * nElem;
}

template<>
void doWrite(P_s* objPtr, size_t /*nElem*/)
{
	std::cout << objPtr->p << " ";
}

Open in new window

Of course, with the previous changes in the structs
evilrixSenior Software Engineer (Avast)

Commented:
Paul, the only real issue I see here is that your structs need to be generic in the sense that you need to rename the members such that are consistent. As long as the member names are consistent within the structs and assuming you can group those structs into different types (B_s and E_s being the same type as they have an identical layout), there's no reason why you can have a template function for each type.

So, I guess my question is, are you able to make these changes to ensure the structs have a consistent naming convention for the members are can they be classified into different types based on their layout? If so, I don't really see the problem here. If not, then I guess we'll need to explore this further. It would appear that this is the point Mihai Barbos is making, so all credit to them for that.

Regarding "recursive" template calls, that's perfectly fine as long as you have a terminating template. That's pretty much how you unravel the arguments of a parameter pack.

https://evilrix.com/2013/10/12/variadic-functions-the-cpp11-way/

I look forward to your feedback. Hopefully Mihia has already nailed this for you, but if you I'm awake now and happy to work on this with you both. You know me, I do enjoy a good static polymorphism challenge :)

Author

Commented:
The structs are defined by the application and do not look similar. In the simple example I created on the fly, I may have accidentally made the structs look too similar. They are completely different in size and layout. One thing they have in common - for every pointer in a struct that represents a dynamic array, there will be a corresponding struct member, n, that indicates the length of the array. To be concrete, a struct could look like this:
struct Z
{
int x;
int y;
struct X* pX;
size_t number_X_elements;
double z;
bool flags[8];
struct W* pW;
size_t number_W_elements;
};

The corresponding writeZ(), writeX() and writeW() functions don't have to worry about most of the details in a struct, since it is just doing a memcpy of the struct array to its destination. Hope that clears up the general problem.

Author

Commented:
@evilrix,
>>  which you can the extrapolate to solve your actual problem. I often find working on a simplified example is better.

When I rewrote the OP to make it easier to read and to provide a made up example, I accidentally deleted a sentence that indicated that there can be multiple pointers in a struct each with its own integer indicating the length of the corresponding array. When you asked me to keep it simple, I tried to keep it simpler by having only one pointer in a struct.

So, if B_s had three pointers, then writeB would do the memcpy to copy B_s array, and then have three write()'s for the three pointers. I'll try to get time to update with a better example.
evilrixSenior Software Engineer (Avast)

Commented:
I mean, if each struct is going to be totally different, you can't really use a generic solution. Templates rely on the type having an identical interface. Of course, you can specialise templates for those they break the rules, but in your case it sounds like you're going to be specialising for each struct and in that case you may as well use function overloading. If there are commonalities between the structs you could have template that part and maybe pass in a functor to the template function to take care of the differences. Another solution would be to use templates that accept policies, such that the template takes care of the generic stuff and the policy takes care of the non-generic stuff. Yet another solution would be to use the adaptor pattern, to provide a common interface for each struct - if that's even possible.
evilrixSenior Software Engineer (Avast)

Commented:
Oh, and another thing. Your method of serialisation is potentially dangerous since structs can have padding. Without knowing how this is going to be used, I can't really say it will or will not be problematic, but you really should consider serialising each member rather than the whole struct.

Incidentally, have you looked at Boost Serialization?
https://www.boost.org/doc/libs/1_69_0/libs/serialization/doc/index.html

It's designed to facilitate the sort of thing you're trying to do here. It may or may not be helpful, but I think it's worth a look - even if to give you some ideas on how to improve your mechanisms.

Author

Commented:
The attribute names are fixed. I thought we could handle it using pointers to data members as shown here.
template <typename T,
          typename R,
          R T::*M
         >
constexpr std::size_t offset_of()
{
    return reinterpret_cast<std::size_t>(&(((T*)0)->*M));
};

Open in new window

https://stackoverflow.com/questions/12811330/c-compile-time-offsetof-inside-a-template

The above looks to me like M is a data member of type T, and M is actually of type R.
If my interpretation is right, then maybe this kind of construct can be used to handle the different data member (pointer) names and their different types.
evilrixSenior Software Engineer (Avast)

Commented:
The problem is, when you start trying to figure out offsets like this you are in the realm of non-portable code that could result in undefined behaviour. Also, you've got to cast that offset back to a type. How do you plan to do that?

Author

Commented:
The approach is used for offsetof, but as you can see from the OP, I avoided that. That is why I explicitly refer to the pointers in the struct explicitly instead of doing an offsetof to get their position. BTW, I know about the padding issue, but didn't think it would be a problem since the shared memory is within a Pod of Dockers on a single VM.

I thought hopefully that the construct of
template <typename T,
          typename R,
          R T::*M
         >

Open in new window

might be usable for my deep copy to shared memory.
evilrixSenior Software Engineer (Avast)

Commented:
So, in this case, what would the function M be/do? What is it a member of? In other words, what is T and what is return type R?

Author

Commented:
Heh, if I knew for sure, I wouldn't be asking this question. :) So all I can do is take a wild template guess, and let you both tell me what is really going on.
struct B_s
{
   size_t b;
   size_t nE;
   E_s* ptrE;
};

Open in new window

When I saw
R T::*M

Open in new window

I was just guessing that it would correspond to
E_s* B_s::ptrE

Open in new window

where R ~ E_s*, T ~ B_s, and M ~ ptrE
since ptrE  (M) is an attribute of type E_s* (R), and ptrE is a attribute of B_s (T).
But this was just a wild guess. I have not done meta-programming and don't know its full deduction capabilities.
Mihai BarbosTrying to tame bits. They're nasty.

Commented:
Stupid question: what do you actually want to do ?
Do you want to serialize / deserialize data or is it something else ?
evilrixSenior Software Engineer (Avast)

Commented:
Okay as I understand this, all that SO code is doing is casting a null pointer (0) to type T to use it as a baseline address. It then references the member via it's address M of type R as an address offset from T (which is zero) and finally casts the result to a size_t. Basically, it's just giving you a relative address offset (relative to the start of the struct) as to where you can find the member without you needing to instantiate the type.

Unless I'm very much mistaken, it's not really any different that doing this...

B_s bs;
size_t offset = (size_t) &bs.b - &bs;

My original point still stands: you still need to be able to convert that offset back to a type (or size in bytes, I suppose) to do anything useful with it. So, how do you plan to do that? In other words, I don't see what you gain by doing this as you'll still end up having to have some way of passing the type information into the function that is using this offset.

Am I missing something?
Mihai BarbosTrying to tame bits. They're nasty.

Commented:
Don't get me wrong here, all what evilrix has said here is valid, but it obviously doesn't fit your problem. Maybe we are looking to the problem from the wrong angle. Can you just tell us what do you want to do ?

Author

Commented:
@evilrix,
I am not using offsets. I just gave that link to show what I thought was a template technique that might be applicable to dealing with data members (i.e., the M part of the template).

@Mihai,
>> what do you actually want to do ?
In a RHEL VM are several processes. One process will create a shared memory segment and fill in the A_s instance tree. One or more other processes will read the tree either directly from shared memory or copy the tree to its own local virtual address space. (Naturally, the pointer values that the writer uses are not valid for the reader - I took care of that issue.)
Mihai BarbosTrying to tame bits. They're nasty.

Commented:
All you want to do is a deep copy of the tree ?
Just implement a simple = operator for each structure, allocating and copying the data and setting right the pointers. Maybe a template can help here, but I think that wouldn't simply things too much, just calling the template would be close to the malloc / memcopy calls.
What I mean is that you should look at it a liitle bit the other way round. Define copy at the local level only, let the children deal with it themselves.
Sorry, I'll also do what evilrix has done earlier, sleep :D

Author

Commented:
@Mihai, @evilrix,
Goodnight, and thank you for your inputs. My goal was to simplify the API so that other developers would be able to easily write data to shared memory. I had asked about google Protocol Buffers in December. I am now verifying whether the shared memory is guaranteed to always be read on the same VM with programs all built with the same compiler release. I didn't pursue Protocol Buffers because at another company, those who had to maintain it complained about the extension (IIRC) capability - was a pain, they told me. Easy to build the first time - my own experience - but hard to maintain when things change - their experience; I didn't have to maintain. :)

>> Define copy at the local level only, let the children deal with it themselves.
Problem with this is the management of a child instance. Given one particular struct instance (say one P_s struct array defined in the OP simple example), that instance could have come down one of many different B_s --> E_s paths in the tree. Keeping track of which P_s struct array belonged to which path, made me choose the approach I took. But I am open to suggestions. Maybe no approach will be easy for everyone to develop for their own structures. I hope that is not the case.

Author

Commented:
>> Just implement a simple = operator for each structure, allocating and copying the data and setting right the pointers. Maybe a template can help here
I have a buffer manager which is a template and takes care of the pointers in shared memory. Instead of '=', I just called it appendArray().
evilrixSenior Software Engineer (Avast)

Commented:
Paul, I'll be honest, I don't see a generic solution here (at least not one that's going to be standards compliant and safe) because your data types have no commonality. The best you can do is implement serialisation functions for each type as overloaded methods (either in the struct - remember, structs can be safely sub-classed - or stand alone). To use templates you need a common interface on your types. It's fine if you only need to specialise a few, but in your case it sounds like you'll be specialising every type and so it makes more sense to just have overloaded functions for those types. Maybe you can factor out common stuff into separate functions (template or overloaded), but given my understanding of your problem (which could be wrong), I really don't see a generic solution waiting for you. I fear this is quite probably a case of "you can't do that".

Author

Commented:
Oh darn. I was afraid of that "you can't do that"; and it appears that is what both of you are saying.

Author

Commented:
template <typename T,
          typename R,
          R T::*M
         >

Open in new window

Just wondering how the above template form can be used in some other capacity than offsetof.
evilrixSenior Software Engineer (Avast)

Commented:
Well, you can get from offset back to the original member, but to be useful you'll either need type information or size in bytes. I mean, for serialisation maybe size in bytes is all you need. You can then pass in the original member as a pointer to void (or char).

Author

Commented:
OP: "Here are my goals: I would appreciate a simpler approach than copy/pasting which is tedious and error prone when making changes to the pasting. I would like the function to have as small number of args as possible."

We have an understanding now that the structs are written in stone; can have multiple attributes and multiple pointers, where each pointer is pointing to an array of structs. The OP example just illustrates a simple exercise to illustrate the simplification issues I have with the writeX functions.

The variadic template idea looks interesting for simpler cases (like in the OP); but can get pretty nasty if there are a 1-2 dozen pointers in the struct tree structure. (In future, I might try defining this tree structure in XML, and see if a single write function can parse it and determine the next step; but that is not for now. At this point, I am no longer on that project, and am continuing this exercise just for learning purposes.

The following code is a step in the right direction. This program spits out original source data as well as the data in the global buffer. I checked, and verified the values match. With this baseline, I will try to templatize the body of the for-loop in the writeN functions. This loop was one of the original stumbling blocks, but now I have an idea on solving it with a small number of args.
	// g++ -g -std=c++11 write.cpp 

#include "structs.h"

#include <cstring>
#include <cstdint>
#include <cstdio>

	void writeA(A_s* myA, size_t nElem);
	void writeB(B_s* myB, size_t nElem);
	void writeE(E_s* myE, size_t nElem);
	void writeP(P_s* myP, size_t nElem);

	void createDummyRoot(A_s* ptrA);

	template <typename T>
	void doSomething(T* ptr) {}

	// GLOBAL VARIABLES
	uint8_t* buf = new uint8_t[1024000];
	uint8_t* curBuf = buf;

	int main()
	{
		A_s* ptrA = new A_s;
		createDummyRoot(ptrA);

		writeA(ptrA, 1);
	}

	template <typename T>
	T* appendArray(T* objPtr, size_t nElem)
	{
		memcpy(curBuf, objPtr, sizeof(T)*nElem);
		T* dstPtr = reinterpret_cast<T*>(curBuf);
		curBuf += sizeof(T)*nElem;
		return dstPtr;
	}

	void writeA(A_s* objPtr, size_t nElem)
	{
		A_s* dstPtr = appendArray(objPtr, 1);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			A_s& src = objPtr[ii];
			A_s& dst = dstPtr[ii];
			printf("A[%02d]: nB: %d\n", ii, dst.nB);

			doSomething(dst.ptrB); 
			writeB(src.ptrB, src.nB);
		}
	}


	void writeB(B_s* objPtr, size_t nElem)
	{
		B_s* dstPtr = appendArray(objPtr, nElem);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			B_s& src = objPtr[ii];
			B_s& dst = dstPtr[ii];
			printf("  B[%02d] b:%3d  nE:%2d\n", ii, dst.b, dst.nE);

			doSomething(dst.ptrE);
			writeE(src.ptrE, src.nE);
		}
	}


	void writeE(E_s* objPtr, size_t nElem)
	{
		E_s* dstPtr = appendArray(objPtr, nElem);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			E_s& src = objPtr[ii];
			E_s& dst = dstPtr[ii];
			printf("    E[%02d] e:%3d  nP:%2d\n", ii, dst.e, dst.nP);

			doSomething(dst.ptrP);
			writeP(src.ptrP, src.nP);
		}
	}

	void writeP(P_s* objPtr, size_t nElem)
	{
		P_s* dstPtr = appendArray(objPtr, nElem);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			P_s& src = objPtr[ii];
			P_s& dst = dstPtr[ii];
			printf("      P[%d] p:%4d\n", ii, dst.p);
		}
	}


	void createDummyRoot(A_s* ptrA)
	{
		size_t nB = 3;
		*ptrA = { nB, new B_s[nB] };
		printf("A: nB: %d\n", ptrA->nB);

		auto pB = ptrA->ptrB;
		for (size_t ib = 0; ib < ptrA->nB; ++ib)
		{
			size_t nE = ib + 2;
			pB[ib] = { ib * 99 + 1, nE, new E_s[nE] }; // fill in B_s array
			printf("  B[%02d] b:%3d  nE:%2d\n", ib, pB[ib].b, pB[ib].nE);

			auto pE = pB[ib].ptrE;
			for (size_t ie = 0; ie < pB[ib].nE; ++ie)
			{
				size_t nP = 2 * ie + 2;
				pE[ie] = { 10 * ie + 11, nP, new P_s[nP] }; // fill in E_s array
				printf("    E[%02d] e:%3d  nP:%2d\n", ie, pE[ie].e, pE[ie].nP);

				auto pP = pE[ie].ptrP;
				for (size_t ip = 0; ip < pE[ie].nP; ++ip)
				{
					pP[ip].p = 1 + 10 * ib + 100 * ie + 1000 * ip;
					printf("      P:[%2d] p: %d\n", ip, pP[ip].p);
				}
			}
		}
		printf("-------------\n\n");
	}

Open in new window

In this code, I have reduced the total number of copy/paste/modification lines by a little bit. I hope to do more.

Author

Commented:
Here is the output of the program in the previous post.
A: nB: 3
  B[00] b:  1  nE: 2
    E[00] e: 11  nP: 2
      P:[ 0] p: 1
      P:[ 1] p: 1001
    E[01] e: 21  nP: 4
      P:[ 0] p: 101
      P:[ 1] p: 1101
      P:[ 2] p: 2101
      P:[ 3] p: 3101
  B[01] b:100  nE: 3
    E[00] e: 11  nP: 2
      P:[ 0] p: 11
      P:[ 1] p: 1011
    E[01] e: 21  nP: 4
      P:[ 0] p: 111
      P:[ 1] p: 1111
      P:[ 2] p: 2111
      P:[ 3] p: 3111
    E[02] e: 31  nP: 6
      P:[ 0] p: 211
      P:[ 1] p: 1211
      P:[ 2] p: 2211
      P:[ 3] p: 3211
      P:[ 4] p: 4211
      P:[ 5] p: 5211
  B[02] b:199  nE: 4
    E[00] e: 11  nP: 2
      P:[ 0] p: 21
      P:[ 1] p: 1021
    E[01] e: 21  nP: 4
      P:[ 0] p: 121
      P:[ 1] p: 1121
      P:[ 2] p: 2121
      P:[ 3] p: 3121
    E[02] e: 31  nP: 6
      P:[ 0] p: 221
      P:[ 1] p: 1221
      P:[ 2] p: 2221
      P:[ 3] p: 3221
      P:[ 4] p: 4221
      P:[ 5] p: 5221
    E[03] e: 41  nP: 8
      P:[ 0] p: 321
      P:[ 1] p: 1321
      P:[ 2] p: 2321
      P:[ 3] p: 3321
      P:[ 4] p: 4321
      P:[ 5] p: 5321
      P:[ 6] p: 6321
      P:[ 7] p: 7321
-------------

A[00]: nB: 3
  B[00] b:  1  nE: 2
    E[00] e: 11  nP: 2
      P[0] p:   1
      P[1] p:1001
    E[01] e: 21  nP: 4
      P[0] p: 101
      P[1] p:1101
      P[2] p:2101
      P[3] p:3101
  B[01] b:100  nE: 3
    E[00] e: 11  nP: 2
      P[0] p:  11
      P[1] p:1011
    E[01] e: 21  nP: 4
      P[0] p: 111
      P[1] p:1111
      P[2] p:2111
      P[3] p:3111
    E[02] e: 31  nP: 6
      P[0] p: 211
      P[1] p:1211
      P[2] p:2211
      P[3] p:3211
      P[4] p:4211
      P[5] p:5211
  B[02] b:199  nE: 4
    E[00] e: 11  nP: 2
      P[0] p:  21
      P[1] p:1021
    E[01] e: 21  nP: 4
      P[0] p: 121
      P[1] p:1121
      P[2] p:2121
      P[3] p:3121
    E[02] e: 31  nP: 6
      P[0] p: 221
      P[1] p:1221
      P[2] p:2221
      P[3] p:3221
      P[4] p:4221
      P[5] p:5221
    E[03] e: 41  nP: 8
      P[0] p: 321
      P[1] p:1321
      P[2] p:2321
      P[3] p:3321
      P[4] p:4321
      P[5] p:5321
      P[6] p:6321
      P[7] p:7321

Open in new window

Author

Commented:
Still have to copy and paste, but a little less tedious than in OP.

Original OP
void writeB(B_s* objPtr, size_t nElem)
{
  memcpy(curBuf,  objPtr, sizeof(B_s)*nElem);
  B_s* dstPtr = reinterpret_cast<B_s*>(curBuf);
  
  for( int ii=0; ii < nElem; ++ii )
  {
    B_s& src = objPtr[ii];
    B_s& dst = dstPtr[ii];
    doSomething(dst.ptrE);
    writeE(src.ptrE, src.nE);
  }
  curBuf += sizeof(B_s)*nElem;
}

Open in new window


Latest knocked out a good number of lines that have to be copied and pasted:
void writeB(B_s* objPtr, size_t nElem)
{
	B_s* dstPtr = appendArray(objPtr, nElem);

	for (size_t ii = 0; ii < nElem; ++ii)
	{
		auto srcdst = fixDstPtr(objPtr, dstPtr, ii, dstPtr[ii].ptrE);
		writeE(srcdst.first.ptrE, srcdst.first.nE);
	}
}

Open in new window


1) The member function, ptrE is repeated twice, and is one of the items that has to be copied and modified. I am thinking that the ptrE can be the actual value in a template (slightly modified) with a better API.

2) writeB calls writeE. I'd like to be able to just have writeE as a parameter in a function or template. Think that's possible? I saw examples where the signatures are the same and it is straightforward in that case. The examples use either typedef, using, or even macros. Not sure, but I think I saw a template with std::forward that may handle functions with templated argument types. Will have to revisit that one.

If you can shed light on this and help reduce this further, that would be interesting, I think.

Here is the whole program. Starting to look a little simpler IMO.
	// g++ -g -std=c++11 write.cpp 

#include "structs.h"

#include <cstring>
#include <cstdint>
#include <cstdio>
#include <utility>  

	void writeA(A_s* myA, size_t nElem);
	void writeB(B_s* myB, size_t nElem);
	void writeE(E_s* myE, size_t nElem);
	void writeP(P_s* myP, size_t nElem);

	void createDummyRoot(A_s* ptrA);

	template <typename T>
	void doSomething(T* ptr) {}

	// GLOBAL VARIABLES
	uint8_t* buf = new uint8_t[1024000];
	uint8_t* curBuf = buf;

	int main()
	{
		A_s* ptrA = new A_s;
		createDummyRoot(ptrA);

		writeA(ptrA, 1);
	}

	template <typename T>
	using Func = void(*)(void* objPtr, size_t nElem);


	template <typename T>
	T* appendArray(T* objPtr, size_t nElem)
	{
		memcpy(curBuf, objPtr, sizeof(T)*nElem);
		T* dstPtr = reinterpret_cast<T*>(curBuf);
		curBuf += sizeof(T)*nElem;
		return dstPtr;
	}


	template <typename T, typename U>
	std::pair<T, T> fixDstPtr(T* objPtr, T* dstPtr, size_t ii, /* U* srcNxtPtr, size_t srcNumElem, */ U* dstNxtPtr )
	{
		T& src = objPtr[ii];
		T& dst = dstPtr[ii];
		doSomething(dstNxtPtr);
		return std::make_pair(src, dst);
	}


	void writeA(A_s* objPtr, size_t nElem)
	{
		A_s* dstPtr = appendArray(objPtr, 1);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			auto srcdst = fixDstPtr(objPtr, dstPtr, ii, dstPtr[ii].ptrB);
			printf("A[%02d]: nB: %d\n", ii, srcdst.second.nB);
			writeB(srcdst.first.ptrB, srcdst.first.nB);
		}
	}


	void writeB(B_s* objPtr, size_t nElem)
	{
		B_s* dstPtr = appendArray(objPtr, nElem);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			auto srcdst = fixDstPtr(objPtr, dstPtr, ii, dstPtr[ii].ptrE);
			printf("  B[%02d] b:%3d  nE:%2d\n", ii, srcdst.second.b, srcdst.second.nE);
			writeE(srcdst.first.ptrE, srcdst.first.nE);
		}
	}


	void writeE(E_s* objPtr, size_t nElem)
	{
		E_s* dstPtr = appendArray(objPtr, nElem);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			auto srcdst = fixDstPtr(objPtr, dstPtr, ii, dstPtr[ii].ptrP);
			printf("    E[%02d] e:%3d  nP:%2d\n", ii, srcdst.second.e, srcdst.second.nP);
			writeP(srcdst.first.ptrP, srcdst.first.nP);
		}
	}

	void writeP(P_s* objPtr, size_t nElem)
	{
		P_s* dstPtr = appendArray(objPtr, nElem);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			P_s& src = objPtr[ii];
			P_s& dst = dstPtr[ii];
			printf("      P[%d] p:%4d\n", ii, dst.p);
		}
	}


	void createDummyRoot(A_s* ptrA)
	{
		size_t nB = 3;
		*ptrA = { nB, new B_s[nB] };
		printf("A: nB: %d\n", ptrA->nB);

		auto pB = ptrA->ptrB;
		for (size_t ib = 0; ib < ptrA->nB; ++ib)
		{
			size_t nE = ib + 2;
			pB[ib] = { ib * 99 + 1, nE, new E_s[nE] }; // fill in B_s array
			printf("  B[%02d] b:%3d  nE:%2d\n", ib, pB[ib].b, pB[ib].nE);

			auto pE = pB[ib].ptrE;
			for (size_t ie = 0; ie < pB[ib].nE; ++ie)
			{
				size_t nP = 2 * ie + 2;
				pE[ie] = { 10 * ie + 11, nP, new P_s[nP] }; // fill in E_s array
				printf("    E[%02d] e:%3d  nP:%2d\n", ie, pE[ie].e, pE[ie].nP);

				auto pP = pE[ie].ptrP;
				for (size_t ip = 0; ip < pE[ie].nP; ++ip)
				{
					pP[ip].p = 1 + 10 * ib + 100 * ie + 1000 * ip;
					printf("      P:[%2d] p: %d\n", ip, pP[ip].p);
				}
			}
		}
		printf("-------------\n\n");
	}

Open in new window

evilrixSenior Software Engineer (Avast)

Commented:
You probably want to look at std::function, which allows you to package up any function into an object that can be passed as a functor, thus fulfilling the properties of the Command design pattern. As a companion to this, take a look at std::bind, which allows you to construct command objects from existing functions. You can either use lambdas to package up existing functions or std::bind. It's really up to you to decide which is better suited to your needs. The std::bind tool can be a bit confusing to use, but it's incredibly powerful and worth learning.

I'm happy to give you an example of using these if you need it. I'll hold off on that until you've had a chance to take a look at them. You may just read up and go, "oh year, that makes sense". Heh. :)

Incidentally, why do you have writeB and writeE and so on? What's wrong with just write and using function overloading?

Author

Commented:
IIRC, I read that in C++11 that lambdas could not be templates (although it could still be used in a templates function as long as it did not use the templates arguments.
evilrixSenior Software Engineer (Avast)

Commented:
Correct, lambdas can't be templated until C++20.

Author

Commented:
>> Incidentally, why do you have writeB and writeE and so on? What's wrong with just write and using function overloading?
Nothing is wrong. For some reason, when we approached this subject earlier, there seemed to be a conclusion that the pointers in the structs would have to have the same member name. I temporarily stopped thinking about just naming the functions, write(), since I was still researching "pointers to data members" topic. Revisiting this is a good step in the right direction. One less copy/paste/modify operation to change in the function prototype, and in the function's body. So, now we do have a clearer recursion where write() calls write(). I like that. But, I still think I am not done in reducing the amount of copy/paste/modification steps.

Starting to look much better than OP:
	// g++ -g -std=c++11 write.cpp 

#include "structs.h"

#include <cstring>
#include <cstdint>
#include <cstdio>
#include <utility>  

	void write(A_s* myA, size_t nElem);
	void write(B_s* myB, size_t nElem);
	void write(E_s* myE, size_t nElem);
	void write(P_s* myP, size_t nElem);

	void createDummyRoot(A_s* ptrA);

	template <typename T>
	void doSomething(T* ptr) {}

	// GLOBAL VARIABLES
	uint8_t* buf = new uint8_t[1024000];
	uint8_t* curBuf = buf;

	int main()
	{
		A_s* ptrA = new A_s;
		createDummyRoot(ptrA);

		write(ptrA, 1);
	}


	template <typename T>
	T* appendArray(T* objPtr, size_t nElem)
	{
		memcpy(curBuf, objPtr, sizeof(T)*nElem);
		T* dstPtr = reinterpret_cast<T*>(curBuf);
		curBuf += sizeof(T)*nElem;
		return dstPtr;
	}


	template <typename T, typename U>
	std::pair<T, T> fixDstPtr(T* objPtr, T* dstPtr, size_t ii, /* U* srcNxtPtr, size_t srcNumElem, */ U* dstNxtPtr )
	{
		T& src = objPtr[ii];
		T& dst = dstPtr[ii];
		doSomething(dstNxtPtr);
		return std::make_pair(src, dst);
	}


	void write(A_s* objPtr, size_t nElem)
	{
		A_s* dstPtr = appendArray(objPtr, 1);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			auto srcdst = fixDstPtr(objPtr, dstPtr, ii, dstPtr[ii].ptrB);
			printf("A[%02d]: nB: %d\n", ii, srcdst.second.nB); // this is just debug msg to verify results - maybe we can separate this out with specialization?
			write(srcdst.first.ptrB, srcdst.first.nB);
		}
	}


	void write(B_s* objPtr, size_t nElem)
	{
		B_s* dstPtr = appendArray(objPtr, nElem);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			auto srcdst = fixDstPtr(objPtr, dstPtr, ii, dstPtr[ii].ptrE);
			printf("  B[%02d] b:%3d  nE:%2d\n", ii, srcdst.second.b, srcdst.second.nE);
			write(srcdst.first.ptrE, srcdst.first.nE);
		}
	}


	void write(E_s* objPtr, size_t nElem)
	{
		E_s* dstPtr = appendArray(objPtr, nElem);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			auto srcdst = fixDstPtr(objPtr, dstPtr, ii, dstPtr[ii].ptrP);
			printf("    E[%02d] e:%3d  nP:%2d\n", ii, srcdst.second.e, srcdst.second.nP);
			write(srcdst.first.ptrP, srcdst.first.nP);
		}
	}

	void write(P_s* objPtr, size_t nElem)
	{
		P_s* dstPtr = appendArray(objPtr, nElem);

		for (size_t ii = 0; ii < nElem; ++ii)
		{
			P_s& src = objPtr[ii];
			P_s& dst = dstPtr[ii];
			printf("      P[%d] p:%4d\n", ii, dst.p);
		}
	}


	void createDummyRoot(A_s* ptrA)
	{
		size_t nB = 3;
		*ptrA = { nB, new B_s[nB] };
		printf("A: nB: %d\n", ptrA->nB);

		auto pB = ptrA->ptrB;
		for (size_t ib = 0; ib < ptrA->nB; ++ib)
		{
			size_t nE = ib + 2;
			pB[ib] = { ib * 99 + 1, nE, new E_s[nE] }; // fill in B_s array
			printf("  B[%02d] b:%3d  nE:%2d\n", ib, pB[ib].b, pB[ib].nE);

			auto pE = pB[ib].ptrE;
			for (size_t ie = 0; ie < pB[ib].nE; ++ie)
			{
				size_t nP = 2 * ie + 2;
				pE[ie] = { 10 * ie + 11, nP, new P_s[nP] }; // fill in E_s array
				printf("    E[%02d] e:%3d  nP:%2d\n", ie, pE[ie].e, pE[ie].nP);

				auto pP = pE[ie].ptrP;
				for (size_t ip = 0; ip < pE[ie].nP; ++ip)
				{
					pP[ip].p = 1 + 10 * ib + 100 * ie + 1000 * ip;
					printf("      P:[%2d] p: %d\n", ip, pP[ip].p);
				}
			}
		}
		printf("-------------\n\n");
	}

Open in new window

I will ask another question about templates of function calls, where the arguments are expected to be template arguments.
evilrixSenior Software Engineer (Avast)

Commented:
I'm happy for you to continue here if you like. You know me, I'm not driven by points, so feel free to keep the discussion here unless you feel opening a new question would be better for you.

I'm not really sure experts provided anything concrete here, so if you do ask a new question, I'd have no objections to you just deleting this one. Again, I leave that up to you.

Obviously, I only speak for myself and not the other contributing expert. ;)

Author

Commented:
Now that we have only one name, write(), instead of writeX(), I have a feeling that my problem in passing in a function pointer (or functor, or whatever else C++11 has to offer - functoids?) may now be a different issue - similar, but different. Also might be easier for me to ask a really simple question.

I think the development of this thread with all the revisions may be useful in the end. We'll see. Now that my contract just ended a couple days ago, maybe I'll write a paper on refactoring using templates to make a developer's job less tedious.
evilrixSenior Software Engineer (Avast)

Commented:
I'd be interested in reading your paper.

Author

Commented:
@Mihai
>> All you want to do is a deep copy of the tree ?
Yes, right to that global buffer.

>> Just implement a simple = operator for each structure, allocating
In the OP, the global buffer is pre-allocated. I have an overloaded write() function in my latest code post. I think I could do an in-place new operation to act as if I had dynamic allocation, and then do the = operation. I can always change a name from write() to "=". Aside from the name change, I am not yet clear whether it will reduce the number of required modifications needed for each structure. We will see.

>> setting right the pointers.
Agreed, and the code does not show that. I think that's just a small detail that I left out, since my focus is on the problem of reducing the number of copy/paste/modify manual steps required as we move down the tree. I'll do it eventually once I finish what I already have (or can't proceed further). Setting the pointers right could mean a correct virtual address, or an integer pointer type (per C++11 types) that provide an offset to what the pointer is pointing to in the global buffer. Latter is a good way to go if that global buffer is actually shared memory between processes (on same platform, all built with same compiler); former is a good way to go if the global buffer is shared among threads.

>> you should look at it a liitle bit the other way round. Define copy at the local level only, let the children deal with it themselves.
Ok, this intrigues me now that I got to a point where I can be intrigued. Could you elaborate a little more, perhaps with a code snippet that I can plug in after refactoring a bit. If this reduces the number of copy/paste/modify manual steps, then this would be another interesting path to follow. If it makes this thread to cumbersome, I can always put this new path in another question. (Not about points, but about keeping a thread coherent.)

Author

Commented:
Made a little more progress on the write.cpp, but having some trouble converting the 3-line for-loop to a single templated function. (If not possible using a template function, I may have to consider a MACRO - oh no!)
#include "structs.h"
#include "write.h"

#include <cstring>
#include <cstdint>
#include <cstdio>

void createDummyRoot(A_s* ptrA);

// GLOBAL VARIABLES
uint8_t* buf = new uint8_t[1024000];
uint8_t* curBuf = buf;



void write(A_s* srcPtr, size_t nElem)
{
	A_s* dstPtr = appendArray(srcPtr, 1);

	for (size_t ii = 0; ii < nElem; ++ii)
	{
		doWrite(srcPtr, dstPtr, ii, srcPtr[ii].ptrB, srcPtr[ii].nB, dstPtr[ii].ptrB);
	}
}


void write(B_s* srcPtr, size_t nElem)
{
	B_s* dstPtr = appendArray(srcPtr, nElem);

	for (size_t ii = 0; ii < nElem; ++ii)
	{
		doWrite(srcPtr, dstPtr, ii, srcPtr[ii].ptrE, srcPtr[ii].nE, dstPtr[ii].ptrE);
	}
}


void write(E_s* srcPtr, size_t nElem)
{
	E_s* dstPtr = appendArray(srcPtr, nElem);
					// TODO: What if there are two ptrs, ptrP1 and ptrP2?
	for (size_t ii = 0; ii < nElem; ++ii)	
	{
		doWrite(srcPtr, dstPtr, ii, srcPtr[ii].ptrP, srcPtr[ii].nP, dstPtr[ii].ptrP);
	}
}

void write(P_s* srcPtr, size_t nElem)
{
	P_s* dstPtr = appendArray(srcPtr, nElem);

	for (size_t ii = 0; ii < nElem; ++ii)
	{
		doWrite(srcPtr, dstPtr, ii, dstPtr, 0, dstPtr);
	}
}

Open in new window

Doesn't matter how complicated the header file gets, since the only changes for a different tree structure are the kind of write and debugPrint functions - write.h:
#pragma once

#include "structs.h"

#include <utility>  
#include <cstdio>
#include <cstdint>

// Declare functions
void write(A_s* myA, size_t nElem);
void write(B_s* myB, size_t nElem);
void write(E_s* myE, size_t nElem);
void write(P_s* myP, size_t nElem);

void debugPrint(A_s& srcPtr, size_t ii);
void debugPrint(B_s& srcPtr, size_t ii);
void debugPrint(E_s& srcPtr, size_t ii);
void debugPrint(P_s& srcPtr, size_t ii);

void createDummyRoot(A_s* ptrA);

// Declare Global variables
extern uint8_t* buf;
extern uint8_t* curBuf;


// TEMPLATES
template <typename T>
void doSomething(T* ptr) {}

template <typename T>
T* appendArray(T* srcPtr, size_t nElem)
{
	memcpy(curBuf, srcPtr, sizeof(T)*nElem);
	T* dstPtr = reinterpret_cast<T*>(curBuf);
	curBuf += sizeof(T)*nElem;
	return dstPtr;
}


// Set the destination pointer (in the parent) to point to the destination.
template <typename T, typename U>
std::pair<T, T> fixDstPtr(T* srcPtr, T* dstPtr, size_t ii, U* dstNxtPtr)
{
	T& src = srcPtr[ii];
	T& dst = dstPtr[ii];
	doSomething(dstNxtPtr);
	return std::make_pair(src, dst);
}


template <typename T, typename U>
void doWrite(T* srcPtr, T* dstPtr, size_t ii, U* srcPtrChild, size_t srcNumElem, U* dstPtrChild)
{
	auto srcdst = fixDstPtr(srcPtr, dstPtr, ii, dstPtrChild);
	debugPrint(srcdst.second, ii);
	write(srcPtrChild, srcNumElem);
}

Open in new window


createFakeData.cpp has the main():
#include "structs.h"
#include "write.h"


int main()
{
	A_s* ptrA = new A_s;
	createDummyRoot(ptrA);

	write(ptrA, size_t(1) );
}


void createDummyRoot(A_s* ptrA)
{
	size_t nB = 3;
	*ptrA = { nB, new B_s[nB] };
	debugPrint(*ptrA, 1);

	auto pB = ptrA->ptrB;
	for (size_t ib = 0; ib < ptrA->nB; ++ib)
	{
		size_t nE = ib + 2;
		pB[ib] = { ib * 99 + 1, nE, new E_s[nE] }; // fill in B_s array
		debugPrint(pB[ib], ib);

		auto pE = pB[ib].ptrE;
		for (size_t ie = 0; ie < pB[ib].nE; ++ie)
		{
			size_t nP = 2 * ie + 2;
			pE[ie] = { 10 * ie + 11, nP, new P_s[nP] }; // fill in E_s array
			debugPrint(pE[ie], ie);

			auto pP = pE[ie].ptrP;
			for (size_t ip = 0; ip < pE[ie].nP; ++ip)
			{
				pP[ip].p = 1 + 10 * ib + 100 * ie + 1000 * ip;
				debugPrint(pP[ip], ip);
			}
		}
	}
	printf("-------------\n\n");
}

void debugPrint(A_s& srcPtr, size_t ii) { printf("A[%02d]: nB: %d\n", ii, srcPtr.nB); }
void debugPrint(B_s& srcPtr, size_t ii) { printf("  B[%02d] b:%3d  nE:%2d\n", ii, srcPtr.b, srcPtr.nE); }
void debugPrint(E_s& srcPtr, size_t ii) { printf("    E[%02d] e:%3d  nP:%2d\n", ii, srcPtr.e, srcPtr.nP); }
void debugPrint(P_s& srcPtr, size_t ii) { printf("      P[%d] p:%4d\n", ii, srcPtr.p); }

Open in new window

If there is only one pointer per struct, then the above write.cpp helps a bit in handling other structs. Wondering whether I have come up with a pattern that is well-known by others. Having write() call doWrite() which calls write() came out of the wash, as I kept making small changes by adding additional templates. Looks like I got my recursion down the tree indirectly. Now I'm wondering whether I could have taken the OP and just zoomed close to the above result. I'll be looking to further simplify the write.cpp code, and deal with multiple pointers per struct another time.

Author

Commented:
In previous post, I wrote:
>> Made a little more progress on the write.cpp, but having some trouble converting the 3-line for-loop to a single templated function.
Now I am convinced that this goal is impossible. So, I switched gears. The following approach makes the copy and paste operations significantly easier to do than in the OP or even in the previous post.
write.cpp
#include "structs.h"
#include "write.h"

#include <cstring>
#include <cstdint>
#include <cstdio>


// GLOBAL VARIABLES
uint8_t* buf = new uint8_t[1024000];
uint8_t* curBuf = buf;


void write(A_s* srcPtr, size_t nElem)
{
   writeParentAndChild(A_s, nB, ptrB);
}


void write(B_s* srcPtr, size_t nElem)
{
   writeParentAndChild(B_s, nE, ptrE);
}

//// TODO: What if there are two ptrs, ptrP1 and ptrP2?
void write(E_s* srcPtr, size_t nElem)
{
   writeParentAndChild(E_s, nP, ptrP);
}

void write(P_s* srcPtr, size_t nElem)
{
   P_s* dstPtr = appendArray(srcPtr, nElem);
   //writeEachArrayElement(0, dstPtr);
   for (size_t ii = 0; ii < nElem; ++ii)
   {
      doWrite(srcPtr, dstPtr, ii, dstPtr, 0, dstPtr);
   }
}

Open in new window

I added the following to write.h:
// srcPtr, nElem, and dstPtr must be defined in this Macro's scope
#define writeParentAndChild(TYPE_PARENT, NumChild, PtrChild) \
TYPE_PARENT* dstPtr = appendArray(srcPtr, nElem); \
writeEachArrayElement(NumChild, PtrChild);

#define writeEachArrayElement(NumChild, PtrChild) \
for (size_t ii = 0; ii < nElem; ++ii) { \
   doWrite(srcPtr, dstPtr, ii, srcPtr[ii].##PtrChild, srcPtr[ii].##NumChild, dstPtr[ii].##PtrChild); \
}

Open in new window

Now, I am done... Or, maybe not. To be continued, maybe...

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start 7-Day Free Trial