C/C++ Pointer Alignment Style: A Justification

Jason C. McDonald - Feb 9 '19 - - Dev Community

Virtually all coding style issues are a matter of subjective opinion and personal taste. Tabs v. spaces, Allman v. K&R, operator padding v. none; wherever you stand, you almost certainly have a list of justifications for your chosen style, and you can guarantee the other camps do too.

Some languages, like Python, go as far as to define a large swath of standard style conventions (PEP-8). C and C++ have no such official, unified standard. As a result, there are countless permutations of style conventions in those languages. Just look at how vast AStyle's documentation is!

And that's O.K.! Every developer, project, and team is able to find the style conventions that work for them, and everyone wins. Personally, I will even switch between Allman, K&R, and Linux Kernal bracketing styles, depending on the project and my mood (although I only use one style per project).

However, there is one point of C and C++ coding convention I believe is beyond subjective opinion: pointer alignment.

(P.S. Yes, the title is a typesetting joke.)

Pointer Alignment: The Options

In case you need a refresher, there are generally three ways you can align pointer and reference tokens in C and C++:



// LEFT ALIGNMENT
int aVal;
int* aPtr;
int& aRef;
int& someFunc(int byVal, int* byPtr, int& byRef);

// CENTER ALIGNMENT
int aVal;
int * aPtr;
int & aRef;
int & someFunc(int byVal, int * byPtr, int & byRef);

// RIGHT ALIGNMENT
int aVal;
int *aPtr;
int &aRef;
int &someFunc(int byVal, int *byPtr, int &byRef);

// RELATED: Use of the * and & operators:
passByVal(*aPtr);
passByVal(*(aPtr+1));
passByPtr(&aVal);
passByRef(&aVal);


Enter fullscreen mode Exit fullscreen mode

So, should we use left, center, or right alignment? I'll give you a moment to choose a side here.

Final Jeopardy

...Ready?

Left Is Right

I would argue that the left alignment is (almost) always the best option! Naturally, I don't expect you to take my word for it, so here's my logic...

What's Your Type?

Whether or not a variable is a pointer or reference is a property of the type. In other words, this following is illegal.



int aVal = 5;
int *aPtr = aVal + 6;
std::cout << aPtr << std::endl;


Enter fullscreen mode Exit fullscreen mode

I deliberately used the right alignment to demonstrate my point. Can you see how it obfuscates the problem?

error: invalid conversion from 'int' to 'int*'

The variable aPtr is statically typed as a pointer to an int. That's a property of the variable itself, and has to be taken into account in much the same manner as if it were a bool instead of an int. The fact it is a pointer is literally part of the data type of the variable!

Counterpoint: Multiple Declarations

(Thanks to @bluma for pointing me in this direction.)

Advocates of right-alignment do helpfully point out that the following doesn't work like you'd expect:



int* a, b, c;


Enter fullscreen mode Exit fullscreen mode

a is a pointer to an int, while b and c are just integers. To achieve the desired effect, you'd need:



int *a, *b, *c;


Enter fullscreen mode Exit fullscreen mode

This, they say, is an argument for right-alignment. It is one point I'll concede to that camp, however, it's generally considered bad practice to declare multiple variables on one line anyway! Besides that, a would be uninitialized, and you should habitually initialize pointers as nullptr (or at least 0) to prevent undefined behavior from sneaking into your code.

So, while the above example is one argument for right-aligning a pointer, I'd argue you should never have this example alive in your code. Initialize one variable per line. You'll thank yourself later.

What's In A Name?

Let's consider another example:



int *aPtr;
std::cout << *aPtr << std::endl;


Enter fullscreen mode Exit fullscreen mode

Sure, the outcome may look obvious to you, but consider the following people:

  • Someone who hasn't already absorbed the essence of C++ into their very being yet.
  • Yourself at 2 A.M. without sufficient caffeine in your system.
  • Yourself after writing user documentation for four months, when your C++ knowledge has begun leaking out of your ear.

When we right-align a pointer, it makes the asterisk appear to be part of the name. Yes, that's dead obviously a wrong conclusion to draw, but when you're tired or out of practice, brain glitches abound. We can forget that the asterisk is both part of the data type and a unary operator in its own right.

This point is even more obvious with the ampersand (&):



int aVal = 5;
int &aRef = aVal;
std::cout << &aRef << std::endl;


Enter fullscreen mode Exit fullscreen mode

What's that going to print out? Not 5! It will print the address of the variable that aRef holds a reference to.

0x76178327ee0c

Imagine making that mistake somewhere in a messy bit of mathematics, with a few typecasts in just the right place to obscure the compiler error. You could be chasing your tail for hours!

Additionally, consider this:



int &someFunc(int byVal, int *byPtr, int &byRef);


Enter fullscreen mode Exit fullscreen mode

Even if you understand that the function is called someFunc, not &someFunc, you can see how easy it would be to visually "lose" the return type's & (or *) in the mess.

By aligning our pointer and reference symbols to the data type, we visually clarify the difference between its use in a type, and its use as an operator.

What About Center Align?

Great, so there goes one option. Now, what about center alignment?



int aVal = 5;
int * aPtr = & aVal;
int & aRef = aVal;
std::cout << * aPtr << std::endl;
std::cout << & aPtr << std::endl;
std::cout << aRef << std::endl;


Enter fullscreen mode Exit fullscreen mode

Two problems. First, and least important, that's just plain ugly to many developers.

Second, and far more importantly, you've just created a tripping hazard for your brain. Every time you see an * or &, you have to parse the context to determine if it is being used as an operator or part of the data type.

So, you could do this, but you'll be unnecessary creating more mental work for yourself and others. And more mental work means a higher probability of error.

How About A Combination, Then?

The first rule of style is consistency! If your convention is fraught with exceptions to the rule, then it's going to be far harder to maintain.

To see what I mean, try and work out what the rules are for this style:



int aVal;
int *aPtr;
int &aRef;
int& someFunc(int byVal, int *byPtr, int & byRef);

passByVal(* aPtr);
passByPtr(& aVal);
passByRef(& aVal);


Enter fullscreen mode Exit fullscreen mode

Look ridiculous? Actually, I can offer a quick one-liner of my logic for each piece, although I won't sport your intelligence with it. The point is, unless I offered a clear explanation of each rule in my style guide (assuming anyone would read it!), a new developer to my code would have to work all that out for herself, in a much vaster code base than this!

In other words, keep it simple.

Summary

I can hear one guy in the second row now: "That's all well and good, but I've been coding in C and C++ for over twenty years. I know the difference by now."

And I'm sure you do. But coding style isn't about you! Coding style, like most programming conventions, is about other people, your non-optimized self included. The behavior of code should always be made as obvious as possible. I believe I've demonstrated how left-aligned pointers do that.

  • In declarations, align the * or & to the type (left). This clarifies that the data type is a pointer or reference. Allow no exceptions to this rule.
  • The above also lends clarity to the obvious: * or & is never part of the name!
  • When the * or & is serving as an operator, align to its operand (right), e.g. the variable name.
  • BONUS RULE: Don't pad unary operators! This clarifies that they only have one operand - the one connected. (i++ is typically better than i ++).

(If you really, truly, want to pad unary operators, be sure to apply that to & and * as well, but only when they're used as operators!)

Let me demonstrate again how this looks in practice:



int aVal;
int* aPtr;
int& aRef;
int& someFunc(int byVal, int* byPtr, int& byRef);

passByVal(*aPtr);
passByVal(*(aPtr+1));
passByPtr(&aVal);
passByRef(&aVal);


Enter fullscreen mode Exit fullscreen mode

I hope I've made a clear argument for left pointer alignment! If I've overlooked some practical angle on this - something more objective than aesthetics - please comment below.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player