I'm having an issue with passing a string as reference to a lambda, when it is in a container. I guess it disappears (goes out of scope) when I call the init()
function, but why? And then, why doesn't it disappear when I just pass it as a string reference?
#include <iostream>
#include <string>
struct Foo {
void init(int num, const std::string& txt)
{
this->num = num;
this->txt = txt;
}
int num;
std::string txt;
};
int main()
{
auto codegen1 = [](std::pair<int, const std::string&> package) -> Foo* {
auto foo = new Foo;
foo->init(package.first, package.second); //here string goes out of scope, exception
return foo;
};
auto codegen2 = [](int num, const std::string& txt) -> Foo* {
auto foo = new Foo;
foo->init(num, txt);
return foo;
};
auto foo = codegen1({3, "text"});
auto foo2 = codegen2(3, "text");
std::cout << foo->txt;
}
I know the way to go would be to use const std::pair<int, std::string>&
, but I want to understand why this approach doesn't work.
CodePudding user response:
Part 1: This looks busted at a glance.
"text"
is not a string, it's a literal that is being used to create a temporary std::string
object, which is then used to initialize the std::pair
. So you'd think it would make sense that the string, which is only needed transiently (i.e only until the std::pair
is constructed), is gone by the time it is being referred to.
Part 2: But it shouldn't be busted.
However, any temporaries that are created as part of an expression are supposed to be guaranteed to live until the end of the current "full-expression" (simplified: until the semicolon).
That's why the call to codegen2()
works fine. A temporary std::string
is created, and it stays alive until the call to codegen2()
is complete.
Part 3: yet it is busted, in this case.
So why does the string get destroyed prematurely in codegen1()
's case? The conversion from "text"
to std::string
does not happen as a sub-expression, but as part of a separate function being called with its own scope.
The constructor of std::pair
that is being used here is:
// Initializes first with std::forward<U1>(x) and second with std::forward<U2>(y).
template< class U1 = T1, class U2 = T2 >
constexpr pair( U1&& x, U2&& y );
The constructor gets "text"
as a parameter and the construction of the std::string
is being done inside of std::pair
's constructor, so the temporary variable created as part of that process gets cleaned up when we return from that constructor.
The funny thing is, if that constructor did not exist, std::pair
's basic constructor: pair( const T1& x, const T2& y );
would handle this just fine.
How do we fix this?
There's a few alternatives:
Force the conversion to happen at the right "level":
auto foo = codegen1({3, std::string("text")});
Effectively the same thing, but with a nicer syntax:
using namespace std::literals::string_literals;
auto foo = codegen1({3, "text"s});
Use a std::string_view
, which removes the need for the conversion altogether:
auto codegen1 = [](std::pair<int, std::string_view> package) -> Foo* {
...
}
Though, in your case, since Foo
will take ownership of the string, passing it by value and moving it around is clearly the way to go:
struct Foo {
Foo(int num, std::string txt)
: num(num)
, txt(std::move(txt))
{}
int num;
std::string txt;
};
int main()
{
auto codegen1 = [](std::pair<int, std::string> package) {
return std::make_unique<Foo>(package.first, std::move(package.second));
};
auto foo = codegen1({3, "text"});
std::cout << foo->txt;
}