Il resterait possible de définir des macros globales ? Je pense notamment à des macros comme DEBUG.
Version imprimable
L'idée de base d'un module, c'est qui puisse être compilé indépendamment, et que l'ordre d'inclusion ne compte pas. Donc si tu définis NDEBUG dans ton code, même si tu le fais avant d'importer des modules, les modules en question ne seront pas impactés. Seul ton code le sera. Si tu veux un module en mode debug, récupère une version différente du module compilée avec des options différentes.
Je ne sais pas trop si la proposition des gens de Clang gèrerais cette situation, j'ai l'impression qu'ils se focalisent plus sur le fait qu'un module puisse exporter une macro à son utilisateur que par le fait qu'un module dépende d'une macro définie par son utilisateur.
Désolé de t'avoir fait si peur ! :) Effectivement j'ai du lire trop vite les notes car je n'étais pas là au moment des débats. En revanche j'étais là au moment des échanges sur les détails d'implémentation, et c'était assez tendu comme échanges, notamment à cause du format binaire utilisé par l'implémentation de Gaby pour les modules. Ce format devrait être ouvert et documenté, et paradoxalement, tout le monde ne voit pas cela d'un bon oeil.
Je vais voir avec Gaby dans un futur proche pour faire un petit récapitulatif de tout ça.
Je sais qu'il y a des échanges en cours avec l'AFNOR en ce sens, et j'apprends aujourd'hui que pour le langage C ils sont apparemment pas loin de pouvoir monter un groupe aussi. Je pense juste organiser des rencontres pour qu'on puisse mieux faire connaissance et voir comment on peut mieux collaborer tous ensemble !
A propos de la programmation par contrat
Il y a eu deux sessions du soir sur le sujet, et les échanges ont été très intenses. Au final, tout le monde était satisfait des avancées. La piste explorée pour la PpC est de spécifier les pré-conditions via des attributs, dans ce genre:
Le point qui a posé beaucoup de problèmes est de savoir quoi faire quand le contrat n'est pas respecté.Code:
1
2
3 [[pre: n >= 0 && n < size()]] T & operator[](int n) noexcept { }
Il a été discuté de pouvoir spécifier l'exception levée au niveau du contrat, dans ce genre: [[pre(logic_error): n >= 0 && n < size()]]. Mais la distinction entre préciser le contrat d'une part, et préciser que faire quand le contrat est rompu d'autre part a été faite. En effet, préciser comment réagir dans le contrat modifie le contrat de départ.
L'autre point de discussion intense concerne le fait que l'utilisateur remplace le handler par défaut par son propre handler. Que se passe-t-il si l'utilisateur lance une exception depuis ce handler, alors que la fonction dont le contrat a été rompu est marqué noexcept?
Un consensus a été trouvé qu'une fonction noexcept doit toujours rester noexcept, et donc si le handler lance une exception dans un tel cas, alors le programme est terminé.
De ce que j'ai pu en comprendre, il semble que les bases aient été posées pour pouvoir discuter de l'intégration de la PpC à C++ lors des prochains meetings.
Pour ceux que ce sujet intéresse, il est important de regarder la conférence de John Lakos "Defensive programming done right" pour avoir toutes les notions de base. A noter que les post-condition ne semblent pas être au menu. D'après John tester les post-conditions revient à tester son propre code et les gens veulent le faire juste par mimétisme avec les pre-conditions même si ça n'a pas grand sens (c'est ma compréhension de ce qu'il m'a dit).
a- J'aime bien. Cela a l'avantage d'être présent dans le prototype des fonctions. On rejoint pleins de papiers (hormis ceux de John Lakos qui n'exploraient que la piste de la vérification dynamique)
b- L'idéal est bien évidemment le support par des outils d'analyse statique de code/de preuve formelle. Mieux vaut savoir avant la première exécution que sqrt(sin(x)-1) peut poser des problèmes. C'est là que la spécification dans les signatures importe. Ainsi du code client peut savoir ce qu'il doit respecter. Si on embarque la spec uniquement dans l'implémentation (voie des assertions), c'est plus compliqué pour annoter les binaires de façon portable (sur une même machine) ... déjà que l'on n'a même pas d'ABI.
Les autres possibilités sont
- les assertions, déjà utiles et utilisées pour la vérification des préconditions en phase de dev & test -- mais cela sous entend des modes de compilation (sans vérifications, avec vérifications légères, avec toutes les vérifications (même si elles coutent très cher))
- les TU pour les post-conditions (cf la conf de John Lakos)
- les exceptions pour passer en mode défensif en production quand on n'a pas confiance. Si on passe la phase d'analyse statique, j'ai envie de dire que cette approche n'aura plus grand intérêt. De plus, ce mode si pratique pour des TU qui exécutent plein de choses, il est beaucoup moins pratique pour investiguer dans de bonnes conditions -- assert est parfait pour un coredump, avec les exceptions, il faut feinter. Reste qu'il semble que ce soit elle qui mette la pagaille d'après ce que tu nous rapportes.
c- J'ai vu à ce propos qu'ils confiaient la rédaction des prochains papiers à des gens qui n'étaient pas encore impliqués sur le sujet, histoire de garder une neutralité.
d- Et les 3 derniers billets de mon blog aussi :)
A noter qu'avec ces évolutions, le pattern NVI va perdre son utilisation principale.
Pour être sûr d'avoir été bien compris pour /b, ce qui a été débattu c'est de la possibilité, dans le contrat, de spécifier le nom de l'exception levée si le contrat est violé, dans ce genre :
en cas de violation du contrat, std::logic_error serait levée... c'est ce point là qui a suscité beaucoup de controverse avant de pouvoir formuler correctement que la gestion du non respect du contrat n'avait pas à se trouver mélangée avec la spécification même du contrat.Citation:
[[pre(logic_error): n >= 0 && n < size()]].
J’ai l’impression que John Lakos est passé à côté de l’intérêt principal des postconditions. Dans sa présentation (que je recommande chaudement à tout le monde par ailleurs, en complément des billets de luc et de l’article sur ce site), il détaille les postconditions comme l’explication sémantique de la valeur de retour (cf son exemple sur sqrt). Effectivement, ça ne sert pas à grand chose.
En revanche, il est tout à fait possible de définir des postconditions « utiles ». Ces postconditions doivent plutôt être de la forme « qu’est-ce que je garantis sur mes valeurs de retour ? ». Par exemple, une postcondition « utile » de sqrt et « return_value >= 0 ». C’est beaucoup plus utile car cela peut être utilisé derrière par un analyseur de code statique (qui va pouvoir détecter un appel correct avec une fonction dont la précondition est x >= 0). Dans la même veine, on peut citer la garantie de non-nullité d’un pointeur retourné.
C’est une très bonne chose qu’ils soient arrivés à ce consensus. Faire l’inverse aurait entraîné une grande confusion entre programmation par contrats et programmation défensive (déjà que les gens ont du mal à voir la différence…).Citation:
en cas de violation du contrat, std::logic_error serait levée... c'est ce point là qui a suscité beaucoup de controverse avant de pouvoir formuler correctement que la gestion du non respect du contrat n'avait pas à se trouver mélangée avec la spécification même du contrat.
J'espère que ça verra jamais le jour cette forme là : un contrat qui contredit le prototype, ça ne devrait pas compiler tout simplement.Code:
1
2
3 [[pre(logic_error): n >= 0 && n < size()]] T & operator[](int n) noexcept { }
John Lakos n'a pas tord de dire que la post-condition de sqrt est de renvoyer un nombre telle que son élévation au carré vaut la paramètre entrant, à un epsilon près. Après les maths font que x*x est positif. Sur sqrt ce n'est pas très flagrant, mais sur sort() ou sur sin(), cela devient plus complexe et il peut être intéressant de pouvoir disposer de la post-condition complète.
Si nous avons une propriété sur chaque élément d'un conteneur et que l'on trie ces éléments, si on a la garantie qu'il n'y ait pas de synthonisation de nouveaux éléments ou d'altération de ceux déjà en place, alors cette garantie est gardée après le sort(). e.g. Un conteneur de nombres positifs que l'on trie est un conteneur trié de nombres positifs et pas juste un conteneur trié.
Souvent on se contente de la garantie utile évidente, mais dans de la preuve formelle, je ne vois pas pourquoi nous ne pourrions pas aller plus loin.
Après, John Lakos me semblait plus dans la voie des assertions et des tests unitaires que celle de la preuve formelle que d'autres ont réintroduit dans les discussions.
-----
J'ai eu la même conclusion pour le mélange spécification des exceptions dans le contrat : ouf!
J'ai l'impression que l'on commence à retomber sur les problématiques qu'un langage très axé sur la programmation défensive a déjà rencontrées. En Java, les runtime errors (si je me souviens bien du vocabulaire, et si je ne dis pas de bétise) échappent aux spécifications obligatoires d'exceptions. Et il se trouve que justement, les erreurs de logiques (comme des paramètres invalides, des null pointers) sont celles appelées "runtime error".
Je connais très mal Java, mais en C++ ça pose un problème :Code:
1
2
3
4
5
6
7
8
9
10
11
12
13 [[pre(logic_error): n >= 1]] void bar(int n) noexcept { } struct Foo { }; int main() { Foo *foo = new Foo(); // si ça throw pas de leak bar(0); // si ça throw, on leak foo, mais c'est noexcept donc on devrait être tranquille delete foo; return 0; }
Je sais bien. Mais ma question sous-entendue : les exceptions de logiques devraient-elle vraiment aller dans le même sac que les autres exceptions ? -- certes le C++ a une spécification technique des exceptions et de fait, cela ne va pas être possible de faire autrement.
Ceci dit ... AMA, la meilleure chose que l'on puisse faire d'une exception de logique : un core-dump (comment ça c'est ce que fait déjà assert et je suis un peu extrémiste en matière d'erreurs de programmation ?)
Une application ayant des contraintes de stabilité ne devrait pas core-dumper si elle rencontre une erreur, même grave, sur une partie du service rendu. Dans la majorité des cas, le core-dump est préférable ; mais dans la poursuite de la logique C++ ça ne devrait pas être imposé. Après tout, pour avoir un core-dump ne suffit-il pas de lever une exception catché nulle part ?
Et on ne peut pas juste rien faire, et laisser planter comme ca le sans contrat?
Il suffirait d'attendre que les respects de contrats soit détectables à la compilation.
Je suis bien d'accord avec ça (contrainte de stabilité), je parlais du cas spécifiqueSoit c'est noexcept, soit ça l'est pas.Code:
1
2
3
4 [[pre(logic_error): n >= 1]] void bar(int n) noexcept { }
Après de manière générale je trouve que les exceptions sont trop utilisées, il y à pas mal de cas où un assert est meilleur (à mon goût).
Ça ne l’est pas (imposé). C’est pour ça qu’une rupture de contrat doit rester un UB.
Cela dit, la contrainte de stabilité impose que tu aies une vraie politique de gestion de tes propres bugs. Un seul gros try/catch tout en haut, c’est complètement insuffisant.
Le problème est que des gens se sont dit que c’était une bonne idée de tout catcher. Par exemple, la boucle principale d’une QApplication catche tout --> assert est nettement meilleur que throw pour détecter les problèmes.Citation:
Après tout, pour avoir un core-dump ne suffit-il pas de lever une exception catché nulle part ?
Si j'ai bien compris sa position, les post-conditions devraient être testées dans des tests unitaires. Ou dit autrement : tester les post-cond directement dans son code, c'est embarquer des TU dans son code.
C'est bien ce qui a été décidé, après avoir un certain temps exploré une piste où y'aurait 2 version de noexcept en C++ 8O
La vérification des contrats par analyse statique de code est une fonctionnalité supplémentaire attendue en complément de l'analyse dynamique par le langage (certainement désactivable au niveau du compilo).
Dans la cas de QApplication, c'est une contrainte technique. En effet, le code C++ [d'un slot] est appelé en tant que callback depuis une dll C du système (suite à un event, genre clic de souris). Si une exception C++ remonte jusqu'à l'appelant C, alors ça va mal se passer. C'est pour ça qu'elles doivent toutes être capturées.
Sur ce point, il a parfaitement raison. Les postconditions sont avant tout utiles pour l’analyse statique et pour la doc.
Néanmoins, compte tenu des limitations des TUs (on ne teste jamais pour l’ensemble des entrées possibles), cela a du sens de garder la vérification des postconditions / invariants dans une build « debug ».