views:

199

answers:

5

I have an array of pointers to a base class, so that I can make those pointers point to (different) subclasses of the base class, but still interact with them. (really only a couple of methods which I made virtual and overloaded) I'm wondering if I can avoid using the pointers, and instead just make an array of the base class, but have some way to set the class to the subclass of my choosing. I know there must be something there specifying the class, as it needs to use that to look up the function pointer for virtual methods. By the way, the subclasses all have the same ivars and layout.

Note: the design is actually based on using a template argument instead of a variable, due to performance increases, so really the abstract base class is just the interface for the subclasses, which are all the same except for their compiled code.

Thanks

EDIT: ALL THE SUBCLASSES (and if need be) THE BASE CLASS HAVE THE SAME LAYOUT / SIZE

That aside, the strategy pattern would be good, but it adds a pointer to the class, while what I'm trying to avoid is exactly one deference.

In simpler terms, what I'm trying to do is

class base {
int ivars[100];
public:
virtual char method() = 0;
}

template <char c>
class subclass : base {
public:
  char method() {
    return c;
  }
}

base things[10];
some_special_cast_thingy(things[2], subclass);
printf("%c", things[2].method());

obviously it's far more complex, but as far as the classes / whatever go thats pretty much all I'm looking for. This is probably a language feature, btw.

+2  A: 

Use the pointers and polymorphism, that's what they are there for. There is some overhead but it is miniscule unless you are in an incredibly demanding environment. This gives you a lot of flexibility later on as well to add new subclasses.

Mark M
A: 

you can use an array of pointers. If you want access to parts of the derived classes interface you can use dynamic_cast. http://en.wikipedia.org/wiki/Dynamic_cast

Anatoly Fayngelerin
-1: How are you going to know which derived class to cast to?
quamrana
A: 

An array has to be exact type because the compiler has to be able to calculate the exact position of every element. So you can't have a mixture of subclasses or even treat the array of subclass as array of base class.

Ben Voigt
+1  A: 

This sounds like either the state pattern or strategy pattern.

Separate out the data from the class and have the array contain the data and a pointer to the base class. Set the pointer so it points to a different subclass as needed, but the base class methods take a pointer or reference to the element of the array to access the data. The subclasses implement the base class methods, but otherwise have no data apart from what is passed into the methods.

class Base;

class Data{
  string name;
  int age;
  Base* base;
};

class Base{
  virtual void method1(Data&)=0;
  // other methods
};

class Subclass1: public Base{
  void method1(Data& data){ data.age=0; }
};

vector<Data> dataVector;
Subclass1 subclass1;
Data* someData = ...
someData->base=&subclass1;

someData->base->method1(*someData);
quamrana
Worth noting that if you keep your strategies "pure" then you need only allocate one of them for each strategy class, not one per Data and then just share them around. This is important if the reason you wanted to go from a std::vector<Data*> to a std::vector<Data> was to avoid lots of small allocations.
Michael Anderson
+2  A: 

The issue you're running into has to do with storage allocation. When arrays are allocated, they need to contain storage for all of their elements. Let me give a (highly simplified) example. Say you have classes set up like this:

class Base
{
public:
  int A;
  int B;
}

class ChildOne : Base
{
public:
  int C;
}

class ChildTwo : Base
{
public:
  double C;
}

When you allocate a Base[10], each element in the array will need (on a typical 32-bit system*) 8 bytes of storage: enough to hold two 4-byte ints. However, a ChildOne class needs the 8 bytes of storage of its parent, plus an additional 4 bytes for its member C. A ChildTwo class needs the 8 bytes of its parent, plus an additional 8 bytes for its double C. If you try to push either of these two child classes into an array that was allocated for an 8-byte Base, you'll wind up overflowing your storage.

The reason that arrays of pointers work is that they're constant size (4 bytes each on a 32-bit system), regardless of what they point at. A pointer to a Base is the same as a pointer to a ChildTwo, despite the fact that the latter class is twice the size.

The dynamic_cast operator allows you to perform type-safe downcasting to change the Base* to a ChildTwo*, so it will solve your problem in this particular case.

Alternatively, you can decouple the processing logic from the data storage (the Strategy Pattern), by creating a class layout something like this:

class Data
{
public:
  int A;
  int B;

  Data(HandlerBase* myHandler);
  int DoSomething() { return myHandler->DoSomething(this) }
protected:
  HandlerBase* myHandler;
}

class HandlerBase
{
public:
  virtual int DoSomething(Data* obj) = 0;
}

class ChildHandler : HandlerBase
{
public:
  virtual int DoSomething(Data* obj) { return obj->A; }
}

This pattern would be appropriate in cases where the algorithmic logic of DoSomething may require significant setup or initialization that is common to a large number of objects (and could be handled in the ChildHandler construction), but not universal (and therefore not appropriate for a static member). The data objects then maintain consistent storage and point to the handler process that will be used to perform their operations, passing themselves as a parameter when they need to invoke something. Data objects of this sort have a consistent, predictable size and can be grouped into arrays to preserve referential locality, but still have all of the flexibility of the usual inheritance mechanism.

Note that you're still building what amounts to an array of pointers, however -- they're just nestled another layer deep below the actual array structure.

* For nitpickers: yes, I realize the numbers I gave for storage allocation ignore class headers, vtable information, padding, and a large number of other potential compiler considerations. This wasn't meant to be exhaustive.

Edit Part II: All of the following material is incorrect. I posted it off the top of my head without testing it, and confused the ability to reinterpret_cast two unrelated pointers with the ability to cast two unrelated classes. Mea culpa, and thanks to Charles Bailey for pointing out my gaffe.

The general effect is still possible -- you can forcibly grab an object out of the array and use it as another class -- but it requires taking the object address and forcing a pointer cast to the new object type, which defeats the theoretical purpose of avoiding a pointer dereference. Either way, my original point -- that this is a horrible "optimization" to be trying to make in the first place -- still holds.

Edit: Okay, I think with your latest edits I've figured out what you're trying to do. I'm going to give you a solution here, but please, for the love of all that is holy, swear to me that you will never use this in production code. This is an engineering curiosity, not a good practice.

You seem to be trying to avoid making a pointer dereference (possibly as a performance micro-optimization?) but still want the flexibility of invoking submethods on objects. If you know for certain that your base and derived classes are identical sizes -- and the only way you are going to know this is to examine the physical class layout generated by the compiler, because it can make all kinds of adjustments as it deems necessary, and the spec doesn't give you any guarantees -- then you can use reinterpret_cast to forcibly treat the parent as a child in the array.

class Base
{
public:
  int A;
  int B;

  void DoSomething();
}

class Derived : Base
{
  void DoSomething();
}

void DangerousGames()
{
  // create an array of ten default-constructed Base on the stack
  Base items[10];
  // force the compiler to treat the bits of items[5] as a Derived,
  // and make a ref
  Derived& childItem = reinterpret_cast<Derived>(items[5]);
  // invoke Derived::DoSomething() using the data bits of items[5], 
  // since it has an identical layout
  childItem.DoSomething();
}

This will save you a pointer dereference, and has no performance penalty, because reinterpret_cast is not a runtime cast, it's essentially a compiler override that says, "no matter what you think you know, I know what I'm doing, shut up and do it." The "slight downside" is that it makes your code ultra-fragile, because any change to the layout of Base or Derived, whether initiated by you or the compiler, will cause the whole thing to come crashing down in flames, with what are likely to be extremely subtle and almost impossible to debug undefined behaviors. Again, never use this in production code. Even in the most performance-critical realtime systems, the cost of a pointer dereference is always worth it compared to building what amounts to a hair-trigger nuclear bomb in the middle of your codebase.

Dan Story
In the presence of virtual functions, I don't think this will always do what you want. Derived objects are going to have a different vptr than Base objects, and reinterpret_cast'ing won't change that the objects were originally created as Base. So the underlying class hasn't actually changed as the poster wanted -- if he reinterpret_casts one of his Base to Derived, but later calls a virtual function through a Base class pointer, it will still call the Base version of the function, not Derived.
Joseph Garvin
You're quite right, I wasn't thinking clearly. Changing the functions to non-virtual (shadowing, rather than overriding) will fix that problem. Post corrected.
Dan Story
Charles Bailey
You're quite right. I should know better than to post something like this off the top of my head without testing it first. While you can reinterpret_cast a Base* to a Derived* and produce the effect I was going for, the compiler will not let you cast a Base to a Derived directly. I'm going to edit the post to make it clear I was wrong, but leave the mistake in place since the discussion makes no real sense without it there.
Dan Story