3

Is the following code causing undefined behavior?

std::map<int, vector<int>> foo()
{
return ...
}

BOOST_FOREACH(const int& i, foo()[42])
{
std::cout << i << std::endl;
}

If undefined, What is the good way to fix it? What if I use c++11 range-for loop instead of BOOST_FOREACH?

balki
  • 26,394
  • 30
  • 105
  • 151
  • Why do you think there can be UB here? – zch Feb 10 '14 at 14:53
  • I am seeing a memory corruption in similar code. But not sure if it is because of such usage. – balki Feb 10 '14 at 14:55
  • Check out the [sourcecode](http://www.boost.org/doc/libs/1_55_0/boost/foreach.hpp). There seems to be some macro magic to detect if argument is rvalue and in that case, it copies the argument. – eerorika Feb 10 '14 at 14:59
  • 1
    @user2079303: unfortunately, `std::map<...>::operator[]` returns a l-value reference (into a temporary object); not an r-value; thus the lifetime of the `map` temporary is not extended properly and we end up with a reference into the nether. – Matthieu M. Feb 10 '14 at 15:48

3 Answers3

3

This is, unfortunately, most probably undefined behavior.

The problem is that you have two levels here:

  1. std::map<...> is an r-value, its lifetime will be expanded until the end of the full-expression
  2. std::vector<int>& is an l-value reference (into an object), its lifetime is that of the object.

The problem arises because the code (roughly) expands to something like:

// from
for (<init>: <expr>) {
    <body>
}

// to
auto&& __container = <expr>;
for (auto __it = begin(container), __e = end(container); __it != __e; ++__it)
{
    <init> = *__it;
    <body>
}

The issue here is in the initialization of __container:

auto&& __container = foo()[42];

If it where just foo(), this would work, because the lifetime of std::map<...> would be extended to match that of __container, however in this case we get:

// non-standard gcc extension, very handy to model temporaries:
std::vector<int>& __container = { std::map<...> m = foo(); m[42] };

And thus __container ends up pointing into the nether.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Can you state with certainty that it is always undefined? – balki Feb 10 '14 at 16:13
  • @balki: no, though I harbor few doubts. As far as I know, the range-for could be improved to handle this -- because the transformation presented in the Standard is non-normative and the compiler knows about all temporaries in presence. However I just see now way how Boost could handle this because the type system is insufficiently expressive to reflect the issue. – Matthieu M. Feb 10 '14 at 17:33
  • The transformation in 6.5.4/1 is normative, except for the names of the exposition-only variables `__range`, `__begin`, and `__end`. I don't believe a compiler could remove the undefined behavior here without necessarily violating the rules for the lifetime of temporaries - so it would be at best a non-conforming extension. It would seem to require a core language change to extend the lifetime of any temporaries created in `` to the entire loop body. – Casey Feb 10 '14 at 18:05
  • @Casey: Ah, thanks for the clarification. Yes, obviously extending the lifetime of any temporary would have to be normed or there would be portability issues. – Matthieu M. Feb 10 '14 at 18:32
2

The return value exists until the end of the full expression which creates it. So it all depends on how BOOST_FOREACH expands; if it creates a scope outside of the for loop, and copies the return value to a variable in it (or uses it to initialize a reference), then you're safe. If it doesn't, you're not.

The C++11 range-for loop basically has the semantics of binding to a reference in a scope outside of a classic for-loop, so it should be safe.

EDIT:

This would apply if you were capturing the return value of foo. As Benjamin Lindley points out, you aren't. You're capturing the return value of operator[] on a map. And this is not a temporary; it is a reference. So no extension of lifetime occurs, neither in BOOST_FOREACH nor in range-for. Which means that the map itself will be destructed at the end of the full expression which contains the function call, and that undefined behavior occurs. (Boost could, I suppose, make a deep copy of the map, so you'd be safe. But somehow, I doubt that it does.)

END OF EDIT:

Never the less, I would question the wisdom of returning an std::map when all you want is a single entry in it. If the map actually exists outside the function (is not on the heap), then I'd return a reference to it. Otherwise, I'd find some what that it did.

James Kanze
  • 150,581
  • 18
  • 184
  • 329
  • Are you certain it should work in range-for loops? http://coliru.stacked-crooked.com/a/165d826532b7eb84 – Benjamin Lindley Feb 10 '14 at 15:23
  • @BenjaminLindley The standard certainly seems to say so. In §6.5.4, it gives the equivalent code of a range-based for, and there, the initialization expression is bound to a reference, so it's lifetime should be extended to match that of the reference. (Of course, given that this is a new feature, I wouldn't trust any compiler to have gotten it 100% right.) – James Kanze Feb 10 '14 at 16:30
  • @BenjaminLindley But as you point out in a comment to another answer, which I've just read, he's not initializing the reference with the return value of the function; he's initializing it with the return value of `[]` on the map. And since this returns a reference (rather than a temporary), there is no extension of lifetime. Well spotted; I'd missed that. – James Kanze Feb 10 '14 at 16:32
0

From: http://www.boost.org/doc/libs/1_55_0/doc/html/foreach.html

Iterate over an expression that returns a sequence by value (i.e. an rvalue):

extern std::vector<float> get_vector_float();
BOOST_FOREACH( float f, get_vector_float() )
{
    // Note: get_vector_float() will be called exactly once
}

So it is well defined and works.

As well, it is well defined in C++11 (and works):

for (const int& i : get_vector()) // get_vector() computed only once
{
    std::cout << i << std::endl;
}

The problem here is that foo()[42] returns a reference from a temporary (via a method).

auto& v = foo()[42];

life of foo() temporary is not extended...

You may solve that by extending foo temporary lifetime

auto&& m = foo();

for (const int& i : m[42]) {
    std::cout << i << std::endl;
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302