🌎 δΈ­ζ–‡ | English

Type Deduction - auto and decltype

auto and decltype are powerful type deduction tools introduced in C++11. They not only make code more concise but also enhance the expressive power of templates and generics.

Why were they introduced?

  • Solve the problem of overly complex type declarations
  • Need to obtain object or expression types in template applications
  • Support lambda expression definitions

What's the difference between auto and decltype?

  • auto is often used for variable definitions, and the deduced type may lose const or reference (can be explicitly specified with auto &)
  • decltype obtains the exact type of an expression
  • auto generally cannot be used as a template type parameter

I. Basic Usage and Scenarios

Declaration and Definition

Acts as a type placeholder to assist in variable definition or declaration. When using auto, the variable must be initialized, while decltype can be used without initialization.

int b = 2;
auto b1 = b;
decltype(b) b2 = b;
decltype(b) b3; // Can be used without initialization

Expression Type Deduction

Often used for complex expression type deduction to ensure calculation precision

int a = 1;

auto b1 = a + 2;
decltype(a + 2 + 1.1) b2 = a + 2 + 1.1;

auto c1 = a + '0';
decltype(2 + 'a') c2 = 2 + 'a';

Complex Type Deduction

Iterator Type Deduction

std::vector<int> v = {1, 2, 3};

// std::vector<int>::iterator it = v.begin();
auto it = v.begin(); // Automatically deduce iterator type
// decltype(v.begin()) it = v.begin();
for (; it != v.end(); ++it) {
    if (*it == 2) {
        v.insert(it, 0);
        break;
    }
}

Function Type Deduction

For complex types like functions or lambda expressions, auto and decltype are commonly used. Generally, lambda definitions use auto, while template type parameters use decltype.

int add_func(int a, int b) {
    return a + b;
}

int main() {
    auto minus_func = [](int a, int b) { return a - b; };

    std::vector<std::function<decltype(add_func)>> funcVec = {
        add_func,
        minus_func
    };

    funcVec[0](1, 2);
    funcVec[1](1, 2);
    //...
}

Function Return Type Deduction

Syntax Sugar Usage

auto supports trailing return type function definitions and can be used with decltype for return type deduction.

auto main() -> int {
    return 0;
}

auto add(int a, double b) -> decltype(a + b) {
    return a + b;
}

Function Template Return Type Deduction

When the template return type cannot be determined, auto + decltype can be used for deduction, allowing add to support general types like int, double,... and complex types like Point, Vec,... enhancing generic programming expressiveness. (In C++14, decltype can be omitted)

template<typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) {
    return a + b;
}

Class/Structure Member Type Deduction

struct Object {
    const int a;
    double b;
    Object() : a(1), b(2.0) { }
};

int main() {
    const Object obj;

    auto a = obj.a;
    std::vector<decltype(obj.b)> vec;
}

II. Real-World Case - auto/decltype in the STL

The examples above illustrate syntax; the practical value of auto/decltype is demonstrated most directly in the standard library's own implementation. The following picks two common pieces of code from the in-repo MSVC STL to demonstrate auto and decltype respectively; _STD and the like are internal library macros and qualifiers β€” focus on the auto / decltype when reading.

auto deduces iterator and element types: traversing a container

Traversing a container is the most common scenario for auto. Below is a traversal from path normalization in <filesystem>: auto _Pos deduces the iterator type, and const auto _Elem deduces the element type obtained by dereferencing β€” the very same form as auto it = v.begin() from "Complex type deduction - iterators" in ## I.

// MSVC STL Β· msvc-stl/stl/inc/filesystem (abridged, original indentation kept)
            auto _New_end = _Vec.begin();
            for (auto _Pos = _Vec.begin(); _Pos != _Vec.end();) {
                const auto _Elem = *_Pos++;
                // ...(decide whether to write _Elem back to _New_end; omitted)
            }

_Vec is the container of path components; both its iterator type and its element type are left to auto, with no need to spell out the concrete types.

decltype takes the type of a variable: the binary search in std::lower_bound

The most direct use of decltype is "take the type of a variable or expression". In the standard library's binary search std::lower_bound, auto first deduces the range length _Count, then decltype(_Count) denotes "the same type as _Count" to convert _Count / 2 back to that type:

// MSVC STL Β· msvc-stl/stl/inc/xutility (abridged) β€”β€” std::lower_bound
    auto _UFirst = _STD _Get_unwrapped(_First);
    auto _Count  = _STD distance(_UFirst, _STD _Get_unwrapped(_Last));

    while (0 < _Count) { // divide and conquer, find half that contains answer
        const auto _Count2 = static_cast<decltype(_Count)>(_Count / 2);
        const auto _UMid   = _STD next(_UFirst, _Count2);
        // ...(compare at _UMid, narrow the range; omitted)
    }

This is the same use as decltype(b) b2 from "Declaration and definition" in ## I: decltype(_Count) is simply "the type of _Count". auto deduces the type, and decltype reuses that same type elsewhere.

Takeaway: traversing a container, reusing the type of some variable β€” these everyday forms are, inside the standard library, exactly the auto + decltype toolkit taught in this chapter. This is one of the core motivations for introducing them in C++11.

III. Important Notes

auto and const / reference stripping

auto deduction strips top-level const and references; to keep them you must write const auto& / auto& explicitly, whereas decltype preserves the declared type exactly

int a = 1;
int &b = a;
const int c = 1;
const int &d = c;

auto a1 = a; // int
auto b1 = b; // int
auto c1 = c; // int
auto d1 = d; // int

const auto c2 = c;  // const int
const auto &d2 = d; // const int &

decltype(c) c3 = c; // const int
decltype(d) d3 = d; // const int &

This is also why auto a = obj.a; in "Class/Struct Member Type Deduction" yields int rather than const int β€” auto stripped the top-level const.

Difference between decltype(obj) and decltype( (obj) )

  • Generally, decltype(obj) obtains its declared type
  • While decltype( (obj) ) obtains the type of the (obj) expression (lvalue expression)
int a = 1;
decltype(a) b; // Deduction result is a's declared type int
decltype( (a) ) c; // Deduction result is the type of (a) lvalue expression int &

Difference between decltype(obj.b) and decltype( (obj.b) )

  • decltype( (obj.b) ): Type deduction from expression perspective, obj's definition type affects deduction result. For example, if obj is const-qualified, const will limit obj.b access to const.
  • decltype(obj.b): Since it deduces the member's declared type, it won't be affected by obj's definition.
struct Object {
    const int a;
    double b;
    Object() : a(1), b(2.0) { }
};

int main() {
    Object obj;
    const Object obj1;

    decltype(obj.b)  // double
    decltype(obj1.b) // double

    decltype( (obj.b) ) // double &
    decltype( (obj1.b) ) // Affected by obj1's const qualification, so it's const double &
}

Rvalue Reference Variables are Lvalues in Expressions

int &&b = 1;

decltype(b) // Deduction result is declared type int &&
decltype( (b) ) // Deduction result is int &

IV. Practice Code

Practice Topics

Auto-Checker Commands

Don't have d2x yet? Click to expand setup
# 1. Install xlings (Linux / macOS)
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash
# Windows PowerShell:
# irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex

# 2. Install d2x and fetch this tutorial
xlings install d2x -y
d2x install d2mcpp

# 3. Enter the project directory & run the checker
cd d2mcpp
d2x checker auto-and-decltype

Exercise Discussion

V. Additional Resources