3

I have two classes that are used in a project. One class, Callback, is in charge of holding information from a callback. Another class, UserInfo, is the information that is exposed to the user. Basically, UserInfo was supposed to be a very thin wrapper that reads Callback data and gives it to the user, while also providing some extra stuff.

struct Callback {
  int i;
  float f;
};

struct UserInfo {
  int i;
  float f;

  std::string thekicker;
  void print();
  UserInfo& operator=(const Callback&);
};

The problem is that adding members to Callback requires identical changes in UserInfo, as well as updating operator= and similarly dependent member functions. In order to keep them in sync automatically, I want to do this instead:

struct Callback {
  int i;
  float f;
};

struct UserInfo : Callback{
  std::string thekicker;
  void print();
  UserInfo& operator=(const Callback&);
};

Now UserInfo is guaranteed to have all of the same data members as Callback. The kicker is, in fact, the data member thekicker. There are no virtual destructors declared in Callback, and I believe the other coders want it to stay that way (they feel strongly against the performance penalty for virtual destructors). However, thekicker will be leaked if a UserInfo type is destroyed through a Callback*. It should be noted that it is not intended for UserInfo to ever be used through a Callback* interface, hence why these classes were separate in the first place. On the other hand, having to alter three or more pieces of code in identical ways just to modify one structure feels inelegant and error-prone.

Question: Is there any way to allow UserInfo to inherit Callback publicly (users have to be able to access all of the same information) but disallow assigning a Callback reference to a UserInfo specifically because of the lack of virtual destructor? I suspect this is not possible since it is a fundamental purpose for inheritance in the first place. My second question, is there a way to keep these two classes in sync with each other via some other method? I wanted to make Callback a member of UserInfo instead of a parent class, but I want data members to be directly read with user.i instead of user.call.i.

I think I'm asking for the impossible, but I am constantly surprised at the witchcraft of stackoverflow answers, so I thought I'd ask just to see if there actually was a remedy for this.

Suedocode
  • 2,504
  • 3
  • 23
  • 41
  • 2
    why inheritance when the relationship looks like a containment? – Karoly Horvath Jan 12 '15 at 18:09
  • 1
    Maybe: Make a `Callback` a member of `UserInfo` and provide a conversion operator? –  Jan 12 '15 at 18:12
  • @KarolyHorvath "I wanted to make `Callback` a member of `UserInfo` instead of a parent class, but I want data members to be directly read with `user.i` instead of `user.call.i`." – Suedocode Jan 12 '15 at 18:18
  • Have you *measured* the performance penalty with a virtual destructor? It's going to be tiny. Better yet, when you're not using a pointer or reference to the object, the compiler may omit the virtual lookup entirely. – Mark Ransom Jan 12 '15 at 18:22
  • @MarkRansom If I am to make the destructor virtual, I must first prove via measurements that it has minimal performance impact. I have no doubt that it will not make a difference, but the burden of proof is on me. The thing we are compiling is significantly complicated enough that this is a rather burdensome thing to prove in the many situations it is being used in. – Suedocode Jan 12 '15 at 19:23

4 Answers4

3

You could always enforce the 'can't delete via base class pointer' constraint that you mentioned (to some extent) by making the destructor protected in the base class:

i.e.

// Not deletable unless a derived class or friend is calling the dtor.
struct Callback {
  int i;
  float f;
protected:
  ~Callback() {}
};

// can delete objects of this type:
struct SimpleCallback : public Callback {};


struct UserInfo : public Callback {
  std::string thekicker;
  // ...
};

As others have mentioned, you can delete the assignment operator. For pre-c++11, just make an unimplemented prototype of that function private:

private:
  UserInfo& operator=(const Callback&);
Pete
  • 4,784
  • 26
  • 33
  • Perfect. I can use this. – Suedocode Jan 12 '15 at 19:19
  • This was my first thought, but then it makes destroying the *real* `Callback` objects quite complicated. – Mark B Jan 12 '15 at 19:34
  • I am going to redefine `Callback` to `_Callback`, and then make `struct Callback : _Callback {};` and `struct UserInfo: _Callback {...};`. As long as they don't dig into the header file and actively use `_Callback` (in which case they are *trying* to shoot themselves in the foot), it should have everything I asked for. Correct me if I'm wrong. – Suedocode Jan 12 '15 at 19:49
  • @Aggieboy They won't be able to construct objects of type _Callback on the stack because of the private dtor. So unless they need to use it as a base class then you should be ok. Of course, the other thing you should do is to add a comment in the code regarding the usage of the class and the rationale behind the protected dtor. – Pete Jan 12 '15 at 20:04
  • 1
    The name `_Callback` is reserved for the compiler. Something like `CallbackBase` would be ok though - I assume that attribute changes would only happen in the base class then? – Mark B Jan 12 '15 at 20:04
  • @MarkB Oh woah [I had no idea about that](http://stackoverflow.com/questions/228783/what-are-the-rules-about-using-an-underscore-in-a-c-identifier). Good to point out! – Suedocode Jan 12 '15 at 20:11
  • @Pete I meant something like `_Callback* c = new UserInfo;`, but as I said they would have to be trying to screw themselves at this point. – Suedocode Jan 12 '15 at 20:12
  • 1
    Yes they should be using a smart pointer which would not compile for the _Callback type. And if they are not deleting 'c' then they have other problems. Of course, they could still use placement new and disregard the dtor.. – Pete Jan 12 '15 at 20:18
1
struct UserInfo : Callback {
  ...
  // assignment from Callback disallowed
  UserInfo& operator=(const Callback&) = delete;
  ...
};

Note that the STL features a lot of inheritance without a virtual destructor. The documentation explicitly states that these classes are not designed to be used as base classes.

some examples are vector<>, set<>, map<> ....

Another approach is to consider private inheritance while providing an accessor method to reveal the Callback (in which case you may as well use encapsulation which is cleaner).

Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
0

Yes, there's trickery you can use to keep the members in sync and update operator= automatically. It's ugly though, involving macros and an unusual way of using an include file.

CallBackMembers.h:

MEMBER(int, i)
MEMBER(float, f)

Elsewhere:

struct Callback {
  #define MEMBER(TYPE,NAME) TYPE NAME;
  #include "CallbackMembers.h"
  #undef MEMBER
};

struct UserInfo {
  #define MEMBER(TYPE,NAME) TYPE NAME;
  #include "CallbackMembers.h"
  #undef MEMBER

  std::string thekicker;

  void print();    // you can use the macro trick here too

  UserInfo& operator=(const Callback& rhs)
  {
    #define MEMBER(TYPE,NAME) NAME = rhs.NAME;
    #include "CallbackMembers.h"
    #undef MEMBER
    return *this;
  }
};
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
0

There is no way to meet ALL the criteria you want.

Personally I think your idea to make it a member and then use user.call.i is the best and most clear option. Keep in mind that you write code that uses this just once, but you make up for it in maintainability (since your UserData never has to change) and readability (since it's 100% transparent to the end-use which attribute are part of the callback data and which are auxiliary).

The only other option that might make sense is to use private inheritance instead, and using the attribute or function into UserData. With this you still have to add one using when new data is added to callback, but you get your desired user.i syntax for clients.

Mark B
  • 95,107
  • 10
  • 109
  • 188