2

Some context: (Feel free to skip ahead) I have a module that processes complex data, but only has to know some semantics of it. The data can be considered a packet: The module should only reason about the opaque payload-string, but it will eventually pass the whole thing to a guy who needs more information. However, it has to ... "bundle" packets regarding some unknown packet information, so I came up with this:

struct PacketInfo {
  virtual void operator==(PacketInfo const&) const = 0;
  virtual void operator<(PacketInfo const&) const = 0;
  virtual ~PacketInfo() {}
};

class Processor {
  private:
    template <typename T> struct pless {
      bool operator()(T const* a, T const* b) const {
        assert(a && b);
        return *a < *b;
      }
    };
    // this is where the party takes place:
    std::map<PacketInfo const*,X,pless<PacketInfo> > packets;
  public:
    void addPacket(PacketInfo const*,X const&);
};

Now, the idea is, that the user implements his PacketInfo semantics and passes that through my class. For instance:
(please read carefully the end of the question before answering)

struct CustomInfo : public PacketInfo {
  uint32_t source;
  uint32_t dest;
  void operator==(PacketInfo const& b) const {
    return static_cast<CustomInfo const&>(b).dest == dest
    && static_cast<CustomInfo const&>(b).source == source;
  }
  // operator< analogous
};

At the point where I use a static_cast most people would use dynamic_cast but rtti is deactivated as a project-policy. Of course I can home brew my own type information, and I have done this before, but that is not the question here.

The question is: How can I get what I want (i.e. having a map key without knowing its content) without sacrificing type safety, that is, without casting at all? I would very much like to keep the Processor class a non-template type.

bitmask
  • 32,434
  • 14
  • 99
  • 159

3 Answers3

2

You can't. You either know the types at compile time, or check them at run time. There's no silver bullet.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
1

The answer in full generality should involve double dispatch. The idea is that if you have n different subclasses of PacketInfo, you need n * (n - 1) / 2 implementations of the comparison operator. Indeed, what happens if you compare a CustomInfo with a AwesomePersonalInfo ? This involves knowing the entire hierarchy ahead of time and sample code is presented in this SO question.

If you are certain you can enforce a map with identical types inside (and therefore you are certain you only need n operator implementations), then there is no point in having a map<PacketInfo, X>. Just use map<ConcretePacketInfo, X>.

There are several ways to do this. The simplest thing to do here is to template Processor on the packet type, possibly making it inherit from a BasicProcessor class if you want to "erase" the template parameter somewhere and factor common code.

Another cheap solution is the following: keep the code as is, but make Processor a template which only defines the relevant addPacket:

class BasicProcessor
{
private:
    template <typename T> struct pless 
    {
        bool operator()(T const* a, T const* b) const 
        {
            assert(a && b);
            return *a < *b;
        }
    };

protected:
    std::map<PacketInfo const*, X, pless<PacketInfo>> packets;
};

// You only need these lines in a public header file.
template <typename Packet>
class Processor : public BasicProcessor
{
public:
     void addPacket(Packet const* p, X const& x)
     {
         this->packets[p] = x;
     }
};

This ensures that the caller will manipulate a Processor<CustomPacket> object and only add the correct packet type. The Processor class has to be a template class in my opinion.

This method goes by the name of Thin Template Idiom, where the underlying implementation is not type safe (to avoid code bloat relative to templates) but you add a thin layer of templates to restore type safety at the interface level.

Community
  • 1
  • 1
Alexandre C.
  • 55,948
  • 11
  • 128
  • 197
  • First of all `PacketInfoChild1` does not have to compare to `PacketInfoChild2`. The map should always have only keys of the same most derived type. Second: `Processor` has no clue what a `ConcretePacketInfo` might look like, and that would have nothing to do there. ... I'm pretty concerned with module separation ... – bitmask Oct 28 '11 at 15:53
  • @bitmask: you can factor the common code to all template processors in a base class, or have a template mediating object presenting base pointers to the processor class and keeping the map as a private object. The point is that the map should handle the specific types for which comparison makes sense. The `addPacket` method should be specific too. – Alexandre C. Oct 28 '11 at 15:57
  • But I still would have a large part (almost all of it) of my `Processor` logic in the header. Of course, templating `Processor` trivially solves the problem, but it's a solution I'd like to avoid. – bitmask Oct 28 '11 at 16:04
  • @bitmask: I don't think you should avoid this: this is the type safety you asked for! The client should only be allowed to put a specific type into the processor: templating the processor precisely enables this. You need not change a lot of things to your code: only implement a suitable `addPacket` method into the `Processor` class, leaving all the work you already did into a `BasicProcessor` class. See my updated answer. – Alexandre C. Oct 28 '11 at 16:10
0

The most obvious problem I see is that your operator< and operator== functions aren't const. So you can't call them through a pointer to const or a reference to const. They should be:

virtual voie operator==(PacketInfo const& other) const = 0;
virtual voie operator<(PacketInfo const& other) const = 0;

Also, logically, if you define these two, you should define the other four. The way I'll usually handle this is by defining a polymorphic member function compare, which returns a value <, ==, or > 0, depending on whether its this object is less than, equal to or greater than the other object. This way, the derived classes only have one function to implement.

Also, you definitely need some type of RTTI, or double dispatch, in order to guarantee that both objects have the same type when you're comparing them (and how you order the comparison when they don't).

James Kanze
  • 150,581
  • 18
  • 184
  • 329
  • Sorry I forgot the qualifier when typing the question (fixing it now). I meant it to be `const` as much as you meant `voie` to be `void`. Also, the map does not care about any comparison other than `<` because I told it to use `pless`. The actual point is, that I still have to cast stuff, even if I have RTTI. – bitmask Oct 28 '11 at 16:07
  • @bitmask You still have to cast the stuff, yes. The difference is that `dynamic_cast` is safe; the others aren't. – James Kanze Oct 28 '11 at 16:24