Syntactic Sugar Is More Than Dessert
Here are three code fragments:
// Fragment 1 shared_ptr<int> p(new int(42)); // Fragment 2 shared_ptr<int> q = make_shared<int>(42); // Fragment 3 auto r = make_shared<int>(42);
Each of these fragments ultimately has the same effect: Allocate an object of type
int on the free store, initialize that object to
42, and create a variable of type
shared_ptr<int> that points to that object. The only real difference between these fragments is in the syntactic sugar that they use.
Fragment 1 is the most straightforward: It explicitly constructs a variable of type
shared_ptr<int> and explicitly initializes it to the address of a newly allocated
int object. Its virtue is in its directness and simplicity. However, it has a hidden pitfall: Suppose we were to rewrite it this way:
// Fragment 1a int* obj = new int(42); shared_ptr<int> p(obj);
Programmers break complicated expressions into simpler ones all the time, so this rewrite should come as no surprise. Unfortunately, once the program is rewritten this way, the variable
obj contains a pointer to the allocated object — which, of course, the
shared_ptr will eventually free. In other words, the obvious simplification of Fragment 1 leads to code that might fail in ways that will be difficult to detect. Indeed, detection would be particularly difficult because the memory occupied by freed objects often appears to be valid until that memory is actually reused.
Fragment 2 avoids Fragment 1a's pitfall through encapsulation: It uses the standard-library
make_shared function template to encapsulate the notion of allocating memory, initializing it, and creating a
shared_ptr bound to that memory. We can think of this encapsulation as a kind of syntactic sugar — but it has another useful property as well: Part of what is encapsulated is the pointer to the dynamically allocated object.
Because of this encapsulation, it is much less likely that a raw pointer will leak out of this fragment by mistake than it was for Fragment 1. On the other hand, we now have a new pitfall: Fragment 2 mentions the type
int in two different places. Whenever the correctness of a piece of code relies on writing the same thing twice, there is the risk that someone might change one of those occurrences without changing the other. This hazard is pervasive enough that software engineers use the phrase DRY (Don't Repeat Yourself) to refer to it.
It won't do to look for two different types in code similar to Fragment 2 either, because it is legitimate to convert a
shared_ptr to a derived class into a
shared_ptr to a base class:
// Fragment 2a shared_ptr<Base> q = make_shared<Derived>();
Now the fact that two different types appear in this declaration might be an error or it might not; and the only way to tell is to understand the rest of the code.
Fragment 3 makes our readers' job easier than Fragment 2, because it states the type
int only once. By using
auto, Fragment 3 makes it clear that we intend the type of
r to be the same as the type returned by
make_shared. Moreover, if we change the call to
make_shared and recompile, we will have changed the type of
r, and the compiler will use this new type to check the type of any code that uses
r. Fragment 3 is probably the easiest of the three to understand, too — which is a nice bonus.
It is tempting to think of language or library features such as
auto as mere syntactic sugar. After all, at least in these simple examples, they do not allow us to do anything that we could not have done without them. However, syntactic sugar is often a form of encapsulation, and encapsulation has advantages beyond mere convenience. In particular, Fragment 3 encapsulates both the raw pointer in Fragment 1 and the potentially duplicated type in Fragment 2, and does so in a way that is easier to follow than either of the originals.