3

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.

  • fyi the C++ Standard's papers for `decltype` - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2343.pdf and http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3276.pdf – Richard Critten Nov 11 '21 at 16:32
  • 3
    Perhaps it'd be good to add the [tag:language-lawyer] tag to attract language-lawyers. – Ted Lyngmo Nov 11 '21 at 16:33
  • [C++ decltype and parentheses - why?](https://stackoverflow.com/questions/60375448/c-decltype-and-parentheses-why) – cpplearner Nov 11 '21 at 17:46
  • @RichardCritten Wow, a lot to stomach, but a quick scan shows that the paper details the origin of the issues and why the necessary wording adjustment was made. merci. – slow_down_the_pace Nov 12 '21 at 06:39
  • @cpplearner This seems to resolve the second part, really informative. Many thanks. – slow_down_the_pace Nov 12 '21 at 06:41

1 Answers1

4

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&>);

  • It's sometimes helpful to informally think of it as "the type of the `x` expression is `int&`", but that could lead to issues understanding other technical details. Technically an expression never has reference type. To be more precise, every expression has a type and a value category, and the ordinary `decltype` combines in the value category to sometimes get reference types. `(x)` is an lvalue of type `int`, so `decltype((x))` is `int&`. The `decltype(id-expression)` special case uses the type of the matching declarator instead of the type and category of the expression. – aschepler Nov 11 '21 at 19:11
  • @aschepler Thank you. I've refined the wording a bit to avoid misleading people, but kept the simplified perspective to keep the answer as beginner-friendly as possible. –  Nov 11 '21 at 19:22
  • Really informative and helpful. Thanks. – slow_down_the_pace Nov 12 '21 at 06:33