GotW #93 Solution: Auto Variables, Part 2

Why prefer declaring variables using auto? Let us count some of the reasons why…

Problem

JG Question

1. In the following code, what actual or potential pitfalls exist in each labeled piece of code? Which of these pitfalls would using auto variable declarations fix, and why or why not?

// (a)
void traverser( const vector<int>& v ) {
    for( vector<int>::iterator i = begin(v); i != end(v); i += 2 )
        // ...
}

// (b)
vector<int> v1(5);
vector<int> v2 = 5;

// (c)
gadget get_gadget();
// ...
widget w = get_gadget();

// (d)
function<void(vector<int>)> get_size
    = [](const vector<int>& x) { return x.size(); };

Guru Question

2. Same question, subtler examples: In the following code, what actual or potential pitfalls exist in each labeled piece of code? Which of these pitfalls would using auto variable declarations fix, and why or why not?

// (a)
widget w;

// (b)
vector<string> v;
int size = v.size();

// (c) x and y are of some built-in integral type
int total = x + y;

// (d) x and y are of some built-in integral type
int diff = x - y;
if(diff < 0) { /*...*/ }

// (e)
int i = f(1,2,3) * 42.0;

Solution

As you worked through these cases, perhaps you noticed a pattern: The cases are mostly very different, but what they have in common is that they illustrate reason after reason motivating why (and how) to use auto to declare variables. Let’s dig in and see.

1. In the following code, what actual or potential pitfalls exist, which would using auto variable declarations fix, and why or why not?

(a) will not compile

// (a)
void traverser( const vector<int>& v ) {
    for( vector<int>::iterator i = begin(v); i != end(v); i += 2 )
        // ...
}

With (a), the most important pitfall is that the code doesn’t compile. Because v is const, you need a const_iterator. The old-school way to fix this is to write const_iterator:

vector<int>::const_iterator i = begin(v)     // ok + requires thinking

However, that requires thinking to remember, “ah, v is a reference to const, I better remember to write const_ in front of its iterator type… and take it off again if I ever change v to be a reference to non-const… and also change the “vector” part of i‘s type if v is some other container type…”

Not that thinking is a bad thing, mind you, but this is really just a tax on your time when the simplest and clearest thing to write is auto:

auto i = begin(v)                           // ok, best

Using auto is not only correct and clear and simpler, but it stays correct if we change the type of the parameter to be non-const or pass some other type of container, such as if we make traverser into a template in the future.

Guideline: Prefer to declare local variables using auto x = expr; when you don’t need to explicitly commit to a type. It is simpler, guarantees that you will use the correct type, and guarantees that the type stays correct under maintenance.

Although our focus is on the variable declaration, there’s another independent bug in the code: The += 2 increment can zoom you off the end of the container. When writing a strided loop, check your iterator increment against end on each increment (best to write it once as a checked_next(i,end) helper that does it for you), or use an indexed loop something like for( auto i = 0L; i < v.size(); i += 2 ) which is more natural to write correctly.

(b) and (c) rely on implicit conversions

// (b)
vector<int> v1(5);     // 1
vector<int> v2 = 5;    // 2

Line 1 performs an explicit conversion and so can call vector‘s explicit constructor that takes an initial size.

Line 2 doesn’t compile because its syntax won’t call an explicit constructor. As we saw in GotW #1, it really means “convert 5 to a temporary vector<int>, then move-construct v2 from that,” so line 2 only works for types where the conversion is not explicit.

Some people view the asymmetry between 1 and 2 as a pitfall, at least conceptually, for several reasons: First, the syntaxes are not quite the same and so learning when to use each can seem like finicky detail. Second, some people like line 2’s syntax better but have to switch to line 1 to get access to explicit constructors. Finally, with this syntax, it’s easy to forget the (5) or = 5 initializer, and then we’re into case 2(a), which we’ll get to in a moment.

If we use auto, we have a single syntax that is always obviously explicit:

auto v2 = vector<int>(5);

Next, case (c) is similar to (b):

// (c)
gadget get_gadget();
// ...
widget w = get_gadget();

This works, assuming that gadget is implicitly convertible to widget, but creates a temporary object. That’s a potential performance pitfall, as the creation of the temporary object is not at all obvious from reading the call site alone in a code review. If we can use a gadget just as well as a widget in this calling code and so don’t explicitly need to commit to the widget type, we could write the following which guarantees there is no implicit conversion because auto always deduces the basic type exactly:

// better, if you don't need an explicit type
auto w = get_gadget();

Guideline: Prefer to declare local variables using auto x = expr; when you don’t need to explicitly commit to a type. It is efficient by default and guarantees that no implicit conversions or temporary objects will occur.

By the way, if you’ve been wondering whether that “=” in auto x = expr; causes a temporary object plus a move or copy, wonder no longer: No, it constructs x directly. (See GotW #1.)

Now, what if we said widget here because we know about the conversion and really do want to deal with a widget? Then writing auto is still more self-documenting:

// better, if you do need to commit to an explicit type
auto w = widget{ get_gadget() };

Guideline: Consider declaring local variables auto x = type{ expr }; when you do want to explicitly commit to a type. It is self-documenting to show that the code is explicitly requesting a conversion.

Note that this last version technically requires a move operation, but compilers are explicitly allowed to elide that and construct w directly—and compilers routinely do that, so there is no performance penalty in practice.

(d) creates an indirection, and commits to a single type

// (d)
function<void(vector<int>)> get_size
    = [](const vector<int>& x) { return x.size(); };

Case (d) has two problems, and auto can help with both of them. (Bonus points if you noticed that a form of “auto” is actually already helping in a third way.)

First, the lambda object is converted to a function<>. That can be appropriate when passing or returning the lambda to a function, but it costs an indirection because function<> has to erase the actual type and create a wrapper around its target to hold it and invoke it. In this case, we appear to be using the lambda locally, and so the correct default way to capture it is using auto, which binds to the exact (compiler-generated and otherwise-unutterable-by-you) type of the lambda and so doesn’t incur an indirection:

// partly improved
auto get_size = [](const vector<int>& x) { return x.size(); };

Guideline: Prefer to use auto name = to name a lambda function object. Use std::function</*…*/> name = only when you need to rebind it to another target or pass it to another function that needs a std::function<>.

Second, the lambda commits to a specific argument type—it only works with vector<int>, and not with vector<double> or set<string> or anything else that is also able to report a .size(). The way to fix that is to write another auto:

// best
auto get_size = [](const auto& x) { return x.size(); };

// yes, you could use this "too cute" variation for slightly less typing
//              [](auto&& x) { return x.size(); };
// but you'll also get less const-enforcement and that isn't a good deal

This still creates just a single object, but with a templated function call operator so that it can be invoked with different types of arguments, and so will work with any type of container that supports calling .size()

Guideline: Prefer to use auto lambda parameter types. They are just as efficient as explicit parameter types, and allow you to call the same lambda with different argument types.

… and did you notice the “third auto” that was there all along? Even in the original example, we’ve been implicitly using automatic type deduction in a third place by allowing the lambda to deduce its return type, and so now with the fully generic “best” version of the code that return type will always be exactly whatever .size() returns for whatever kind of object we’re calling .size() on, which can be different for different argument types. All in all, that’s pretty nifty.

Guideline: Prefer to use implicit return type deduction for lambda functions.

2. Same question, subtler examples: In the following code, what actual or potential pitfalls exist, which would using auto variable declarations fix, and why or why not?

(a) might leave the variable uninitialized.

// (a)
widget w;

This creates an object of type widget. However, we can’t tell just looking at this line whether it’s initialized or contains garbage values. As noted in GotW #1, if widget is a built-in type or aggregate type, its members won’t get initialized. Uninitialized variables should be avoided by default, and only used deliberately in cases where you really want to start with an uninitialized memory region for performance reasons—notably when you have a large object, such as an array, that is expensive to zero-initialize and is immediately going to be overwritten anyway, such as if it’s being used as an “out” parameter.

Guideline: Always initialize variables, except only when you can prove garbage values are okay, typically because you will immediately overwrite the contents.

Would auto help here? Indeed it would:

auto w = widget{};    // guaranteed to be initialized

One of the key benefits of declaring a local variable using auto is that the “=” is required—there’s no way to declare the variable without setting an initial value. Further, this is explicit and clear just from reading the above variable declaration on its own during a code review, without having to go inquire in the type’s header about the exact details of the type and poll the neighborhood for character references who will swear it’s not now, and is even under maintenance never likely to become, an aggregate.

Guideline: Prefer to declare local variables using auto. It guarantees that you cannot accidentally leave the variable uninitialized.

(b) might perform a silent narrowing conversion.

// (b)
vector<string> v;
int size = v.size();

This will compile, run, and sometimes lose information because it uses an implicit narrowing conversion. Not the safest route to a happy weekend when the bug report from the field comes in on Friday night—normally from a large and important customer, because the bug will be exercised only with larger data sizes.

Here’s why: The return type of vector<string>::size() is vector<string>::size_type, but what’s that? It depends on your implementation, because the standard leaves it implementation-defined. But one thing I guarantee you is that “it ain’t no int“—for at least two reasons, which lead to at least two ways this can lose information by silent narrowing:

  • Sign:
    size_type is required to be an unsigned integer value, so this code is asking to convert it to a signed value. That’s bad enough even if sizeof(size_type) == sizeof(int) and it throws away the high bit—and with it the upper half of the representable values—to make room for the sign bit. It’s worse than that if sizeof(size_type) > sizeof(int), which brings us to the second problem, because that’s actually likely…
  • Size:
    size_type basically needs to be the same size as a pointer, since it may have to represent any offset in a vector<char> that is larger than half the machine’s address space. In 64-bit code, 64-bit pointers mean 64-bit size_types. However, if on the same system an int is still 32 bits for compatibility (and this is common), then size_type is bigger than int, and converting to int throws away not just the high-order bit, but over half of the bits and the vast majority of the representable values.

Of course, you won’t notice on small vectors as long as .size() < 2(CHAR_BITS*sizeof(int)-1). That doesn’t mean it’s not a bug; it just means it’s a latent bug.

Does auto help? Yes indeed:

auto size = v.size();    // exact type, guaranteed no narrowing

Guideline: Prefer to declare local variables using auto. It guarantees that you get the exact type and cannot accidentally get narrowing conversions.

(c), (d), and (e) have potential narrowing and signedness issues.

// (c) x and y are of some built-in integral type
int total = x + y;

In case (c), we might also have a narrowing conversion. The simplest way to see this is that if either x or y is larger than int, which is what we’re trying to store the result into, then we’ve definitely got a silent narrowing conversion here, with the same issues as already described in (b). And even if x and y are ints today, if under maintenance the type of one later changes to something like long or size_t, the code silently becomes lossy—and possibly only on some platforms, if it changes to long and that’s the same size as int on some platforms you target but larger than int on others.

Note that, even if you know the exact types of x and y, you will get different types for x+y on different platforms, particularly if one is signed and one is unsigned. If both x and y are signed, or both are unsigned, and one’s type has more bits than the other, that’s the type of the result. If one is signed and the other is unsigned then other rules kick in, and the size and signedness of the result can vary on different platforms depending on the relative actual sizes and the signedness of x and y on that platform. (This is one of the consequences of C and C++ not standardizing the sizes of the built-in types; for example, we know a long is guaranteed to be at least as big as an int, but we don’t know how many bits each is, and the answer varies by compiler and platform.)

Does auto help here? Almost always “yes,” but in one case “yes with a little help you really want to reach for anyway.”

By default, write for correctness, clarity, and portability first: To avoid lossy narrowing conversions, auto is your portability pal and you should use it by default. Writing auto is much better than writing it out by hand as std::common_type< decltype(x), decltype(y) >.

auto total = x + y;    // exact type, guaranteed no narrowing

Guideline: Prefer to declare local variables using auto. It guarantees that you get the exact type and so is the simplest way to portably spell the implementation-specific type of arithmetic operations on built-in types, which vary by platform, and ensure that you cannot accidentally get narrowing conversions when storing the result.

However, what if in rare cases this code may be in a tight loop where performance matters, and auto may select a wider type than you know you need to store all possible values? For example, in some cases performing arithmetic using uint64_t instead of uint32_t could be twice as slow. If you first prove that this actually matters using hard profiler data, and then further prove by performing other validation that you won’t (or won’t care if you do) encounter results that would lose value by narrowing, then go ahead and commit to an explicit type—but prefer to do it using the following style:

// rare cases: use auto + <cstdint> type
auto total = uint_fast64_t{ x+y };  // total is an unsigned 64-bit value
             // ^ see note [1]

// or use auto + size-preserving signed/unsigned helper [2]
auto total = as_unsigned( x+y );    // total is unsigned and size of x+y
  • Still use auto to naturally make this more self-documenting and make the code review easy, because auto syntax makes it explicit that you’re performing a conversion.
  • Use a portable sized type name from the standard <cstdint> header, because you almost certainly care about size and this makes the size portable.[1]

    Guideline: Prefer using the <cstdint> type aliases in code that cares about the size of your numeric variables. Avoid relying on what your current platform(s) happen to do.

    Guideline: Consider declaring local variables auto x = type{ expr }; when you do want to explicitly commit to a type. It is self-documenting to show that the code is explicitly requesting a conversion, and won’t allow an accidental implicit narrowing conversion. Only when you do want explicit narrowing, use ( ) instead of { }.

Case (d) is similar:

// (d) x and y are of some built-in integral type
int diff = x - y;
if(diff < 0) { /*...*/ }

This time, we’re doing a subtraction. No matter whether x and y are signed or not, putting the answer in a signed variable like this is the right thing to do—the result could be negative, after all.

However, we have two issues. The first, again, is that int may not be big enough to avoid truncating the result, so we might lose information if x – y produces something larger than an int. Using auto can help with that.

The second is that x – y might give a strange answer, which isn’t the programmer’s fault but is something you want to remember about arithmetic in C and C++. Consider this code:

unsigned long x    = 42;
signed short  y    = 43;
auto          diff = x - y;   // one actual result: 18446744073709551615
if(diff < 0) { /*...*/ }      // um, oops – branch won't be taken

“Wait, what?” you ask. On nearly all platforms, an unsigned long is bigger than a signed short, and because of the promotion rules the type of s – u, and therefore of result, will be… unsigned long. Which is, well, not very signed. So depending on the types of x and y, and depending on your actual platform, it may be that the branch won’t be taken, which clearly isn’t the same as the original code.

Guideline: Combine signed and unsigned arithmetic carefully.

Before you say, “then I always want signed!” remember that if you overflow then unsigned arithmetic wraps, which can be valid for your use, whereas signed arithmetic has undefined behavior, which is quite unlikely to be useful. Sometimes you really need signed, and sometimes you really need unsigned, even though often you won’t care.

From observing auto‘s effect in case (d), it might seem like auto has helped one problem… but was it at the expense of creating another?

Yes, on the one hand, auto did indeed help us: Using auto ensured we could write portable and correct code where the result wasn’t needlessly narrowed. If we didn’t care about signedness, which is often true, that’s quite sufficient.

On the other hand, using auto might not preserve signedness in a computation like x – y that’s supposed to return something with a sign, or it might not preserve unsignedness when that’s desirable. But this isn’t so much an issue with auto itself as that we have to be careful when combining signed and unsigned arithmetic, and by binding to an exact type auto is exposing this issue with some code that might potentially be already nonportable, or have corner cases the developer wasn’t aware of when he wrote it.

So what’s a good answer? Consider using auto together with the as_signed or as_unsigned conversion helper we saw before, which is used in lieu of a cast to a specific type; the helper is written out more fully in the endnotes. [2] Then we get the best of both worlds—we don’t commit to an explicit type, but we ensure the basic size and signedness in portable code that will work as intended on many different compilers and platforms.

Guideline: Prefer to use auto x = as_signed(integer_expr); or auto x = as_unsigned(integer_expr); to store the result of an integer computation that should be signed or unsigned. Using auto together with as_signed or as_unsigned makes code more portable: the variable will both be large enough and preserve the required signedness on all platforms. (Signed/unsigned conversions within integer_expr may still occur.)

Finally, case (e) brings floating point into the picture:

// (e)
int i = f(1,2,3) * 42.0;

Here we have our by-now-yawnworthy-typical narrowing—and an easy case because it isn’t even hiding, it’s saying int and 42.0 right there in the same breath, which is narrowing almost regardless of what type f returns.

Does auto help? Yes, in making our code self-documenting and more reviewable, as we noted before. If we follow the auto x = type{expr}; declaration style, we would be (happily) forced to write the conversion explicitly, and when we initially use { } we get an error that in fact it’s a narrowing conversion, which we acknowledge (again explicitly) by switching to ( ):

auto i = int( f(1,2,3) * 42.0 );

This code is now free of implicit conversions, including implicit narrowing conversions. If our team’s coding style says to use auto x = expr; or auto x = type{expr}; wherever possible, then in a code review just seeing the ( ) parens can immediately connote explicit narrowing; adding a comment doesn’t hurt either.

But for floating point calculations, can using auto by itself hurt? Consider this example, contributed by Andrei Alexandrescu:

float f1 = /*...*/, f2 = /*...*/;

auto   f3 = f1 + f2;   // correct, but on some compilers/platforms...
double f4 = f1 + f2;   // ... this might keep more bits of precision

As Alexandrescu notes: “Machines are free to do intermediate calculations in a larger precision than the target, and in many cases (and traditionally in C) calculations are done in double precision. So for f3 we have a sum done in double precision, which is then truncated down to float. For f4, the sum is preserved at full precision.”

Does this mean using auto creates a potential flaw here? Not really. In the language, the type of f1 + f2 is still float, and the naked auto maintains that exact type for us. However, if we do want to follow the pattern of switching to double early in a complex computation, we can and should say so:

float f1 = /*...*/, f2 = /*...*/;

auto f5 = double{f1} + f2;

Summary

We’ve seen a number of reasons to prefer to declare variables using auto, optionally with an explicit type if you do want to commit to a specific type.

If you’re observed a pattern in this GotW’s Guidelines, you’ll already have a sense of what’s coming in GotW #94… a Special Edition on, you guessed it, auto style.

Notes

[1] Another reason to prefer using the <cstdint> typedef names is because, due to a quirk in the C++ language grammar, only a single-word type is allowed where uint64_t appears in this example. That’s fine nearly always because it’s all you need for class types and all typedef and using alias names and most built-in types, but you can’t directly name arrays or the multi-word built-in types like unsigned int or long long in that position; for the latter, use the uintNN_t-style typedef names instead. The exact ones, such as uint64_t, are “optional” in the standard, but they are in the standard and expected to be widely implemented so I used them. The “least” and “fast” ones are required, so if you don’t have uint64_t you can use uint_least64_t or uint_fast64_t.

[2] The helpers preserve the size of the type while changing only the signedness. Thanks to Andrei Alexandrescu for this basic idea; any errors are mine, not his. The C++98 way is to provide a set of overloads for each type, but a modern version might look something like the following which uses the C++11 std::make_signed/make_unsigned facilities.

// C++11 version
//
template<class T>
typename make_signed<T>::type as_signed(T t)
    { return make_signed<T>::type(t); }

template<class T>
typename make_unsigned<T>::type as_unsigned(T t)
    { return make_unsigned<T>::type(t); }

Note that with C++14 this gets even sweeter, using auto return type deduction to eliminate typename and repetition, and the _t alias to replace ::type:

// C++14 version, option 1
//
template<class T> auto as_signed  (T t){ return make_signed_t  <T>(t); }
template<class T> auto as_unsigned(T t){ return make_unsigned_t<T>(t); }

or you can equivalently write these function templates as named lambdas:

// C++14 version, option 2
//
auto as_signed   =[](auto x){ return make_signed_t  <decltype(x)>(x); };
auto as_unsigned =[](auto x){ return make_unsigned_t<decltype(x)>(x); };

Sweet, isn’t it? Once you have a compiler that supports these features, pick whichever suits your fancy.

Acknowledgments

Thanks in particular to Scott Meyers and Andrei Alexandrescu for their time and insights in reviewing and discussing drafts of this material. Thanks also to the following for their feedback to improve this article: mttpd, Jim Park, Yuri Khan, Arne, rhalbersma, Tom, Martin Ba, John, Frederic Dumont, Sebastian.

42 thoughts on “GotW #93 Solution: Auto Variables, Part 2

  1. Not sure where to send this, so feel free to remove this comment! There is a typo in: CHAR_BITS*izeof(int)-1

  2. > The first, again, is that int may not be big enough to avoid truncating the result, so we might lose information if x – y produces something larger than an int. Using auto can help with that.

    My understanding is that, if x and y are int, then x - y is int and is not converted automatically to a bigger type if the result does not fit in an int, so overflow happens inside the expression x - y and auto doesn’t help here. For example, with int x = INT_MAX, y = INT_MIN;, auto diff = x - y; will be the same as int diff = x - y; and will overflow despite the auto; you should have promoted an operand (or both) to a bigger type before the subtraction operation. Is my understanding correct?

  3. It would be cute to be able to write:

    for (auto s = v.size(), i = {}; i < s; i += 2)

    But `auto` doesn’t like {}, which is understandable in the simple case

    auto i = {};

    , but here the type can be implied by `s`.

  4. Thanks Herb for bringing this to my attention. Replies @Sebastian within.

    >This doesn’t help much. The unexpected type conversion happens as part of integer_expr; all the “helper” does is afterwards cast it back. Yes, the behavior of unsigned to signed when not in range isn’t undefined, just implementation-defined, but that’s enough to make the code non-portable.

    This seems to be a misunderstanding of the intent. You use as_xxx in lieu of a cast, and eliminates committal to the width of the type without eliminating committal to the consequences of changing signedness.

    >> double f4 = f1 + f2; // … this might keep more bits of precision

    >This is incredibly obscure and would definitely not pass code review from me. If you want higher precision, cast the inputs to double. If you don’t want higher resolution, assign the result to a float. Everything else is just a maintenance nightmare.

    Makes sense (especially for +, which would use ADDSS thus operating in single precision). My point here is that a common pattern in math code is:

    * use float for input due to store size restrictions

    * do intermediate computation in double because it costs just a little more for a big win in accuracy

    * get back to float when writing results to the store

    Due to this pattern, you want to actively cast to double early in the computation chain, sometimes as simple as

    double x = array[k];

    If auto were used, x and computations derived from it would be float. That’s what I’m saying.

  5. @Jon: Yes, I’m familiar with Bjarne’s “too pedantic” comment, but I watched one of his presentations from 2012, and the examples in his slides used vector::size_type.

    Having spent many blissful years with the compiler warning on reliably catching real bugs, and then a few years at another company with “int i” for loop (and therefore the warning disabled), I conclude that there’s nothing pedantic about it. Signed/unsigned bugs are insidious and hard to find. I’m not really worried about the for-loop indexes, but what we miss because those indexes force us to disable the warnings.

  6. In 1(d), the original version creates a function which, when invoked, returns void. The alternative versions return size_type. I think you meant something like

    function<size_t(vector<int>)>

    Nice overview of the benefits of auto. It contained some things I had not thought of. Note that there is at least one situation in which auto is [b]extremely[/b] dangerous: expressions that return proxy types. Consider this problem:

    vector<bool> v = { ... };
    auto b = v.back();   // Oops! b is of type vector<bool>::reference, which is NOT a bool
    v.pop_back();
    bool a = b;  // Oops! b has been invalidated!
    

    Some members of the standards committee have suggested ways to fix this in the language, but I am not familiar with specific proposals.

  7. Just for your information: These named lambdas with auto parameters have a very interesting use-case which makes them more flexible than the template function alternative. For instance, if you have a template function

    template <typename T>
    void func( T t  ) { /*...*/ }
    

    and a generic template function

    template <typename F, typename T>
    void call( F f, T && t ) { f(forward<T>(t)); }
    

    then the following code will not work

    call( func, 42 );
    

    since I would have to write func instead of func here. However this code should work, because func(42) is a valid expression. I actually had this problem in real world code in a physics simulation software I wrote. I can fix it by manually inserting the but with C++14 lambda with auto parameters it’s much easier and even less code. I just have to replace the definition of the func template function by

    static const auto func = []( auto T) { /*...*/ }
    

    This is even less typing. It makes the code more flexibly usable. I also wondered, if it might be generally a good idea to write named lambdas with auto parameters instead of template functions where possible. This might be food for another GotW. ;) By the way, I posted this stuff on Stackoverflow: http://stackoverflow.com/questions/17169498/why-do-i-need-to-specify-the-template-argument-type-of-a-templated-function-here

    It is possible to write this code in old C++ also, but it’s a bit more verbose. It’s gonna work with

    struct { template <typename T> void operator()(T t) { /* ... */ } } func;
    

    This is what I’m going to do for my code until C++14 is implemented.

  8. // best
    auto get_size = [](const auto& x) { return x.size(); };
    

    This code for auto lambda parameter types gives compiler error messages in gcc 4.8.0. Is there a compiler where it will work?
    Thanks,
    Amali.

  9. @Adrian: I’ve debugged far too many bugs caused by incorrect use of unsigned variables, and far fewer bugs related to using a signed int with a collection with more elements than MAX_INT. So my experience differs from yours, and I belong to the “size_type should have been unsigned” camp.

    I completely agree with you that mixing signed and unsigned is broken. So, perhaps casting the vector’s size to a signed variable (with an exception if it would overflow) is a solution. This limits the size by half, but avoids mixing signed/unsigned and avoids unsigned-related bugs.

    Note that Bjarne himself doesn’t seem too fussed using a signed variable. See http://www.stroustrup.com/bs_faq2.html#simple-program: “Yes, I know that I could declare i to be a vector::size_type rather than plain int to quiet warnings from some hyper-suspicious compilers, but in this case,I consider that too pedantic and distracting.”

  10. @Herb: C++ isn’t just used on modern machines with 64-bit address spaces. It’s also used on 16-bit embedded processors, where it’s reasonable to have containers with sizes that might peak in the 32K to 64K-element range. I’ve debugged far too many crashes in my career caused by someone using a signed 16-bit int instead of an unsigned one. Even on 32-bit machines, there are too many programs that can’t handle a 4GB file.

    Regardless of whether vector::size_type _should_ have been unsigned, the fact that it _is_ unsigned. Mixing signed and unsigned forces you to disable compiler warnings that can help catch hard-to-reproduce bugs in other parts of the code (including some of the ones where you should have used signed arithmetic instead of unsigned). I’ve also debugged far too many crashes caused by comparing an unsigned 32-bit millisecond counter to a signed timeout value. Style guidelines that require disabling useful compiler warnings are broken.

  11. @Frederic, @Sebastian: I stomped on the similar sentence in GotW #2 when Ville pointed it out, but it resurfaced. Stomped again, thanks.

  12. > Second, some people like line 2′s syntax better but have to switch to line 1 to get access to explicit constructors.

    If the constructor is explicit, chances are that the = syntax is horribly misleading. Do you really think there’s a programmer who would like “vector v = 5;” to create a 5-element vector? I don’t.

    > By the way, if you’ve been wondering whether auto x { value }; would be more efficient than auto x = expr;, wonder no longer: They have identical semantics.

    Watch out, it’s the dreaded initializer_list deduction! In other words, they’re completely different things. Also, you might want to use expr *or* value, not mix them.

    > Prefer to use auto x = as_signed(integer_expr); or auto x = as_unsigned(integer_expr); to store the result of an integer computation that should be signed or unsigned.

    This doesn’t help much. The unexpected type conversion happens as part of integer_expr; all the “helper” does is afterwards cast it back. Yes, the behavior of unsigned to signed when not in range isn’t undefined, just implementation-defined, but that’s enough to make the code non-portable.

    > double f4 = f1 + f2; // … this might keep more bits of precision

    This is incredibly obscure and would definitely not pass code review from me. If you want higher precision, cast the inputs to double. If you don’t want higher resolution, assign the result to a float. Everything else is just a maintenance nightmare.

  13. But Gotw 92 question 4 states the opposite. In auto a { val }; auto b = { val }; both a and b are said to have the same type initializer_list.

    The two compilers I have tried with (gcc 4.8 and clang 3.2) confirm this.

  14. Re auto i = 0, how about a zero() non-member function akin to std::begin and std::end, like this:

    template <typename C>
    typename C::size_type zero(const C& c)
    {
    	return 0;
    }
    
    for (auto i = zero(v); i < v.size(); ++i) {}
    
  15. @Frederic no, they are not identical. x{value} is just an initialization of x. x would be an initializer_list, if you wrote auto x = {value}; note that here you have both = and {}.

  16. About auto x { value } vs auto x = expr: in what sense are they semantically identical? The types are different (std::initializer_list vs type), so they cannot be interchangeable, can they?

  17. @khurshid: What compiler are you using? That code compiles and executes without issue using Clang 3.2.1 on 64-bit Linux.

  18. auto x = widget{}; // doesn’t compiled with non-copyable classes.

    example:
    struct widget{
    widget( const widget& ) = delete;
    widget& operator = (const widget& ) = delete;
    widget() = default;
    ~widget() = default;
    };

    widget w; // compiled OK;
    auto w = widget{}; // compile ERROR!

  19. @Tom, @Martin: Yup, I just realized I accidentally wrote ( ) instead of { } — that should address at least the narrowing issues. My fingers mostly have the habit of writing { } committed to muscle memory now, but that one slipped by. Fixed, thanks.

  20. @Adrian: There are problems if you use an unsigned loop variable, and different problems if you use a signed one. This is one of those cases that’s just problematic.

    Because of these issues, I once asked Bjarne if he felt that container.size() should have returned signed values, and IIRC he agreed. Of course, that wouldn’t be consequence-free either — you couldn’t have a container of chars bigger than half the address space, but that doesn’t matter on 64 bits today and might never matter, depending on whether we ever get a machine with RAM greater than 2^63 bytes — wasn’t there an urban legend that Bill Gates said 9.2 billion GB should be enough for anyone?

  21. @Juraj: IIUC it’s implementation-defined, not undefined. From 4.7/2-3 [conv.integral]:

    2 If the destination type is unsigned, the resulting value is the least unsigned integer congruent to the source
    integer (modulo 2^n where n is the number of bits used to represent the unsigned type). [ Note: In a two’s
    complement representation, this conversion is conceptual and there is no change in the bit pattern (if there
    is no truncation). —end note ]
    3 If the destination type is signed, the value is unchanged if it can be represented in the destination type (and
    bit-field width); otherwise, the value is implementation-defined.

  22. Following up on @Tom “c-style cast vs. static_cast” and @Herb’s answer “Actually those are function-style casts, and for a non-aggregate type will call a constructor etc” — this really deserves some more info.

    I believe … = int(expr); is 100% exactly the same as … = (int)expr; so this is a c-style cast, meaning potential unsafe casting, or doesn’t it?

    That is:

    	int ans = 42;
    	int* p = &ans;
    	auto val1 = int(p); // compiles, loosing data if sizeof(ptr) GRT sizeof(int) - which may or may not be what we want
    	auto val2 = static_cast<int>(p); // doesn't compile, conversion not allowed via static_cast 
    
  23. for( auto i = 0L; i < v.size(); i += 2 )

    Noooo! Now you’re comparing a signed type to an unsigned type. Worse, on an LLP64 system, i will be 32 bits and v.size() will be 64 bits. This forces you to disable useful compiler warnings.

    This is one of those cases you shouldn’t use auto. Just use std::size_t. It’s clear and correct and it won’t force you to disable compiler warnings that might be useful in other parts of your code.

    If there’s a chance v will grow so large that i += 2 could overflow, then you have to deal with that explicitly.

  24. @JohnSmith – pet peeve, it is not true that “on 64-bit Windows long is still 32-bit”. It may be that on some compiler that you are using that is true, but that is not a requirement of Windows. As long as they follow the standard, different compilers on the same OS may have different type sizes.

  25. Using 0L is an improvement, but still does not quite fix it. For example, on 64-bit Windows long is still 32-bit, while void* and size_t is 64. The obvious solution would be to say “use 0ULL everywhere”, which would always be correct. However, this comes as a performance cost for tight loops, where index updates may now have to be done with a fairly high-latency add-with-carry. One solution which always works, but is *very* ugly, is

    for(auto i = v.size()*0; i < v.size(); i += 2)
    
  26. Re: auto f5 = double{f1 + f2};

    Is that statement identical to:

    auto f5 = double{f1} + f2

    ?

    I.e. in the second version, f2 must be promoted to higher precision before the add operation. In the first version, it seems the compiler might be compelled (allowed) to only do the promotion to double *after* the add, rendering the double promotion somewhat pointless.

  27. @Yuri: I agree. Updated, thanks.

    @John: Good point, quick fix: 1L. :)

    @Arne: Argh, that one gets me all the time. Fixed, thanks.

    @Tom: Actually those are function-style casts, and for a non-aggregate type will call a constructor etc.

    @rhalbersma: Yes, uint64_t and a few others are optional but they’re in the standard and expected to be widely implemented so I used them. The “least” and “fast” ones are not optional, so if you don’t have uint64_t you can use uint_least64_t or uint_fast64_t. … Okay, you’ve now convinced me to mention it. :) Added, mostly to note [1].

  28. @Tom: exactly my thought. The knowledge of how these unpredictable “compiler, shut up!”-monsters are working is frightening.

  29. Does the cast in as_signed not exhibit signed overflow? I thought that code like the following exhibits signed overflow which would be UB:

    unsigned u = -1;// A large value not representable by signed int
    auto i = int(u);// <-- UB?
    
  30. You mention

    for( auto i = 0; i < v.size(); i += 2 )

    as a potential solution for 1 when you mention the silent narrowing conversion in 2b. Perhaps we should have a `std::vector::size_type std::vector::zero() const` member function?

  31. Last time I checked the Standard section 18.4.1, the fixed-size integer types from were optional. Are there any proposals that make it mandatory to provide intN_t and uintN_t for platforms that compile for N-bits architectures?

  32. How come we’re back to using c-style casts?
    auto x = uint64_t(x + y);

    Why not
    auto x = static_cast(x + y);

  33. In the last lines of 2(b) you write about vector sizes < 2^(sizeof(int)-1). It should be 2^(CHAR_BIT*sizeof(int)-1). I ran into that pitfall some time ago ;-)

  34. Doesn’t the indexed version for(auto i = 0; i < v.size(); i += 2) set i to be of type int? If v.size() is (say) 64 bits while int is 32 the loop will never end, will it not?

    The C++11 version of as_(un)signed is using make_signed_t, which is really C++14. It should be using typename make_(un)signed::type instead.

  35. In Solution 1a, you suggest checking the iterator increment against distance(i, end) when writing strided loops. While this will work correctly (as opposed to just i += 2), it introduces a potential performance bug: It will compile even if the iterator does not provide random access. In this case, the loop becomes O(n^2).

    I think the cleanest way to write a generic strided loop would be to write a helper template to use instead of the unchecked increment:

    template <typename Iter>
    void advance_up_to(Iter& it, ptrdiff_t n, Iter&& end)
    {
        while (n --> 0 && it != end) ++it;
    }
    

    (suitably specialized for various iterator categories to handle negative counts and take advantage of random access where available).

Comments are closed.