Home > Software design >  why std::decltype is returning reference to a named lvalue object?
why std::decltype is returning reference to a named lvalue object?

Time:12-28

I read in Scott Meyer's effective C that for lvalue expressions of type T other than names, decltype always reports a type of T&, which i seem to understand (explained here too). However, I am seeing that under certain setting when decltype is being called on a non static named member variable of some type Y of a class , the resultant type is a Y& instead of Y which looks unusual to me.

Below is the following code. Background: I am trying to use SFINAE to rule out template function overloads based on the return type. Here is the full code.

#include<iostream>
#include<stdio.h>
#include<string>
#include<typeinfo>
class foo
{
    public:
    using  type1  = std::string;
    std::string someFun();
    std::string somestring;
};

//OVERLOAD 1
//Type T must have a function T::size()
template<typename T>
auto testFun(T& t) ->decltype((void) (t.size()),t.somestring)
{
    std::string hello1{"helloworld"};
    return hello1;
}

//OVERLOAD 2
//Type T must have a function T::someFun()
template<typename T>
// auto testFun(T& t) ->decltype((void) (t.someFun()),t.someFun())       //4
auto testFun(T& t) ->decltype((void) (t.someFun()),t.somestring)   //3
{
    std::cout<<std::boolalpha;
    std::cout<<std::is_same<std::string, decltype(t.somestring)>::value<<std::endl; //5
    std::string hello{"helloWorld"};
    return hello;
}

int main()
{
    foo f1;
    testFun(f1);
    return 0;
}

Explanation: We have 2 overloads for testFun(see comments OVERLOAD 1 and OVERLOAD2).

With the current implementation of foo, the OVERLOAD2 is called because it expects existence of a function someFun, which is declared in the foo. Further the return type of this overload is given by decltype((void) (t.someFun()),t.somestring), which returns the type to be of the type of t.somestring, which is std::string. However when I try to compile the function as it is, it gives me the compilation warning, and no executable is created .

warning: reference to local variable 'hello' returned [-Wreturn-local-addr]
     std::string hello{"helloWorld"};

Which makes me believe that the return type of the testFun(params) is inferred to be std::string& and not std::string. Why is that so ?

Further, if I comment the line //3 and uncomment //4, the code compiles well and the line //5 outputs true, which confirms the type of decltype(t.somestring) is indeed not reference qualified . So why the original setting(line //3 uncomment , line //4 commented) did not work ?

CodePudding user response:

[dcl.type.simple]/4 For an expression e, the type denoted by decltype(e) is defined as follows:

(4.2) — otherwise, if e is an unparenthesized id-expression or an unparenthesized class member access (8.2.5), decltype(e) is the type of the entity named by e
(4.4) — otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e

You seem to expect decltype((void) (t.someFun()),t.someFun()) to behave as described in (4.2) bullet - but its argument is not in fact an unparenthesized id-expression or class member access. So instead it follows bullet (4.4) and produces the type std::string&. On the other hand, decltype(t.somestring) does in fact follow (4.2) and produces std::string; the operand here is an unparenthesized class member access.

CodePudding user response:

tl;dr:

  • ((void) (t.someFun()),t.somestring) is not an id-expression, so the following rules apply:

    (1.4) - if E is an xvalue, decltype(E) is T&&, where T is the type of E;
    (1.5) - if E is an lvalue, decltype(E) is T&, where T is the type of E;
    (1.6) - otherwise, decltype(E) is the type of E.

  • ((void) (t.someFun()),t.somestring) is an lvalue, so its decltype is std::string& according to (1.5).
  • ((void) (t.someFun()),t.someFun()) is an prvalue, so its decltype is std::string according to (1.6).

Long Explanation

1. The Comma Operator

Let's start with the result type of the comma operator:

7.6.20 Comma operator (emphasis mine)

(1) A pair of expressions separated by a comma is evaluated left-to-right; the left expression is a discarded-value expression. The left expression is sequenced before the right expression ([intro.execution]). The type and value of the result are the type and value of the right operand; the result is of the same value category as its right operand, and is a bit-field if its right operand is a bit-field.

So the result of the comma operator will be whatever it's right operand was (including it's value category - so if the right operand is a lvalue, so will be the result of the comma operator)

2. The value category of t.somestring

So what would be the result of ((void) (t.someFun()),t.somestring) ?

This is a class member access, so the following applies:

7.6.1.5 Class member access (emphasis mine)

(3) Abbreviating postfix-expression.id-expression as E1.E2, E1 is called the object expression.
[...]
(6) If E2 is declared to have type “reference to T”, then E1.E2 is an lvalue; the type of E1.E2 is T. Otherwise, one of the following rules applies.
[...]
(6.1) If E2 is a non-static data member and the type of E1 is “cq1 vq1 X”, and the type of E2 is “cq2 vq2 T”, the expression designates the corresponding member subobject of the object designated by the first expression. If E1 is an lvalue, then E1.E2 is an lvalue; otherwise E1.E2 is an xvalue. [...]

So given that t is an lvalue (you declared it as T& t) we can conclude that t.somestring must also be an lvalue.
(if you had written e.g. ((void) (t.someFun()),std::move(t).somestring) the result would be an xvalue instead)

3. The value category of t.someFun()

Given that this is a function call, we can skip right to what the result of a function call should be:

7.6.1.3 Function call

(14) A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.

Given that someFun() returns by value the last case applies: the result will be a prvalue.

4. The resulting type from decltype(...)

9.2.9.5 Decltype specifiers

(1) 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 ([dcl.struct.bind]), decltype(E) is the referenced type as given in the specification of the structured binding declaration;
  • (1.2) otherwise, if E is an unparenthesized id-expression naming a non-type template-parameter ([temp.param]), decltype(E) is the type of the template-parameter after performing any necessary type deduction ([dcl.spec.auto], [dcl.type.class.deduct]);
  • (1.3) otherwise, if E is an unparenthesized id-expression or an unparenthesized class member access ([expr.ref]), decltype(E) is the type of the entity named by E. If there is no such entity, the program is ill-formed;
  • (1.4) otherwise, if E is an xvalue, decltype(E) is T&&, where T is the type of E;
  • (1.5) otherwise, if E is an lvalue, decltype(E) is T&, where T is the type of E;
  • (1.6) otherwise, decltype(E) is the type of E.
4.1 decltype(t.somestring)

decltype(t.somestring) satisfies (1.3) (it is an unparenthesized class member access).

So it's result is the type of t.somestring -> std::string.

4.2 decltype((void) (t.someFun()),t.somestring)

decltype((void) (t.someFun()),t.somestring) doesn't satisfy (1.1) - (1.3) since it isn't an id-expression.

So it must be one of the last 3 cases ( (1.4) - (1.6) )

Given that the type of t.somestring is std::string and the result of the comma operator is an lvalue in this case, we need to apply (1.5).

So the result is std::string&.

4.3 decltype((void) (t.someFun()),t.someFun())

Same as above we can eliminate the first 3 cases since the expression is not an id-expression.

The result of t.someFun() was a prvalue, so neither (1.4) nor (1.5) apply.

So the only option that's left is (1.6), and the result is std::string.

5. A few more examples

A few more examples to demonstrate it:

// case 1: xvalue (1.4)
using K = decltype(((void)0, foo{}.somestring));
// K == std::string&&

// case 2: lvalue (1.5)
foo f{};
using K = decltype(((void)0, f.somestring));
// K == std::string&

// case 3: prvalue (1.6)
using K = decltype(((void)0, std::string{"A"}));
// K == std::string

A better C 20 approach

With C 20 we now have require clauses which make those checks a lot easier (and the error messages you get won't be quite as cryptic as the SFINAE ones)

You could e.g. write those checks like this:

godbolt example

template<class T>
  requires requires(T& t) { t.size(); }
auto testFun(T& t)
{
    std::cout << "size()" << std::endl;
    /* ... */
}

template<class T>
  requires requires(T& t) { t.someFun(); }
auto testFun(T& t)
{
    std::cout << "someFun()" << std::endl;
    /* ... */
}

if you want you can also factor it out into concepts if you want to reuse it:

godbolt example

template<class T>
concept Sizeable = requires(T& t) {
    t.size();
};

template<class T>
concept SomeFunAble = requires(T& t) {
    t.someFun();
};


template<Sizeable T>
auto testFun(T& t)
{
    std::cout << "size()" << std::endl;
    /* ... */
}

template<SomeFunAble T>
auto testFun(T& t)
{
    std::cout << "someFun()" << std::endl;
    /* ... */
}
  • Related