为什么lambda函数默认情况下会删除推断的返回类型引用?

时间:2017-01-08 14:56:55

标签: c++ lambda clang c++14 auto

在C ++ 14中,为什么lambda函数带有推导的返回类型默认情况下从返回类型中删除引用? IIUC,因为C ++ 14 lambda函数带有推导的返回类型(没有显式的尾随返回类型),返回类型为auto,它会丢弃引用(以及其他内容)。

为什么做出这个决定?在我看来,当你的返回语句返回时,就像删除引用一样。

这种行为对我造成了以下令人讨厌的错误:

class Int {
public:
   Int(int i) : m_int{i} {}
   int m_int;
};

class C {
public:
    C(Int obj) : m_obj{obj} {}
    const auto& getObj() { return m_obj; }
    Int m_obj;
};

class D {
public:
    D(std::function<const Int&()> f) : m_f{f} {}
    std::function<const Int&()> m_f;
};

Int myint{5};
C c{myint};
D d{ [&c](){ return c.getObj(); } } // The deduced return type of the lambda is Int (with no reference)
const Int& myref = d.m_f(); // Instead of referencing myint, myref is a dangling reference; d.m_f() returned a copy of myint, which is subsequently destroyed.

初始化d时指定所需的返回类型可解决问题:

D d{ [&c]() -> const Int& { return c.getObj(); } }

有趣的是,即使auto返回类型推导有意义,是不是std::function<const Int&>使用返回非引用的函数快乐地初始化的错误?我也明确地写了这个:

D d{ [&c]() -> Int { return c.getObj(); } }

编译没有问题。 (在Xcode 8上,clang 8.0.0

2 个答案:

答案 0 :(得分:5)

我认为你绊倒的地方实际上是c.getObj()行中的return c.getObj();表达式。

您认为表达式c.getObj()的类型为const Int&。但事实并非如此;表达式永远不会有引用类型正如Kerrek SB在评论中所指出的那样,我们有时会将表达式称为具有引用类型,作为保存冗长的快捷方式,但这会导致误解,因此我认为了解实际情况非常重要。

在声明中使用引用类型(包括作为getObj声明中的返回类型)会影响声明的事物的初始化方式,但一旦初始化,就不再有任何证据它最初是一个参考。

这是一个更简单的例子:

int a; int &b = a;  // 1

int b; int &a = b;  // 2

这两个代码完全相同decltype(a)decltype(b)的结果除外,这对系统来说是一个黑客攻击)。在这两种情况下,表达式ab都具有类型int和值类别“lvalue”,并且表示相同的对象。情况不是a是“真实对象”而b是指向a的某种伪装指针。他们都处于平等地位。这是一个有两个名字的对象。

现在回到您的代码:除了访问权限之外,表达式c.getObj()c.m_obj具有完全相同的行为。类型为Int,值类别为“左值”。返回类型&中的getObj()仅指示这是一个左值,它还将指定一个已经存在的对象(大概说)。

因此return c.getObj();推导出的返回类型与return c.m_obj;相同,其与模板类型推导兼容,如其他地方所述 - 不是引用类型。

NB。如果你理解了这篇文章,你也会理解为什么我不喜欢被称为“自动取消引用的伪装指针”的“参考”教学法,这是错误的和危险的。

答案 1 :(得分:3)

standard(至少是工作草案)已经为您提供了有关正在发生的事情以及如何解决问题的提示:

  

lambda返回类型是auto,如果在[dcl.spec.auto]中描述的提供和/或从return语句中推导出来,则由trailing-return-type指定的类型替换。 [实施例:

    auto x1 = [](int i){ return i; }; // OK: return type is int
    auto x2 = []{ return { 1, 2 }; }; // error: deducing return type from braced-init-list int j;
    auto x3 = []()->auto&& { return j; }; // OK: return type is int& 
  

- 结束示例]

现在考虑以下模板功能:

template<typename T>
void f(T t) {}

// ....

int x = 42;
f(x);

t中的fx的副本或对其的引用是什么?
如果按照以下方式更改功能会发生什么?

template<typename T>
void f(T &t) {}

同样适用于推断的lambda返回类型:如果你想要一个引用,你必须明确这一点。

  

为什么做出这个决定?在我看来,当你的返回语句返回时,就像删除引用一样。

选择与模板从一开始就如何工作一致 相反,我会对相反的事感到惊讶 推导出返回类型以及模板参数,这是一个很好的决定,不为它们定义不同的规则集(至少从我的角度来看)。

那就是说,为了解决你的问题,你有几种选择:

  1. [&c]()->auto&&{ return c.getObj(); }
    
  2. [&c]()->auto&{ return c.getObj(); }
    
  3. [&c]()->decltype(c.getObj())&{ return c.getObj(); }
    
  4. [&c]()->decltype(c.getObj())&&{ return c.getObj(); }
    
  5. [&c]()->decltype(auto){ return c.getObj(); }
    
  6. [&c]()->const Int &{ return c.getObj(); }
    
  7. ...
    
  8. 他们中的一些人很疯狂,其中一些很清楚,所有这些都应该有效 如果预期的行为是返回引用,可能是明确的,这是最好的选择:

    [&c]()->auto&{ return c.getObj(); }
    

    无论如何,这主要是基于意见的,所以请随意选择您喜欢的替代品并使用它。

      

    有趣的是,即使自动返回类型推断有意义,是不是std :: function很快就用一个返回非引用的函数初始化的错误?

    让我们考虑下面的代码(没有理由立即调用std::function):

    int f() { return 0; }
    const int & g() { return f(); }
    int main() { const int &x = g(); }
    

    它会给你一些警告,但它会编译 原因是从rvalue创建临时值,临时值可以绑定到const引用,所以从标准的角度来看,我会说它是 legal
    它会在运行时爆炸的事实是另一个问题。

    使用std:: function时会发生类似情况 无论如何,它是对通用可调用对象的抽象,所以不要指望相同的警告。