Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Why is the new auto keyword from C++11 or C23 dangerous?

+9
−0

In older C and C++ standards, the auto keyword simply meant automatic storage duration. As in the compiler automatically handles where the variable is stored, typically on the stack or in a register. And it was a pretty useless keyword since it can only be used at local scope, where all variables default to automatic storage duration anyway.

The C++11 committee decided to change the meaning of this keyword so that during declaration, the type is picked based on the initializer(s) provided. For example auto i=0; will result in int because the integer constant 0 is of type int.

As I understand it, the main rationale was to get rid of cumbersome declarations in for loops in particular.

for(auto i = cont.begin(); ...

is admittedly easier for the eye than

for(std::vector<std::string>::iterator i = cont.begin(); ...

However, veteran programmers seem to raise concerns about auto being unsafe. It seems to be a topic where there's plenty of personal opinions as seen over at SO: How much is too much with C++11 auto keyword? Some people just happily encourage "go for it everywhere". Others, including various well-known C++ gurus, speak in favour of using it with caution.

Now C too is adapting the same functionality of auto as C++, as per C23.

What exactly is dangerous with the auto keyword?

History

1 comment thread

The question just assumes the `auto` keyword is "dangerous". Are there examples that are "dangerous" ... (3 comments)

3 answers

+7
−0

The auto feature was indeed mainly meant to solve long cumbersome template container declarations in C++. But when introduced to C23 - where there are no templates let alone template containers - it just ends up as a solution without any problem that it solves.

auto can create new problems just fine, however! And that goes for C and C++ both, although this answer will mainly focus on C where the feature is just about to get introduced. In C++ you can use auto as long as you know what you are doing and it is done with caution.

The only problem that the language committee(s) seem to have considered was backwards compatibility with the previous use of auto. For example C++20 (annex C) about compatibility notes that using auto as a classic storage class specifier when no initializers are present is problematic. But I think that scenario is the least concerning use of auto though. The main problem lies in how it behaves as a new feature.

The problem with the new use of the auto keyword is that the actual type of the initializer is not often obvious. In many cases you won't even know which type you actually ended up with, which is something very important to know. A lot of these problems are caused by well-known design mistakes and old language bugs in C, where adding auto to the pot makes things even worse.

In general, when we write an initializer which is wrong for whatever the reason, we like to be informed by the compiler that we messed up, rather than getting the code silently expected. This is the very reason why horribly dangerous language features like "implicit int" were removed from C ages ago.


Old, well-known language problems in C colliding with new language problems in C23
auto is particularly problematic in C23 because C has not come as far as C++ in correcting old sins of the past. For example auto ch = 'A' will give you a char in C++ but an int in C.

Or when dealing with boolean logic, something like auto a = b && c; will give you a bool in C++ but an int in C. Even if b and c happen to be bool operands.

Similarly, auto ptr = NULL may give you an int rather than a void* in both languages. Both languages supposedly encourage the use of nullptr instead, but there's a whole lot of old code out there using NULL.

Re-writing the old malloc(n * sizeof(*ptr)) trick will also suffer as it can't be written as auto ptr = malloc(n * sizeof(*ptr));

Having some typedef enum { A } a; and then auto x = A; will result in an int and not an a. Where a may be a smaller integer type than int.

Except when you use the new enum feature in C23 and do typedef enum : int8_t { A } a;. Now auto x = A; suddenly results in an a type.


Const/qualifier correctness
Another sin of the past would be that auto ptr = "hello" leads to a char* in C and not a const char* as in C++.

Well we can fix that easily enough, we just write const auto ptr or auto const ptr right? Not quite... Just as in the case of hiding a pointer behind a typedef, we end up with a char* const and not a const char* as was the intention.

So it simply turns out that you can't meaningfully combine auto and const in C. Meaning you can't have auto and const correctness at the same time.


Subtle type rules
auto is particularly nasty when used in low-level programming, together with certain operators, resulting in another type and/or signedness than expected.

Consider something like this:

unsigned int i = 1+1;
i = ~i;
printf("%#x\n", i); // prints 0xfffffffd 
i += 3;
printf("%#x\n", i); // prints 0

That's well-defined code. Now how about auto...

auto i = 1+1;
i = ~i;
printf("%#x\n", i); // undefined behavior, wrong conversion specifier
i += 3;             // undefined behavior, integer overflow
printf("%#x\n", i);

Oops. Well how about this?

auto i = 0xFFFFFFFF;
i = ~i;
printf("%#x\n", i); // well-defined, prints 0
i -= 3;             // well-defined
printf("%#x\n", i); // well-defined, prints 0xfffffffd

A slip of the type used by the initializer can obviously have major consequences and tracking down the root cause of that bug may not be easy.

auto f = true ? 1.0f : 0.0; would be another subtle type promotion rule of C. Here f ends up as double, which might not have been expected.

Something like auto c = a | b; where a and b are bool, char or unsigned short etc will result in c becoming an int in both C and C++ due to integer promotion.

In case of short a = 1; auto b = -a; we might have expected b to also become short and not int.

And so on.


Wrong initializer by mistake
When dealing with more complex declarations like 2D arrays and pointers to them, a simple slip of the finger can silently result in the wrong type.

int arr [2][2];
auto p1 = arr;
auto p2 = *arr;
auto p3 = &arr;

Here p1 is int(*)[2] (array decayed), p2 is int* (array decayed) and p3 is int (*)[2][2] (array did not decay). A simple miss of * or & will lead to a very different type.

Now had we typed out this explicitly like int (*p1)[2] = &arr, then I will get a compiler message informing me that I typed & when I shouldn't have. In case of auto anything goes and the program might compile cleanly, but with a different result.

Also throw type qualifiers into the declaration on top of that and we are guaranteed to have a complete mess if we use auto.


Known problems in C23 The C23 standard notes under the 6.7.10 Type inference chapter that using auto together with anonymous struct/union declarations would cause implementation-defined behavior as the declared variable and its members may end up in the tag namespace, rather than the ordinary namespace as may have been expected.


The (lack of) rationale why auto was added to C23

auto was added as per proposal N3007. The main reason appears to be making C in sync with C++. However, in C++ auto is somewhat handy and actually solves a few problems, as previously mentioned. Whereas the "rationale", if there ever was one, in N3007 boils down to subjective statements like.

However when the definition includes an initializer, it makes sense to derive this type directly from the type of the expression used to initialize the variable.

As we can see from the numerous examples I made above, deriving the type from the initializer does not obviously make sense. At all.

Or worse:

...obvious convenience for programmers who are perhaps too lazy to lookup the type

Oh come on! If they are too lazy for proper engineering they should maybe consider a different career. Maybe their boss ought to help them out with a swift career change even!

Or just maybe they should start using a programming IDE that does this for them, by a single keystroke or a few mouse clicks. Such IDEs become popular in the 1990s, it's hardly a new tool for the average programmer out there.


Recommended usage

In C++, it is recommended to use auto to make long object type declarations readable, where you don't really care about the exact type. Particularly when reaching for an iterator or a returned type from a member function in some verbose template class.

In C, it is not recommended to use auto at all, because it only serves to create problems. It is a poorly researched and poorly implemented feature.

If anyone can actually give a non-subjective example of when it makes sense to use auto to clearly improve everyday C code, I will certainly reconsider.

History

3 comment threads

auto is useful in C for avoiding double-evaluation in macros (6 comments)
C++ actually has an answer to the const pointer problem - you can write `const auto* ptr = &x;` just ... (1 comment)
Note to self: stick to C11. (1 comment)
+4
−0

A pitfall in C++ that I didn't see mentioned in the other answer is that it might give unexpected results with libraries using expression templates.

What are expression templates?

In a nutshell, expression templates are a technique that allows to write efficient numeric code with intuitive notation.

Consider for example a matrix library with straightforward implementation using operator overloading. Then when you write e.g.

Matrix A = B + C + D

where B, C and D are also of type Matrix, what will happen is that B + C will generate a temporary matrix, which then is passed to the second operator+ as first argument, where the second argument is C; this will then generate yet another temporary matrix that is used to initialise A. Now with move semantics, one may actually get rid of the temporary storage (I've not checked if that is actually possible), but the fact remains that the order of accesses will be very cache-unfriendly.

Now one way to solve this is to have instead a function that directly implements the optimal access sequence (and also ensures no temporary storage even without optimisations):

add_three_matrices(A, B, C, D);

however that doesn't give the nice intuitive syntax. Now what expression templates do if that the expression 'A + B + C' does not actually calculate the sum, but creates an object built from templates that represents the expression, and initialising the Matrix A then triggers the actual, optimised code. That is, you can now write

Matrix A = B + C + D

and still get the optimised code.

How does auto affect this?

One might think that

auto A = B + C + D

gives equivalent code to the one above, but that is not the case. Instead auto is determined to be the expression template type describing the operation. This is particularly bad if the expression contains some actual temporary that will have been destroyed at the end of the statement; say you are scaling D with a double returned from a function:

auto A = B + C + D*f(x)

The return value of f is bound to a reference inside the expression, but since that reference is not A, but some reference inside the expression, it won't extend the life time of the temporary. So if A is ever used later (in a way that actually triggers the calculation), it will access a dangling reference.

History

0 comment threads

+0
−1

I am not going to answer the topic question directly, because I find it rather disingenuous. It’s not just leading; it seems to already have a conclusion in mind.

I am also not going to consider the C side of things. While I don’t really see the logic in being fanatical about knowing the types in a language that barely qualifies as type-safe, C just isn’t my beat, so I’ll leave that discussion to the C people.

So the topic I am going to address is: Is C++’s auto type deduction dangerous, and if so, how?

Before we can discuss if, why, and when auto is dangerous, we first have to understand why auto was standardized in the first place.

And before we can do that, we have to clear up a misconception:

As I understand it, the main rationale was to get rid of cumbersome declarations in for loops in particular.

No, that would be absurd.

Sure, avoiding long type names is one of the handy ways auto is useful, but it would be pretty silly to make a new keyword and specify the rule set for a whole full-scale deduction facility for just that.

The example in the original question is:

for(auto i = cont.begin(); ...

… versus…:

for(std::vector<std::string>::iterator i = cont.begin(); ...

But you could also just make an alias using it = std::vector<std::string>::iterator;, then do this:

for(it i = cont.begin(); ...

… which is even shorter than auto.

No, auto is not just a convenience to save some typing. When auto was first standardized, there was a lot of FUD about auto going around, and one of the turnkey arguments was that auto was something that only lazy, careless coders would use, because good coders would always know the type, and would always specify it for “clarity” or “safety”. We’ll see why all of that is nonsense shortly, but this is where the idea that auto is just about avoiding long types really found its legs. It’s also an easy thing to sell to newbies; it seems plausible on the surface, until you really think about it (or discover that type aliases exist ).

So why was auto really standardized?

Why auto was standardized

That which cannot be named

Consider lambdas.

Every lambda is a distinct object, with a distinct type. This has to be the case, because every lambda (theoretically) does something different. They must be different things.

The name of a lambda’s type is compiler-defined. What else could it be? How could you define what the name of [] { return true; } is? If you try to name it by its file/line/function, that could change any time the code is edited.

Try to design the language yourself. If a lambda is defined as [] { return true; }, then:

/* what would you put here? */ func = [] { return true; };

You could argue that func is a function returning bool, so you could use a function pointer type like bool (*)(). And sure, yeah, you could do that… except now func is a pointer to a function, which means the call is now indirect. It has to be that way, because you could do:

using f_ptr = bool (*)();

f_ptr f = [] { return true; };

if (something_not_statically_predictable)
    f = [] { return false; };

f();

That’s legal code. Which means that the call to f() has to be indirect. You could argue that this is not a common usage pattern, and in most cases, the compiler could optimize away the indirect call. That’s probably usually true, but it is not always true, and it is certainly not true when passing lambdas to functions… which, really, was the whole point of lambdas.

The only option that maximizes efficiency is for the type of a lambda to be some direct, non-pointer type. So, back to square one: how do you name a lambda?

The answer the C++ committee came up with is: you don’t. The name of a lambda is a compiler-defined internal detail. So if you want to capture the lambda, you need a way to say “whatever the compile decides the type is”:

/* whatever the compile decides the type is */ func = [] { return true; };

Enter: auto.

That is why auto was added to C++.∗ Obviously it’s not the only reason—as we’ll see, auto is incredibly useful.

Is that the only possible way that the problem could have been solved? Well, no. I mean, C++ could have introduced a new keyword like lambda, and said that when you do lambda x = [] { return true; }; lambda y = [] { return false; };, the types of x and y are something-something but not the same. But that just seems more confusing than introducing a general-purpose deduction facility. And don’t forget that C++ already had type deduction; template parameters are deduced, after all.

∗ (I am fudging the history a little bit for simplicity. In fact, language lambdas were only introduced during the C++11 standardization process (the mid- to late-2000s), while auto was something Bjarne Stroustrup was tooling around with in the mid-1980s. For a long time, it was thought that lambdas might be possible using library solutions. The Boost.Lambda library was the state of the art at the time, and it used private, internal types that the user was not really supposed to know the name of. Those are the lambdas Stroustrup was talking about when he brings up lambdas as one of the motivating factors for auto. Language lambdas only came long much later, after auto made them feasible.)

And there are other benefits that come with giving the programmer access to the deduction machinery….

That which is impractical to name

Consider a fairly trivial template:

template<typename T, typename U>
void add(T t, U u)
{
    /* ??? */ v = t + u;
}

Okay, so, what is the type of v?

Impossible? Okay, let me make it simpler!

template<typename T>
void add(T t, T u)
{
    /* ??? */ v = t + u;
}

Now this should be easy, right?

Still no?

I’ll make it even easier! By some means—SFINAE, concepts, whatever—I guarantee that T is one of the standard, signed integer types: that is, it is either signed char, short, int, long, or long long. No extended types. It’s one of those. There are literally only 5 possible instantiations of that template. So this has to be really easy to figure out, right?

And yeah, sure, it’s this:

// T is guaranteed to be signed char, short, int, long, or long long:
template<typename T>
void add(T t, T u)
{
    using type = std::conditional_t<
        std::is_same_v<T, long long>,
        long long,
        std::conditional_t<
            std::is_same_v<T, long>,
            long,
            int
        >
    >;

    type v = t + u;
}

So… auto is unnecessary?

That seems like a tough argument to make, even without considering all the concessions I made to make even that long, nested conditional type definition work.

I mean, yeah, sure, you could not standardize auto, and do this instead:

template<typename T, typename U>
void add(T t, U u)
{
    using type = decltype(t + u);

    type v = t + u;

    // Or just:
    // decltype(t + u) v = t + u;
}

(decltype was originally proposed as typeof, and was a popular extension. typeof would technically be more correct than decltype in the code above, because there is no declaration for t + u, so there can be no declared type. But if you do int& x = /*...*/; auto y = x;, then the type of x in that expression is technically int, even though the declared type of x is int&. Ultimately they standardized decltype but not typeof… although apparently C has now standardized typeof.)

But as you can see, you need the type deduction machinery anyway. And it’s error-prone and silly to require repeating the expression just to get its type.

This was the other main motivation mentioned while standardizing auto.

That which is damned annoying to name

And finally, yes, avoiding long type names was mentioned as a motivation. I checked some of the old standardization documents, and while I didn’t see any mention of loops, container iterators were mentioned (and they were a well-known pain point at the time).

Why you should use auto

Correctness

auto is never wrong.

This is something its detractors never acknowledge, but the whole point of auto is that if the type of some expression is T… then auto will be T. Like… always. That’s the whole point of it.

If you read a lot of C++ code, you will inevitably see people screwing up types. A very common mistake is to put the size of a container in an int (like int size = vec.size();). You can’t make that mistake with auto. Whatever the true, correct type of a vector’s size is, auto size = vec.size(); will always get it right.

Whenever you hear someone say that auto deduced the wrong type, look more carefully at what they are saying. It will always be instead that auto didn’t deduce what they wanted… but it did deduce correctly. In other words, auto was right; they were wrong.

Another thing auto prevents is uninitialized variables.

It is very easy to forget to write an initializer, like int x;. This is especially easy to do in generic code, where, for example, you have assumed T will be some kind of string type, so T s; will make s an empty string… but then someone accidentally instantiates the template with a char const*, and now T s; expands to char const* s;… which is not an empty string, but rather a catastrophe waiting to happen.

If you instead always write auto x = int{}; by default (or auto s = T{} in the generic example), then you will never accidentally have an uninitialized variable. (In the generic example, rather than a strange and unpredictable error, you will get a null pointer violation, which is much less dangerous, and much easier to diagnose.)

(And for those rare cases where you really really do mean to leave a variable uninitialized, not using auto becomes self-documenting of that fact.)

Extensibility

Perhaps auto’s greatest superpower is how easy it makes it to swap out one type for another.

It is extremely rare that you actually care about the precise type of some variable. In fact, I’d say you almost never care.

What you do care about is that whatever type you do get conforms to some kind of interface. For example:

// Given:
constexpr auto is_digit(char) noexcept -> bool;
auto add_byline(std::string_view) -> void;

/* ??? */ name = get_author();

if (std::ranges::any_of(name, is_digit))
    throw invalid_name{};

add_byline("Author: "s + name);

Now, in the code above, does it really matter whether name is a std::string, or a std::string_view, or a std::pmr::string, or string from a third party library? I think not. All that matters is that it is something “string-like”: it has to be some kind of range of characters, and it has to be something that can be added to/with a std::string that then produces a type that is convertible to a string view.

If you really wanted to express that you really want something string-like, you could write an appropriate concept then do string_like auto name = get_author();. Most of the time that isn’t really necessary, though.

When you leave the type unspecified, you allow for potentially replacing the type later. For example, if at some later point it gets noted that all of the potential authors come from a fixed set, rather than returning a string naming the author—which might require allocating a character array—you could return a type that’s really just a handle into a constexpr array of author data, that only acts like a string.

This is not theoretical. I have witnessed several codebases being modernized. Those that embraced auto were usually trivial to update. Going from things like boost::optional or boost::thread to std::optional and std::thread (or std::jthread) were usually completely zero-effort. Even going from raw pointers to smart pointers was usually hassle-free (there is a technique to make this pretty painless). And I’m not alone; there are dozens of talks and articles by people who have overseen massive code update projects who will sing the praises of auto from the mountaintops.

Efficiency

Consider that if you want to acquire some value, if you wrote:

auto value = /* ... */;

You cannot write more efficient code than that.

The best you can do is match the efficiency if you get the type exactly right. If you get the type wrong, you will have to pay some kind of conversion cost. It may be cheap, but it cannot be zero unless optimization really works in your favour, whereas when the types match perfectly you get guaranteed zero-overhead thanks to mandatory elision.

And you have to get the type right not just now, but for all time. If anything changes, that will at least introduce inefficiencies, if not bugs.

The hazards of auto

Is auto dangerous?

No. No, it is not.

Most claims of hazards with auto are either overblown or uninformed.

Are there no dangers that come with auto? No, of course there are dangers that come with auto. No useful tool has no hazards.

Specifically, I can think of three off the top of my head. To be clear, I have heard other “concerns”, but find them all to be misinformed, misguided, or simply uninteresting. These are the only ones (that I can think of at the moment) that I consider real hazards of auto.

List initialization

C++ has far too many ways to initialize variables, for legacy reasons. One of them is list initialization. There are two forms.

This is direct list initialization:

int x{0};

This is copy list initialization:

int y = {0};

It is possible to use auto with both forms:

// DON'T DO THIS:
auto x{0};
auto y = {0};

You may think the types of both x and y are int. You would be wrong. But the really terrible thing is that how wrong you are depends on which version of C++ you are using.

This is a hazard, but how dangerous is it really? On a scale of 1 to 10, I’d give a 2. If you don’t want to be burned by this hazard… just don’t do list initialization. That’s it. Problem solved.

Reference types

Reference types are types like views and non-owning handles. std::string_view is a good example. Reference types don’t hold data, they reference data. Which can cause problems if the data they reference gets destroyed.

Example:

class foo
{
    std::string _name;

public:
    auto get_name() const noexcept -> std::string_view { return _name; }
};

auto f()
{
    auto x = foo{};

    auto name = x.name();

    return name; // uh oh!
}

The class foo holds a string, and for the sake of efficiency, the getter function returns a view of that string. This is all fine, but then that view is captured in a variable, and ultimately returned from the function. Because the function also returns auto, it returns std::string_view… but it is a view of the string in x… which was destroyed when the function exited. That means it is a dangling view.

This is technically not an issue with auto, it is a general issue with reference types, but using auto everywhere does make the problem worse because you can’t see the actual types. You may not be aware of which types of reference types.

There are several solutions, though. The main one is to be cautious when using return type deduction, because it is the very rare case where you are deducing a type that is escaping a scope. In this case, the author of f() obviously wants to return something string-like, and knows—or should know, given that they’re returning a value computed by a local variable—that it can’t be a view. They can either specify a non-view type (like std::string) or constrain the deduced return type to not be a view.

Accidental explicit conversions

There are two types of conversions in C++, implicit and explicit. Generally, implicit conversions are fine and safe, while explicit conversions can be risky or expensive, and so they require the coder to explicitly spell out that they really want the conversion to happen.

An example is converting from a std::string_view to a std::string. This is something you often want to do, but doing that requires allocating a copy of the view’s data. That could fail, and even if it doesn’t, allocation is often expensive. For that reason, converting from a std::string_view to a std::string is an explicit conversion:

std::string_view v = "foo";
std::string s = v; // won't compile

The compiler error alerts you to the dangerous/expensive conversion taking place. If you still really want it, you can just say so explicitly:

std::string_view v = "foo";
std::string s = std::string{v}; // works

Now we introduce auto. Because we want s to be a std::string, we use the “auto to stick” form:

auto v = "foo"sv;
auto s = std::string{v};

The problem with doing this as “the default thing to do” is that you don’t get that compiler error. You get no warning that you are doing an explicit conversion, which may be dangerous or expensive. Yes, you get the conversion you wanted, that you asked for, but you lose the heads-up you would have gotten if you had used the old, non-auto style as your default.

There isn’t really a solution for this. On the other hand, how serious a problem it is is unclear. Nothing will break; everything is perfectly legal, and it’s what you wanted… it’s literally what you asked for. You just don’t get a warning that you were doing something the type designer thought you should think carefully about before doing.

The only practical solution is to not rely on explicit conversion checking, and if a conversion really is going to be dangerous, use a different means of conversion, like a tagged operation:

auto v = "foo"sv;
auto s = std::string{force, v}; // hypothetical only
// instead of "force", could also be something like:
//  * unsafe
//  * expensive

This would arguably be better practice in any case, because it makes the risky operation more visible.

Non-hazards of auto

With respect to the “veteran” programmers that “seem” (weasel word?) to raise concerns about auto being unsafe, I have seen no evidence. The StackOverflow discussion mentioned in the question is mostly just personal preference opinions, some of which strike me as pretty silly (one repeated theme is that people think type x; is fine (it’s not if type is fundamental) but auto x = type{}; is an “abuse” 🤨), but whatever, everyone’s entitled to their opinion. Of the 15 answers, only 4 give any examples of “unsafe” uses of auto. 3 of those are just based on plain ignorance (all 3 are in the bottom half of the answers, one of them being the only one with negative votes). The sole remainder brings up the old expression template “issue” that isn’t really an issue.

Of the answers, here, this one is only about C. None of the issues mentioned there exist in C++.

This one brings up the old expression template “problem”. It’s not really a problem, and never was, but there is some historical validity to the concern.

To understand why, you have to remember that auto was standardized at the same time as rvalue references… and while auto creates this “problem”, rvalue references solves it. However… it took some time for existing C++98/03 libraries to get up to speed with C++11… which meant there was a period where, if you used auto liberally, you might bump up against a library that had not yet been updated with rvalue reference support. Eigen is a famous example.

Legacy libraries aside, the “expression template problem” was never a real problem. It is possible to detect when a function argument is a temporary, and it is even possible to write a single function that does different things depending on whether it has been passed a temporary value as an argument; std::forward is an example of such a function. So all you have to do is write your operator functions to take a copy (via a move, preferably, for efficiency) when passed a temporary, and otherwise just take a reference. Problem solved.

Don’t believe me? Well, let’s look at the allegedly problematic code from the answer:

auto A = B + C + D*f(x);

Now look at something one might write using the standard ranges library:

using namespace std::views;
auto r = vec | reverse | transform(f) | take(3);

All three of those range adaptors are (range adaptor closure) objects whose call operator returns a temporary view object; the only non-temporary object is vec. The result of the expression is an object that lazily evaluates the operations in the adaptor chain… basically, an “expression template”.

Yet nothing dangles.

We could go even further, and make the source data a temporary, too!

auto r = get_data() | reverse | transform(f) | take(3);

Everything there is a temporary. Still, nothing dangles.

You can traverse r multiple times. You can even return it from a function. All perfectly safe. No dangling.

So this has never really been a problem. Or at least, it has never been a problem with auto. If your library produces dangling expression templates, then the problem is in your library. The ranges library proves the problem can be solved.

So should you use auto?

Yes. You should.

In fact, I prefer the “always auto” rule… formerly the “almost always auto” rule, or AAA, but the “almost” is unnecessary since C++17. Not everyone prefers it, but that’s fine. Even those that don’t like “always auto” still recommend “almost always auto”, or something close enough to it.

In fact, the virtually universal recommendation across C++ experts can be generally stated as: you should always use auto unless it makes code less clear. The only variation in opinion is precisely what would be “less clear”. We could quibble about that, but that would obscure the fact that the consensus is that auto should be the default choice “unless ____”… that is, it should be the default choice.

The other fact that is easily missed by quibbling is that the concern raised is clarity… not “danger”. auto just isn’t dangerous. Again, yes, yes, of course, there are hazards associated with auto—there are tautologically always hazards associated with any tool that does anything useful—but on the whole the hazards aren’t really that dangerous, and are mostly easily avoided.

The question mentions “veteran” programmers concerned (seemingly?) about auto being unsafe, but neglects to name anyone (or list any concerns for that matter). On the other hand, I am going to name names:

  • Bjarne Stroustrup, inventor of the language, advocates using auto by default (unless you have a “specific reason”).
  • Herb Sutter, very famous C++ expert and former chair of the C++ standard committee, is the inventor and primary advocate of “almost always auto” (and, frankly, though he characteristically hedges with “almost”, really advocates something much closer to “always”).
  • Guy Davidson, current C++ committee chair, advocates “almost always auto”.

I would be honestly impressed if anyone could name a widely recognized C++ expert who didn’t advocate for at least “prefer auto by default”.

The C++ Core Guidelines recommends using auto (see ES.11: Use auto to avoid redundant repetition of type names, but, really, just look at the guidelines document; it uses auto throughout). Even the most restrictive coding standards recommend auto these days. (I’m thinking specifically of MISRA or AUTOSAR, which, as I recall, only bans auto for return type deduction and (for some strange reason that I never understood) deducing fundamental types 🤷🏼.)

And to be clear, I have only considered the use of auto for type deduction in variable declarations in C++; I have no interest in C. If auto in C is a terrible, dangerous thing… well, that’s someone else’s problem; it is not my concern here.

So, in summary:

  • auto is not unsafe, or dangerous.
  • auto’s few hazards are not that serious, and mostly easily avoidable.
  • auto is not just for being lazy about spelling out types.
  • auto improves correctness.
  • auto improves extensibility.
  • auto improves efficiency.
  • These days, auto by default is by far the most common style of C++.
  • Most C++ experts advocate at least auto by default, if not straight-up “almost always auto

Whether you use auto “always” or just ‘most of the time” is a style choice, but these days, those two options are really the only two style choices you will find in general use.

History

2 comment threads

Rationale for changing auto (3 comments)
No you could use "using" like that at all... (5 comments)

Sign up to answer this question »