Почему Clang и VS2013 принимают перемещаемые аргументы по умолчанию, инициализированные скобками, но не GCC 4.8 или 4.9?

Как следует из названия, у меня есть короткая демонстрационная программа, которая компилируется всеми этими компиляторами, но при запуске с помощью gcc 4.8 и gcc 4.9 сбрасывает дамп ядра:

Есть идеи, почему?

#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);
}

Мои настройки компиляции:

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

Вот трассировка из 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 ***


person vmrob    schedule 07.01.2014    source источник
comment
какие флаги вы используете на gcc?   -  person ThomasMcLeod    schedule 08.01.2014
comment
Почему происходит, когда вы используете этот конструктор: Bar(Foo f = {Foo()}) : _f(std::move(f)) {}   -  person ThomasMcLeod    schedule 08.01.2014
comment
Это основные дампы, по той же причине.   -  person vmrob    schedule 08.01.2014
comment
Можете ли вы запустить обратную трассировку gdb?   -  person ThomasMcLeod    schedule 08.01.2014
comment
gcc 4.9 демонстрирует такое же поведение. И конечно, я опубликую bt через секунду.   -  person vmrob    schedule 08.01.2014
comment
Немного упрощенная версия: coliru.stacked-crooked.com/a/7be23631a99ee9f0 (уменьшено для аргумента по умолчанию {} и std::unordered_map конструкторов/деструкторов)   -  person zch    schedule 08.01.2014
comment
На самом деле это не компилируется в gcc 4.9 error: converting to 'Foo {aka std::unordered_map<int, int>}' from initializer list would use explicit constructor   -  person vmrob    schedule 08.01.2014
comment
деструктор Foo вызывается дважды   -  person user2485710    schedule 08.01.2014
comment
Я думаю, я ожидал бы, что это будет вызвано дважды, верно? Один для объекта, созданного в Bar::Bar и один раз для Bar::~Bar   -  person vmrob    schedule 08.01.2014
comment
Я предполагаю, что это ошибка в GCC. Если вы сравните этот пример с примером @zch, единственное отличие состоит в вызове bar явно передает параметр {} вместо использования аргумента по умолчанию, и эти два параметра должны быть одинаковыми; но этот работает нормально, а второй вылетает.   -  person Adam Rosenfield    schedule 08.01.2014
comment
Нельзя вызывать деструктор дважды для одного и того же объекта. Это ошибка, подлежащая регистрации.   -  person ThomasMcLeod    schedule 08.01.2014
comment
Это явно ошибка в gcc (вероятно, в конструкторе перемещения хэш-таблицы в libstdc++), пожалуйста, сообщите об этом в gcc bugzilla.   -  person Marc Glisse    schedule 08.01.2014
comment
@ThomasMcLeod, это также зависит от того, каков эффект этого std::move   -  person user2485710    schedule 08.01.2014
comment
Ожидается, что будут вызваны два деструктора Foo: один раз для временного объекта, созданного в Bar::Bar (который затем перемещается в Bar::_f), а затем один раз для ~Bar::Bar.   -  person vmrob    schedule 08.01.2014
comment
Я сообщу им об этом, любопытно, несмотря ни на что. Похоже на добавление деструктора в Foo, даже ~Foo() = default; позволяет ему работать.   -  person vmrob    schedule 08.01.2014
comment
@vmrob попытайтесь исправить это с помощью Bar(Foo f = {{1,2}}), у меня работает, очевидно, когда initializer_list пуст, у вас возникают проблемы с выделением и освобождением. Я не знаю, настоящая это ошибка или нет, я хотел бы знать, что команда libstdc++ может сказать об этом.   -  person user2485710    schedule 08.01.2014
comment
@ user2485710, я понимаю. Но, строго говоря, f и _f bar не являются одним и тем же объектом. Когда std::move завершится, в f не останется кишок, которые можно уничтожить. Или я ошибаюсь?   -  person ThomasMcLeod    schedule 08.01.2014
comment
@ThomasMcLeod В двух словах, это зависит от того, как работает реализация unordered_map; std::move — это приведение, это приведение запускает сигнатуру T&& для конструктора, все дело в том, что происходит внутри конструктора. Я предполагаю, что в случае с пустым это не так хорошо обрабатывается, потому что, если вы предоставите хотя бы 1 пару, это сработает. В общем, вы ничего не можете сказать наверняка, когда у вас просто есть приведение типа std::move, конструктор здесь большой ?.   -  person user2485710    schedule 08.01.2014
comment
@DanielFrey, но я не думаю, что это главная проблема.   -  person user2485710    schedule 08.01.2014
comment
@DanielFrey, ссылка? Я не думаю, что наследование стандартных контейнеров является незаконным.   -  person zch    schedule 08.01.2014
comment
@zch Я что-то путаю? Возможно, я не могу найти ссылку на это прямо сейчас, поэтому я удалил комментарий.   -  person Daniel Frey    schedule 08.01.2014
comment
Проблема также связана с std::move, удаление которого устраняет проблему. Что бы это ни стоило, ожидается уничтожение двух объектов, хотя содержимое того, который был перемещен, находится в неопределенном состоянии. Доступ к нему после перемещения приводит к неопределенному поведению.   -  person vmrob    schedule 08.01.2014
comment
@vmrob Я помню, что видел отчет об ошибке, и я думаю, что Foo f = {} является виновником. Вот оно. Хотя, наверное, это не связано. Если да, то игнорируйте мой комментарий.   -  person    schedule 08.01.2014
comment
@ user2485710, поэтому, независимо от того, как реализован конструктор перемещения, это обязательно ошибка в std::unordered_map.   -  person ThomasMcLeod    schedule 08.01.2014
comment
@ThomasMcLeod Я не уверен, что это обязательно правда ... Если простое добавление ~Foo() = default; решит проблему, это вполне может быть драйвер компилятора.   -  person vmrob    schedule 08.01.2014
comment
Что бы это ни стоило, я разместил отчет об ошибке, и в настоящее время он исследуется: gcc.gnu.org/bugzilla/show_bug.cgi?id=59713   -  person vmrob    schedule 11.01.2014
comment
I'm not sure why I first thought it was with libstdc++, but after realizing that just adding that explicitly defined default destructor fixed it, I couldn't imagine it being the library. Должен согласиться, я не думаю, что это связано с unordered_map. См. это и это. Хотя я могу ошибаться.   -  person    schedule 11.01.2014


Ответы (1)


Обновлять

Похоже, исправление проблемы было проверено.


Интересный вопрос. Это определенно похоже на ошибку в том, как GCC обрабатывает = {} инициализированные аргументы по умолчанию, которые были позднее дополнение к стандарту. Проблему можно воспроизвести с довольно простым классом вместо 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;
}

Скомпилировано с помощью g++ (Ubuntu 4.8.1-2ubuntu1~12.04) 4.8.1, отображает та же проблема:

*** 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

Копнув немного глубже, GCC, кажется, создает дополнительный объект (хотя он вызывает конструктор и деструктор только один раз каждый), когда вы используете этот синтаксис:

#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;
}

Вывод:

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

Изменение аргумента по умолчанию с SimpleClass x = {} на SimpleClass x = SimpleClass{} приводит к

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

как и ожидалось.

Кажется, что происходит то, что создается объект, вызывается конструктор по умолчанию, а затем выполняется что-то похожее на memcpy. Этот призрачный объект передается конструктору перемещения и модифицируется. Однако деструктор вызывается для исходного, немодифицированного объекта, который теперь имеет некоторый общий указатель с объектом, сконструированным перемещением. Оба в конечном итоге пытаются освободить его, вызывая проблему.

Четыре изменения, которые вы заметили, исправили проблему, имеют смысл, учитывая приведенное выше объяснение:

// 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);

Передача аргумента конструктору (Bar b(Foo{});) вместо использования аргумента по умолчанию также решает проблему.

person jerry    schedule 15.01.2014
comment
Да, это определенно ошибка, которая сохраняется в gcc 4.9 (или, по крайней мере, в снимке, который у меня есть). У clang такой ошибки нет. Там уже есть отчет об ошибке и несколько других, которые кажутся похожими. Но +1 за разъяснение проблемы с тестовым примером. Проблема исчезнет, ​​если вы сделаете SimpleClass x = SimpleClass{}. - person ; 15.01.2014
comment
Да, извините, теперь я вижу полную цепочку комментариев к вопросу. Я раньше не читал все скрытые комментарии. Ну ладно, я нашел это интересное упражнение в любом случае :) - person jerry; 15.01.2014
comment
@remyabel Я пробовал несколько разных аргументов по умолчанию, только = {...} вызвал ошибку. Я должен предположить, что это связано с поздним добавлением, но это все равно странно, потому что оно должно действовать точно так же, как SimpleClass x = SimpleClass{}, это всего лишь изменение грамматики. Вывод, когда &x == 0x7fffdde483bf был из кода, который использовал этот синтаксис. - person jerry; 16.01.2014
comment
@jerry Я думаю, что семантика = SimpleClass{} и = {} различна. Первый — это инициализация по умолчанию в случае типов, отличных от POD, и почти такой же, как SimpleClass a; memset(a,0,sizeof(a)); для типов POD, тогда как Simpleclass a = {} — это построение с помощью std::initializer_list для типов, отличных от POD, и агрегатная инициализация для типов POD. Хотя я никогда не использую форму = SimpleClass{}, так что могу ошибаться. - person vmrob; 16.01.2014