34

The subject was discussed before, but this is not a duplicate.

When someone asks about the difference between decltype(a) and decltype((a)), the usual answer is - a is a variable, (a) is an expression. I find this answer unsatisfying.

First, a is an expression as well. The options for a primary expression include, among others -

  • ( expression )
  • id-expression

More importantly, the phrasing for decltype considers parentheses very, very explicitly:

For an expression e, the type denoted by decltype(e) is defined as follows:
(1.1)  if e is an unparenthesized id-expression naming a structured binding, ...
(1.2)  otherwise, if e is an unparenthesized id-expression naming a non-type template-parameter, ...
(1.3)  otherwise, if e is an unparenthesized id-expression or an unparenthesized class member access, ...
(1.4)  otherwise, ...

So the question remains. Why are parentheses treated differently? Is anyone familiar with technical papers or committee discussions behind it? The explicit consideration for parentheses leads to think this is not an oversight, so there must be a technical reason I'm missing.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Ofek Shilon
  • 14,734
  • 5
  • 67
  • 101
  • 2
    *"the usual answer is - a is a variable, (a) is an expression"* What they mean is "`(a)` is an expression, and `a` is an expression *and* a variable". – HolyBlackCat Feb 24 '20 at 12:09

3 Answers3

21

It's not an oversight. It's interesting, that in Decltype and auto (revision 4) (N1705=04-0145) there is a statement:

The decltype rules now explicitly state that decltype((e)) == decltype(e)(as suggested by EWG).

But in Decltype (revision 6): proposed wording (N2115=06-018) one of the changes is

Parenthesized-expression inside decltype is not considered to be an id-expression.

There is no rationale in the wording, but I suppose this is kind of extension of decltype using a bit different syntax, in other words, it was intended to differentiate these cases.

The usage for that is shown in C++draft9.2.8.4:

const int&& foo();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1 = 17;        // type is const int&&
decltype(i) x2;                 // type is int
decltype(a->x) x3;              // type is double
decltype((a->x)) x4 = x3;       // type is const double&

What is really interesting, is how it works with the return statement:

decltype(auto) f()
{
    int i{ 0 };
    return (i);
}

My Visual Studio 2019 suggest me to remove redundant parenthesis, but actually they turn into decltype((i)) which changes return value to int& which makes it UB since returning reference to a local variable.

espkk
  • 346
  • 1
  • 9
  • Thank you for pin-pointing the origin! Not only could decltype have been specified differently, turns out it initially was. The rev-6 document explicitly adds this funny parentheses behaviour, but skips the rationale :( . I guess that's as close to an answer as we'd get now.. – Ofek Shilon Feb 24 '20 at 15:22
  • And yet nobody proposed to solve the silly problem by making it two distinct keywords, for two distinct functions? One of the biggest blunders of the committee (that historically made a lot of unforced errors). – curiousguy Mar 01 '20 at 16:41
16

Why are parentheses treated differently?

Parentheses aren't treated differently. It's the unparenthesized id-expression that's treated differently.

When the parentheses are present then the regular rules for all expressions apply. The type and value category are extracted and codified in the type of decltype.

The special provision is there so that we could write useful code more easily. When applying decltype to the name of a (member) variable, we don't usually want some type that represents the properties of the variable when treated as an expression. Instead we want just the type the variable is declared with, without having to apply a ton of type traits to get at it. And that's exactly what decltype is specified to give us.

If we do care about the properties of the variable as an expression, then we can still get it rather easily, with an extra pair of parentheses.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • So for an `int` member `i` of `a`, `decltype(a.i)` is `int` while `decltype((a.i))` is `int&` (assuming `a` is not `const`)? Since the expression `a.i` is assignable? – n314159 Feb 24 '20 at 12:17
  • 1
    @n314159 - That's the gist of it. The expression `a.i` is a non-const lvalue, so you get a non-const lvalue reference type for `(a.i)`. – StoryTeller - Unslander Monica Feb 24 '20 at 12:20
  • Why do 'the regular rules for all expressions' indicate that their type is a reference? Why do 'the properties of the variable as an expression' include the property of being a reference? Is it some natural default? – Ofek Shilon Feb 24 '20 at 15:09
  • 3
    @OfekShilon - Their type is not a reference. [The type of an expression is never analyzed as a reference](https://eel.is/c++draft/expr#type-1.sentence-1). But dectlype can only resolve to a type, and it's meant to tell us not just the type of an expression, but its value category as well. The value category is *codified* by reference types. lvalues are `&`, xvalues are `&&`, and prvalues aren't reference types. – StoryTeller - Unslander Monica Feb 24 '20 at 15:17
  • @StoryTeller exactly, there is no 'natural' difference between types of expressions and non expressions. Which makes the distinction artificial. – Ofek Shilon Feb 24 '20 at 17:46
4

Pre C++11 the language need tool(s) to get two different kinds of information:

  • the type of an expression
  • the type of a variable as it was declared

Because of the nature of this information, the features had to be added in the language (it can't be done in a library). That means new keyword(s). The standard could have introduced two new keywords for this. For instance exprtype to get the type of an expression and decltype to get the declaration type of a variable. That would have been the clear, happy option.

However the standard committee has always tried its hardest to avoid introducing new keywords into the language in order to minimize breakage of old code. Backwards compatibility is a core philosophy of the language.

So with C++11 we got just one keyword used for two different things: decltype. The way it differentiates between the two uses is by treating decltype(id-expression) differently. It was a conscious decision by the committee, a (small) compromise.

bolov
  • 72,283
  • 15
  • 145
  • 224
  • I remember hearing this on a cpp talk. I however have no hope of finding the source. It would be great if someone finds it. – bolov Mar 01 '20 at 11:08
  • 1
    C++ historically added a bunch of new keywords, many w/o an underscore and some with one English word. The rational is really absurd. – curiousguy Mar 01 '20 at 16:42
  • 1
    @curiousguy every keyword introduced will clash with existing user symbols so the committee puts heavy weight in the decision of adding new reserved keywords to the language. It's not absurd imo. – bolov Mar 01 '20 at 18:33
  • Not true: `export` was introduced. If you can have `export` (previously all templates were by default "exported"), you can have stuff like `decltype` and `constexpr`. Obviously adding `register` in another language would be problematic. – curiousguy Mar 01 '20 at 19:13
  • @bolov Thank you! (1) Is this speculation or knowledge? Are you aware of discussions where avoiding an extra keyword was the motivation? (2) Can you give an example where an id-expression actually needs to be treated differently if it is used "as an expression"? (What does this even mean?) – Ofek Shilon Mar 02 '20 at 17:05
  • 1
    @curiousguy bolov is not saying that no keywords are added, but that the committee prefers not adding keywords, precisely because each new keyword is potentially a breaking change. – Caleth Jan 10 '22 at 11:36
  • 1
    @OfekShilon `auto` used to be the (optional) specifier for automatic storage duration variables, to distinguish from `static` and `register`. – Caleth Jan 10 '22 at 11:38