简体   繁体   中英

C++ Inheritance/Class Design Issue

My objective for a given project is to look for and parse a particular serial packet. The good news is that there is a generic packet class already written that handles most of the heavy lifting. However, I'd like to improve on the performance of the class as is shown below. Please excuse me if some of the syntax is slightly off, I've never been good at remembering C++ syntax from memory... :(

class GenericPacket {
 public:
  GenericPacket();  // does nothing except initialize member variables
  ~GenericPacket();
  GenericPacket(const GenericPacket& other);
  GenericPacket& operator=(const GenericPacket& other);
  GenericPacket(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock);
  Parse(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock);
  // "get" functions go here...
  //  ... 

 protected:
  // the functions below are called by Parse()
  ParseHeader(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock);
  ParseData(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock);
  ParseCheckSum(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock);

 private:
  // member variables go here...

The class's basic function is to read in a data stream that has been queued and process it into the various components of the packet. It also checks the packet it stripped out of the queue for validity. There are also two more constructors and corresponding "Parse" functions that are associated with cases where the caller isn't modifying the queue during construction and also another version that uses a simple array instead of a queue. Both of these are just wrappers around the Parse() function call shown above. Also, note that the caller can either use the default constructor and call Parse manually, or call the non-default constructor which will attempt to make the object "useful" by filling in the member variables with the data from the first parsed packet. Also, note that this class does nothing to decode the data it finds in the ParseData call. It simply stores the raw hexadecimal in a uint8_t array.

Now, with that background info out of the way, I have the current issue of looking for a highly specific packet that accounts for MAYBE 2% of all traffic. Also, I desire to have the ability to decode the data, which will add more member variables. Something like this:

class HighlySpecificPacket {
 public:
  HighlySpecificPacket();
  // non-default constructor that calls parse
  HighlySpecificPacket(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock, int* status);
  ~HighlySpecificPacket ();
  // copy constructor and the like...
  // ...
  Parse(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock, int* status);
  // ...
 private:
  double real_data;
  // etc...

Basically, the Parse() function in the HighlySpecificPacket class can leverage many of the protected functions in GenericPacket. It can use these functions, but stop processing after ParseHeader() when it notes that the packet does not have the signature of the packet I am specifically looking for. Also, int* status can be used to signal back to the caller the number of bytes that can effectively be ignored (thus saving a producer thread from needlessly pushing to the queue). Also, (I should have mentioned this earlier), the protected functions in GenericPacket should and need to be abstracted away from the user since they are always called by GenericPacket's Parse().

Overall, I'm lost as to the best way to move forward. I don't want all of the features of GenericPacket to be exposed to the client (namely, the non-thread safe constructors), yet I can reuse a good deal of code via inheritance due to the protected functions in the base class. I have the ability to modify the GenericPacket class as needed as well. To me this is clearly an "is-a" relationship as well, but I'm lost as to the mechnaisms of how to achieve this. I would like to use private inheritance, but I've been told numerous times that this is bad pratice and only a band-aid fix.

Overall, I am stuck and have the following questions (Please excuse if these are noobish since the last time I actively used inheritance was when I was back in school...):

  1. What's the best way of going about this? If I use composition, I lose access to the functions I want to reuse. But using private inheritance is known to be hacky, especially since I will have to manually write wrapper functions to expose the portions of the base class that I want the client to use (namely "getter" functions)...

  2. Is there a way to prevent dynamic_casts, or to intercept them? Casting from the derived class to the base class makes sense in this case. However, casting from the base class to the derived class should only happen if the header matches the signature for my specific packet.

  3. Would it make sense to include a constructor that takes in the base class as an argument for the derived class? Is there a special name for this? Here's what I'm thinking of: DerivedClass& DerivedClass(const BaseClass& base); Essentially I would check the header signature and then only complete construction in the event that the header signature matched the case of the specific packet.

  4. Now that I opened up a can of worms by asking about casting and constructors from the base to the derived... what about equality/inequality/assignment operators, etc? Do I have to write specific cases of each to check derived vs. base? For example, I can return "true" if all the base class elements were the same when doing something like:

    if (base==derived)

    What about something like:

     derived = base; // take in all elements from base and attempt to construct it as a specific case of the base class 
  5. Do I even need to worry about the copy constructor / assignment operator for the derived class if all it has is doubles/ints/etc. (no pointers/dynamic memory)? The base class has a copy constructor and assignment operator due to dynamically allocated memory. Doesn't the default derived copy constructor just call the base copy constructor?

Thanks for all the help. I know my post has alot to take in, so I appreciate the patience. I'm thinking that I stumbled across the first "real" case for me to use inheritance besides the examples of Shape, Rectangle, Square, Circle, etc. that I learned in school. Thanks again.

Edited to add for clarity:

Here's why I want access to the ParseHeader(), etc. functions:

In GenericPacket:

Parse(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock) {
  ParseHeader(data_stream, the_lock);
  ParseData(data_stream, the_lock);  // generic, only an array of hex
  ParseCRC(data_stream, the_lock);  //determines validity
}

In HighlySpecificPacket:

Parse(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock, int* status) {
  ParseHeader(data_stream, the_lock);
  // do checks here to see if the packet is actually the kind I want
  if (header_ == WHAT_I_WANT) {
    ParseData(data_stream, the_lock);
    ParseCRC(data_stream, the_lock);
  } else {
    *status = header_.packet_length_;  // number of bytes to ignore.
  }
}

Thanks to the excellent discussion in the comments, here's a proposed solution that I am considering implementing that just hit me while I was in the shower (as an aside, anyone else find that the best thinking is done in the shower?):

namespace packets {
ParseHeader(...);
ParseData(...);
ParseCheckSum(...);

class GenericPacket {
  // same stuff as before, except the scope of the "protected" functions...
};

class HighlySpecificPacket {
  // same stuff as before...
  // new stuff:
 public:
  // will probably have to add wrappers here to expose
  //   some GenericPacket member variables...
 private:
  GenericPacket packet;  // composition for all the necessary member variables

I think this is a good way to expose only the things I want to the client, except for the fact that I now have to give them direct access to the ParseHeader/ParseData/ParseChecksum functions which is what I hoping to avoid (and which is why they were originally made protected. Additionally, I could still just use composition if I make these functions public, but my problem remains that I want to let HighlySpecificPacket have access to these functions, without letting the user of HighlySpecificPacket have access to them.

The pros of this approach is that it would alleviate alot of the dynamic_cast and equating one type to another issues that I was having...

class GenericPacket
{
public:
    static std::shared_ptr<GenericPacket> fromStream(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock)
    {
         // extract the header and return std::make_shared<MySpecificPacket>(...) where you've chosen MySpecificPacket accordingly
    }

    bool parse(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock)
    {
        return this->parse_(data_stream, the_lock);
    }

protected:
    // override this method for specific implementations of parse, can also be made abstract if there is no default behaviour
    virtual bool parse_(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock);
};

class MySpecificPacket: public GenericPacket
{
protected:
    bool parse_(std::queue<uint8_t>* data_stream, pthread_mutex_t* the_lock)
    {
         // do something specific, including decoding the data if you need to
    }
};

And how a client (producer) class uses this:

std::shared<GenericPacket> packet = GenericPacket::fromStream(stream, lock);
if (packet->parse(stream, lock))
{
    // push onto event queue...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM