Rationale behind C++ member variables declaration

98 views Asked by At

I am still pretty new to c++ and would like to understand the thought process behind creating member variables the way it is. For example, if I go through a header file, I see some of the member variables are declared as pointers while some others are declared as references.

// in foo.h

class Foo {
  private:
    A* const a;
    B& b;
    std::unique_ptr<C> c;
};

What is the difference between declaring (is this even the correct terminology?) a,b & c the way it is? I understand that a is const pointer, i.e., it points to some memory holding an object of type A & b is a reference. c is a unique_ptr holding an object of type C. But I don't understand why you would want to declare something this way. I come from a Java/Python background so please bear with my confusion.

2

There are 2 answers

2
Jeremy Friesner On BEST ANSWER
B& b;

The above is a reference to a B object. As a reference, it can never be NULL, and it cannot be re-targeted to refer to a different object than the one it was originally initialized to refer to. You can also access it via the same syntax that you would use for a "normal" member-variable (e.g. as if it were B b;). Since it is unchangeable, its presence means that you must set it as via an initializer in each of your constructors, otherwise you will get a compile-time error.

A* const a;

The above is a pointer to a read-only A object... or it could be set to NULL, in which case it isn't pointing to anything valid at all. Since its value can be changed at run-time, it does not need to be initialized by your object's constructors (although it is considered good practice to at least initialize it to NULL, so that if any of your code tries to access it before it is set, you'll be more likely to get an obvious/reproducible crash)

std::unique_ptr<C> c;

This is similar to C * c; except better (assuming your object is intended to "own" the C object), because it explicitly indicates the ownership relation and will automatically delete the pointed-to C object when the unique_ptr is deleted.

3
Kuba hasn't forgotten Monica On

would like to understand the thought process behind creating member variables the way it is

Note that this has nothing to do with member variables - the same constraints apply to pointers/references/smart pointers in any scope. I.e. those may well be global variables, automatic local variables in a function, static globals or static locals, etc.

A lot of it has to do with making the code understandable to the human beings. The convention is, usually:

  • a is a non-owning pointer,
  • b is an immutable reference (non-owning) to a single object that must exist as long as the reference exists,
  • c is an owning pointer that manages the lifetime of the pointed-to object.

The choice between a non-owning pointer and a reference boils down to how the pointer/reference is used. If the APIs/libraries used already pass certain objects via object pointer and not by reference, then that's what you'd go with. If you ever need to change what object is referred to, a pointer must be used.

The reference makes it very clear that there is a single object (and not, let's say an array of them, or a nullptr), and it is immutable. That means that once the reference is bound to an object, it will remain bound to that object for as long as the reference exists.

In C++ null references are undefined behavior, so you should never expect a reference to be null. In fact, any tests like Obj& ref; ... if (&ref != nullptr) { do_something(); } will reduce to if (true) { do_something(); }.

A reference makes a clear statement (to a human) that the class Foo requires the object at b to exist throughout Foo's lifetime. Since the reference is immutable, the only chance to set it up is in the constructor. After construction, whatever object it refers to will remain until Foo instance gets destroyed.

It is undefined behavior to end the life of an object with references to it. "Undefined behavior" is a colloquial term for "if you do this, the compiler is allowed to do whatever with your code unless otherwise specified in compiler documentation". A behavior that's undefined in the C++ standard may well still be implementation-defined, but if you depend on that, you're making your code non-portable and dependent on a particular compiler implementation. It's sometimes unavoidable, but generally not a good thing.