One of the most iconic C++ features is the language’s ability to deduce types with the auto keyword. In this post, I’ll give a series of code snippits. Your job is to assess what will be deduced for v in each case. Determine for each:

  1. The deduced type
  2. If it is a value, a lvalue or rvalue reference, or a pointer
  3. Which CV qualifiers are applicable

Some of these may not even compile, so “this won’t work” is a totally valid answer.

Each section increases in difficulty. Good luck!

Enter the Gauntlet

Basics

Basic assignments and deduction from constants and straightforward types.


auto v = 5;
Answer
Type: int
Explanation: Straightforward type deduction from an integer constant.

auto v = 0.1;
Answer
Type: double
Explanation: Notably different than integers. Floating points default to the larger double instead of float.

int x;
auto v = x;
Answer
Type: int
Explanation: Simple type derived from the assigned-from variable.

auto v = 5, w = 0.1;
Answer
Type: Fails to compile.
Explanation: All types in an expression defined with auto have to be the same.

int x;
auto v = &x;
Answer
Type: int*
Explanation: Auto will deduce pointers.

auto v = nullptr;
Answer
Type: std::nullptr_t
Explanation: nullptr has its own type.

auto v = { 1, 2, 3 };
Answer
Type: std::initializer_list<int>
Explanation: It might seem like this should create a container, but it won’t!

int x[5];
auto v = x;
Answer
Type: int*
Explanation: C-style arrays decay to a pointer. The decay happens before auto is evaluated.

int foo(int x) {
    return x;
}
auto v = foo;
Answer
Type: int (*) (int)
Explanation: auto can deduce function pointers.

Intermediate

Exploring how references and CV-qualifiers are handled.


volatile const int x = 1;
auto v = x;
Answer
Type: int
Explanation: auto drops CV qualifiers.


volatile const int x = 1;
auto v = &x;
Answer
Type: volatile const int*
Explanation: Unless, of course, they are applied to a pointed-to or reference type

int x;
int& y = x;
auto v = y;
Answer
Type: int
Explanation: auto will never deduce a reference on its own.

int x;
auto& v = x;
Answer
Type: int&
Explanation: lvalue references are deduced via auto&.

int x[5];
auto& v = x;
Answer
Type: int (&) [5]
Explanation: When binding arrays to references, they don’t decay. So auto deduces the actual array type.

int foo(const int x) {
    return x;
}
auto v = foo;
Answer
Type: int (*) (int)
Explanation: Remember - CV qualifiers on parameters are thrown away during function resolution!

Advanced

Forwarding references, decltype(auto), inheritance, structured binding, and lambdas.


int x;
auto&& v = x;
Answer
Type: int&
Explanation: A forwarding reference like auto&& can bind to lvalue or rvalue expressions. Here we get an lvalue reference because x is an lvalue.

auto x = [] () -> int { 
    return 1;
};
auto&& v = x();
Answer
Type: int&&
Explanation: x() returns a prvalue, and prvalues assigned to forwarding references yield an rvalue reference.

int x;
auto y = [&] () -> int& { 
    return x;
};
auto&& v = y();
Answer
Type: int&
Explanation: This time x() returns an lvalue, and lvalues assigned to forwarding references yields an lvalue reference.

int x;
decltype(auto) v = (x);
Answer
Type: int&
Explanation: (x) is an expression. decltype(expression) yields a rvalue reference when the expression is an lvalue.

struct Foo {};
auto&& v = Foo{};
Answer
Type: Foo&&
Explanation: prvalues like Foo{} will bind to an rvalue reference.

struct Foo {};
decltype(auto) v = Foo{};
Answer
Type: Foo
Explanation: For any prvalue expression e, decltype(e) evaluates to the type of e.

int x;
decltype(auto) v = std::move(x);
Answer
Type: int&&
Explanation: For any xvalue expression e, decltype(e) evalutes to an rvalue reference to the type of e.

int foo(int x) {
    return x;
}
decltype(auto) v = foo;
Answer
Type: Fails to compile.
Explanation: Function id-expressions do not decay to pointers when evaluating with decltype!

int foo(int x) {
    return x;
}
decltype(auto) v = (foo);
Answer
Type: int (&) (int)
Explanation: Parenthesized function symbol expressions are deduced as a reference to the function.

class Base {
    public:
        auto foo() {
            return this;
        };
};

class Derived : public Base {
};

Derived d;
auto v = d.foo();
Answer
Type: Base*
Explanation: foo is defined in the Base class, so this has to refer to a Base*, even when foo is called from a child class.

Oof

Abandon all hope, ye who attempt to deduce the types of lambda captures in expressions with decltype(auto).


int x;
[&] {
    decltype(auto) v = x;
}();
Answer
Type: int
Explanation: decltype(auto) always deduces id-expressions strictly with the type of the target symbol. Even though x is captured by reference, it is not deduced as a reference.

int x;
[&] {
    decltype(auto) v = (x);
}();
Answer
Type: int&
Explanation: However, lvalues in parenthesized expressions are deduced as references via decltype(auto).

int x;
[=] {
    decltype(auto) v = x;
}();
Answer
Type: int
Explanation: lvalue id-expressions are deduced strictly as their type via decltype(auto).

int x;
[=] {
    decltype(auto) v = (x);
}();
Answer
Type: const int&
Explanation: Captures by value are const, so the type of x is const int. lvalues in a parenthesized expression are deduced as references - so we end with const int&.

int x;
[=] mutable {
    decltype(auto) v = (x);
}();
Answer
Type: int&
Explanation: Unless, of course, we make the lambda mutable. Then the captures are non-const.

int x;
int& y = x;
[=] () {
    decltype(auto) v = y;
}();
Answer
Type: Fails to compile.
Explanation: Lambdas cannot capture references. When y is captured, it captures the referred-to value x. Because captures are const, the captured value ends up as const int. However, decltype(auto) sees the symbol y’s declaration as an int& and deduces the type of v as int&. Compilation fails on discarding the const qualifier when trying to assign a const int to an int&.

One last really tricky to correctly answer:


std::pair x {1, 2.0};
auto [v, w] = x;
Answer
Type: int (but more like an alias to the first element of a hidden copy of x)
Explanation: The GodBolt linked at the bottom of this thread will print this is a copy. In reality, it is an alias of a hidden copy of x. There is active discussion on the right answer in this Reddit thread, but my original version of this post stating that v is an int is, at best, an oversimplification.

Conclusion

You can check any of these by using this GodBolt link. Shoutout to this Stack Overflow thread which provided the code snippit to print human readable names for a variable.

Do you have any interesting examples to share? Reach out at sam@volatileint.dev and let me know! If you found this article interesting, consider subscribing to the newsletter to hear about new posts!