Argumento std::map con inicializadores de llaves vacíos para fallas de segmento de argumento predeterminadas en GCC

6 minutos de lectura

avatar de usuario
v154c1

Problema

Recibí un informe de error del usuario que informa una falla de segmento en la biblioteca que desarrollo.

El ejemplo mínimo del código defectuoso es:

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

Cuando se compila con GCC (probé 4.8.2 y 4.7.3) se imprime correctamente 0 como tamaño del contenedor, pero fallas de segmento dentro del ciclo (que no debería ejecutarse en absoluto).

Soluciones alternativas

Sin embargo, puedo arreglar el problema cambiando la declaración a:

void f(std::map<std::string, std::string> m = std::map<std::string, std::string>{})

copiando el map funciona también:

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

Cambiar el parámetro a const std::map<...>& también funciona

GCC 4.9.1 funciona bien.

Clang también compila y ejecuta el código correctamente. (incluso cuando se usa la misma libstdc++ que falla en gcc 4.8.2)

Laboral ejemplo: http://coliru.stacked-crooked.com/a/eb64a7053f542efd

Pregunta

El mapa definitivamente no está en un estado válido dentro de la función (detalles a continuación). Parece un error de GCC (o libstdc++), pero quiero estar seguro de que no estoy cometiendo un error estúpido aquí. Es difícil creer que un error así permanezca en gcc durante al menos 2 versiones principales.

Entonces mi pregunta es: ¿Es la forma de inicializar por defecto std::map parámetro incorrecto (y error en mi código) o es un error en stdlibc++ (o gcc)?

No estoy buscando soluciones alternativas (ya que sé qué hacer para que el código funcione) Cuando está integrado en la aplicación, el código infractor se ejecuta bien en algunas computadoras (incluso cuando se compila con gcc 4.8.2) en otras no.

Detalles

Lo compilo usando:

g++-4.8.2 -g -Wall -Wextra -pedantic  -std=c++11 /tmp/c.cpp -o /tmp/t

Seguimiento desde 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 es la línea con std::cout << ...

ASAN informa:

AddressSanitizer: SEGV on unknown address 0xffffffffffffffe8 

esto parece nullptr - 8

valgrind muestra:

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

Mirar el estado interno del mapa muestra que el código realmente tiene que fallar:

std::map::begin() en libstdc++ devuelve el valor de

this->_M_impl._M_header._M_parent

de su representación interna, std::map::end() devoluciones:

&this->_M_impl._M_header

gdb muestra:

(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

Entonces valor de begin() y end() no son lo mismo (begin() es nullptr) según lo exige el estándar para vacío std::map.

  • FWIW, cuando reemplacé std::cout << s.first << "->" << s.second <<"\n"; por std::cout << "Came here\n";el programa imprime la línea una vez y luego se cuelga.

    – R Sahu

    3 de marzo de 2015 a las 16:51


  • “Es difícil creer que un error así permanezca en gcc durante al menos 2 versiones principales”. – Debes ser nuevo aquí

    –MM

    03/03/2015 a las 21:55

avatar de usuario
Shafik Yaghmour

Se ve como esto el error se corrigió en 4.8.3/4.9.0el informe de error que tiene un ejemplo similar y también fallas de segmentación dice:

El caso de prueba mínimo adjunto tiene la siguiente función con un argumento predeterminado construido por defecto:

void do_something( foo f = {} )
{     std::cout << "default argument is at " << &f << std::endl;
}

El constructor de foo genera su dirección; Obtuve el siguiente resultado de una sola ejecución: construido foo @ 0x7ffff10bdb7f el argumento predeterminado está en 0x7ffff10bdb60

Muestra que solo se construyó 1 foo, y no en la misma dirección que la del argumento predeterminado. Ha sido una semana muy larga, pero no puedo ver nada malo con el código. En el código real en el que se basó esto, se producía un error de segmento al ejecutar el destructor de un foo que se construyó con movimiento a partir del argumento predeterminado, porque la memoria subyacente aparentemente no estaba inicializada.

Podemos ver desde un ejemplo en vivo que 4.9.0 no demuestra este problema.

Podemos ver que esta fue una funcionalidad intencional de informe de defectos 994 y la posterior resolución N3217:

Este documento presenta cambios de redacción detallados en relación con el actual Borrador de Trabajo N3126 de C++ para implementar inicializadores de llaves para argumentos predeterminados para funciones, como se propone en N3139 “Una característica de lenguaje incompleta” de Bjarne Stroustrup, por lo que también aborda el problema central 994.

Esto también está contemplado en la propuesta. N3139: una función de idioma incompleta.

Es interesante notar que Visual Studio también tiene un error con respecto a los inicializadores de llaves como argumentos predeterminados que creo que aún no se ha resuelto.

  • ¡Gracias! Eso es exactamente lo que quería saber y no pude encontrar.

    – v154c1

    03/03/2015 a las 22:37

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad