typeid(type primitif) == référence nulle

Le problème exposé dans ce sujet a été résolu.

Bonjour,

L'opérateur typeid retourne une référence nulle lorsque je l'utilise sur un type primitif.

Programme pour le démontrer rapidement:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include<typeinfo>
#include<string>
#include<vector>
#include<cstdio>

#define T(x) printf("%s: %p\n", #x, &typeid(x));

int main (int argc, char** argv) {
T(std::string)
T(std::vector<int>)
T(int)
T(double)
return 0;
}

La première question qu'on peut se poser c'est, pourquoi ? Nulle part je n'ai vu ça dans une quelconque norme ou documentation.

Concrètement, ça me pose un problème dans l'utilisation de boost::any. J'ai un code qui ressemble au suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void fonction (any a) {
if (a.empty() { ... }
else if (a.type()==typeid(std::string)) { 
std::string str = any_cast<std::string>(a);
// je fais quelque chose avec str
}
else if (a.type()==typeid(int)) {
int n = any_cast<int>(a);
// je fais quelque chose avec n
}
}

Puisque typeid(int) retourne une référence nulle, la ligne a.type()==typeid(int) provoque un segfault.

Si a contient bien un int et si je fais any_cast<int>(a) directement sans passer par le test de type, je retrouve bien mon int.

Cependant, je ne peux pas risquer de faire any_cast<int>(a) directement sans faire le test de type avant, car à ce stade je ne sais pas si ma variable contient un int, un string ou autre chose. Y a-t-il une alternative au test de type, puisqu'il ne fonctionne pas ?

J'utilise MinGW, sous Windows 10, la version de G++ est 5.3.0, et je compile en mode gnu++14.

Merci pour vos réponses.

+0 -0

L'output que j'obtiens en executant ton exemple (g++ v4.9.2, -std=gnu++14)

1
2
3
4
std::string: 0x4008b0
std::vector<int>: 0x400880
int: 0x600d40
double: 0x600c80

Par ailleurs je comprends pas bien comment la ligne peut generer une segfault autrement que par le fait que a vailles NULL ou quelque chose dans ce gout la, mais je dois probablement rater quelque chose.

+0 -0

Par ailleurs je comprends pas bien comment la ligne peut generer une segfault autrement que par le fait que a vailles NULL ou quelque chose dans ce gout la, mais je dois probablement rater quelque chose.

Tu as de la chance, tu n'as pas le problème chez toi.

Ma sortie chez moi est:

1
2
3
4
std::string: 0040516C
std::vector<int>: 0040517C
int: 00000000
double: 00000000

Si &typeid(int) == nullptr, ce qui est le cas chez moi comme tu peux le voir, alors a.type()==typeid(int) provoque une segfault, quelque soit le type contenu dans a. Cela car typeid() renvoie une référence std::type_info&, et donc ça équivaut à appeler une fonction operator==(const std::type_info&, const std::type_info&). JE sais, une référence n'est jamais censé pointer sur nullptr, mais ici c'est un fait, je n'ai pas le contrôle sur ce que fait typeid, ni sur la classe std::type_info et son operator==.

Petite correction par contre, je suis en mode gnu++11, je ne vois pas ce que ça change au problème mais ne sait-on jamais.

+0 -0

Autre fait très troublant, ce code ne provoque aucune exception.

La deuxième ligne renvoie un nombre totalement loufoque comme on peut peut-être s'y attendre, mais ne provoque pas de std::bad_cast. En gros il est incapable de faire la différence entre un int et un double.

1
2
3
4
int l = 123;
any a = l;
printf("%d\n", any_cast<int>(a)); // 123, tout va bien
printf("%g\n", any_cast<double>(a)); // valeur loufoque

Qu'est-ce que je dois activer dans mon compilateur pour que ça marche ?

+0 -0

J'ai envie de dire que c'est un bug du compilateur :/

Je te conseille de ne pas utiliser typeid, mais seulement any_cast (la version pointeur si tu ne veux pas d'exception) et de désactiver le rtti pour voir (flags -fno-rtti). Normalement, boost se rabat sur une implémentation maison (boost::ctti).

Tu peux aussi essayer avec std::experimental::any qui n'utilise pas le rtti pour vérifier les types.

Je te conseille de ne pas utiliser typeid, mais seulement any_cast (la version pointeur si tu ne veux pas d'exception) et de désactiver le rtti pour voir (flags -fno-rtti). Normalement, boost se rabat sur une implémentation maison (boost::ctti).

Tu peux aussi essayer avec std::experimental::any qui n'utilise pas le rtti pour vérifier les types.

Bien joué, les deux fonctionnent; et sitôt que je réactive le rtti, ça plante à nouveau. Quelle est la meilleure solution entre les deux ?

JE serais curieux de savoir comment leur rtti maison peut fonctionner, mais juste à des fins de curiosité.

Merci !

J'ai envie de dire que c'est un bug du compilateur :/

Sérieusement ? S'il y a un seul logiciel qui ne doit pas avoir de bug, c'est bien le compilateur ! Mais bon, ce ne serait pas le premier truc bizarre que ferait MinGW. Ils ont laissé passer quelques trucs dans les .h des API windows.

+0 -0

typeid n'est pas une fonction qui retourne quelque chose. C'est une expression (c'est un mot-clé du langage).

Mais le résultat est une lvalue avec un static storage duration.

5.2.8 Type identification

The result of a typeid expression is an lvalue of static type const std::type_info (18.7.2) and dynamic type const std::type_info or const name where name is an implementation-defined class publicly derived from std :: type_info which preserves the behavior described in 18.7.2. 69 The lifetime of the object referred to by the lvalue extends to the end of the program. Whether or not the destructor is called for the std::type_info object at the end of the program is unspecified.

Sur GCC, cela cree un type_info sur une chaine. Meme typeid(void) n'est pas null.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.LC0:
        .string "std::string"
.LC1:
        .string "%s: %p\n"
main:
        mov     edx, OFFSET FLAT:typeinfo for std::basic_string
                     <char, std::char_traits<char>, std::allocator<char> >
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:.LC1
        mov     eax, 0
        call    printf

Donc a priori, pas possible que cela retourne a une reference nulle. Il faudrait voir l'assembleur généré qui produit ce résultat. Avec mingw, c'est l'option -S.

HS : utilises std::cout en C++. Et active les warnings, tu devrais avoir un avertissement sur le cast en void*.

Sérieusement ? S'il y a un seul logiciel qui ne doit pas avoir de bug, c'est bien le compilateur ! Mais bon, ce ne serait pas le premier truc bizarre que ferait MinGW. Ils ont laissé passer quelques trucs dans les .h des API windows.

QuentinC

Hum… https://gcc.gnu.org/bugzilla/buglist.cgi?component=c%2B%2B&product=gcc&resolution=--- 2281 bugs ouverts dans gcc et 399 dans libc++.

+0 -0

Concernant std::type_info, on ne peut le manipuler qu'à travers une référence étant donné qu'il n'a pas de constructeurs.

Je serais curieux de savoir comment leur rtti maison peut fonctionner, mais juste à des fins de curiosité.

La méthode utilisée par libstdc++ est de comparer un pointeur de fonction. Un type = une fonction = une adresse différente. À chaque ajout d'une valeur dans le any, il y a l'adresse de &une_function<T> en plus.

boost::ctti se sert de __PRETTY_FUNCTION__ ou équivalent comme identifiant.

HS: Youpi, je suis tombé sur 0.5% des bugs \o/.

La méthode utilisée par libstdc++ est de comparer un pointeur de fonction. Un type = une fonction = une adresse différente. À chaque ajout d'une valeur dans le any, il y a l'adresse de &une_function<T> en plus. boost::ctti se sert de PRETTY_FUNCTION ou équivalent comme identifiant.

Merci ! Effectivement c'est bien vu comme astuce.

typeid n'est pas une fonction qui retourne quelque chose. C'est une expression (c'est un mot-clé du langage). Mais le résultat est une lvalue avec un static storage duration.

Justement. Donc ce n'est jamais censé être nul.

HS : utilises std::cout en C++. Et active les warnings, tu devrais avoir un avertissement sur le cast en void*.

J'aime pas cout. Et bon, dans le programmde preuve c'était voulu, pour afficher l'adresse de la référence renvoyée; j'aurais fait comment avec cout ? En vrai j'ai commencé par constater ce retour de référence nulle avec gdb dans mon logiciel concret; en l'occurence celui-ci, python34.h, autour de la ligne 339.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<> struct PyConverter<any> {
static PyObject* inCast (const any& a) { 
if (a.empty() || isoftype(a, nullptr_t)) Py_RETURN_NONE;
#define T(...) else if (isoftype(a,##__VA_ARGS__)) return PyConverter<__VA_ARGS__>::inCast( any_cast<__VA_ARGS__>(a) ); 
T(int) T(std::wstring) T(std::string) T(bool)
T(vector<tstring>) T(vector<int>)
T(pair<tstring,int>) T(pair<vector<tstring>,int>) T(pair<int,int>)
#undef T
else Py_RETURN_NONE; 
}

Donc a priori, pas possible que cela retourne a une reference nulle. Il faudrait voir l'assembleur généré qui produit ce résultat. Avec mingw, c'est l'option -S.

Voici ce que ça donne pour mon petit programme de test :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    .section .rdata,"dr"
__ZStL19piecewise_construct:
    .space 1
    .def    ___main;    .scl    2;  .type   32; .endef
LC0:
    .ascii "int\0"
LC1:
    .ascii "%s: %p\12\0"
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB6255:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $16, %esp
    call    ___main
    movl    $__ZTIi, 8(%esp)
    movl    $LC0, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf
    movl    $0, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE6255:
    .ident  "GCC: (GNU) 5.3.0"
    .def    _printf;    .scl    2;  .type   32; .endef

Dans ce code rien n'a l'air d'être anormal pour ce que j'en connais. Par contre il ne prouve rien, on n'a pas la définition de $__ZTIi, qui pourrait très bien être nul.

Le bug doit être dans libstdc++-6.dll. C'est de là que la valeur est importée. Voyons voir:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Exports from libstdc++-6.dll
  5243 exported name(s), 5243 export addresse(s).  Ordinal base is 1.
  Sorted by Name:
    RVA      Ord. Hint Name
    -------- ---- ---- ----
    000BD068    1 0000 _ZGVNSt10moneypunctIcLb0EE2idE
    000BD070    2 0001 _ZGVNSt10moneypunctIcLb1EE2idE
...
    000C68C8 4685 124C _ZTISt9strstream
    000C68D4 4686 124D _ZTISt9time_base
    000C68DC 4687 124E _ZTISt9type_info
    901C0000 4688 124F _ZTIa
    901C0000 4689 1250 _ZTIb
    901C0000 4690 1251 _ZTIc
    901C0000 4691 1252 _ZTId
    901C0000 4692 1253 _ZTIe
    901C0000 4693 1254 _ZTIf
    901C0000 4694 1255 _ZTIg
    901C0000 4695 1256 _ZTIh
    901C0000 4696 1257 _ZTIi
    901C0000 4697 1258 _ZTIj
    901C0000 4698 1259 _ZTIl
    901C0000 4699 125A _ZTIm
    901C0000 4700 125B _ZTIs
    901C0000 4701 125C _ZTIt
    000C68E4 4702 125D _ZTIv
    901C0000 4703 125E _ZTIw
    901C0000 4704 125F _ZTIx
    901C0000 4705 1260 _ZTIy
    000C6920 4706 1261 _ZTSN10__cxxabiv116__enum_type_infoE
    000C6960 4707 1262 _ZTSN10__cxxabiv117__array_type_infoE
    000C69A0 4708 1263 _ZTSN10__cxxabiv117__class_type_infoE
...

Tous les typeinfo des types primitifs (les _ZTI?) ont la même référence: 901C0000. C'est louche. J'en déduis qu'ils sont tous nuls.

Et je le prouve :

1
2
3
4
5
HINSTANCE dll = LoadLibrary("LIBSTDC++-6.DLL");
void* data = (void*)GetProcAddress(dll, "__ZTIi");
printf("Last error = %d\n", GetLastError());
printf("dll=%p\n", dll);
printf("data=%p\n", data);

Résultat :

1
2
3
Last error = 0                                                                  
dll=6FE40000                                                                    
data=00000000

Si GetProcAddress m'avait retourné NULL parce que le symbole n'était pas trouvé dans la DLL, alors GetLastError() m'aurait retourné l'erreur 127, cf. MSDN

Maintenant qu'on est convaincu que c'est effectivement bien un bug du compilateur, c'est quoi la suite ?

JE suppose qu'il ne sera jamais corrigé vu le nombre ahurissant qu'ils en ont déjà ? Si c'est comme les bugs de Firefox… En même temps à leur place si j'étais développeur en chef d'un mégaprojet, j'en aurais probablement rien à f### du bug à la con d'un bouzeux; et je les comprends, ils ont autre chose à faire.

+0 -0

J'aime pas cout. Et bon, dans le programmde preuve c'était voulu, pour afficher l'adresse de la référence renvoyée; j'aurais fait comment avec cout ?

QuentinC

Comme tu as fait avec printf. cout << (&typeid(x)) << endl.

Et osef que tu aimes ou pas, c'est pas une question de goût. Il y a des arguments techniques qui expliquent pourquoi cout est préférable à printf (typage fort).

Maintenant qu'on est convaincu que c'est effectivement bien un bug du compilateur, c'est quoi la suite ?

JE suppose qu'il ne sera jamais corrigé vu le nombre ahurissant qu'ils en ont déjà ? Si c'est comme les bugs de Firefox… En même temps à leur place si j'étais développeur en chef d'un mégaprojet, j'en aurais probablement rien à f### du bug à la con d'un bouzeux; et je les comprends, ils ont autre chose à faire.

QuentinC

Il n'y a pas de dev en chef qui décide qui fait quoi, c'est un projet libre. Si un dev a les connaissances et veut corriger ce bug, il peut le faire (et tu peux proposer un patch, si t'arrives a trouver le probleme).

Mais le probleme va etre de reproduire l'erreur. Tu as bien vu qu'on a été plusieurs à exécuter ton code et ne pas reproduire l'erreur. (Pas avec MingW, ce qui indiquerait que ce soit un bug spécifique à MingW, pas a GCC).

Et même si le bug est réel, comme le RTTI est assez décrié, cela ne doit pas impacter la majorité des gros projets.

Essaies avec un GCC pour Windows : http://www.equation.com/servlet/equation.cmd?fa=fortran. Ou testes avec MingW 6: https://nuwen.net/mingw.html

+0 -0

Merci pour vos réponses.

Je vais me contenter de l'alternative std::experimental::any qui fonctionne plutôt bien pour une feature expérimentale. Le sujet est donc résolu.

Par contre je n'ai pas compris: c'est quoi la différence entre mingw et mingw-w64 ? Perso j'ai mingw, peut-être pas la toute dernière version (j'ai la 5.3), mais tout de même périodiquement mis à jour depuis plusieurs années (j'ai commencé avec une 3.x)

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte