Passive Objects in C++

2.8k views Asked by At

The Google C++ style guide advises using structs for passive objects as opposed to classes. What are some good use cases (with examples) of passive objects ? What are the advantages of using structs (as opposed to classes) for passive objects ?

2

There are 2 answers

0
cehnehdeh On

A passive object is usually an object that won't be interacting with other objects by itself. An example of this would be the mathematical representation for a point:

struct Point
{
    float x, float y;
}

A point won't technically interact with other objects, it is passive.

A non-passive object would be an object that would use other objects to perform tasks, especially with functions. Writing a class that would handle networking I/O should be a class, as it will use many, many other objects to handle sockets, addresses, ect. A quick example from a current program I have:

As far as benefits, having a struct means you won't need to have gettors/settors for your members. Struct members/methods are implicitly public unless explicitly defined so. This means less code, as well as a minor memory improvement. If an object has no functions, a function table will not be created. Not that big, but still an improvement. You also don't have the overhead of function calls.

Classes are the opposite. They are implicitly private, and will only be public if explicitly defined as such. Private data means you need functions to interact with said data. This usually means hiding it behind an interface. Here's my interface for an IRC client I'm currently making:

enum IRC_RESPONSE
{
  RSP_IGNORE = 0,
  RSP_CONTINUE,
  RSP_RET_TRUE,
  RSP_RET_FALSE,
  RSP_BREAK
};

class IRC_Client
{
public:
  //Initialize all sockets and stuff given IP, port, etc
  bool Initialize(const char *address, const int port);

  //login to server, give login info
  bool Login(const char *password, const char *nickname, const char *user);

  //disconnect
  bool Disconnect();

  //main update loop
  void Run();

  //join/leave channel
  void Join(const std::string channel);
  void Part(const std::string channel);

  //main update

private:
  // MEMBERS ////////////////////////////////////////////////////////
  SOCKET ClientSocket_;         //socket of the client
  sockaddr_in *ServerAddress_;  //address of the server
  std::string Nickname_;        //nickname of user

  // METHODS ////////////////////////////////////////////////////////
  //parsing user input to decide what to do
  bool HandleUserInput(std::string message);

  //handling any message from server
  IRC_RESPONSE HandleServerMessage(std::string message);

  //send message
  void SendMessage(std::string message);
  void SendMessage(std::string prefix, std::string command, std::string         parameters, std::string trail);

  // UTILITIES //////////////////////////////////////////////////////
  //pull nickname from prefix
  std::string GetNickname(std::string prefix);

  //parse
  void ParseServerMessage(std::string message, std::string &prefix,     std::string &command, std::string &end, std::vector<std::string> &parameters);

  // USER INPUT /////////////////////////////////////////////////////
  static void *_inputThread(void*);
};

This class is not passive at all! I use structs such as sockaddr_in. I also use some classes like std::string. Making this is class is good, because I hide the implementation for the user to keep life simple.

0
Tony Delroy On

What are some good use cases (with examples) of passive objects?

With a fully Object Oriented, encapsulated user-defined type developers may reasonably expect to be able to freely add, remove, reorder or change the type of private data as long as the client-facing API/functionality is honoured.

Sometimes you don't want such encapsulation, and need to emphasise to developers that that freedom doesn't exist and that instead "clients" at some level - whether elsewhere in the same class (for nested classes/structs) or namespace, implementation file, library, application or IPC-connected "system" are deliberately allowed to couple themselves to the chosen data members.

That might be done because:

  • You want a specific data representation that matches needed encoding when reading or writing memory or streams because there's an agreement with the client(s) about that data members or layout (e.g. the ping protocol, Tokyo Stock Exchange Arrowhead Standard protocol, MS Excel version 7.0 document standard, an assembly language routine you'll call that expects a pointer to specific data, some hardware bus accessed via "memory" read/writes) that's "bigger" (more stable/longer lived, official) than any single data-processing implementation: the possibility of a data change is so extremely unlikely (or impossible - you can't retrospectively change the data format used by an Excel version from yesteryear) and/or fundamental that you'd be prepared to have your program break and have to revisit all uses of the data to fix it.

  • The scope of "client" use is relatively small (e.g. a private class/struct nested in another, or one in an anonymous namespace in an implementation file), such that there's limited rework to be done if the data changes, and there's a net code writing/maintenance/understandability benefit from the simplicity and concision of public data members, and avoiding access specifiers and trivial get/set functions.

An example: a Weighted_Average class that internally kept a list of however-many samples with timestamps for when they were captured - it might reasonably lump an instance of the data type along with say a timeval or std::chrono::time_point into a struct it can use to instantiate a vector or list.

What are the advantages of using structs (as opposed to classes) for passive objects ?

There are minimal functional advantages (the only language difference is in the default access to bases and members, which makes struct more concise if you want such exposed).

The motivation for Google's style guide is clearly expressed therein:

We add our own semantic meanings to each keyword

That's simply saying that they're using struct vs class to encode whether a type is passive (by their definition) or not.


More generally, many coding styles use the choice of class vs struct to encode some vaguely similar aspect of the encapsulation level of the type. Personally, there are lots of cases I'd use struct that aren't explicitly permitted by the Google guide, such as:

  • deriving a concrete observer from an abstract observer while overriding virtual callbacks for various events, where the observer was private to the class it was used in anyway

  • writing simple stateless functors (e.g. Hash, Equal for instantiating std::unordered_map) or predicates

  • private struct Impl; for the pImpl Idiom

Further, I decide based on the dependencies/coupling I'm prepared to let clients have, which is unrelated to whether the struct will have more complex member functions. I see no sound reason for Google's restriction on struct member functions where those functions aren't making pretentions of enforcing post-conditions or invariants that are actually unenforcable given the direct access available to data members. In other words - as long as the struct's data's good after the function call, who cares whether the function did. As a concrete example, if struct timeval { int64_t tv_sec, tv_usec; }; were given a normalise() function that ensured tv_usec was in the [0..1,000,000) microsecond range and sometimes adjusted tv_sec accordingly, well and good: it doesn't interfere with any other use of the data.