Significance of Parentheses in Decltype((C))

Significance of parentheses in decltype((c))?

  • c is the name of a variable;

  • (c) is an expression, in this case an lvalue expression, whose value is identical to the value of the variable c.

And the two are treated differently by decltype. Consider, for example, decltype(1+2), which is also an example taking an expression. It just so happens that your example is a simple version of an expression: one which merely names a single variable and does nothing exciting with it.

It's one of those differences that you generally only really care about if you're rationalising about the subtle parts of the language specification; though, as you have identified, it has quite a significant practical effect in this case.

Please note though that there is no operator usage here. It's all simply a deduction from the layout of the grammar.

C++ decltype and parentheses - why?

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.

decltype((x)) with double brackets what does it mean?

According to C++ Primer:

When we apply decltype to a variable without any parentheses, we get
the type of that variable. If we wrap the variable’s name in one or
more sets of parentheses, the compiler will evaluate the operand as an
expression. A variable is an expression that can be the left-hand side
of an assignment. As a result, decltype on such an expression yields
a reference:

// decltype of a parenthesized variable is always a reference
decltype((i)) d; // error: d is int& and must be initialized
decltype(i) e; // ok: e is an (uninitialized) int

decltype parenthesis syntax for a lvalue

The rules for decltype(e) are, as far as the C++ standard goes, pretty clear, so I'll just copy them (from [dcl.type.simple]):

For an expression e, the type denoted by decltype(e) is defined as follows:

(4.1) — if e is an unparenthesized id-expression or an unparenthesized class member access (5.2.5), decltype(e)
is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions,
the program is ill-formed;

(4.2) — otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;

(4.3) — otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;

(4.4) — otherwise, decltype(e) is the type of e.

So going through your examples in order:

  1. decltype(a): a is an unparenthesized id-expression, so this is just the type of a: double.
  2. decltype((a)): now it's parenthesized, so we skip the first bullet. a isn't an xvalue, it's an lvalue, so this is double&.
  3. decltype(a)&: this is just the first case again, so it's double&.
  4. decltype((5)): this is neither an id-expression (parenthesized or otherwise), an xvalue, or an lvalue - it's a prvalue, so we drop to the last bullet to just get the type of the expression: int.
  5. decltype(5)&: same as the last point, except now you're explicitly adding a &, so int&.

Should I use the first or the second way?

It depends on what type you actually want to get. The two ways mean different things - you should use whichever one solves the direct problem you're trying to solve.

More generally, decltype(expr)& is always an lvalue reference due to reference collapsing rules.

decltype((expr)) could be a non-reference prvalue (as with decltype((5))), an lvalue reference (as with decltype((a))), or an rvalue reference (as with decltype((std::move(a)))).

What is the rationale behind decltype behavior?

They wanted a way to get the type of declaration of an identifier.

They also wanted a way to get the type of an expression, including information about if it is a temporary or not.

decltype(x) gives the declared type of the identifier x. If you pass decltype something that is not an identifier, it determines the type, then appends & for lvalues, && for xvalues, and nothing for prvalues.

Conceptually you can think of it as the difference between the type of a variable and the type of an expression. But that is not quite how the standard describes it.

They could have used two different keywords to mean these two things. They did not.

Are parentheses around the result significant in a return statement?

As of C++14, they often are.

C++14 adds a fringe case where parentheses around a return value may alter the semantics. This code snippet shows two functions being declared. The only difference is parentheses around the return value.

int var1 = 42;
decltype(auto) func1() { return var1; } // return type is int, same as decltype(var1)
decltype(auto) func1() { return(var1); } // return type is int&, same as decltype((var1))

In the first func1 returns an int and in the second one func1 returns an int& . The difference in semantics is directly related to the surrounding parentheses.

The auto specifier in its latest form was introduced in C++11. In the C++ Language Spec it is described as:

Specifies that the type of the variable that is being declared will be automatically
deduced from its initializer. For functions, specifies that the return type is a trailing
return type or will be deduced from its return statements (since C++14)

As well C++11 introduced the decltype specifier which is described in the C++ Language Spec:

Inspects the declared type of an entity or queries the return type of an expression.

[snip]

  1. If the argument is either the unparenthesised name of an object/function, or is a member access expression (object.member or pointer->member), then the decltype specifies the declared type of the entity specified by this expression.

  2. If the argument is any other expression of type T, then

    a) if the value category of expression is xvalue, then the decltype specifies T&&

    b) if the value category of expression is lvalue, then the decltype specifies T&

    c) otherwise, decltype specifies T


[snip]

Note that if the name of an object is parenthesised, it becomes an lvalue expression, thus decltype(arg) and decltype((arg)) are often different types.

In C++14 the ability to use decltype(auto) was allowed for function return types. The original examples are where the semantic difference with parentheses comes into play. Revisiting the original examples:

int var1 = 42;
decltype(auto) func1() { return var1; } // return type is int, same as decltype(var1)
decltype(auto) func1() { return(var1); } // return type is int&, same as decltype((var1))

decltype(auto) allows the trailing return type in the function to be deduced from the entity/expression on the return statement. In the first version return var1; is effectively the same as returning the type decltype(var1) (an int return type by rule 1 above) and in the second case return (var1); it's effectively the same as decltype((var1)) (an int & return type by rule 2b).

The parentheses make the return type int& instead of int, thus a change in semantics. Moral of the story - "Not all parentheses on a return type are created equal"

Why and how do extra parentheses change the type of an expression in C++ (C++11)?

decltype treats its arguments differently depending on the additional parentheses.

The second pair of parentheses makes it a primary-expression (but not itself an id-expression or class-member-access), so the special rule doesn't apply.

Here is a discussion: Significance of parentheses in decltype((c))?

Why does decltype(auto) return a reference here?

7.1.6.2 [dcl.type.simple]


  1. For an expression e, the type denoted by decltype(e) is defined as follows:

    — if e is an unparenthesized id-expression or an unparenthesized class member access (5.2.5), decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed;

    — otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;

    — otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;

    — otherwise, decltype(e) is the type of e.

In your example you have return (m) so e is (m). That is not an unparenthesized id-expression or class member access, so we go to the second bullet. It is not an xvalue so we go to the third bullet. It is an lvalue, so the type is T& where T is int.

decltype and parentheses

Just above that example, it says

  • if e is an unparenthesized id-expression or a class member access (5.2.5), decltype(e) is the type of the entity named by e.
  • if e is an lvalue, decltype(e) is T&, where T is the type of e;

I think decltype(a->x) is an example of the "class member access" and decltype((a->x)) is an example of lvalue.

What does the parenthesis operator does on its own in C++

If I can answer informally,

(444);

is a statement. It can be written wherever the language allows you to write a statement, such as in a function. It consists of an expression 444, enclosed in parentheses (which is also an expression) followed by the statement terminator ;.

Of course, any sane compiler operating in accordance with the as-if rule, will remove it during compilation.

One place where at least one statement is required is in a switch block (even if program control never reaches that point):

switch (1){
case 0:
; // Removing this statement causes a compilation error
}


Related Topics



Leave a reply



Submit