Decltype and Parentheses

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 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.

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)))).

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.

decltype and parenthesis answers are wrong?

This question was answered in the comments. However I would like to teach future readers how to answer this question yourself. Adding a little code to this example can make the compiler itself tell you what the types are:

#include <type_traits>
#include <typeinfo>
#include <iostream>
#ifndef _MSC_VER
# include <cxxabi.h>
#endif
#include <memory>
#include <string>
#include <cstdlib>

template <typename T>
std::string
type_name()
{
typedef typename std::remove_reference<T>::type TR;
std::unique_ptr<char, void(*)(void*)> own
(
#ifndef _MSC_VER
abi::__cxa_demangle(typeid(TR).name(), nullptr,
nullptr, nullptr),
#else
nullptr,
#endif
std::free
);
std::string r = own != nullptr ? own.get() : typeid(TR).name();
if (std::is_const<TR>::value)
r += " const";
if (std::is_volatile<TR>::value)
r += " volatile";
if (std::is_lvalue_reference<T>::value)
r += "&";
else if (std::is_rvalue_reference<T>::value)
r += "&&";
return r;
}

#include <iostream>

struct A { double x; };

int main()
{
A *a=new A;
std::cout << "decltype(a->x) has type " << type_name<decltype(a->x)>() << '\n';
std::cout << "decltype((a->x)) has type " << type_name<decltype((a->x))>() << '\n';
// decltype(a->x) x3;
// decltype((a->x)) x4 = x3; // is it really const double& ??
// x4=3;// no error !
//
// const double& x5=x3;
// x5=5;//error
}

which outputs:

decltype(a->x) has type double
decltype((a->x)) has type double&

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.

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.

decltype(auto) type deduction: return x vs. return (x)

The FAQ is correct, intuition notwithstanding

(@lubgr pointed out a relevant answer to another question)

The language specification says:

If the argument is either the unparenthesised name of an object/function ... then the decltype specifies the declared type of the entity specified by this expression.

If the argument is any other expression of type T, then ...
b) if the value category of expression is lvalue, then the decltype specifies T&
...
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.

So, the non-parenthesized case is an exceptional/special case, apparently introduced to facilitate returning references with decltype(auto); without the special-casing, the rule @StoryTeller quotes is in effect:

If an expression initially has the type “reference to T” ([dcl.ref], [dcl.init.ref]), the type is adjusted to T prior to any further analysis.

and this would have make it difficult to return references.

Definitely ranks up there with other magick definitions such as the destruction order of tuples or empty strings having 0 at index 0, temporary lifetime extension with references and other similar wonders...



Related Topics



Leave a reply



Submit