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

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.