Why are you changing declarations everywhere to use auto?

Arrrggg — please stop switching to “auto” everywhere instead of declaring the actual type. It makes things much harder to figure out the type of something so one can see what methods can be used with it. One has to keep right-clicking on the function call on the RHS and find the definition to see what it returns.

3 Likes

You can option-click the function to see the documentation pop-up in xcode. That shows you the return type.

I know – that’s another way — but one would like to be able to read a chunk of code without having to click all over the place just to see what things are.
And one is not always looking at the code with xCode for that matter.

I think the point is to not need to know the type of everything in the function. if you name your variables appropriately, you don’t need to know their types because the names will give you all of the context that you need. Maybe that’s what the juce people are trying to do

auto is a powerful tool in modern c++. ‘Always Auto Everywhere (or Always)’ while contentious, is leaned into by many people (including me). In some ways it’s no different than having to look up the type of a variable when you aren’t near it’s declaration.

I tend to use auto always except for when I type something like
auto isThisAnIntOrALongIntOrALongLongInt = 5
then I prefer int defenitelyAnInt = 5

But when a function returns an int I just type auto id = comboBox.getSelectedId(), it’s just easy to read, It prevents me from getting distracted by type declarations.

1 Like

Yes, I understand that – and I think it’s a mistake in general. I’m a huge fan of strongly typed languages but I think inferring types is misguided. I don’t necessarily need to know the low level implementation of the type but I generally need to know what it is.

What is easier to understand (picking something at random from the JUCE library) when you are not deeply knowledgeable about it

auto data = getRawData()

or

 const uint8* data = getRawData();

where the latter makes it clear that you get access to data that you can’t change, but you can look at the data a byte at a time (as opposed to an integer at a time, for example)?

What is newValue? An integer? A double? An instance of complex number, perhaps?

const auto newValue = denormalise (parameter.getValue());

You have to go and look at denormalise to see what it returns.

auto notes = createRandomNotes();

Am I getting back a MidiBuffer? Or perhaps a MidiMessageSequence?
Or a std::vector<MidiMessage>? Or an Array<MidiMessage>?

When I get something back, I want to know what can I do with it. What operations can be applied to it, etc. That’s completely hidden when you use auto.

Further, if I define an explicit type for the variable on the LHS, I will get an instant compiler error at the right place, if someone changes the return type of the function on the RHS. When you’re developing a library, that kind of early warning is incredibly important. You may need to know that, oops, I can’t change this library function return type.

Of course, it also ignores the fact that the compiler can infer the wrong type.

The only benefit of auto that I can see is laziness.

7 Likes

But you (and anybody else who wants to use that code) have to know that the function returns an integer. Explicitly declaring the LHS to be an int informs the user of that.

Different issue, if your code is too far away from your variable declaration, then your function is probably too long!

This topic was already discussed in length here:

I stated my opinion there, but am happy to reiterate:

auto has more advantages in my opinion:

  • a variable deduced with auto can never be uninitialised
  • when the code is refactored, you avoid unconscious unnecessary implicit casts

And I follow Herb Sutter here, who has more good reasons to prefer almost always auto

7 Likes

I fully understand your arguments against using auto for understanding pieces of code.

I guess another consideration to use auto is to prevent mixing higher level with lower levels of abstraction. As proposed in the clean code book, we should try to not mix too much low with high level code within one function.
For instance, when I am writing low level embedded C++ code with a lot of inline arithmetic operations, it might be more important to know the exact type of each variable. But, when I’m in a function calling a bunch of high level functions dealing with objects, then auto prevent us from getting distracted by object type declarations we don’t necessarily care about.

Yeah, but you also discover what you can and can’t (and shouldn’t change).

Herb is great, I’ve met him a few times - but I disagree with some of his opinions.

(Remember, if we all agreed on everything, most of us wouldn’t be necessary :slight_smile: )

No, the type could be in a header file of your class… or even a publicly accessible member of a class you are using. Anyways, to each his own, I appreciate your frustrations, and I was sharing that I don’t have the same experience as you. I find that auto make my life easier.

Agree completely — until you suddenly find that your code broke because you changed some library function at the other end that now causes your usage to break. If you explicitly declare the type, you get early warnings when something somewhere else changes unexpectedly.

auto x = getSomething()
/// 
/// 20 lines later 
bar(x)

Now, somebody changes the return type of getSomething and now maybe bar will do the right thing with a new type or maybe you’ll get a compile failure when you call bar and it will take you some time before you realize that the problem is due to the return type of getSomething having changed under your feet.

If you declare x with the correct type, you eliminate completely that issue as well as, of course, knowing what kind of object you’re dealing with and what operations can be applied to it.

Look - clearly I’m not going to change anybody’s mind here. But, for all the reasons I mentioned in this and earlier posts, I think that “auto” is a bad idea and I wish JUCE hadn’t adopted it.

2 Likes

Indeed - and that’s also an example where the declaration is far away from usage – all the more reason you should be able to see the type where you are actually using it.

Unless the “correct type” changed, that’s why you refactor. It might have been the correct type at the time of writing, somehow you or your coworker decided, the other type is a better choice. So luckily the compiler will tell you, if it cannot fix it silently.

I often found code like this:

float normalise (float value)
{
    // ...
    float factor = 1 / (max - min);
    // ...

    return (value - min) * factor;
}

If that factor was declared auto, I simply change the function to

double normalise (double value);

I can even template:

template<typename FloatType>
FloatType normalise (FloatType value);

and thanks to auto all works as expected.
But if you are unlucky and missed the float type, you would get an implicit narrowing cast for no good reason.

But like you said, we don’t have to agree :slight_smile:

I struggle to see how would this trigger a compiler error:

SomeClass x = getSomething();    // getSomething has been refactored to return SomeOtherClass
// ...
// 20 lines later
bar(x);

and not the previous example? Even if exchanged for some trivial type, wouldn’t you just get a warning that you’re implicitly casting at the call site instead of at the return point with the right compiler flags?

Here’s a trivial blatantly made up example to demonstrate the point.

Assume that foo is in a library currently under development by someone else in your team.

So here’s the example (Image 1) - note that foo returns a String and bar consumes a string.

Image 1
screenshot_906

Now, change foo so that it returns a Rect object (Image 2). You don’t see the error until you hit the call to bar and you go off on a wild goose chase wondering what’s wrong with bar because you don’t see that foo has changed.

Image 2
screenshot_907

On the other hand, had you explicitly declared the expected type returned by foo (Image 3), then when foo gets changed by your team member, you see immediately the problem - the compiler can give you a decent error message, i.e, oops, I declared a String but foo now returns a Rectangle.

Image 3
screenshot_908

The problem I see in your code examples is the ‘many lines’ part. You shouldn’t be declaring a local so far from its usages, and shouldn’t have long functions as they indeed make the code harder to understand.

2 Likes

In my opinion we all make up code to find a reason for our preferences. But they are preferences. There is little point to debate them. We can fight or embrace our environment we work in.

1 Like