This is very similar to a question I asked earlier today. However, the example I cited in that question was incorrect; by mistake, I was looking at the wrong source file, not the one that actually had the error I described. Anyway, here's an example of my problem:
struct base { };
struct child1 : base { };
struct child2 : base { };
child1 *c1;
child2 *c2;
// Goal: iterate over a few derived class pointers using a range-based for loop.
// initializer_lists are convenient for this, but we can't just use { c1, c2 }
// here because the compiler can't deduce what the type of the initializer_list
// should be. Therefore, we have to explicitly spell out that we want to iterate
// over pointers to the base class.
for (auto ptr : std::initializer_list<base *>({ c1, c2 }))
{
// do something
}
This was the site of some subtle memory corruption bugs in my application. After experimenting a bit, I found that changing the above to the following made the crashes that I was observing go away:
for (auto ptr : std::initializer_list<base *>{ c1, c2 })
{
// do something
}
Note that the only change was an extra set of parentheses around the braced initializer list. I'm still trying to wrap my mind around all of the forms of initialization; am I right in surmising that in the first example, the inner braced initializer is a temporary that doesn't get its lifetime extended for the lifetime of the loop, due to the fact that it is an argument to the std::initializer_list
copy constructor? Based on this previous discussion of what the equivalent statement for the range-based for is, I think that's the case.
If this is true, then does the second example succeed because it uses direct list-initialization of the std::initializer_list<base *>
, and therefore the contents of the temporary braced list get their lifetime extended to the duration of the loop?
Edit: After some more work, I now have a self-contained example that illustrates the behavior that I'm seeing in my application:
#include <initializer_list>
#include <memory>
struct Test
{
int x;
};
int main()
{
std::unique_ptr<Test> a(new Test);
std::unique_ptr<Test> b(new Test);
std::unique_ptr<Test> c(new Test);
int id = 0;
for(auto t : std::initializer_list<Test*>({a.get(), b.get(), c.get()}))
t->x = id++;
return 0;
}
If I compile this on macOS with Apple LLVM version 8.1.0 (clang-802.0.42), as follows:
clang++ -O3 -std=c++11 -o crash crash.cc
then the resulting program dies with a segfault when I run it. Using an earlier version of clang (8.0.0) doesn't exhibit the issue. Likewise, my tests with gcc on Linux did not have any issues either.
I'm trying to determine whether this example illustrates undefined behavior due to the temporary lifetime issues I mentioned above (in which case my code would be in the wrong, and the newer clang version would be vindicated), or whether this is potentially just a bug in this particular clang release.
Edit 2: The test is live here. It looks like the change in behavior occurred between mainline clang v3.8.1 and v3.9.0. Previous to v3.9, the effective behavior of the program was just int main() { return 0; }
, which is reasonable as there are no side effects to the code. In later versions, however, the compiler seems to optimize out the calls to operator new
but keeps the writes to t->x
in the loop, hence trying to access an uninitialized pointer. I suppose that might point to a compiler bug.
Base* ptr1 = new Child1
... you can directly writefor( auto ptr: { ptr1, ptr2 })...
As all ptr have the same type ( the base ) there is no problem with type deduction. The same if using smart pointers with base type. – Illinois