auto and decltype — Type Deduction, decltype(auto), Trailing Return Types, C++14 Return Type Deduction
In this tutorial, you will learn about auto and decltype. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ auto and decltype provide compile-time type deduction — auto infers a variable's type from its initializer, while decltype yields the exact type of an expression, including references and qualifiers.
What You'll Learn
You will use auto for variable declarations and return types, understand the difference between auto (decayed) and decltype (exact), apply decltype(auto) for perfect type forwarding, write trailing return types with -> decltype(...), and master the deduction rules to avoid surprises.
Why It Matters
Type deduction eliminates redundancy (std::vector<int>::<a href="/design-patterns/iterator/">Iterator</a> it = v.begin() becomes auto it = v.begin()), ensures correctness when types change, and is essential for generic code where the exact type is unknown. Every C++ developer writes auto daily — understanding its rules prevents subtle bugs.
Learning Path
graph LR
A["50: Lambda Expressions"] --> B["51: auto & decltype"]
B --> C["52: Move Semantics"]
C --> D["53: Perfect Forwarding"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Basic auto Deduction
auto deduces the type from the initializer, following template argument deduction rules.
#include <iostream>
#include <vector>
#include <type_traits>
int main() {
auto x = 42; // int
auto y = 3.14; // double
auto z = "hello"; // const char*
auto w = {1, 2, 3}; // std::initializer_list<int> (special case)
// auto with qualifiers
const int ci = 5;
auto copy = ci; // int (const is dropped for non-reference)
const auto ca = ci; // const int
int& ref = x;
auto copyRef = ref; // int (reference dropped)
// With pointers
int* ptr = &x;
auto p = ptr; // int*
const int* cptr = &ci;
auto cp = cptr; // const int* (top-level const on pointer is kept)
}
auto& and auto&&
Adding & or && changes deduction rules.
#include <iostream>
#include <type_traits>
#include <vector>
int main() {
int x = 42;
// auto& deduces as reference (must be lvalue)
auto& ref = x; // int&
// auto& bad = 42; // Error: cannot bind lvalue ref to rvalue
// const auto& works with anything
const auto& cref = 42; // const int& (binds to rvalue)
const auto& cref2 = x; // const int&
// auto&& : forwarding reference
auto&& rref = 42; // int&& (binds to rvalue)
auto&& lref = x; // int& (binds to lvalue — reference collapsing)
// Range-for with auto& avoids copies
std::vector<std::string> words = {"hello", "world"};
for (auto& word : words) { // reference, no copying
word += "!";
}
for (const auto& word : words) { // const reference
std::cout << word << " ";
}
std::cout << "\n"; // hello! world!
}
decltype — The Exact Type
decltype(expr) returns the exact declared type of the expression, preserving references and qualifiers.
#include <iostream>
#include <type_traits>
#include <vector>
int main() {
int x = 42;
int& ref = x;
const int ci = 5;
// decltype of variables
using T1 = decltype(x); // int
using T2 = decltype(ref); // int& (preserves reference)
using T3 = decltype(ci); // const int (preserves const)
std::cout << "T2 is int&: " << std::is_same_v<T2, int&> << "\n"; // true
// decltype of expressions
// For an expression that is not a variable, decltype gives:
// - if expression is lvalue: T&
// - if expression is xvalue: T&&
// - if expression is prvalue: T
int a = 10, b = 20;
using T4 = decltype(a + b); // int (prvalue: result is a temporary)
using T5 = decltype((x)); // int& (parenthesized name is lvalue expr!)
std::cout << "T4 is int: " << std::is_same_v<T4, int> << "\n"; // true
std::cout << "T5 is int&: " << std::is_same_v<T5, int&> << "\n"; // true
// Practical example: deduce container element type
std::vector<int> vec = {1, 2, 3};
using ElemType = decltype(vec.front()); // int& (front returns reference)
using ValueType = std::remove_reference_t<ElemType>; // int
}
Trailing Return Types
Use -> decltype(...) for return types that depend on template parameters.
#include <iostream>
#include <vector>
// Before C++11: cannot express return type that depends on parameters
// C++11: trailing return type
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
// C++14: simpler — just use auto (deduced from return statement)
template <typename T, typename U>
auto add14(T a, U b) {
return a + b;
}
// C++20: abbreviated function template
auto add20(auto a, auto b) {
return a + b;
}
int main() {
std::cout << add(3, 4.5) << "\n"; // 7.5 (decltype returns double)
std::cout << add14(1, 2) << "\n"; // 3 (deduced int)
std::cout << add20(2.5, 1.5) << "\n"; // 4.0 (deduced double)
// Trailing return type with lambda
auto lambda = [](auto a, auto b) -> decltype(a + b) {
return a + b;
};
std::cout << lambda(10, 20) << "\n"; // 30
}
decltype(auto) (C++14)
decltype(auto) deduces the type using decltype rules instead of auto rules.
#include <iostream>
#include <type_traits>
int global = 42;
int& getRef() { return global; }
int getVal() { return global; }
// auto deduces by value (reference dropped)
auto autoRef() { return getRef(); } // returns int (not int&!)
// decltype(auto) preserves reference
decltype(auto) declRef() { return getRef(); } // returns int&
// Another example: perfect forwarding wrapper
int& foo(int& x) { return x; }
decltype(auto) wrapper(auto&& x) {
return foo(std::forward<decltype(x)>(x));
}
int main() {
global = 42;
auto a = autoRef(); // int copy
a = 99;
std::cout << "global: " << global << "\n"; // 42 (unchanged)
decltype(auto) b = declRef(); // int&
b = 100;
std::cout << "global: " << global << "\n"; // 100 (modified)
// Use case: generic forwarding
int x = 5;
decltype(auto) result = wrapper(x);
std::cout << "result: " << result << "\n"; // 5 (reference to x)
}
Structured Bindings (C++17) with auto
Structured bindings use auto to decompose tuples, pairs, arrays, and structs.
#include <iostream>
#include <tuple>
#include <map>
#include <string>
int main() {
// With pair/map
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << "\n";
}
// With tuple
auto tup = std::make_tuple(42, 3.14, "hello");
auto [i, d, s] = tup;
std::cout << i << " " << d << " " << s << "\n"; // 42 3.14 hello
// With array
int arr[3] = {10, 20, 30};
auto& [a, b, c] = arr;
a = 99;
std::cout << arr[0] << "\n"; // 99
// With struct
struct Point { double x, y; };
Point p{1.5, 2.5};
auto [px, py] = p;
std::cout << px << " " << py << "\n"; // 1.5 2.5
}
When NOT to Use auto
#include <iostream>
#include <vector>
#include <string>
int main() {
// Surprise: vector<bool> returns proxy, not bool&
std::vector<bool> flags = {true, false, true};
auto flag = flags[0]; // Not bool! It's a proxy reference type
// bool flag = flags[0]; // Forces conversion to bool
// auto with expression templates (Eigen, etc.)
// auto result = matrixA * matrixB; // Might capture expression template type
// Better: explicit type or evaluate
// auto with large objects: might copy unintentionally
std::string big(10000, 'x');
// auto copy = big; // Copies! Use const auto& for read-only
// Readability: sometimes explicit is clearer
// int index = 0; // Clearer than auto index = 0
// std::map<int, std::string> m; // Clearer than auto m = ...
}
Common Mistakes
Mistake 1: auto drops references and top-level const
const int& ref = 5;
auto copy = ref; // int (not const int&)
Use const auto& or decltype(auto) to preserve qualifiers.
Mistake 2: auto with brace initializers
auto x = {1, 2, 3}; // std::initializer_list<int>, NOT std::vector<int>
auto y {1, 2, 3}; // Error in C++17 (direct init with multiple values)
auto z {1}; // int in C++17, std::initializer_list<int> in C++11/14
Mistake 3: decltype((x)) vs decltype(x)
int x = 5;
decltype(x) a = x; // int
decltype((x)) b = x; // int& — double parentheses create an lvalue expression
Mistake 4: Using auto for function return type without seeing the body
auto compute(); // Declaration: return type unknown until definition
Make sure the definition is visible or use trailing return type for interfaces.
Mistake 5: auto&& is not always an rvalue reference
auto&& always_valid = 42; // int&&
int x = 5;
auto&& also_valid = x; // int& (forwarding reference behavior)
auto&& follows forwarding reference rules, not rvalue reference rules.
Practice Questions
- What types are deduced?
const int ci = 5;
auto a = ci; // int
auto& b = ci; // const int&
decltype(ci) c = 6; // const int
What is
decltype(auto)useful for? Answer: Preserving references and qualifiers in return type deduction, especially for forwarding wrappers.What is the difference between
autoanddecltypein deduction rules? Answer:autofollows template deduction (decays, drops references).decltypereturns the exact declared type including references and cv-qualifiers.Why does
decltype((x))differ fromdecltype(x)? Answer: Parentheses make(x)an lvalue expression rather than a name. For lvalue expressions, decltype adds&.Can
autobe used for non-static data members? Answer: No (until C++20 for some cases). Non-static data members cannot useautobecause the type cannot be deduced without an initializer in the class definition.
FAQ
Mini Project
Build a generic apply function that takes a callable and a tuple of arguments, forwarding each argument with the correct type:
#include <iostream>
#include <tuple>
#include <string>
// Your apply function using auto and decltype
int main() {
auto sum = [](int a, int b, int c) { return a + b + c; };
auto args1 = std::make_tuple(1, 2, 3);
std::cout << apply(sum, args1) << "\n"; // 6
auto concat = [](const std::string& a, const std::string& b) {
return a + b;
};
auto args2 = std::make_tuple("Hello, ", "World!");
std::cout << apply(concat, args2) << "\n"; // Hello, World!
auto nothing = []() { return 42; };
std::cout << apply(nothing, std::tuple{}) << "\n"; // 42
}
This project demonstrates how C++ uses auto, decltype, and variadic templates for generic programming. The standard library's std::apply (C++17) works exactly like this.
What's Next
You now master type deduction with auto and decltype. Next, you will explore move semantics — the C++11 feature that eliminates unnecessary copies and enables efficient resource ownership transfer.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro