views:

208

answers:

10

I have joined a new group that has coding guidelines that (to me) seem dated.

But just rallying against the machine without valid backup is not going to get me anywhere.
So I am turning to SO to see if we can up with rational reasons for/against (hey I may be wrong in my option so both sides of the argument would be appreciated).

The guideline that is up for argument is:

Tip: Use pointers instead of references for return arguments.

void Func1( CFoo &Return );  // bad  
void Func2( CFoo *pReturn ); // good  

Justification:
When you use a reference, it looks the same as a value. The caller may be surprised that his value has been changed after calling the function. The callee may innocently modify the value without meaning to affect the caller's value. By using a pointer, it is clear to both the caller and callee that the value can be changed. Using references can be particularly misleading in code reviews.

+3  A: 

While I wouldn't use the tip's advice myself, the justification is valid, which is why languages like C# introduced the out and ref keywords for use at the call site.

The best argument I can come up for against it is this: instead of requiring people to use pointers, you should instead require that people write function names that reflect what the function does. When I call std::swap, I know it's going to change the value of the arguments because the name implies that. On the other hand, if I were to call a function getSize, I wouldn't expect that to modify any arguments.

Peter Alexander
+6  A: 

When you use a reference, it looks the same as a value.

Only if you really aren't paying attention to what you are doing. Ok, sometimes that happens, but really... no amount of coding standards can correct for people not paying attention or not knowing what they are doing.

The caller may be surprised that his value has been changed after calling the function.

If you are surprised by what happens when you call a function, then the function is poorly documented.

Given a function's name, its parameter list, and perhaps some very brief and descriptive documentation, it should be eminently clear what the function does and what its observable side effects are (including whether any arguments are modified).

The callee may innocently modify the value without meaning to affect the caller's value.

If the function is const correct, then this isn't a problem. If the function isn't const correct, then it should be made const correct, if you can (retroactively making code const correct can be an absolute beating).

This rationale doesn't make much sense, though: when you are actually writing the code for a function, you should be able to see the declarations of the parameters. If the function is so long that you can't, it's time for refactoring.

By using a pointer, it is clear to both the caller and callee that the value can be changed.

This is not entirely correct. A function can take a pointer to const object, in which case the object cannot be changed.

Using references can be particularly misleading in code reviews.

Only if the people doing the code reviews don't know what they are doing.


All of that is well and good, but why should pass-by-reference be used instead of pass-by-pointer? The most obvious reason is that a reference cannot be null.

In a function that takes a pointer, you have to check that the pointer is not null before you use it, at least with a debug assertion. During a proper code review you have to analyze more code to be sure that you don't accidentally pass a null pointer to a function that doesn't expect one. I've found that it takes much longer to review functions that take pointer arguments for this very reason; it's so much easier to get it wrong when using pointers.

James McNellis
One benefit for using references is that the caller can't pass in a null value (C++ references can't be null unless the caller intentionally hacks around it), so a null pointer check before assignment is unnecessary for out parameters.
In silico
You also need to analyze code paths to make sure you're not passing a dereferenced null pointer to a function taking a reference, so you haven't gained much.
Mark Ransom
@Mark Ransom: That's not true. Pointers need to be checked before you de-reference them. But once you have a valid reference it is not going to become invalid. So if you use references consistently (and not pointers you can;t get into that situation).
Martin York
@Insilico: And that hack results in UB, so they get what they deserve.
Roger Pate
@Martin, theoretically you're right. But in practice you're going to have a pointer, and you're going to want to call a function that takes a reference. You still need to check for a null pointer, but now you do it at the calling site rather than the function. I don't see how this makes the code analysis easier. I only bring this up because it bit me once, and it was a bugger to track down because of that implicit assumption that a reference is always OK.
Mark Ransom
@Mark Ransom: The assumption that references can't be null is part of the contract of the function. If the caller violates that, don't be surprised that it doesn't work. The same goes for pointers, but the advantage with references is that its non-null requirement is codified into the language itself. With pointers, it may not be immediately obvious if a null value is allowed or not. In my experience, I very rarely have to dereference a pointer to obtain a reference anyway.
In silico
@In silico, you're talking theory and I'm talking practice. It's much harder to debug a bad reference than a bad pointer. And sometimes it's useful to put in some debug checks to see that the contract is adhered to; easy with pointers, near impossible with references.
Mark Ransom
@Martin, @In silico: Mark is absolutely right here. You're just moving the onus of null-checking from the library function to every single call site. Clearly this violates DRY.
Ben Voigt
@Ben: It would violate the DRY rule if using pointers was the prevalent way of doing things (but its not). Passing pointers is now the exception to the rule, so when you do use them you need to check your usage. Its like an external facing API versus an internal facing API. External inputs need to be validated, but once you are on the inside the library you no longer need to check. By narrowed the external API to the very small case of pointers we have actually reduced the repeated code to only a small number of places (where pointers are passed around) rather than everywhere.
Martin York
[Also, as a general note, I'm using _you_ in the general sense, not to mean _you_ specifically, @Martin]
James McNellis
@James: Did not even notice. I suppose if English had a third person possessive pronoun that worked, but `they` does not fell correct.
Martin York
+3  A: 

It seems to me that the proper use of const would (mostly) eliminate the need for that tip. The part that still seems useful is when reading caller code, seeing:

Func1(x);

it isn't quite clear what is being done with x (particularly with a nondescript name like Func1). Instead using:

Func2(&x);

with the above convention, indicates to the caller that they should expect x to be modified.

Greg Hewgill
+1 for common sense!
fbrereto
The use of pointers also implies it can be `NULL`, requiring extra checking on the side of the function.
fbrereto
Actually there is a separate tip that says passing in parameters by const reference is OK (and recommended to avoid the cost of copy associated with value parameters).
Martin York
@fbrerto - Sometimes passing NULL is an _advantage_ of these methods. Sometimes you don't always care for the return value and passing NULL is a nice way to let the function know that you don't need it.
Michael Anderson
+1  A: 

The justification is logically true.
It may surprise coders that the value has changed (because they thought the value was being passed by value).

But does logically true provide any meaning in this context.
So the value may change. How does this affect the correctness of the code?
Apart from it may print out a different value then an illogical human expects, but the code is doing what it is supposed to be doing and the compiler is enforcing constraints.

Martin York
A: 

I would disagree with this guideline. The confusion mentioned in the justification can be easily resolved by making sure the code is const-correct. If you are passing an input parameter to a function by reference, then it should be a const reference. If the reference is not const, that is an indication that it is an output parameter, whose value may be changed by the function.

Furthermore, when you pass a pointer to a function, rather than a reference, that instantly raises a question about whether or not this is a pointer to dynamically allocated memory, and whether or not it should be freed. Using a reference removes the temptation to call delete.

There are times when passing a pointer is appropriate, such as when it actually is a pointer to a dynamically allocated object or array, or when it makes sense for it to be null. Although, you should prefer a smart pointer in such cases. In all other cases a reference is better, IMHO.

Dima
It's no easier to tell the difference between pass-by-const-reference and pass-by-reference than between pass-by-value and pass-by-reference. Neither can be done by looking at the caller code (with the obvious exception of rvalues).
Ben Voigt
You tell the difference by looking at the function signature in the header which is something you typically do anyway. Or your IDE will tell you what the parameters to the functions are.
Dima
If you still see this as a problem, then you have the same problem with passing by pointer. The argument may be a `const` pointer, and thus not an output argument. How would you tell the difference?
Dima
+1  A: 

i recommend:

  • pass by reference (do not pass by pointer)
  • pass by const reference wherever possible (assuming you've used const correctly throughout your codebase)
  • place arguments/parameters which mutate at the beginning of the list
  • label the function appropriately
  • label the argument appropriately (and create methods/functions with detailed and descriptive names and few arguments)
  • document the result
  • if multiple arguments/parameters mutate, consider creating a simple class which holds these arguments (even if by reference themselves)
  • if they still can't function (sic) without visual and documented cues, create a lightweight template container object for the parameter which mutates, which is then passed to the method or function
Justin
A list of I recommends is not going to cut it. I need to have justification for each recommendation otherwise they are just opinions and the other side has equally valid opinions.
Martin York
+3  A: 

Coding standards are based on habits as much as common sense. Some of your coworkers may rely on years of ingrained assumptions that a parameter not passed by pointer won't change - have pity on them.

The important part of coding standards is not that they're optimal, but that they're adhered to by everybody so that there's some consistency to the body of code.

Mark Ransom
I agree that coding standard are good and need to be followed. But I think that all standards should be a living breathing document (not a stagnating beast). We must rage against the machine and put forward our best arguments. Ultimately we need to to follow the party line (because to do otherwise would cause confusion) but that does not preclude us from trying to influence the party and educate our colleges and bring them forward into the new age we have seen but as yet has not been glimpsed by them.
Martin York
@Martin, it's a judgment call for sure. What do you do with the existing code base when you change the standards?
Mark Ransom
There are always casualties of war. Hopefully the old code base is just that, old and stable. Don;t fix anything that is not broken. But if we never changed thing we would still be programming in C.
Martin York
@Martin, if the code base is old and stable it likely doesn't have multiple people working on it; having a coding standard wouldn't be so critical then.
Mark Ransom
I agree with this answer, at least partially. It took me over a year, a lot of arguing, and at least three separate revisions to get my last team's coding standards to line up with good, modern development practices. One of the best ways to go about making changes to such a standard (in my opinion, of course) is to write code that violates the current standard and then in code review defend why your code is better than the code that you'd have to write if you followed the standard. Obviously you don't want to be an ass and do it all the time, but if you have the opportunity, take it.
James McNellis
+1  A: 

If they really want explicit mention of out parameters at the call site, they should actually require that instead of hacking around it by trying to make pointers mean something they don't. Pointers don't imply modification any more than references do, and it's not uncommon to pass pointers for non-modified objects.

One potential way to express out parameters explicitly:

template<class T>
struct Out {
  explicit Out(T& obj) : base(obj) {}
  T& operator*() { return base; }
  T* operator->() { return &base; }
private:
  T& base;
};
template<class T>
Out<T> out(T& obj) {
  return Out<T>(obj);
}

void f(Out<int> n) {
  ++*n;
}

int main() {
  int n = 3;
  f(out(n));
  cout << n << '\n';
}

And as a temporary measure until they change old code to this, you can make the Out convertible to a pointer and/or reference:

// in class definition
operator T*() { return &base; }
operator T&() { return base; }

// elsewhere
void old(int *p);
void g() {
  int n;
  old(out(n));
}

I went ahead and wrote the various classes required for this, and for in-out parameters, in a way that should degrade nicely. I doubt I'll be using that convention any time soon (in C++, at least), but it'll work for anyone that wants to make call sites explicit.

Roger Pate
+1  A: 

I found there are two schools of though about this:

  • (a) use a pointer to show a parameter may be modified
  • (b) use a pointer if and only if the parameter may be null.

I agree with your motivation for (a): when reading code, you can't know all declarations, even if a mouseover gives you the declaration of the function. Mousing over hundreds of functions in thousands of lines just takes time.

I certainly see a problem here if you mix in and out parameters:

bool GetNext(int index, Type & result);

A call to this fuinction would look like this:

int index = 3;
Type t; 
if (!GetNext(index, t)) 
  throw "Damn!";

In that example, the call itself is fairly obvious, to potentially modify t. But what about index? Maybe GetNext increments the index, so you always get the next item, without the callee needing to keep caller state?

Which usually raises the reply Then the method should be GetNextAndIncrementIndex, or you should use an iterator anyway. I bet these people never had to debug code written by electrical engineers that still think Numerical Recipes is the Holy Grail of programming.

Howver I still tend to (b): simply because the problem can be avoided for new code being written, and "may be null or not" is usually the more common problem.

peterchen
+1  A: 

If you have not already, buy a copy of Herb Sutter and Andrei Alexandrescu's "C++ Coding Standards: 101 Rules, Guidelines and Best Practices." Read it. Recommend it to your co-workers. It's a good base for a local coding style.

In Rule 25, the authors recommend:

"Prefer passing by reference if the argument is required and the function won't store a pointer to it or otherwise affect its ownership. This states that the argument is required and makes the caller responsible for providing a valid object."

"Argument is required" means NULL is not a valid value.

One of the most frequent causes of defects is accidental de-referencing of null pointers. Using references instead of pointers in these cases can eliminate these at compile-time.

So you have a trade-off -- eliminate a frequent source of errors, or ensure understandability of calling code by means other than the function name. I personally lean toward eliminating risk.

Andy Thomas-Cramer