I have this example code:
// Copyright 2019 Google LLC.
// SPDX-License-Identifier: Apache-2.0
#include <functional>
#include <iostream>
#include <string>
void f(std::function<const std::string&()> fn) {
std::cout << "in f" << std::endl;
std::cout << "str: " << fn() << std::endl;
}
int main() {
std::string str = "a";
auto fn1 = [&]() { return str; };
auto fn2 = [&]() { const std::string& str2 = str; return str2; };
auto fn3 = [&]() -> const std::string& { return str; };
std::cout << "in main" << std::endl;
std::cout << "fn1: " << fn1() << std::endl;
std::cout << "fn2: " << fn2() << std::endl;
std::cout << "fn3: " << fn3() << std::endl;
f(fn1); // Segfaults
f(fn2); // Also segfaults
f(fn3); // Actually works
return 0;
}
When I first wrote this I expected that calling fn1()
inside f()
would properly return a reference to the str
in main
. Given that str
is allocated until after f()
returns, this looked fine to me. But what actually happens is that trying to access the return of fn1()
inside f()
segfaults.
The same thing happens with fn2()
, but what is surprising is that fn3()
works properly.
Given that fn3()
works and fn1()
doesn't, is there something I'm missing about how C++ deduces the return values of lambda functions? How would that create this segfault?
For the record, here are the outputs if I run this code:
calling only f(fn3)
:
in main
fn1: a
fn2: a
fn3: a
in f
str: a
calling only f(fn2)
:
in main
fn1: a
fn2: a
fn3: a
in f
Segmentation fault (core dumped)
calling only f(fn1)
:
in main
fn1: a
fn2: a
fn3: a
in f
Segmentation fault (core dumped)
The return type for a lambda is specified using a C++ feature named 'trailing return type'. This specification is optional. Without the trailing return type, the return type of the underlying function is effectively 'auto', and it is deduced from the type of the expressions in the body's return statements.
Syntax and Return values A C++ lambda function executes a single expression in C++. A value may or may not be returned by this expression. It also returns function objects using a lambda.
A lambda without trailing return type as in:
[&](){return str;};
Is equivalent to:
[&]()->auto{return str;};
So this lambda returns a copy of str.
Calling the std::function
object will result in this equivalent code:
const string& std_function_call_operator(){
// functor = [&]->auto{return str;};
return functor();
}
When this function is called, str
is copied inside a temporary, the reference is bound to this temporary and then the temporary is destroyed. So you get the famous dangling reference. This is a very classical scenario.
The return type deduction of lambda is changed N3638. and now the return type of a lambda
uses the auto
return type deduction rules, which strips the referenceness.
Hence, [&]() { return str;};
returns string
. As a result, in void f(std::function<const std::string&()> fn)
calling fn()
returns a dangling reference. Binding a reference to a temporary extends the lifetime of the temporary, but in this case the binding happened deep inside std::function
's machinery, so by the time f()
returns, the temporary is gone already.
lambda deduction rule
auto and lambda return types use slightly different rules for determining the result type from an expression. auto uses the rules in 17.9.2.1 [temp.deduct.call], which explicitly drops top-level cv-qualification in all cases, while the lambda return type is based on the lvalue-to-rvalue conversion, which drops cv-qualification only for non-class types. As a result:
struct A { }; const A f(); auto a = f(); // decltype(a) is A auto b = []{ return f(); }; // decltype(b()) is const A This seems like an unnecessary inconsistency.
John Spicer:
The difference is intentional; auto is intended only to give a const type if you explicitly ask for it, while the lambda return type should generally be the type of the expression.
Daniel Krügler:
Another inconsistency: with auto, use of a
braced-init-list
can deduce a specialization ofstd::initializer_list;
it would be helpful if the same could be done for a lambda return type.Additional note, February, 2014:
EWG noted that g++ and clang differ in their treatment of this example and referred it back to CWG for resolution.
Let's see what is deduced in your code:
fn1: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >
fn2: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >
fn3: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&
as you can see only the last one is actually a const&
you can check return types of your lambdas with the following code:
//https://stackoverflow.com/a/20170989/10933809
#include <functional>
#include <iostream>
#include <string>
void f(std::function<const std::string&()> fn) {
std::cout << "in f" << std::endl;
std::cout << "str: " << fn() << std::endl;
}
#include <type_traits>
#include <typeinfo>
#ifndef _MSC_VER
# include <cxxabi.h>
#endif
#include <memory>
#include <string>
#include <cstdlib>
template <class T>
std::string
type_name()
{
typedef typename std::remove_reference<T>::type TR;
std::unique_ptr<char, void(*)(void*)> own
(
#ifndef _MSC_VER
abi::__cxa_demangle(typeid(TR).name(), nullptr,
nullptr, nullptr),
#else
nullptr,
#endif
std::free
);
std::string r = own != nullptr ? own.get() : typeid(TR).name();
if (std::is_const<TR>::value)
r += " const";
if (std::is_volatile<TR>::value)
r += " volatile";
if (std::is_lvalue_reference<T>::value)
r += "&";
else if (std::is_rvalue_reference<T>::value)
r += "&&";
return r;
}
int main() {
std::string str = "a";
auto fn1 = [&]() { return str; };
auto fn2 = [&]() { const std::string& str2 = str; return str2; };
auto fn3 = [&]() -> const std::string& { return str; };
std::cout << "in main" << std::endl;
std::cout << "fn1: " << fn1() << std::endl;
std::cout << "fn2: " << fn2() << std::endl;
std::cout << "fn3: " << fn3() << std::endl;
auto f1=fn1();
std::cout << "fn1: " << type_name<decltype(fn1())>() << std::endl;
std::cout << "fn2: " << type_name<decltype(fn2())>() << std::endl;
std::cout << "fn3: " << type_name<decltype(fn3())>() << std::endl;
f(fn1); // Segfaults
f(fn2); // Also segfaults
f(fn3); // Actually works
return 0;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With