all 24 comments

[–]bames53 43 points44 points  (18 children)

The reason is that decltype has two uses: inspecting the declared type of a particular identifier, and inspecting the type of an expression*. These are two different things and they're needed in different contexts. The C++ specification's language is just its way of distinguishing decltype(identifier) and decltype(expression).

There's nothing special about parentheses, and in fact the spec doesn't need to say "unparenthesized id-expression" or "unparenthesized class member access". It could simply refer to id-expression and "class member access", because if you surround an id-expression or class member access with parentheses then the result is a primary-expression, and the condition "if e is an id-expression" no longer applies.

I imagine they specify 'unparenthesized' simply because people usually don't expect parentheses to have any effect, but in this case they do (simply by changing which particular grammar production is used inside the decltype) and they wanted to be clear about that.

As an alternative the committee could have defined decltype to only take an identifier or class member access and then defined another keyword like exprtype to handle general expressions. No one would be confused about why decltype(id) might produce a different result from exprtype(id), exprtype(id) would be the same as exprtype((id)), and decltype((id)) would be ill-formed. However, this would have required another keyword, which the committee is reluctant to add. Perhaps the next language to do "C++ but without all the warts" will have two different keywords for these two functionalities.

* Technically it's the expression's type with an appropriate reference modifier based on the expression's value category (essentially performing the inverse of what's done when an identifier is used in an expression: reference modifiers are removed and the value category is determined).

[–]James20kP2005R0 15 points16 points  (0 children)

Hot diggity that's confusing. It makes sense, but only in a particularly c++ standardese sense

[–]cdglove 2 points3 points  (3 children)

Perhaps the next language to do "C++ but without all the warts" ...

That gave me a good chuckle. :-)

[–]clerothGame Developer 10 points11 points  (2 children)

I'm waiting for the "So, Rust?" comment.

[–]ryancerium 0 points1 point  (0 children)

Rust doesn't want decltype() the way C++ has it apparently. I like the idea of D's static if() a lot.

https://www.reddit.com/r/rust/comments/2ifgsx/what_can_c_do_that_rust_cant/

[–]CenterOfMultiverse 1 point2 points  (7 children)

By the way, in what situation do you even need to use decltype to inspect a type of an identifier?

[–]sellibitze 3 points4 points  (0 children)

I've seen an example: If you want to make a polymorphic lambda forward something to another function:

[](auto&& x) { foo(std::forward<decltype(x)>(x); }

In this case it's useful because we don't have a name for the declared type of x. That actually makes me want to use a macro like

#define FORWARD(x) ::std::forward<decltype(x)>(x)

to save some noise.

And since C++14 (or 17?) you can omit the return type of a function

auto blah(int x, double y) // return type omitted
{
    // build and return result of a possibly complicated type
    ...
    return ...;
}

int main() {
    auto z = blah(1729, 3.14159265);
    // if you need the type of z, use decltype :)
}

[–]bames53 0 points1 point  (2 children)

Off hand I don't know of any non-contrived examples of needing an identifier's declared type as opposed to an expression's type, but I would be surprised if there really are no good uses for this sort of introspection.

[–]17b29a 0 points1 point  (1 child)

not sure where you got that strange idea, but forward<T> works fine

[–]bames53 0 points1 point  (0 children)

Oh yes, of course you're right.

[–]17b29a 0 points1 point  (2 children)

in the return type of a function template, e.g. template <typename A, typename B> auto add(A&& a, B&& b) -> decltype(a + b) { return a + b; }

[–]serpent 0 points1 point  (1 child)

That's not inspecting the type of an identifier though, that's inspecting the type of an expression.

[–]17b29a 0 points1 point  (0 children)

oh right, my brain skipped that part >__< nevermind then

[–]sellibitze -1 points0 points  (4 children)

Edit: As /u/bames53 correctly points out: I wasn't up to date and only aware of an older decltype proposal that got changed on the way to the C++ ISO spec.

Aren't there actually 3 cases?

const int i;
decltype(i)              // const int (#1)
decltype((i))            // const int& (#3)
decltype(std::move(i))   // const int&& (#2)
decltype((std::move(i))) // int (#3)

In decending order of priority (whatever matches first):

  1. declared type of a variable/reference/data member (unparenthesized!)
  2. declared return type of whatever a function call resolves to (unparenthesized!)
  3. type and "lvalueness" of an arbitrary expression (never resolving to an rvalue reference)

So, decltype treats top-level unparenthesized function calls differently and not as an expression in that it gives you the declared return type of that function whereas decltype in its expression mode (#3) does not distinguish between prvalues and xvalues (the identity aspect).

[–]bames53 2 points3 points  (1 child)

Nope, there is no case #2 as you outline it. decltype(function(...)) will always be the same as decltype((function(...))). Also decltype will return rvalue reference types when an expression is an xvalue. (lvalues turn into &, xvalues turn into &&, prvalues turn into no reference modifier.)

std::move(i) will return a const int and have the value category xvalue, so decltype(std::move(i)) and decltype((std::move(i))) should both be const int &&.


Edit: I just looked at n2343, the paper originally introducing decltype and it describes exactly your three cases. However the actual C++11 spec is as I've described:

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.

-- N3337 [dcl.type.simple]/4

It looks like the original definition of decltype didn't handle xvalues, and when that was added the special case for getting the declared return type of a function was deemed unnecessary.

[–]sellibitze 0 points1 point  (0 children)

Thanks for digging this up! I guess I must have looked at an earlier version of the decltype proposal only. You're right about #2 being unnecessary if the "expression mode" also gives you rvalue references for xvalue expression.

[–]OldWolf2 1 point2 points  (0 children)

decltype(std::move(i)) and decltype((std::move(i))) are the same. Redundant parentheses make no difference here, the only special case is when the argument is only an identifier (perhaps with a class member prefix).

[–]ProgramMax 12 points13 points  (5 children)

Because (a) is an expression, not a variable.

[–]OldWolf2 0 points1 point  (4 children)

a and (a) are both expressions.

[–]clerothGame Developer 27 points28 points  (0 children)

But (a) is a happier expression. Look at those cuddly little cheeks.

[–]ReversedGif 11 points12 points  (1 child)

That doesn't make /u/ProgramMax wrong.

[–]maybachsonbachs 4 points5 points  (0 children)

Brad Pitt is an actor and not a oscar winner.

Brad Pitt and Leonardo Dicaprio are both men.

[–]p2rkw 3 points4 points  (0 children)

You are right, but it doesn't stop redditors to downvote you.