【问题标题】:why std::decltype is returning reference to a named lvalue object?为什么 std::decltype 返回对命名左值对象的引用?
【发布时间】:2021-12-27 22:10:29
【问题描述】:

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

以下是以下代码。 背景: 我正在尝试使用 SFINAE 根据返回类型排除模板函数重载。这是完整的代码。

#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 个重载(参见 cmets OVERLOAD 1 和 OVERLOAD2)。

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

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

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

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

【问题讨论】:

标签: c++ templates sfinae decltype


【解决方案1】:

tl;博士:

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

    (1.4) - 如果E 是一个xvalue,则decltype(E)T&amp;&amp;,其中TE 的类型;
    (1.5 ) - 如果 E 是左值,decltype(E)T&amp;,其中 TE 的类型;
    (1.6) - 否则, decltype(E)E 的类型。

  • ((void) (t.someFun()),t.somestring) 是一个左值,所以根据 (1.5) 它的 decltype 是 std::string&amp;
  • ((void) (t.someFun()),t.someFun()) 是纯右值,因此根据 (1.6),它的 decltype 是 std::string

详细说明

1。逗号运算符

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

7.6.20 Comma operator(强调我的)

(1) 一对用逗号分隔的表达式从左到右求值;左边的表达式是discarded-value expression。左表达式在右表达式之前排序 ([intro.execution])。 结果的类型和值是右操作数的类型和值结果与其右操作数的值类别相同,如果其右操作数是位域,则结果是位域。

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

2。 t.somestring的值类别

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

这是一个类成员访问,所以以下适用:

7.6.1.5 Class member access(强调我的)

(3) postfix-expression.id-expression 缩写为 E1.E2,E1 称为对象表达式。
[...]
(6) 如果 E2 被声明为具有“对 T 的引用”类型,则 E1.E2 是左值; E1.E2 的类型为 T。否则,适用以下规则之一。
[...]
(6.1) 如果 E2 是非静态数据成员,并且 E1 的类型是“cq1 vq1 X”,而 E2 的类型是“cq2 vq2 T” ,表达式指定第一个表达式指定的对象的对应成员子对象。 如果 E1 是左值,则 E1.E2 是左值;否则 E1.E2 是一个 xvalue。 [...]

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

3。 t.someFun()的值类别

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

7.6.1.3 Function call

(14) 如果结果类型是左值引用类型或对函数类型的右值引用,则函数调用是左值,如果结果类型是对对象类型的右值引用,则为xvalue,否则为prvalue。

鉴于someFun() 按值返回最后一种情况适用:结果将是prvalue。

4。来自decltype(...) 的结果类型

9.2.9.5 Decltype specifiers

(1) 对于表达式Edecltype(E)表示的类型定义如下:

  • (1.1) 如果E 是一个无括号的id-expression 命名结构化绑定([dcl.struct.bind]),decltype(E) 是结构化绑定声明规范中给出的引用类型;李>
  • (1.2) 否则,如果E 是无括号的id-expression 命名非类型template-parameter ([temp.param]),则decltype(E)template-parameter 在执行任何操作后的类型必要的类型推导([dcl.spec.auto],[dcl.type.class.deduct]);
  • (1.3) 否则,如果E 是无括号的id-expression 或无括号的类成员访问([expr.ref]),则decltype(E)E 命名的实体的类型。如果没有这样的实体,则程序格式错误;
  • (1.4) 否则,如果E 是xvalue,则decltype(E)T&amp;&amp;,其中TE 的类型;
  • (1.5) 否则,如果E 是左值,则decltype(E)T&amp;,其中TE 的类型;
  • (1.6) 否则,decltype(E)E 的类型。
4.1decltype(t.somestring)

decltype(t.somestring) 满足 (1.3)(这是一个无括号的类成员访问)。

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

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

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

所以它必须是最后三种情况之一((1.4)-(1.6))

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

所以结果是std::string&amp;

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

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

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

【讨论】:

    【解决方案2】:

    [dcl.type.simple]/4 对于表达式edecltype(e)表示的类型定义如下:

    (4.2) — 否则,如果 e 是无括号的 id-expression 或无括号的类成员访问 (8.2.5),decltype(e) 是由 @ 命名的实体的类型987654325@
    (4.4) — 否则,如果e 是左值,则decltype(e)T&amp;,其中Te 的类型

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-12-21
      • 1970-01-01
      • 2012-02-22
      • 2014-10-08
      • 2014-07-30
      • 2022-10-24
      • 2020-02-11
      相关资源
      最近更新 更多