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.
Post History
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 t...
#1: Initial revision
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:
```c++
for(auto i = cont.begin(); ...
```
… versus…:
```c++
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:
```c++
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:
```c++
/* 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:
```c++
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”:
```c++
/* 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:
```c++
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!
```c++
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:
```c++
// 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:
```c++
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 **decl**ared **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:
```c++
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](https://en.cppreference.com/cpp/language/list_initialization). There are two forms.
This is **direct list initialization**:
```c++
int x{0};
```
This is **copy list initialization**:
```c++
int y = {0};
```
It is possible to use `auto` with both forms:
```c++
// 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:
```c++
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:
```c++
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:
```c++
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:
```c++
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:
```c++
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](https://software.codidact.com/posts/291981/291982#answer-291982) is only about C. None of the issues mentioned there exist in C++.
[This one](https://software.codidact.com/posts/291981/292401#answer-292401) 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:
```c++
auto A = B + C + D*f(x);
```
Now look at something one might write using the standard ranges library:
```c++
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!
```c++
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](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) recommends using `auto` (see [ES.11: Use auto to avoid redundant repetition of type names](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es11-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.
