When I was reading this online c++ reference page about decltype
I was wondering about this paragraph:
If expression is a function call which returns a prvalue of class type or is a comma expression whose right operand is such a function call, a temporary object is not introduced for that prvalue. (until C++17)
My question is: Introducing or not introducing a temporary, does that matter?
And this paragraph:
Note that if the name of an object is parenthesized, it is treated as an ordinary lvalue expression, thus decltype(x) and decltype((x)) are often different types.
And again my question: What's the rationale behind treating them differently?
Can anyone out there give me a hand and shed some light on the dark corners I'm in. Thanks.
Introducing or not introducing a temporary, does that matter?
This makes a bit more sense if you look at an expression that uses the result of a function:
// given
template<typename T> struct Foo {};
template<typename T> Foo<T> foo();
template<typename T> void bar(const Foo<T>&);
// This:
bar(foo<int>());
// Is equivalent to:
{
Foo<int>&& tmp = foo<int>();
bar(tmp);
}
// So should tmp "exist" here?
using T = decltype(foo<int>());
You could easily argue that the foo<int>()
expression actually is equivalent to Foo<int>&& tmp = foo<int>()
. If the existence of tmp
was somehow undesirable in a decltype()
context, then it needs to be made clear.
And tmp
is definitely undesirable here. It causes the Foo<int>
specialization to be created. But just because you have identified a type does not mean you are actually using it. The same goes for type aliases.
Demonstration: (see on godbolt)
#include <type_traits>
template<typename T>
struct Foo {
// Will cause a compile error if it's ever instantiated with int
static_assert(!std::is_same_v<T, int>);
};
using FooInt = Foo<int>;
template<typename T>
Foo<T> foo() {
return {};
}
void bar() {
// Does not cause Foo<int> to exist just yet
using T = decltype(foo<int>());
// This instantiates Foo<int> and causes the compile error
// T x;
}
What's the rationale behind treating them differently?
N.B. This following is more of a guideline for how to reason about this. The actual technical details are different, but it can get rather convoluted.
Don't think of it as the parentheses being anything special. Instead, think of it as decltype(id-expression)
being the special case.
Let's say we have the following declaration: int x;
The x
expression does not behave like an int
, but a int&
instead. Otherwise x = 3;
wouldn't make sense.
Despite that, decltype(x)
is still int
. That's the special case: decltype(id-expression)
returns the type of the identifier itself instead of the type of the expression.
On the other hand, (x)
also behaves like int&
, but since it's not an id-expression
, it gets interpreted as just any regular expression. So decltype((x))
is int&
.
#include <type_traits>
int x = 0;
using T = decltype(x);
using U = decltype((x));
static_assert(std::is_same_v<T, int>);
static_assert(std::is_same_v<U, int&>);