Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do Clang and VS2013 accept moving brace-initialized default arguments, but not GCC 4.8 or 4.9?

Like the title suggests, I have a short demo program that compiles on with all of those compilers, but core dumps when ran after compiling with gcc 4.8 and gcc 4.9:

Any ideas as to why?

#include <unordered_map>  struct Foo : std::unordered_map<int,int> {     using std::unordered_map<int, int>::unordered_map;     // ~Foo() = default; // adding this allows it to work };  struct Bar {     Bar(Foo f = {}) : _f(std::move(f)) {}     // using any of the following constructors fixes the problem:     // Bar(Foo f = Foo()) : _f(std::move(f)) {}     // Bar(Foo f = {}) : _f(f) {}      Foo _f; };  int main() {     Bar b;      // the following code works as expected     // Foo f1 = {};     // Foo f2 = std::move(f1); } 

My compilation settings:

g++ --std=c++11 main.cpp 

Here is a backtrace from GDB:

#0  0x00007fff95d50866 in __pthread_kill () #1  0x00007fff90ba435c in pthread_kill () #2  0x00007fff8e7d1bba in abort () #3  0x00007fff9682e093 in free () #4  0x0000000100002108 in __gnu_cxx::new_allocator<std::__detail::_Hash_node_base*>::deallocate () #5  0x0000000100001e7d in std::allocator_traits<std::allocator<std::__detail::_Hash_node_base*> >::deallocate () #6  0x0000000100001adc in std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<int const, int>, false> > >::_M_deallocate_buckets () #7  0x000000010000182e in std::_Hashtable<int, std::pair<int const, int>, std::allocator<std::pair<int const, int> >, std::__detail::_Select1st, std::equal_to<int>, std::hash<int>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_deallocate_buckets () #8  0x000000010000155a in std::_Hashtable<int, std::pair<int const, int>, std::allocator<std::pair<int const, int> >, std::__detail::_Select1st, std::equal_to<int>, std::hash<int>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::~_Hashtable () #9  0x000000010000135c in std::unordered_map<int, int, std::hash<int>, std::equal_to<int>, std::allocator<std::pair<int const, int> > >::~unordered_map () #10 0x00000001000013de in Foo::~Foo () #11 0x0000000100001482 in Bar::~Bar () #12 0x0000000100001294 in main () 

*** error for object 0x1003038a0: pointer being freed was not allocated ***

like image 397
vmrob Avatar asked Jan 07 '14 21:01

vmrob


1 Answers

Update

It appears a fix for the problem has been checked in.


Interesting question. It definitely seems to be a bug with how GCC handles = {} initialized default arguments, which was a late addition to the standard. The problem can be reproduced with a pretty simple class in place of std::unordered_map<int,int>:

#include <utility>  struct PtrClass {     int *p = nullptr;       PtrClass()     {         p = new int;     }      PtrClass(PtrClass&& rhs) : p(rhs.p)     {         rhs.p = nullptr;     }      ~PtrClass()     {         delete p;     } };  void DefArgFunc(PtrClass x = {}) {     PtrClass x2{std::move(x)}; }  int main() {     DefArgFunc();     return 0; } 

Compiled with g++ (Ubuntu 4.8.1-2ubuntu1~12.04) 4.8.1, it displays the same problem:

*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x0000000001aa9010 *** ======= Backtrace: ========= /lib/x86_64-linux-gnu/libc.so.6(+0x7eb96)[0x7fc2cd196b96] ./a.out[0x400721] ./a.out[0x4006ac] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed)[0x7fc2cd13976d] ./a.out[0x400559] ======= Memory map: ======== bash: line 7:  2916 Aborted                 (core dumped) ./a.out 

Digging a little deeper, GCC seems to create an extra object (though it only calls the constructor and destructor once each) when you use this syntax:

#include <utility> #include <iostream>  struct SimpleClass {         SimpleClass()     {         std::cout << "In constructor: " << this << std::endl;     }      ~SimpleClass()     {         std::cout  << "In destructor: " << this << std::endl;     } };  void DefArgFunc(SimpleClass x = {}) {         std::cout << "In DefArgFunc: " << &x << std::endl; }  int main() {     DefArgFunc();     return 0; } 

Output:

In constructor: 0x7fffbf873ebf In DefArgFunc: 0x7fffbf873ea0 In destructor: 0x7fffbf873ebf 

Changing the default argument from SimpleClass x = {} to SimpleClass x = SimpleClass{} produces

In constructor: 0x7fffdde483bf In DefArgFunc: 0x7fffdde483bf In destructor: 0x7fffdde483bf 

as expected.

What seems to be happening is that an object is created, the default constructor is called, and then something similar to a memcpy is performed. This "ghost object" is what is passed to the move constructor and modified. However, the destructor is called on the original, unmodified, object, which now shares some pointer with the move-constructed object. Both eventually try to free it, causing the issue.

The four changes that you noticed fixed the problem make sense given the above explanation:

// 1 // adding the destructor inhibits the compiler generated move constructor for Foo, // so the copy constructor is called instead and the moved-to object gets a new // pointer that it doesn't share with the "ghost object", hence no double-free ~Foo() = default;  // 2 // No  `= {}` default argument, GCC bug isn't triggered, no "ghost object" Bar(Foo f = Foo()) : _f(std::move(f)) {}  // 3 // The copy constructor is called instead of the move constructor Bar(Foo f = {}) : _f(f) {}  // 4 // No  `= {}` default argument, GCC bug isn't triggered, no "ghost object" Foo f1 = {}; Foo f2 = std::move(f1); 

Passing an argument to the constructor (Bar b(Foo{});) rather than using the default argument also solves the problem.

like image 76
jerry Avatar answered Oct 26 '22 04:10

jerry