c++ - ¿Por qué Clang y VS2013 aceptan mover los argumentos predeterminados de la llave al inicio, pero no GCC 4.8 o 4.9?
c++11 brace-initialization (1)
Actualizar
Parece que se ha registrado una solución para el problema .
Interesante pregunta. Definitivamente parece ser un error con la forma en que GCC maneja = {}
argumentos por defecto inicializados, que fue una adición tardía al estándar . El problema se puede reproducir con una clase bastante simple en lugar de 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;
}
Compilado con g ++ (Ubuntu 4.8.1-2ubuntu1 ~ 12.04) 4.8.1 , muestra el mismo problema:
*** 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
Cavando un poco más profundo, GCC parece crear un objeto adicional (aunque solo llama al constructor y al destructor una vez cada uno) cuando usa esta sintaxis:
#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
Cambiar el argumento predeterminado de SimpleClass x = {}
a SimpleClass x = SimpleClass{}
produce
In constructor: 0x7fffdde483bf
In DefArgFunc: 0x7fffdde483bf
In destructor: 0x7fffdde483bf
como se esperaba.
Lo que parece estar sucediendo es que se crea un objeto, se llama al constructor predeterminado y luego se realiza algo similar a una memcpy
. Este "objeto fantasma" es lo que se pasa al constructor de movimiento y se modifica. Sin embargo, se llama al destructor en el objeto original, sin modificar, que ahora comparte algún puntero con el objeto construido por movimiento. Ambos finalmente intentan liberarlo, causando el problema.
Los cuatro cambios que notó arreglaron el problema tienen sentido dada la explicación anterior:
// 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);
Pasar un argumento al constructor ( Bar b(Foo{});
) en lugar de usar el argumento predeterminado también resuelve el problema.
Como sugiere el título, tengo un breve programa de demostración que se compila con todos esos compiladores, pero los volcados centrales se ejecutan después de compilar con gcc 4.8 y gcc 4.9:
¿Alguna idea de por qué?
#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);
}
Mi configuración de compilación:
g++ --std=c++11 main.cpp
Aquí hay una traza inversa de 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 ***