auto in C23

Paul J. Lucas - Jan 18 - - Dev Community

Introduction

The auto keyword in C actually predates C. It first appeared in B for use when declaring automatic (meaning “automatically created in the stack frame of the enclosing function”) variables.

B is a typeless language in that all variables are integers, so, for example, the following declares some variables in B:

auto a, b, c;
Enter fullscreen mode Exit fullscreen mode

C, of course, is a typed language; but to make migration of code from B to C easier, Ritchie:

  1. Adopted auto into C.
  2. Made int the default type (meaning if a type were omitted in a declaration, it was assumed to be int).

Hence, the above declaration means the same thing in C as it does in B. However, as a consequence, in C:

void f() {
  auto int x;  // Explicit "auto" adds nothing ...
  int y;       // ... since "auto" is the default.
  // ...
Enter fullscreen mode Exit fullscreen mode

the declaration of y is the same as x since auto is the default storage class (as opposed to either static or extern), so explicit use of auto adds nothing.

Since int requires typing one fewer character than typing auto, being explicit about int eventually became common practice. (In C99, int stopped being assumed.) Hence there was no reason to specify auto explicitly in new code and its use faded away.

For those interested in a much more detailed history of C, see The Development of the C Language, Dennis M. Ritchie, April, 1993.

Resurrection

Eventually, C++11 repurposed auto to mean “automatically deduced type” where the type of the variable being declared is deduced based on the type of its initializer:

double sqrt( double );

auto r = sqrt( x );  // deduces double
Enter fullscreen mode Exit fullscreen mode

As of C23, the repurposed auto of C++11 was back-ported to C (more or less, but mostly less).

auto in C23

There are a few differences between C++11’s and C23’s auto. In general, auto in C is a weaker version of that in C++, specifically:

  1. In C++, auto can declare more than one variable in the same declaration; in C23, it can’t.
  2. In C++, auto can be used with * to deduce a pointed-to type; in C23, it can’t.
  3. In C++, auto can not be used for its original purpose of being a storage class; in C23, it can.
  4. In C++, auto can be used as the return type of an inline (or constexpr) function; in C23, it can’t.
  5. In C++, auto can be used as a function parameter type as a shorthand for templates; in C23, it can’t.
  6. In C++23, auto can be used to get an explicit copy of an object; in C23, it can’t.

C23’s auto is modeled on the existing gcc __auto_type extension.

No Multiple Declaration Support

An example of the first difference is:

auto x = get_x(), y = get_y(); // C++: OK; C: error
Enter fullscreen mode Exit fullscreen mode

Why didn’t the C committee allow auto to declare multiple variables in the same declaration? Because __auto_type doesn’t allow it and the committee simply wanted to standardize the existing __auto_type.

No * Support

An example of the second difference is:

auto *p = strchr( s, '.' );  // C++: 'char'; C: error
auto  q = strchr( s, '.' );  // C++ & C: 'char*'
Enter fullscreen mode Exit fullscreen mode

Why didn’t the C committee allow *? First, in C++, you need to be able to do auto&, that is optionally use references with auto. Since & is allowed, * is also allowed in C++ for symmetry.

C, of course, doesn’t have references so there’s no symmetry issue. And __auto_type doesn’t allow *.

Still a Storage Class

An example of the third difference is:

auto int i;  // C++: error; C: 'int i'
Enter fullscreen mode Exit fullscreen mode

That is auto, when paired with an explicit type, retains its original purpose as a storage class (like either static or extern). Since the C committee is a conservative group, my guess is that they wanted to maintain backwards compatibility even with the likely trivially few C programs that use the original auto explicitly.

No Function Support

Examples of the fourth and fifth differences are:

inline auto max( auto x, auto y ) {  // C++: OK; C: error
  return x > y ? x : y;
}
Enter fullscreen mode Exit fullscreen mode

In C++, being able to have the compiler deduce a return type helps a lot since it can vary depending on the type(s) of the arguments. Also in C++, allowing auto for parameters is a light-weight version of templates since the above is equivalent to:

template<typename T1, typename T2>
inline auto max( T1 x, T2 y ) {
  return x > y ? x : y;
}
Enter fullscreen mode Exit fullscreen mode

Since C doesn’t have templates, there was no compelling reason to allow auto for functions.

Allowing auto for functions would be a nice way to add a “lite” version of templates to C without all the boilerplate template syntax. But I wouldn’t hold my breath for the C committee to adopt it.

No Copy Support

In C++23, you can use auto(x) to get a copy of x without having either to know its type or to name it. Again, since C doesn’t have templates (which is why you wouldn’t know its type), there was no reason to allow this in C.

If you’re interested in the use-case for auto(x) in C++, you can read its original proposal.

Uses for auto in C

Since auto in C is a weaker version of that in C++, is auto in C really useful? It’s useful in a few cases:

  1. Future-proofing code.
  2. Type-agnostic macros.
  3. Coordination with _Generic.

Future-Proofing

Consider a function to generate a unique ID number every time it’s called:

unsigned get_new_id();

void f() {
  unsigned id = get_new_id();
  // ...
Enter fullscreen mode Exit fullscreen mode

What if at some point you need to change the return type of get_new_id() to, say, unsigned long or UUID? You’d have to change the types of all variables that stored an ID throughout your program.

While the traditional way to solve this is to use a typedef for an ID, you can now just use auto and it will always be the right type:

auto id = get_new_id();  // future-proof
Enter fullscreen mode Exit fullscreen mode

Type-Agnostic Macros

Sometimes in a macro, it’s convenient to be able to declare a variable that’s the same type as one of the arguments, for example:

#define SWAP(X,Y)          \
  do {                     \
    auto const temp = (X); \
    (X) = (Y);             \
    (Y) = temp;            \
  } while (0)
Enter fullscreen mode Exit fullscreen mode

will work for any type (even structs).

Coordination with _Generic

When a macro can return a value of a varying type via _Generic, auto allows you to always get the matching type, for example:

#define div(N,D)       \
  _Generic( (N) + (D), \
    int      : div,    \
    long     : ldiv,   \
    long long: lldiv   \
  )( (N), (D) )

auto result = div( 38484848448, 448484844 );
Enter fullscreen mode Exit fullscreen mode

The type of result will be the same as the return type of whichever function the div() macro ends up expanding into.

Remember that _Generic does not evaluate expressions. The use of (N) + (D) causes the compiler to determine what the common type of N and D are via the usual arithmetic conversions. For example, if one is int and the other is long, the int will be promoted to long, the result will be long, and that’s the type that’s selected.

Conclusion

Despite C23’s auto being a weaker version of C++’s auto, it has its few niche uses. Perhaps some day, the C committee will adopt more of C++’s auto.

Epilogue

You may be aware that C23 also added typeof as another way to deduce types. So what about typeof? How does it differ from auto? Why did C23 add both? That’s a story for another time.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .