Skip to content

Instantly share code, notes, and snippets.

@Eisenwave
Last active September 19, 2024 16:53
Show Gist options
  • Save Eisenwave/5cca27867828743bf50ad95d526f5a6e to your computer and use it in GitHub Desktop.
Save Eisenwave/5cca27867828743bf50ad95d526f5a6e to your computer and use it in GitHub Desktop.
The case against Almost Always `auto` (AAA)

The case against Almost Always auto (AAA)

Introduction

I've been writing C++ for half a decade now, and auto has always been a great source of discomfort to me. Whenever I came back to a past project that makes extensive use of it, I found myself confused, and first had to look at all the types before I could make sense of it.

Similarly, I've worked as an intern at a company that had a AAA policy for its code base. Whenever something didn't work, and I had to debug some code, half the time was spent just looking up types. I was not alone in this struggle; my senior colleagues were wasting their time the same way when debugging the code they wrote.

Were we all too stupid to handle auto, or is there something wrong with it? Well, I haven't been able to articulate my thoughts until now; now I understand the issues with auto, and why it confuses me so much.

This article seeks to raise awareness for the problems auto causes, and makes a reasonable recommendation for when exactly auto can be used.

Motivating Issue - Heavy use of auto can be vaguely intuited but not properly understood

In code bases I've worked on (some of which were mine, some of which were work-related), I frequently dug up auto-based code like this:

void Container::do_things() {
    auto chunk = search_for_chunk();
    add_extra_stuff_to_chunk(chunk);
    use_somehow(std::move(chunk));
}

While you can vaguely intuit what this code does judging by the names involved, it is very difficult to understand the exact semantics.

  • chunk could be a container such as std::vector<ChunkItem>, or it could be a singular item like Chunk. The name chunk isn't really a bad name, but without knowing the type, we know very little about how to use it.
  • Similarly, std::move might be
    • unnecessary if chunk is a trivial type, or
    • it could be crucial for the code to compile, if chunk is of type std::unique_ptr<Chunk>, or
    • it could be performance-critical, such as if chunk is of type std::vector

Let's explore in depth why auto is so problematic.

How auto makes code fragile, vulnerable, and difficult to reason about

The basic issue with auto is that it hides information. This is very useful when the information is redundant, such as in

int x = static_cast<int>(...);
// vs
auto x = static_cast<int>(...);

However, it is way too easy to hide crucial information which would prevent bugs and help us reason about code.

auto hides whether we have any ownership

One negative consequence of auto is that it hides whether we have ownership over the objects involved. Ownership information is crucial to write safe and efficient code.

Consider the following example, which contains a likely bug and a performance issue:

std::string_view a = get_name_1(); // getting a non-owning view into a name
store(a); // DANGER!!, the viewed name might expire while we store it
std::string b = get_name_2();      // copying, or taking ownership of a name
store(b); // OK, but we should probably use std::move instead

At first glance, we can spot a potential bug in store(a), and a possible optimization for store(b). Even with nonsensical variable names like a and b, we can reason about this easily.

auto a = get_name_1(); // owning or non-owning? who knows ...
store(a); // hmmm, whoever wrote this probably knew what they were doing ...
auto b = get_name_2(); // owning or non-owning? who knows ...
store(b); // hmmm, whoever wrote this probably knew what they were doing ...

Without revealing the types involved, we have no idea that this code contains a bug (possibly security vulernability) and a missed optimization. Even if we used more meaningful variable names like name instead of a, this wouldn't have prevented anything.

Essentially, C++ uses types in order to encode ownership (e.g. T* is drastically different from std::unique_ptr<T>) and such information is lost when using auto. Garbage-collected languages like JavaScript can make type inference more comfortable, because you don't have to worry about ownership nearly as much when everything is magically owned by the run-time environment.

auto hides the kind of ownership

Besides hiding whether we own something at all, auto hides the style of ownership. auto thing = get_thing() could mean a lot of things:

  • If thing is a small trivial value, then we don't need to std::move it, and we can use it more freely.
  • If thing is a large value, we need to std::move it and concern ourselves with accidental expensive copies.
  • If thing is a std::optional<Thing>, we have ownership, and we have value semantics, but only if the value is valid.
  • If thing is a std::unique_ptr<Thing>, we have unique ownership, and we must std::move it to avoid errors.
  • If thing is a std::shared_ptr<Thing>, we have shared ownership, and we must make sure not to create circular shared ownership to avoid memory leaks; copying is possible.
  • If thing is a Thing*, then we probably don't have any ownership.

Each possibility implies that we have to use thing in a radically different way. If we misuse it, we either get a compiler error, or we create memory leaks, performance regressions, etc.

Type information is too important in C++ to be erased, in many cases.


Compromise: we can use class template argument deduction CTAD in C++17 to get ownership information without the exact type. For example, std::unique_ptr p = ...;*


Counter-argument: "even when you know the type, ownership may be unclear. For example, T* can be owned or not owned. If so, auto doesn't solve anything."

However, it is possible to avoid these ambiguous ownership schemes. The ownership that a std::unique_ptr has is unambigous. CppCoreGuidelines says I.11 Never transfer ownership by a raw pointer (T*) or reference (T&).

auto hides collection-ness

Roughly, we have "things", and "collections of things". If we diligently used names such as users to refer to Collection<User>, and user to refer to a single User, it would be obvious whether we have a collection, even with auto.

However, collections of things often have a singular name, such as

  • Collection<Fish> becomes school
  • Collection<ChunkItem> becomes chunk
  • Collection<RadioButton> becomes button_group
  • Collection<Number> becomes histogram

With knowledge about the domain, we can infer that a histogram has to be a collection. Intuitively, it's also obvious that button_group must consist of multiple buttons. However, it is not so obvious that school and chunk are containers, and this breaks our expectations.

If we see school.remove(fish), we might assume that the "school removes the fish (somehow??)", not that the "fish is removed from the school (collection)".

If we see chunk.clear(), and chunk is something like an array, this operation could perform a reset of every element inside. This could be a fairly expensive operation, but it looks quite harmless to us.


Counter-argument: "these problems aren't an inevitable outcome of using auto. Rather, they result from the difficulty of naming things."

However, auto exacerbates these problems, and it's unclear whether developers can reliably come up with names so good, that auto becomes obsolete. In the end, this is opinion-based.

auto encourages including type information in variable names, and this is bad

So far, all the mentioned problems can be mitigated somewhat by making our variable names more elaborate. For example, we could write

  • auto name_view for a std::string_view
  • auto name_owned for a std::string.

However, this is a foolish practice because nothing ensures the correctness of the name. If the code is refactored and still compiles (due to std::string having a similar interface as std::string_view), then we end up with a misleading name (std::string name_view). In the end, variable names are just comments, and no comment is better than a misleading comment.

If it's crucial that we are using a std::string_view instead of std::string, we should show the type instead of mirroring it in our variable name. This is robust, and conveys more information.


Compromise: catch these attempts of including type information in variable names during code review. Advice against them in a style guide. auto encourages this bad practice, but it's not an inevitable result of auto.

auto requires us to be much better at naming, and naming is hard

Naming is one of the hardest problems in software development. A good name can help you understand code vastly faster; a bad name can mislead you and waste your time.

Think about how often we name things:

  • We almost never name namespaces.
  • Much more often, we name types (classes, enumerations, etc.).
  • Much more often, we name functions.
  • Much more often, we name parameters and variables.

If we remove type information, then our variable names better be good. Suddenly, the most frequent naming problem becomes the most important one, and any naming mistake has much greater impact.

Type information provides a safety net for bad naming of local variables, such as

  • quasi-useless names like std::vector v = ...;, or
  • outright misleading names like int ptr = ...;,

Counter-argument: "this has nothing to do with auto, and everything to do with naming. These are separate issues."

In a sense, this is correct, but naming is always going to remain a hard problem. To error is human; everyone will come up with bad variable names at some point. auto removes a safety net for when this inevitably happens.

auto hides the type, and a subset of the type is almost always part of the interface

Herb Sutter's article on AAA notes that we should program towards an interface. The exact types are often implementation details, and can be omitted, which is what auto does. For example, we can write auto it = begin(c) without needing to know the exact type of iterator.

This argument has a lot of merit, but auto is only a half-measure. Consider that auto it = begin(c) communicates that we obtain an object, not a reference. To be consistent, we should be writing auto&& or decltype(auto) to completely ignore the type, but AAA advocates typically don't take this next logical step, even when applicable.

In practice, this is a very minor issue, but it demonstrates something important: the exact type may only be an implementation detail, but some portion of the type isn't. The exact type container::iterator is an implementation detail, but we still want begin(c) to give us an object of some iterator type.

The intention of not caring about the exact type, but caring about the type to some extent is more accurately implemented by type aliases than by auto:

// Throughout the project, we don't care whether a quantity is int, long, or some other integer type.
// We decide on this implementation detail only in one place.
using quantity = int;

// We don't care what specific type get_amount() returns, but we do care that it returns an object,
// and this object is of some quantity type.
quantity get_amount();

// Even when refactoring by changing the definition of quantity, this code remains correct.
quantity a = get_amount();

The standard library already follows this appraoch, and so do many other libraries. A proper library will almost never use the fundamental types directly, e.g. use float or double directly. It is more common to see aliases such as float_type.


Counter-argument: "even the alias may be too verbose. How are you going to shorten container::iterator?"

This is true. For begin(c), we expect to get some iterator type, and common sense tells us that this is the iterator type of the the container. Use of auto for iterators is a good, and I recommend it.


Counter-argument: "the alias may not be broad enough. We don't care that the type is container::size_type, we only care about the fact that it's some integer type."

This is also true. There are cases where the properties we care about are a middle ground between

  • auto, which just says that we want some object, whatever that may be
  • a_specific_alias, which may encode more information than we want

We have to decide whether we make our code less specific, or more specific to the type than we actually want. This is a dilemma; the solution is subjective and case-by-case.


Compromise: There is an ugly, but philosophically optimal solution to the dilemma where auto is too little, and the alias is too much.

auto s = end - start; // we want s to be some integral type, but don't care about the exact type
static_assert(std::integral<decltype(s)>); // this encodes intention which was missing from auto
// we could also us a macro such as
TYPE_ASSERT(s, std::integral); // expands to []<std::integral T>(T&) {}(s)

The diagnostics quality is questionable for the macro (see Compiler Explorer).


Counter-argument: "it takes a lot of effort to use proper type aliases throughout a project; especially for integer types. Are we really supposed to have aliases for quantity, index, size, etc.?"

This argument is true; it takes effort. And yes, we really should be using these aliases instead of leaking implementation through direct use of int, float, etc. The granularity of type aliases can vary between projects though. For many projects, it is sufficient to use a single alias using integer = long long;.

auto hides the type, and in some domains, the type is more important than a good name

Consider this mathematical code:

vec3 d = p - q;

We can tell that d is a three-dimensional vector, and due to conventions, it probably represents a "distance" or "difference". Even with poor variable names, we can understand this code.

On the contrary,

auto difference = end - start;

We have much better variable names, but we cannot intuit whether difference is one-dimensional or three-dimensional. The first example is more concise and has worse names, yet it conveys more critical information.

In some domains, type information is too important to omit. Also, if we include type names, we can get away with much worse naming for our variable names. That is a good thing, because naming is hard, and having tremendously more leeway in the required quality of variable names makes our job easier.

Of course, type information isn't a full replacement for variable names. We should have good names and type information:

vec3 difference = end - start;

Note: expressions like p * q are even worse, because they could be scalar/vector/matrix multiplication. Multiplication with * can mean so many things that use of auto in linear algebra can be royally confusing.

auto hides cost

Different operations, even if named the same can have vastly different cost. For example:

  • std::string_view::substr is trivially cheap.
  • std::string::substr might involve dynamic allocations.

Similarly, items.insert(position, Item{}) could be

  • O(1) if items is a std::list.
  • O(n) if items is a std::vector.

It becomes virtually impossible to reason about the costs in our code if we don't know the data structures involved. auto also makes our code vulnerable to performance regressions. For example:

auto str = get_string();
auto all_but_first = str.substr(1);

If get_string() returns std::string_view (and it might at the time of writing), this code is perfectly fine. However, if it later gets changed to std::string, we might see a performance regression. Worse yet, we don't even know that this is the case, because our code still compiles, and likely passes all test cases.

Extremely sophisticated regression testing is required to detect the potential damage of auto.


Counter-argument: "this is not an inherent issue with auto; it's a problem related to std::string and std::string_view having an identical interface."

While this is true, it's also unclear what the alternative is. Conventional names like .substring for a portion of a string are intuitive, and they mean that users only have to memorize one interface, not two. There is no right or wrong here, only trade-offs.

Using explicit types wins the trade-off because we get to use a common and memorable interface, and we don't suffer from unexpected performance issues.

auto hides assumptions about reference/iterator invalidations

To understand this, consider the following code:

auto& container = get_container();
auto& a = container.emplace_back(0);
auto& b = container.emplace_back(1);
use_two(a, b);

At the time of writing, container might be std::deque. std::deque does not invalidate references on emplace_back, so keeping a and b like this is perfectly fine.

However, if it is subsequently changed to std::vector, then this code is no longer correct. If the second emplace_back resizes the std::vector, then a is invalidated and this code contains undefined behavior. Worse yet, it still compiles, we are just not aware that it is broken.

In this way, auto may introduce undetected security vulnerabilities (CWE-416 use after free).


Counter-argument: "auto isn't the underlying problem here. The actual problem is using the same insert() name in the library with different semantics."

However, unless we replace the C++ standard library with one that doesn't have these design issues, we may run into these problems. For many projects, it isn't practical to avoid the standard library, so these issues may as well be problems with the core language.

auto in templates may not be correct

A classic example:

template <typename T>
void process(const std::vector<T> &v) {
    for (auto x : v) {
        if (/* ... */) {
            v = T{}; // reset
        }
        consume(x);
    }
}

This code would fail to compile for the std::vector<bool> specializaton. In the example above, the correct type would have been std::vector<T>::value_type. std::vector<bool>::reference is an object, not a refeence, so auto x doesn't perform a copy in this context.

From my experience, people see this mostly as a design blunder in the standard library, and std::vector<bool> is frowned upon. However, it illustrates that in generic code (especially the unconstrained style that C++ has), static type inference is problematic.

auto may not be the type we expect, even in simple cases

A classic example:

std::uint8_t a = /* ... */, b = /* ... */;
std::uint8_t c = a + b;
auto d         = a + b; // d is int

Integer promotion means that a + b isn't the type we expect, if our goal is to perform 8-bit unsigned addition.

Another example is taken from Herb Sutter's article on AAA:

// Classic C++ declaration order     // Modern C++ style

const char* s = "Hello";             auto s = "Hello";

It may not be obvious to inexperienced developers that the string literal "Hello" decays to a pointer here. auto s is neither an array, nor a std::string. I've seen many new developers get confused by the type of string literals, and by pointer decay.

A good alternative to Herb's code in modern C++ would be:

std::string_view s = "Hello";

If we used auto, we wouldn't have the convenient interface of std::string_view. We would work with a pointer.


Counter-argument: "for string literals, this can be avoided by using "Hello"sv."

This is true, however it's up to the project style guide whether use of these literals is appropriate or not. After all, they are a relatively niche opt-in feature, and may be quite surprising to many readers.

Also, not every type has a literal suffix, e.g. there is no literal for short or signed char. Either this leads to inconsistency (auto x = short{10} vs auto x = 10u), or we need to create additional user-defined literals.

auto x = type{expr} is not a full replacement for type x = expr

Herb Sutter's article on AAA recommends auto x = type{expr} as a modern replacement for the type x = expr style, at least when the user explicitly uses a type, rather than whatever the deduced type is.

Herb Sutter acknowledges that this syntax has its limitations, some of which will likely never be resolved:

  • unsigned char{expr} is ill-formed (uchar{expr} with an extra type alias could be used instead)
  • int[] {expr} is ill-formed (std::array could be used instead)
  • class T{expr} is ill-formed (T{expr} could be used instead)

A major issue is that type{expr} may inadvertently call a std::initializer_list constructor, and this possibility can make it unsuitable for template code where we don't know what constructors type has. Note that std::initializer_list constructors win against the copy constructor in overload resolution.

There are other minor issues. Prior to C++17, type{exp} would not be a case of guaranteed copy elision, and this code may invoke a copy/move constructor. In practice, this is a minor problem because compilers perform optional copy elision in these simple cases.

It is also illegal to write auto m = std::mutex{};, and necessary to fall back to std::mutex m; before C++17. In practice, this is a minor problem because we can make an exception to AAA for immovable types, while using the rule elsewhere. It's only a minor inconsistency.

auto x = type{expr} to force initialization may be counter-productive

Herb Sutter also mentions that preferring auto x = type{expr} is a guideline for developers because it requires the variable to be initialized, and prevents implicit narrowing conversions.

The problem with using this declaration style to always initialize a variable is that it sweeps bugs under the rug. It will initialize numeric types to zero, which prevents compiler diagnostics, static analyzers, or sanitizers from catching mistakes. Some tool could have caught int x; use(x); as a bug, possibly before compilation (during linting). If we sweep the issue under the rug by initializing everything to zero, tooling has no chance at spotting this mistake.


Counter-argument: "tooling isn't perfect, and if, for whatever reason, a use-before-initialized bug slips through the cracks, then the ramifications might be more severe (CWE-457: Use of Uninitialized Variable)."

This is true. In some domains, the damage caused by this bug far outweighs the benefits of catching it more easily through tooling. The project manager has to make an informed trade-off; there is no strictly right/wrong here.

auto x = type{expr} is only a bandaid solution to type conversions; prefer tooling

auto x = type{expr} for the purpose of avoiding type conversions is not a complete solution. It does prevent some of them, but doesn't catch:

  • narrowing conversions in function calls, such as take_float(my_double)
  • sign mismatch in expressions, such as signed + unsigned, signed < unsigned, ...,
  • narrowing conversions in return statements, unless someone commits to writing return {expr}; (but that is unusual and unpopular style).

List-initialization is only a bandaid, not a full solution. To catch a larger set of mistakes, it is inevitable that we need to use additional diagnostics such as -Wconversion for GCC/Clang. This isn't to say that {expr} is a useless, just that the "added safety" argument is extremely weak.

auto x = type(expr) is problematic in its own way

This is one of the major design blunders of C++. type(expr) is not simply construction; it's a function-style cast and allows for more conversions than type x(expr). This can lead to surprising behavior when type is a pointer or reference.

Overall, neither auto x = type{expr} nor auto x = type(expr) are universal replacements for non-auto initialization. The promised consistency is not achievable, and the safety arguments become minor or irrelevant in the face of tooling. Both are more verbose than the non-auto counterpart, and choosing them is largely a stylistic preference.

auto fn() -> type trailing return types are questionable

Herb Sutter also makes the argument that using auto in other places creates consistency with deduced return types or trailing return types. This is true, but do we actually want to use trailing return types?

Even though they seem like a viable replacement for the old-school syntax, there are some caveats:

  1. The overwhelming majority of teaching material does not teach auto main() -> int from the start. Trailing return types violate the principle of least surprise to any newcomers to a code base who don't already practice this style. Teaching/reference materials almost always have to be translated into the trailing return type style first.
  2. auto main() -> int is significantly more verbose than int main(), making it innately unattractive.
  3. Unlike in languages like Rust, there is no distinction between let and fn, there is only auto. This makes auto a noisy and relatively long keyword that conveys little information.
  4. The alleged benefit of aligning function names can also be achieved by breaking the line after the return type.

Counter-argument: "you an always use trailing return types (e.g. in cases involving decltype) where you could use classic return types, but not the other way around. Trailing return types are more consistent."

This is true, but the cases where it makes a difference are very rare. In the vast majority of cases, the return type is simple. Sometimes it is so simple (e.g. int() vs auto() -> int that we more than double the length of the declaration.


Counter-argument: "trailing return types are consistent with lambda expressions."

In some sense, yes, but lambda expressions are only weakly consistent with other language features. The location of constexpr and other keywords is different for lambdas, and they are the only expression that begins with []. () is also optional for lambdas but isn't for functions. Aiming for more consistency with lambdas is a noble (though distant and fully unachievable) goal and auto gets closer to that than classic return types. It's entirely subjective how much weight this goal has.

auto as a parameter creates implicit templates, and this is bad

Note: This is only semi-related to this article, which is mostly focused on static type inference. However, it is worth mentioning auto parameters.

Besides use of auto on local variables, you can use it in function parameters, creating an abbreviated function template. For example:

void foo(std::string_view x) {
    print(x.substring(4)); // typo, we should have written substr
}
void bar(auto x) {
    print(x.substring(4));
}

This produces two errors:

<source>: In function 'void foo(std::string_view)':
<source>:6:13: error: 'using std::string_view = class std::basic_string_view<char>' {aka 'class std::basic_string_view<char>'} has no member named 'substring'; did you mean 'substr'?
    6 |     print(x.substring(4));
      |             ^~~~~~~~~
      |             substr
<source>: In instantiation of 'void bar(auto:11) [with auto:11 = const char*]':
<source>:14:8:   required from here
<source>:10:13: error: request for member 'substring' in 'x', which is of non-class type 'const char*'
   10 |     print(x.substring(4));
      |           ~~^~~~~~~~~

While the second error is shorter, it has two separate locations. A language server would give us an error at the call site of bar, which is misleading: the cause of our mistake is the implementation of bar, not what we call it with. The first error is overall better, and even makes a suggestion related to the typo we've made. It is located only in one place, and describes the issue more clearly.

Besides that, we've also created an unconstrained template that can be called with anything. This is bug-prone. The same problems apply to generic lambdas (to a lesser extent). If a generic lambda is immediately used (e.g. std::some_algorithm(..., [](auto x) { ... });), it's probably okay to use auto.

Strong counter-arguments in favor of auto

auto avoids implicit conversions when refactoring

In all fairness, avoiding auto has some problems too. One issue is seen in this code:

int x = get_amount();

get_amount could have returned int at the time of writing, but what if it's updated? If it's not long get_amount(), there is a narrowing conversion. Or conversely, if x is long and get_amount() returns int, then this requires a sign-extending conversion.

auto solves these issues because we always use the returned type, no implicit conversions performed. I believe that this argument is not strong enough to justify auto though, given that:

  • Consensus among C++ developers is that heavy use of implicit conversions is bad.
  • Safety could be gained by flagging implicit conversions with automatic tools.
  • In a sane type system, implicit conversions should not be costly.
    • For example, there is a conversion std::string -> std::string_view, but not the other way around.

Note: an alternative solution this refactoring dilemma is to use type aliases like quantity as described in the prior sections.

But can't IDEs just show you the type?

IDEs aren't always available

Firstly, this relies on powerful language servers, and such tools aren't always available. For example, we don't have a language server running in the background when looking at code on GitHub, and we would still like to understand it.

IDEs aren't good enough, even when available

Secondly, IDEs aren't good enough at dealing with auto. As an anecdote, QtCreator displays (or used to) std::string as std::__cxx11::__basic_string<char> when revealing the type of a variable. This can be somewhat mitigated through special case (e.g. display std::string as string like in CLion).

However, such solutions don't work for user-defined types. For example, we might define an SI unit system in the type system, and instead of si::km, we might get si_unit<1000, 0, 1, 0, 0, 0, 0>. We cannot make sense of such a type.

C++ has too powerful of a type system as that the IDE would be a viable solution to the problem, even when available.

Even an almighty IDE doesn't solve all problems

Even if we could magically reveal the optimal type on demand in all cases, this doesn't solve all issues. We can still run into performance regressions and security vulnerabilities, as described above.

When to use auto - rule of thumb and examples

I follow this rule of thumb when deciding whether to use auto:

If the information that is hidden by auto could be locally inferred, or doesn't matter, use auto.

Examples

GOOD - Casting

auto x = static_cast<To>(y);

This is a good use of auto. We know exactly what the type is from the right hand side. Repeating ourselves would be bad.

GOOD - Printing a string

auto text = prefix + get_name() + suffix;
print(text);

Even though we don't have a clue about ownership or any types involved, this code is fine. We only print(text), which works regardless of what ownership we have, if any.

We also expect print to accept a view or a const& to a string, so there should be no performance issues, intuitively.

GOOD - Forwarding type information

auto s = sqrt(x);

No matter the type of x, we have the expectation that it's a small, cheap, numeric type. We also understand that a sqrt function should return the same type as the input. It's extremely unlikely that we see any vulnerability or performance regression, so auto is fine here, even if we know nothing about x.


Note: this example is similar to the prior p - q example against auto. auto is acceptable in this case because sqrt(x) conveys that the result is one-dimensional, which cannot be inferred from subtraction.

GOOD - Using iterators

for (auto it = c.begin(); it != c.end(); ) {
    if (...) it = c.erase(it);
    else           ++it;
}

Here, we can locally infer that it is an iterator based on the interface of c. This code is obviously correct, because we know what an iterator is and what interface it has. We don't care about the specific type name container::iterator.

BAD - Appending to a container

void insert_some(int n) {
    auto& c = get_container();
    for (int i = 0; i < n; ++i) {
        c.insert(make(n));
    }
}

Depending on the type of container, this could have vastly different performance impliciations. Also, if the container has a member function for bulk insertion, maybe this code could be improved. A senior developer familiar with the container could spot such an opportunity at first glance, but only if they knew the type of the container.

The type c cannot be locally inferred, and we do care about specifics, so this is a malicious use of auto.

Recommended practice

The rule of thumb I've given above is obviously subjective, and you might question whether useful information is hidden at times. I recommend to choose caution over brevity, when in doubt.

If you're not sure whether useful information is hidden, don't use auto.

@louis-langholtz
Copy link

Caveats

It’s more my interest in this comment to support people and perspectives than to argue for or against arguments on technical concerns like AAA. That said, hopefully my comments herein reflect that interest.

Perhaps debates like these can be incentives in favor of code ownership and coding standards that give more room to the individual developer on whether to use or not use certain things like auto in the new code they’re developing for the team.

Appreciations

As someone who's been doing C++ since the early days of the Cfront compiler and who is also a fan of AAA, I'll still appreciate some things in this gist like:

  • It shows an impressive amount of effort IMO.
  • I'm happy to see the references to resources like the C++ Core Guidelines.
  • I like seeing so many code examples as this has.

Suggestions

  • I'd love to see Herb Sutter's AAA Style web page referenced by this gist.
  • I'd love to see the against AAA positions written as counters to Herb's web page.
  • I'd prefer seeing the point on auto as a parameter creates implicit templates clarify that abbreviated function templates is since C++20.
  • I'd prefer the use of terms like prefer and avoid over terms like good and bad. I'm of the opinion that such a preference favors less judgmental language and that being less judgmental is easier to relate to.
  • Leave out how long you’ve done C++ for. Otherwise it motivates authority by years experience debate which isn’t as compelling as other reasons, at least for me.

Errors?

Is there perhaps an error or two in auto in templates is may not be correct?

The example is:

template <typename T>
void process(const std::vector<T> &v) {
    for (auto x : v) {
        if (/* ... */) {
            v = T{}; // reset
        }
        consume(x);
    }
}

Did you mean to assign to x instead of v, or to take v as a non-const parameter? Also, it seems that v is const qualified prevents compilation, not bool. Did I misunderstand perhaps?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment