c++ - ¿Cuál es la rama en el destructor informada por gcov?
gcc code-coverage (3)
El problema de Destructor sigue ahí para la versión 5.4.0 de gcc, pero parece no existir para Clang.
Probado con:
clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Luego use "llvm-cov gcov ..." para generar cobertura como se describe here .
Cuando uso gcov para medir la cobertura de prueba del código C ++, informa ramas en destructores.
struct Foo
{
virtual ~Foo()
{
}
};
int main (int argc, char* argv[])
{
Foo f;
}
Cuando ejecuto gcov con probabilidades de sucursal habilitadas (-b) obtengo el siguiente resultado.
$ gcov /home/epronk/src/lcov-1.9/example/example.gcda -o /home/epronk/src/lcov-1.9/example -b
File ''example.cpp''
Lines executed:100.00% of 6
Branches executed:100.00% of 2
Taken at least once:50.00% of 2
Calls executed:40.00% of 5
example.cpp:creating ''example.cpp.gcov''
La parte que me molesta es "Tomado al menos una vez: 50.00% de 2".
El archivo .gcov generado brinda más detalles.
$ cat example.cpp.gcov | c++filt
-: 0:Source:example.cpp
-: 0:Graph:/home/epronk/src/lcov-1.9/example/example.gcno
-: 0:Data:/home/epronk/src/lcov-1.9/example/example.gcda
-: 0:Runs:1
-: 0:Programs:1
-: 1:struct Foo
function Foo::Foo() called 1 returned 100% blocks executed 100%
1: 2:{
function Foo::~Foo() called 1 returned 100% blocks executed 75%
function Foo::~Foo() called 0 returned 0% blocks executed 0%
1: 3: virtual ~Foo()
1: 4: {
1: 5: }
branch 0 taken 0% (fallthrough)
branch 1 taken 100%
call 2 never executed
call 3 never executed
call 4 never executed
-: 6:};
-: 7:
function main called 1 returned 100% blocks executed 100%
1: 8:int main (int argc, char* argv[])
-: 9:{
1: 10: Foo f;
call 0 returned 100%
call 1 returned 100%
-: 11:}
Observe la línea "rama 0 tomada 0% (caída)".
¿Qué causa esta rama y qué debo hacer en el código para obtener un 100% aquí?
- g ++ (Ubuntu / Linaro 4.5.2-8ubuntu4) 4.5.2
- gcov (Ubuntu / Linaro 4.5.2-8ubuntu4) 4.5.2
En el destructor, GCC generó un salto de condición para una condición que nunca puede ser verdadera (% al no es cero, ya que se le asignó un 1):
[...]
29: b8 01 00 00 00 mov $0x1,%eax
2e: 84 c0 test %al,%al
30: 74 30 je 62 <_ZN3FooD0Ev+0x62>
[...]
En una implementación típica, el destructor generalmente tiene dos ramas: una para la destrucción de objetos no dinámicos, otra para la destrucción dinámica de objetos. La selección de una rama específica se realiza a través de un parámetro booleano oculto que la persona que llama pasa al destructor. Por lo general, se pasa a través de un registro como 0 o 1.
Supongo que, dado que en tu caso la destrucción es para un objeto no dinámico, la rama dinámica no se toma. Intente agregar un new
-ed y luego delete
-ed objeto de la clase Foo
y la segunda rama debería tomarse también.
La razón por la que esta ramificación es necesaria está arraigada en la especificación del lenguaje C ++. Cuando alguna clase define su propia operator delete
, la selección de un operator delete
específico operator delete
para llamar se hace como si se hubiera buscado desde dentro del destructor de clase. El resultado final de esto es que para las clases con el operator delete
virtual destructor operator delete
comporta como si fuera una función virtual (a pesar de ser formalmente un miembro estático de la clase).
Muchos compiladores implementan este comportamiento literalmente : se llama directamente a la operator delete
desde la implementación del destructor. Por supuesto, la operator delete
solo debe invocarse al destruir objetos asignados dinámicamente (no para objetos locales o estáticos). Para lograr esto, la llamada a la operator delete
se coloca en una rama controlada por el parámetro oculto mencionado anteriormente.
En tu ejemplo, las cosas parecen bastante triviales. Esperaría que el optimizador elimine todas las ramificaciones innecesarias. Sin embargo, parece que de alguna manera logró sobrevivir a la optimización.
Aquí hay un poco de investigación adicional. Considera este código
#include <stdio.h>
struct A {
void operator delete(void *) { scanf("11"); }
virtual ~A() { printf("22"); }
};
struct B : A {
void operator delete(void *) { scanf("33"); }
virtual ~B() { printf("44"); }
};
int main() {
A *a = new B;
delete a;
}
Así es como se verá el código para el destructor de A
cuando se compila con GCC 4.3.4 en la configuración de optimización predeterminada
__ZN1AD2Ev: ; destructor A::~A
LFB8:
pushl %ebp
LCFI8:
movl %esp, %ebp
LCFI9:
subl $8, %esp
LCFI10:
movl 8(%ebp), %eax
movl $__ZTV1A+8, (%eax)
movl $LC1, (%esp) ; LC1 is "22"
call _printf
movl $0, %eax ; <------ Note this
testb %al, %al ; <------
je L10 ; <------
movl 8(%ebp), %eax ; <------
movl %eax, (%esp) ; <------
call __ZN1AdlEPv ; <------ calling `A::operator delete`
L10:
leave
ret
(El destructor de B
es un poco más complicado, por eso utilizo A
aquí como ejemplo. Pero en lo que se refiere a la bifurcación en cuestión, destructor de B
hace de la misma manera).
Sin embargo, justo después de este destructor el código generado contiene otra versión del destructor para la misma clase A
, que se ve exactamente igual , excepto que la instrucción movl $0, %eax
se reemplaza con la instrucción movl $1, %eax
.
__ZN1AD0Ev: ; another destructor A::~A
LFB10:
pushl %ebp
LCFI13:
movl %esp, %ebp
LCFI14:
subl $8, %esp
LCFI15:
movl 8(%ebp), %eax
movl $__ZTV1A+8, (%eax)
movl $LC1, (%esp) ; LC1 is "22"
call _printf
movl $1, %eax ; <------ See the difference?
testb %al, %al ; <------
je L14 ; <------
movl 8(%ebp), %eax ; <------
movl %eax, (%esp) ; <------
call __ZN1AdlEPv ; <------ calling `A::operator delete`
L14:
leave
ret
Tenga en cuenta que el código bloquea I etiquetado con flechas. Esto es exactamente de lo que estaba hablando. Register al
sirve como ese parámetro oculto. Se supone que esta "pseudo-sucursal" invocará u omitirá la llamada al operator delete
de acuerdo con el valor de al
. Sin embargo, en la primera versión del destructor este parámetro está codificado en el cuerpo como siempre 0
, mientras que en el segundo está codificado como siempre 1
.
La clase B
también tiene dos versiones del destructor generado para él. Así que terminamos con 4 destructores distintivos en el programa compilado: dos destructores para cada clase.
Adivino que al principio el compilador pensó internamente en términos de un único destructor "parametrizado" (que funciona exactamente como lo describí anteriormente). Y luego decidió dividir el destructor parametrizado en dos versiones independientes no parametrizadas: una para el valor del parámetro codificado de 0
(destructor no dinámico) y otra para el valor del parámetro codificado de 1
(destructor dinámico). En el modo no optimizado, lo hace literalmente, asignando el valor del parámetro real dentro del cuerpo de la función y dejando todas las ramificaciones totalmente intactas. Esto es aceptable en un código no optimizado, supongo. Y eso es exactamente con lo que estás tratando.
En otras palabras, la respuesta a su pregunta es: es imposible hacer que el compilador tome todas las ramas en este caso. No hay forma de lograr una cobertura del 100%. Algunas de estas ramas están "muertas". Es solo que el enfoque para generar código no optimizado es más bien "flojo" y "flojo" en esta versión de GCC.
Podría haber una manera de evitar la división en modo no optimizado, creo. Simplemente no lo he encontrado todavía. O, muy posiblemente, no se puede hacer. Las versiones anteriores de GCC usaban verdaderos destructores parametrizados. Quizás en esta versión de GCC decidieron cambiar al enfoque de dos destrutores y mientras lo hacían, "reutilizaban" el generador de códigos existente de una manera tan rápida y sucia, esperando que el optimizador eliminara las ramas inútiles.
Cuando está compilando con la optimización habilitada, GCC no se permitirá lujos tales como la ramificación inútil en el código final. Probablemente deberías tratar de analizar el código optimizado. El código no optimizado generado por GCC tiene muchas ramas inaccesibles sin sentido como esta.