Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Segfault when not specifying return type of lambda function

Tags:

c++

c++11

lambda

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)
like image 471
RenatoUtsch Avatar asked Jun 18 '19 16:06

RenatoUtsch


People also ask

Does lambda function have return type?

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.

Can a function return a lambda C++?

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.


2 Answers

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.

like image 152
Oliv Avatar answered Sep 19 '22 10:09

Oliv


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 of std::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;
}
like image 28
Oblivion Avatar answered Sep 22 '22 10:09

Oblivion