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.
Comments on Why is the new auto keyword from C++11 or C23 dangerous?
Parent
Why is the new auto keyword from C++11 or C23 dangerous?
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?
Post
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
autoby 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:
-
autois not unsafe, or dangerous. -
auto’s few hazards are not that serious, and mostly easily avoidable. -
autois not just for being lazy about spelling out types. -
autoimproves correctness. -
autoimproves extensibility. -
autoimproves efficiency. - These days,
autoby default is by far the most common style of C++. - Most C++ experts advocate at least
autoby default, if not straight-up “almost alwaysauto”
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.

1 comment thread