OVH Cloud OVH Cloud

Precision sur la "one definition rule"

14 réponses
Avatar
Aurelien Regat-Barrel
Bonjour,
J'ai eu affaire à un drôle de bug dans mon programme, provoqué par un
code de ce genre:

// montemplate.h
template<typename T>
class montemplate
{
#ifdef MODIF
// quelque chose
#else
// autre chose
#endif
};

// test.cpp
#define MODIF
#include "montemplate.h"

// utilisation de montemplate...


// main.cpp
#include "montemplate.h"

// utilisation de montemplate...


Je pensais pas que ce genre de code était problématique (unités de
compilations différentes), mais apparement si (dixit la one definition
rule).
Vous confirmez ?
Merci.

--
Aurélien Regat-Barrel

4 réponses

1 2
Avatar
Jean-Marc Bourguet
Aurelien Regat-Barrel writes:

Cette histoire de template instanciée de manière identique à
l'ensemble des codes sources compilés, ça fait un peu effet de bord
je trouve...
On pourrait desirer que le non respect de l'ODR soit detecte, mais a

part ca je vois mal ce que tu veux. Tu peux mettre la definition du
template dans un namespace anonyme, mais alors tu ne peux plus
passer tes objets d'une unite de compilation a l'autre.


Je veux juste comprendre. J'ai fait une erreur, je cherche pas à ce qu'elle
soit acceptée. Par contre j'aurai bien aimé qu'elle soit détectée.
Pour te donner une idée de code qui crash sous VC++ 8 :

// test.cpp
#define _SECURE_SCL 0
#include <vector>
#include <algorithm>

void test()
{
std::vector<int> v( 150, 10 );
std::vector<int>::iterator max_i > std::max_element( v.begin(), v.end() );
int max = *max_i;
}

// main.cpp
#include <vector>
template class std::vector<int, std::allocator< int > >;

void test();

int main()
{
test();
return 0;
}

Pas besoin de passer des instances de std::vector entre les unités de
compilation, la spécialisation explicite dans main.cpp suffit.


Ce qui se passe vraissemblablement ici c'est que tu as des membres
inlines dans test.cpp qui utilisent une structure pour
std::vector<int> qui suppose _SECURE_SCL a 0. Dans test.cpp il y a
aussi des appels a des membres non inlines qui dans le cas de
l'optimisation globale sont prises dans main.cpp et qui donc supposent
_SECURE_SCL non definis. Rien n'empecherait l'editeur de liens de
faire le meme choix sans l'optimisation globale, ou meme en debug...

Et si c'est main.cpp qui définit _SECURE_SCL à 0, ça marche...


Il suffit que la version choisie paraisse marcher. Soit que l'editeur
fasse un autre choix et choisisse la version dans test.cpp, soit que
par hasard la fonction avec _SECURE_SCL fonctionne dans les deux cas,
soit qu'il y a un probleme de corruption ou autre que tu n'as pas
detecte sur un exemple aussi simple.

Je me demande s'il serait possible de provoquer des erreurs à
l'édition des liens si l'ODR n'était pas respectée.


Je ne vois rien a faire que de subir ce que fait l'implementation pour
qui ne la controle pas. Si tu controles l'implementation, c'est
possible avec un editeur de liens suffisemment sophistique (ce qui ne
le serait pas beaucoup). Il "suffirait" d'emettre dans chaque unite
de compilation qui voit (ou utilise simplement si on veut etre moins
strict) la definition d'un type une signature de la partie
significative du type et que l'editeur de liens verifie que tous les
types ont bien la meme signature dans toutes les unites de
compilation. C'est pas complique. La partie Ada de GCC fait plus ou
moins l'equivalent pour l'Ada (ils utilisaient d'autres fichiers que
les .o la derniere fois que j'ai regarde, mais rien n'empeche de
mettre cette info dans les .o et il me semble que la doc faisait
allusion a cette possibilite).

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org



Avatar
Aurelien Regat-Barrel

Pas besoin de passer des instances de std::vector entre les unités de
compilation, la spécialisation explicite dans main.cpp suffit.



Ce qui se passe vraissemblablement ici c'est que tu as des membres
inlines dans test.cpp qui utilisent une structure pour
std::vector<int> qui suppose _SECURE_SCL a 0. Dans test.cpp il y a
aussi des appels a des membres non inlines qui dans le cas de
l'optimisation globale sont prises dans main.cpp et qui donc supposent
_SECURE_SCL non definis. Rien n'empecherait l'editeur de liens de
faire le meme choix sans l'optimisation globale, ou meme en debug...


Et si c'est main.cpp qui définit _SECURE_SCL à 0, ça marche...



Il suffit que la version choisie paraisse marcher. Soit que l'editeur
fasse un autre choix et choisisse la version dans test.cpp, soit que
par hasard la fonction avec _SECURE_SCL fonctionne dans les deux cas,
soit qu'il y a un probleme de corruption ou autre que tu n'as pas
detecte sur un exemple aussi simple.


Je me demande s'il serait possible de provoquer des erreurs à
l'édition des liens si l'ODR n'était pas respectée.



Je ne vois rien a faire que de subir ce que fait l'implementation pour
qui ne la controle pas. Si tu controles l'implementation, c'est
possible avec un editeur de liens suffisemment sophistique (ce qui ne
le serait pas beaucoup). Il "suffirait" d'emettre dans chaque unite
de compilation qui voit (ou utilise simplement si on veut etre moins
strict) la definition d'un type une signature de la partie
significative du type et que l'editeur de liens verifie que tous les
types ont bien la meme signature dans toutes les unites de
compilation. C'est pas complique. La partie Ada de GCC fait plus ou
moins l'equivalent pour l'Ada (ils utilisaient d'autres fichiers que
les .o la derniere fois que j'ai regarde, mais rien n'empeche de
mettre cette info dans les .o et il me semble que la doc faisait
allusion a cette possibilite).


Merci pour ces explications.

--
Aurélien Regat-Barrel


Avatar
kanze
Aurelien Regat-Barrel wrote:

Pour info, j'ai eu ce problème "à l'insu de mon plein grès"
avec std::vector sous VC++ 8 en définissant _SECURE_SCL à 0
dans 1 fichier de mon projet pour éviter d'avoir des
warnings (ailleurs que dans std::vector). Sauf que ça
modifie pas mal de choses dans std::vector aussi, et voilà.


_SECURE_SCL, ça ne serait pas un peu comme _GLIBCXX_DEBUG
sous G++ ? C'était exactement mon problème, sauf que ça
linkait bien. (Dans mon cas, c'était une erreur dans le
fichier de make. J'étais convaincu que je me servais des
bibliothèques de debug, alors que ce n'était pas le cas.
J'avais un vecteur qui était créer dans la bibliothèque,
mais dont je me servais des itérateurs dans une fonction
templatée instantiée dans main.)


Dans mon cas c'est un peu plus pervers : c'est la fonctions
inline qui manipulait un vector (std::max_element) renvoyé par
une fonction non inline qui provoquait le problème.


Ce qui revient au même. L'important, c'est que l'instance de
vector s'est fait (allocation et appel du constructeur) dans une
contexte sans le debug, et l'utilisation dans une contexte avec.

En gros, le but de _GLIBCXX_DEBUG (et, je suppose, de
_SECURE_SCL), c'est de détecter des erreurs du genre utilisation
d'un itérateur invalid. Quand tu effectues une opération qui
peut invalider un itérateur (comme par exemple push_back sur un
vector), tous les itérateurs sont marqués invalides, et si on
essaie d'utiliser un itérateur marqué invalide, tu as une erreur
claire et nette (et immédiate). Pour pouvoir marquer les
itérateurs, en revanche, il faut bien qu'on puisse les trouver à
partir du vector -- dans l'implémentation g++, les itérateurs se
trouvent tous dans une liste chaîné dont la racine se trouve
dans le vecteur. Si le vecteur est construit dans une contexte
sans _GLIBCXX_DEBUG, il ne contient pas cette racine, et si on
construit ou copie un itérateur dans une contexte avec
_GLIBCXX_DEBUG, on déréférence des pointeurs dans cette racine
qui n'y est pas (et qui donc n'a pas été initialisée).

En régle générale : il faut que toutes les unités de
compilation soient compilées avec exactement les mêmes
options, y compris les mêmes -D. Et que tu ne définisses
jamais de macro dans ton code avant d'inclure les en-têtes,
et que les en-têtes aussi ne définissent que les macros qui
les concernent.


J'ai enfreint ce principe dans un seul fichier de tout le
projet, et voilà :-)


Moi aussi, il n'y avait qu'un seul fichier compilé avec debug.
En plus, ce fichier n'utilisait même pas les itérateurs. Mais il
utilisait un template qui les utilisait, et l'instantiation du
template s'est fait dans la contexte du fichier compilé avec
debug.

J'ai eu un mal fou à trouver l'erreur, parce que j'étais
convaincu de me servir des bibliothèques de debug, et il ne
m'était pas venu à l'esprit de vérifier le fichier de make.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
Aurelien Regat-Barrel

Dans mon cas c'est un peu plus pervers : c'est la fonctions
inline qui manipulait un vector (std::max_element) renvoyé par
une fonction non inline qui provoquait le problème.



Ce qui revient au même. L'important, c'est que l'instance de
vector s'est fait (allocation et appel du constructeur) dans une
contexte sans le debug, et l'utilisation dans une contexte avec.

En gros, le but de _GLIBCXX_DEBUG (et, je suppose, de
_SECURE_SCL), c'est de détecter des erreurs du genre utilisation
d'un itérateur invalid. Quand tu effectues une opération qui
peut invalider un itérateur (comme par exemple push_back sur un
vector), tous les itérateurs sont marqués invalides, et si on
essaie d'utiliser un itérateur marqué invalide, tu as une erreur
claire et nette (et immédiate). Pour pouvoir marquer les
itérateurs, en revanche, il faut bien qu'on puisse les trouver à
partir du vector -- dans l'implémentation g++, les itérateurs se
trouvent tous dans une liste chaîné dont la racine se trouve
dans le vecteur. Si le vecteur est construit dans une contexte
sans _GLIBCXX_DEBUG, il ne contient pas cette racine, et si on
construit ou copie un itérateur dans une contexte avec
_GLIBCXX_DEBUG, on déréférence des pointeurs dans cette racine
qui n'y est pas (et qui donc n'a pas été initialisée).


J'ai pas pris le temps d'étudier le but exact de _SECURE_SCL par rapport
à _HAS_ITERATOR_DEBUGGING. Dans le cadre de std::vector, pour ce que
j'en ai vu c'est à peu près pareil. J'avais défini _SECURE_SCL à 0 afin
d'utiliser <algorithm> sans avoir de warnings comme quoi std::transform
"a été déclaré désapprouvé".
Message : 'You have used a std:: construct that is not safe. See
documentation on how to use the Safe Standard C++ Library'

Mais pas de bol j'avais un include <vector> en dessous...

Merci.

--
Aurélien Regat-Barrel


1 2