When I have to store primitive data, I prefer std::vector
over
array. When I need to use an object of a class, I prefer to create the
object on the stack and pass it by reference; no new
/delete
, no
smart pointers.
Most C++ developers would agree with you on all these points. Proper containers are safer than primitive arrays, and if you can avoid allocating something on the heap then you should.
There are some instances, however, when you do need pointers. If you can avoid them in your code, then that's great, but they become more difficult to avoid in larger projects.
Scope Extrusion
This is a fancy name for a fairly simple concept. The scope of a function is the body of that function together with all the bodies of functions that it calls, the bodies of functions that they call, and so on. If I declare a local variable in a function, then I know that it will stay alive until that function exits. So I can freely pass it (by reference) to any other functions within that function, and know that I'm doing something safe.
But what if I want to create an object inside my function, store it somewhere and have it persist after the function has exited? If we were writing Java, we would not think twice about writing something like this (for example, in a factory class).
public BigObject createBigObject()
{
BigObject bigObject = new BigObject(param1, param2);
return bigObject;
}
Even after createBigObject()
has run, we want the variable bigObject
still to be alive. Since Java is a garbage-collected language, this is fine.
In C++, if we tried to write
BigObject& createBigObject() const
{
BigObject bigObject(param1, param2);
return bigObject;
}
then the behaviour of the program is undefined. At best, your compiler will warn you that you are returning a reference to a local variable. If you try to run the code, you might find that it works some times, and crashes at others, since the lifetime of bigObject
ends when createBigObject
returns. You need to use dynamic allocation, either with new
or with a smart pointer. E.g.:
BigObject* createBigObject() const
{
BigObject* const bigObject = new BigObject(param1, param2);
return bigObject;
}
If you wanted to, you could return *bigObject
instead and return a BigObject&
here.
Mutable References
References in C++ are immutable: they have to be initialized to refer to some other object when they are created and cannot then be pointed towards some other object. In that respect, a T&
is a bit like a T* const
(and a T const&
is a bit like a T const* const
). Now suppose you are setting up a class and you want to hold a reference to some other object as a member variable in that class. If the other object is all set up when you construct the class then this is fine - you can initialized the reference straight away - but if you have to set things up later in an initialization step, then you'll need to use a pointer so that you can point it at the object only once it's been created.
Polymorphism (sort of)
If you've programmed in Java, you've probably made a lot of use of interface
s. In C++, if you want to do the same thing, you normally need to use pointers (sort of - sometimes you want to use templates instead). In Java, we can write
List<String> list = new ArrayList<String>();
to indicate that we only care that list
is some sort of list - the fact that it is an ArrayList
is an implementation detail. In C++, writing
List<std::string> list = ArrayList<std::string>();
(supposing that List
and ArrayList
existed as types in your code) would try to construct a List
and then set it equal to the ArrayList
, which would be a compile error if List
had pure virtual members, since then it would be impossible to create List
objects. list
would have to be either a List&
reference or a List*
pointer.
Now sometimes you can use references to do polymorphism (as you could in this example), but in such cases you're usually better off doing compile-time polymorphism using templates. In C++ we usually use runtime polymorphism when we want to store the polymorphic objects in some container (e.g., a std::vector
). And this comes back to the previous point: it's impossible to just store a reference on the fly, since references must be initialized when they are created. So we have to store them as pointers.