为什么 std::decltype 返回对命名左值对象的引用?

why std::decltype is returning reference to a named lvalue object?

我在 Scott Meyer 的 effective C++ 中读到,对于名称以外的类型 T 的左值表达式,decltype 总是报告 T& 的类型,我似乎理解这一点( ).但是,我看到 在某些设置下 decltype 在 class 的某种类型 Y 的非静态命名成员变量上被调用时,结果类型是 Y& 而不是 Y,这对我来说很不寻常。

下面是下面的代码。 背景: 我正在尝试使用 SFINAE 来排除基于 return 类型的模板函数重载。这是完整的代码。

#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;
}

说明: testFun 我们有 2 个重载(参见注释 OVERLOAD 1 和 OVERLOAD2)。

对于 foo 的当前实现,调用 OVERLOAD2 是因为它期望存在函数 someFun,该函数在 foo.h 文件中声明。此外,此重载的 return 类型由 decltype((void) (t.someFun()),t.somestring) 给出,return 是 t.somestring 类型的类型,即 std::string。但是,当我尝试按原样编译该函数时,它会给我编译警告,并且没有创建可执行文件。

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

这让我相信 testFun(params) 的 return 类型被推断为 std::string& 而不是 std::string。为什么会这样?

此外,如果我注释行 //3 并取消注释 //4,代码编译良好并且行 //5 输出 true,这确认了 decltype(t.somestring) 确实没有参考资格。那么为什么原来的设置(line //3 uncomment , line //4 commented)不起作用?

[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

您似乎期望 decltype((void) (t.someFun()),t.someFun()) 的行为如 (4.2) 项目符号中所述 - 但其参数实际上不是未加括号的 id 表达式或 class 成员访问。因此,它遵循项目符号 (4.4) 并生成类型 std::string&。另一方面,decltype(t.somestring) 实际上遵循 (4.2) 并产生 std::string;这里的操作数是一个未加括号的 class 成员访问。

tl;博士:

  • ((void) (t.someFun()),t.somestring) 不是 id-expression,因此适用以下规则:

    (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)是一个左值,所以根据(1.5)它的decltype是std::string&
  • ((void) (t.someFun()),t.someFun()) 是一个纯右值,所以根据 (1.6) 它的 decltype 是 std::string

详细说明

1。逗号运算符

让我们从逗号运算符的结果类型开始:

7.6.20 Comma operator(强调我的)

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

所以逗号运算符的结果将是它的右操作数(包括它的值类别 - 所以如果右操作数是左值,那么逗号运算符的结果也是如此)

2。 t.somestring
的值类别

那么 ((void) (t.someFun()),t.somestring) 的结果是什么?

这是 class 成员访问权限,因此以下内容适用:

7.6.1.5 Class member access(强调我的)

(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. [...]

所以鉴于 t 是一个左值(你将其声明为 T& t)我们可以得出结论 t.somestring 也必须是一个左值。
(如果你写了例如 ((void) (t.someFun()),std::move(t).somestring) 结果将是一个 xvalue)

3。 t.someFun()
的值类别

鉴于这是一个函数调用,我们可以直接跳到函数调用的结果应该是什么:

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.

鉴于 someFun() returns 按值计算最后一种情况适用:结果将是纯右值。

4。 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) 满足 (1.3)(这是一个未加括号的 class 成员访问)。

所以它的结果是 t.somestring -> std::string 的类型。

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

decltype((void) (t.someFun()),t.somestring) 不满足 (1.1) - (1.3) 因为它不是 id 表达式。

所以它一定是最后 3 个案例之一 ( (1.4) - (1.6) )

鉴于t.somestring的类型是std::string,并且在这种情况下逗号运算符的结果是左值,我们需要应用(1.5).

所以结果是std::string&.

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

同上,我们可以排除前 3 种情况,因为表达式不是 id-expression。

t.someFun() 的结果是一个纯右值,所以 (1.4) 和 (1.5) 都不适用。

所以剩下的唯一选项是(1.6),结果是std::string

5。再举几个例子

再举几个例子来证明:

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

更好的 C++20 方法

在 C++20 中,我们现在有了 require 子句,使这些检查变得容易得多(而且您收到的错误消息不会像 SFINAE 的那样神秘)

你可以例如像这样写这些支票:

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;
    /* ... */
}

如果你想重用它,你也可以把它分解成概念:

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;
    /* ... */
}