Disclaimer: ChatGPT generated document.
decltype in C++ has a surprisingly small set of rules, but one special case makes it look complicated.
The key is to separate two rule sets.
If the expression is an unparenthesized name (technically an unparenthesized id-expression or class member access), decltype gives you the declared type of that entity.
int x = 10;
const int cx = 20;
decltype(x) a; // int
decltype(cx) b; // const intReferences are preserved:
int x = 10;
int& r = x;
decltype(r) a = x; // int&Essentially:
If you give
decltypea plain variable name, ask: "What type was this variable declared as?"
int x;
const int y;
int& z = x;
int&& w = 10;
decltype(x) // int
decltype(y) // const int
decltype(z) // int&
decltype(w) // int&&Notice the last one:
int&& w = 10;
decltype(w) // int&&Even though the expression w is an lvalue, the special decltype(name) rule looks at its declared type.
For other expressions, decltype examines the expression's type and value category.
Suppose the expression has type T.
| Expression category | decltype(expression) |
|---|---|
| lvalue | T& |
| xvalue | T&& |
| prvalue | T |
This table is the most important thing to memorize.
int x = 10;
decltype((x)) // int&Why?
(x) is no longer an unparenthesized variable name, so rule #1 doesn't apply.
Now C++ asks:
What is the value category of
(x)?
It's an lvalue of type int.
Therefore:
decltype((x))
// ^^^
// lvalue of type int
// → int&This gives us the famous difference:
decltype(x) // int
decltype((x)) // int&For example:
decltype(42) // int
decltype(x + 1) // int42 is a prvalue of type int, so:
prvalue int
↓
int
No reference gets added.
For example:
int x;
decltype(std::move(x)) // int&&std::move(x) produces an xvalue of type int.
Therefore:
xvalue int
↓
int&&
When you see:
decltype(expr)you can mentally do this:
decltype(expr)
│
▼
Is expr an unparenthesized
variable/member name?
/ \
YES NO
│ │
▼ ▼
declared type value category?
│
┌────────────┼────────────┐
▼ ▼ ▼
lvalue xvalue prvalue
│ │ │
▼ ▼ ▼
T& T&& T
For example:
int x = 10;
decltype(x) // int
decltype((x)) // int&
decltype(x + 1) // int
decltype(std::move(x)) // int&&Suppose:
const int x = 10;Then:
decltype(x) // const intbecause the declared type is const int.
But:
decltype((x)) // const int&because (x) is an lvalue whose type is const int.
And:
decltype(std::move(x)) // const int&&because std::move(x) is an xvalue whose type is const int.
So:
expression type/category decltype
--------------------------------------------------------
x special case const int
(x) const int lvalue const int&
std::move(x) const int xvalue const int&&
This connects directly to your previous question about qualifiers: unlike many forms of type deduction, decltype is deliberately good at preserving const and reference information.
One additional rule worth learning after this is decltype(auto), because it applies these exact decltype rules to deduce a variable or function return type—and that's where the difference between return x; and return (x); can become very important.
