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;
}
