Home > Software engineering >  Reference in container?
Reference in container?

Time:10-15

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