为什么 lambda 函数默认删除推导的 return 类型引用?
Why do lambda functions drop deduced return type reference by default?
在 C++14 中,为什么推导的 return 类型的 lambda 函数默认从 return 类型删除引用? IIUC,因为具有推导的 return 类型(没有显式尾随 return 类型)的 C++14 lambda 函数具有 return 类型的 auto
,它会删除引用(在其他事情)。
为什么做出这个决定?在我看来,当你的 return 声明 returns.
时,删除引用就像一个陷阱
此行为对我造成了以下讨厌的错误:
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.
在初始化时指定所需的 return 类型 d
解决了问题:
D d{ [&c]() -> const Int& { return c.getObj(); } }
有趣的是,即使 auto
return 类型推导是有道理的,std::function<const Int&>
使用 return 的函数愉快地初始化不是一个错误吗?非参考?我也通过明确写作看到了这一点:
D d{ [&c]() -> Int { return c.getObj(); } }
编译没有问题。 (在 Xcode 8
、clang 8.0.0
上)
standard(至少是工作草案)已经为您提供了有关正在发生的事情以及如何解决的提示:
The lambda return type is auto
, which is replaced by the type specified by the trailing-return-type if provided and/or deduced from return statements as described in [dcl.spec.auto]. [ Example:
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&
— end example ]
现在考虑以下模板函数:
template<typename T>
void f(T t) {}
// ....
int x = 42;
f(x);
f
中的 t
是什么,x
的副本或对它的引用?
如果我们按如下方式更改函数会发生什么?
template<typename T>
void f(T &t) {}
这同样或多或少地适用于推导的 return 类型的 lambda:如果你想要一个引用,你必须对此明确。
Why was this decision made? It seems to me like a gotcha to remove a reference when that's what your return statement returns.
选择与模板从一开始的工作方式一致。
相反,我会感到惊讶。
Return 类型和模板参数一起推导出来,不为它们定义不同的规则集是一个很好的决定(至少从我的角度来看)。
也就是说,要解决您的问题,您有多种选择:
[&c]()->auto&&{ return c.getObj(); }
[&c]()->auto&{ return c.getObj(); }
[&c]()->decltype(c.getObj())&{ return c.getObj(); }
[&c]()->decltype(c.getObj())&&{ return c.getObj(); }
[&c]()->decltype(auto){ return c.getObj(); }
[&c]()->const Int &{ return c.getObj(); }
...
有的很牛逼,有的很清楚,应该都行。
如果预期的行为是 return 引用,最好的选择可能是明确说明:
[&c]()->auto&{ return c.getObj(); }
无论如何,这主要是基于意见,所以请随意选择您喜欢的替代方案并使用它。
Interestingly, even if the auto return type deduction makes sense, isn't it a bug that std::function gets happily initialized with a function that returns a non-reference?
让我们考虑下面的代码(现在没有理由调用 std::function
):
int f() { return 0; }
const int & g() { return f(); }
int main() { const int &x = g(); }
它会给你一些警告,但它会编译。
原因是临时文件是从右值创建的,并且临时文件可以绑定到 const 引用,所以我认为从标准的角度来看,它是 合法的。
它会在运行时爆炸的事实是另一个问题。
使用 std:: function
时会发生类似的情况。
无论如何,它是对通用可调用对象的抽象,所以不要期待相同的警告。
我认为你绊倒的地方实际上是return c.getObj();
行中的表达式c.getObj()
。
您认为表达式 c.getObj()
的类型为 const Int&
。然而事实并非如此;表达式永远没有引用类型。正如 Kerrek SB 在评论中指出的那样,我们有时会把表达式当作具有引用类型来讨论,作为避免冗长的捷径,但这会导致误解,所以我认为了解真正发生的事情很重要。
在声明中使用引用类型(包括 getObj
声明中的 return 类型)会影响被声明的事物的初始化方式,但一旦初始化,不再有任何证据表明它最初是一个参考。
这是一个更简单的例子:
int a; int &b = a; // 1
对比
int b; int &a = b; // 2
这两个代码完全相同(除了 decltype(a)
或 decltype(b)
的结果对系统有点 hack)。在这两种情况下,表达式 a
和 b
都具有类型 int
和值类别 "lvalue" 并且表示相同的对象。 a
并不是 "real object" 而 b
是指向 a
的某种变相指针。他们都是平等的。这是一个有两个名字的对象。
现在回到您的代码:表达式 c.getObj()
与 c.m_obj
具有完全相同的行为,除了访问权限。类型为 Int
,值类别为 "lvalue"。 getObj()
的 return 类型中的 &
仅指示这是一个左值,它还会指定一个已经存在的对象(大约来说)。
因此从 return c.getObj();
推导的 return 类型与 return c.m_obj;
的推导类型相同,这与模板类型推导兼容,如其他地方所述 -不是引用类型。
注意。如果你理解了这一点 post,你也会理解为什么我不喜欢 "references" 的教学法被教导为 "disguised pointers that auto dereference",这是介于错误和危险之间的方法。
在 C++14 中,为什么推导的 return 类型的 lambda 函数默认从 return 类型删除引用? IIUC,因为具有推导的 return 类型(没有显式尾随 return 类型)的 C++14 lambda 函数具有 return 类型的 auto
,它会删除引用(在其他事情)。
为什么做出这个决定?在我看来,当你的 return 声明 returns.
时,删除引用就像一个陷阱此行为对我造成了以下讨厌的错误:
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.
在初始化时指定所需的 return 类型 d
解决了问题:
D d{ [&c]() -> const Int& { return c.getObj(); } }
有趣的是,即使 auto
return 类型推导是有道理的,std::function<const Int&>
使用 return 的函数愉快地初始化不是一个错误吗?非参考?我也通过明确写作看到了这一点:
D d{ [&c]() -> Int { return c.getObj(); } }
编译没有问题。 (在 Xcode 8
、clang 8.0.0
上)
standard(至少是工作草案)已经为您提供了有关正在发生的事情以及如何解决的提示:
The lambda return type is
auto
, which is replaced by the type specified by the trailing-return-type if provided and/or deduced from return statements as described in [dcl.spec.auto]. [ Example:
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&
— end example ]
现在考虑以下模板函数:
template<typename T>
void f(T t) {}
// ....
int x = 42;
f(x);
f
中的 t
是什么,x
的副本或对它的引用?
如果我们按如下方式更改函数会发生什么?
template<typename T>
void f(T &t) {}
这同样或多或少地适用于推导的 return 类型的 lambda:如果你想要一个引用,你必须对此明确。
Why was this decision made? It seems to me like a gotcha to remove a reference when that's what your return statement returns.
选择与模板从一开始的工作方式一致。
相反,我会感到惊讶。
Return 类型和模板参数一起推导出来,不为它们定义不同的规则集是一个很好的决定(至少从我的角度来看)。
也就是说,要解决您的问题,您有多种选择:
[&c]()->auto&&{ return c.getObj(); }
[&c]()->auto&{ return c.getObj(); }
[&c]()->decltype(c.getObj())&{ return c.getObj(); }
[&c]()->decltype(c.getObj())&&{ return c.getObj(); }
[&c]()->decltype(auto){ return c.getObj(); }
[&c]()->const Int &{ return c.getObj(); }
...
有的很牛逼,有的很清楚,应该都行。
如果预期的行为是 return 引用,最好的选择可能是明确说明:
[&c]()->auto&{ return c.getObj(); }
无论如何,这主要是基于意见,所以请随意选择您喜欢的替代方案并使用它。
Interestingly, even if the auto return type deduction makes sense, isn't it a bug that std::function gets happily initialized with a function that returns a non-reference?
让我们考虑下面的代码(现在没有理由调用 std::function
):
int f() { return 0; }
const int & g() { return f(); }
int main() { const int &x = g(); }
它会给你一些警告,但它会编译。
原因是临时文件是从右值创建的,并且临时文件可以绑定到 const 引用,所以我认为从标准的角度来看,它是 合法的。
它会在运行时爆炸的事实是另一个问题。
使用 std:: function
时会发生类似的情况。
无论如何,它是对通用可调用对象的抽象,所以不要期待相同的警告。
我认为你绊倒的地方实际上是return c.getObj();
行中的表达式c.getObj()
。
您认为表达式 c.getObj()
的类型为 const Int&
。然而事实并非如此;表达式永远没有引用类型。正如 Kerrek SB 在评论中指出的那样,我们有时会把表达式当作具有引用类型来讨论,作为避免冗长的捷径,但这会导致误解,所以我认为了解真正发生的事情很重要。
在声明中使用引用类型(包括 getObj
声明中的 return 类型)会影响被声明的事物的初始化方式,但一旦初始化,不再有任何证据表明它最初是一个参考。
这是一个更简单的例子:
int a; int &b = a; // 1
对比
int b; int &a = b; // 2
这两个代码完全相同(除了 decltype(a)
或 decltype(b)
的结果对系统有点 hack)。在这两种情况下,表达式 a
和 b
都具有类型 int
和值类别 "lvalue" 并且表示相同的对象。 a
并不是 "real object" 而 b
是指向 a
的某种变相指针。他们都是平等的。这是一个有两个名字的对象。
现在回到您的代码:表达式 c.getObj()
与 c.m_obj
具有完全相同的行为,除了访问权限。类型为 Int
,值类别为 "lvalue"。 getObj()
的 return 类型中的 &
仅指示这是一个左值,它还会指定一个已经存在的对象(大约来说)。
因此从 return c.getObj();
推导的 return 类型与 return c.m_obj;
的推导类型相同,这与模板类型推导兼容,如其他地方所述 -不是引用类型。
注意。如果你理解了这一点 post,你也会理解为什么我不喜欢 "references" 的教学法被教导为 "disguised pointers that auto dereference",这是介于错误和危险之间的方法。