I got a bug report from user reporting a segfault in library I develop.
The minimal example of the faulty code is:
#include <map>
#include <string>
#include <iostream>
void f(std::map<std::string, std::string> m = {})
{
std::cout << m.size() << "\n";
for (const auto& s: m) {
std::cout << s.first << "->" << s.second <<"\n";
}
}
int main()
{
f();
}
When compiled with GCC (I tested 4.8.2 and 4.7.3) it correctly prints 0
as size of the container, but segfaults inside the loop (which shouldn't be executed at all).
However, I can fix the problem by changing the declaration to:
void f(std::map<std::string, std::string> m = std::map<std::string, std::string>{})
Copying the map
works as well:
void f(std::map<std::string, std::string> mx = {})
{
auto m = mx;
std::cout << m.size() << "\n";
for (const auto& s: m) {
std::cout << s.first << "->" << s.second <<"\n";
}
}
Changing the parameter to const std::map<...>&
also works.
GCC 4.9.1 works fine.
Clang also compiles and runs the code just fine. (even when using the same libstdc++ as failing gcc 4.8.2)
Working example: http://coliru.stacked-crooked.com/a/eb64a7053f542efd
The map is definitely not in valid state inside the function (details bellow). It looks like a GCC (or libstdc++) bug, but I want to be sure I'm not making some stupid mistake here. It's hard to believe such a bug would stay in gcc for at least 2 major version.
So my question is: Is the way of initializing default std::map
parameter wrong (and bug in my code) or is it a bug in stdlibc++
(or gcc
)?
I'm not looking for workarounds (as I know what to do to make to code work) When integrated in the application, the offending code executes fine on some computers (even when compiled with gcc 4.8.2) on some doesn't.
I compile it using:
g++-4.8.2 -g -Wall -Wextra -pedantic -std=c++11 /tmp/c.cpp -o /tmp/t
Backtrace from gdb:
#0 std::operator<< <char, std::char_traits<char>, std::allocator<char> > (__os=..., __str=...) at /usr/src/debug/sys-devel/gcc-4.8.2/build/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/basic_string.h:2758
#1 0x0000000000400f36 in f (m=std::map with 0 elements) at /tmp/c.cpp:9
#2 0x0000000000400fe0 in main () at /tmp/c.cpp:15
/tmp/c.cpp:9 is the line with std::cout << ...
ASAN reports:
AddressSanitizer: SEGV on unknown address 0xffffffffffffffe8
This seems like nullptr - 8
valgrind shows:
==28183== Invalid read of size 8
==28183== at 0x4ECC863: std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (in /usr/lib64/gcc/x86_64-pc-linux-gnu/4.8.2/libstdc++.so.6.0.18)
==28183== by 0x400BD5: f(std::map<std::string, std::string, std::less<std::string>, std::allocator<std::pair<std::string const, std::string> > >) (c.cpp:9)
==28183== by 0x400C7F: main (c.cpp:15)
==28183== Address 0xffffffffffffffe8 is not stack'd, malloc'd or (recently) free'd
Looking at internal state of the map shows that the code really has to fail:
std::map::begin()
in libstdc++ returns value of
this->_M_impl._M_header._M_parent
from it's internal representation, std::map::end()
returns:
&this->_M_impl._M_header
gdb shows:
(gdb) print m._M_t._M_impl._M_header
$5 = {_M_color = std::_S_red, _M_parent = 0x0, _M_left = 0x7fffffffd6d8, _M_right = 0x7fffffffd6d8}
(gdb) print &m._M_t._M_impl._M_header
$6 = (std::_Rb_tree_node_base *) 0x7fffffffd6a8
So value of begin()
and end()
are not the same (begin()
is nullptr) as mandated by standard for empty std::map
.
Looks like this bug was fixed in 4.8.3/4.9.0, the bug report which has a similar example and also seg-faults says:
The attached minimal testcase has the following function with default-constructed default argument:
void do_something( foo f = {} ) { std::cout << "default argument is at " << &f << std::endl; }
The constructor for foo outputs its address; I got the following output from a single run: constructed foo @ 0x7ffff10bdb7f default argument is at 0x7ffff10bdb60
It shows that only 1 foo was constructed, and not at the same address as that of the default argument. It's been a loooong week, but I can't see anything wrong with the code. In the real code on which this was based, a segfault was occurring when running the destructor of a foo that was move-constructed from the default argument, because the underlying memory was seemingly uninitialised.
We can see from a live example that 4.9.0
does not demonstrate this problem.
We can see this was intentional functionality from defect report 994 and the subsequent resolution N3217:
This paper presents detailed wording changes relative to the current C++ Working Draft N3126 to implement brace-initializers for default arguments for functions, as proposed in N3139 "An Incomplete Language Feature" by Bjarne Stroustrup, thereby also addressing core issue 994.
This is also covered in the proposal N3139: An Incomplete Language Feature.
Interesting to note that Visual Studio also has a bug with respect to brace-initializers as default arguments which I think is still unresolved.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With