Comment fonctionnent les exceptions (en coulisse) en c ++
-
08-07-2019 - |
Question
Je continue à voir des gens dire que les exceptions sont lentes, mais je ne vois jamais aucune preuve. Ainsi, au lieu de demander si elles le sont, je vais demander comment les exceptions fonctionnent en arrière-plan, afin que je puisse prendre une décision concernant le moment de les utiliser et leur lenteur.
D'après ce que je sais, les exceptions sont la même chose que de faire beaucoup de retours, mais il vérifie également le moment où il doit cesser de faire le retour. Comment vérifie-t-il quand s'arrêter? Je suppose que je dis qu'il existe une deuxième pile qui contient le type d'exception et l'emplacement de la pile revient ensuite jusqu'à ce qu'il y parvienne. Je suppose également que le seul moment où la pile est touchée est un lancer et chaque essai / attrapé. AFAICT implémentant un comportement similaire avec un code retour prendrait le même temps. Mais tout cela n’est qu’une hypothèse, alors je veux savoir.
Comment fonctionnent réellement les exceptions?
La solution
Au lieu de deviner, j'ai décidé de regarder le code généré avec un petit morceau de code C ++ et une installation un peu ancienne de Linux.
class MyException
{
public:
MyException() { }
~MyException() { }
};
void my_throwing_function(bool throwit)
{
if (throwit)
throw MyException();
}
void another_function();
void log(unsigned count);
void my_catching_function()
{
log(0);
try
{
log(1);
another_function();
log(2);
}
catch (const MyException& e)
{
log(3);
}
log(4);
}
Je l'ai compilé avec g ++ -m32 -W -Wall -O3 -save-temps -c
, puis j'ai examiné le fichier d'assemblage généré.
.file "foo.cpp"
.section .text._ZN11MyExceptionD1Ev,"axG",@progbits,_ZN11MyExceptionD1Ev,comdat
.align 2
.p2align 4,,15
.weak _ZN11MyExceptionD1Ev
.type _ZN11MyExceptionD1Ev, @function
_ZN11MyExceptionD1Ev:
.LFB7:
pushl %ebp
.LCFI0:
movl %esp, %ebp
.LCFI1:
popl %ebp
ret
.LFE7:
.size _ZN11MyExceptionD1Ev, .-_ZN11MyExceptionD1Ev
_ZN11MyExceptionD1Ev
est MyException :: ~ MyException ()
, le compilateur a donc décidé qu'il fallait une copie non en ligne du destructeur.
.globl __gxx_personality_v0
.globl _Unwind_Resume
.text
.align 2
.p2align 4,,15
.globl _Z20my_catching_functionv
.type _Z20my_catching_functionv, @function
_Z20my_catching_functionv:
.LFB9:
pushl %ebp
.LCFI2:
movl %esp, %ebp
.LCFI3:
pushl %ebx
.LCFI4:
subl $20, %esp
.LCFI5:
movl .text
.align 2
.p2align 4,,15
.globl _Z20my_throwing_functionb
.type _Z20my_throwing_functionb, @function
_Z20my_throwing_functionb:
.LFB8:
pushl %ebp
.LCFI6:
movl %esp, %ebp
.LCFI7:
subl $24, %esp
.LCFI8:
cmpb .weak _ZTI11MyException
.section .rodata._ZTI11MyException,"aG",@progbits,_ZTI11MyException,comdat
.align 4
.type _ZTI11MyException, @object
.size _ZTI11MyException, 8
_ZTI11MyException:
.long _ZTVN10__cxxabiv117__class_type_infoE+8
.long _ZTS11MyException
.weak _ZTS11MyException
.section .rodata._ZTS11MyException,"aG",@progbits,_ZTS11MyException,comdat
.type _ZTS11MyException, @object
.size _ZTS11MyException, 14
_ZTS11MyException:
.string "11MyException"
, 8(%ebp)
jne .L21
leave
ret
.L21:
movl $1, (%esp)
call __cxa_allocate_exception
movl Au lieu de deviner, j'ai décidé de regarder le code généré avec un petit morceau de code C ++ et une installation un peu ancienne de Linux.
class MyException
{
public:
MyException() { }
~MyException() { }
};
void my_throwing_function(bool throwit)
{
if (throwit)
throw MyException();
}
void another_function();
void log(unsigned count);
void my_catching_function()
{
log(0);
try
{
log(1);
another_function();
log(2);
}
catch (const MyException& e)
{
log(3);
}
log(4);
}
Je l'ai compilé avec g ++ -m32 -W -Wall -O3 -save-temps -c
, puis j'ai examiné le fichier d'assemblage généré.
.file "foo.cpp"
.section .text._ZN11MyExceptionD1Ev,"axG",@progbits,_ZN11MyExceptionD1Ev,comdat
.align 2
.p2align 4,,15
.weak _ZN11MyExceptionD1Ev
.type _ZN11MyExceptionD1Ev, @function
_ZN11MyExceptionD1Ev:
.LFB7:
pushl %ebp
.LCFI0:
movl %esp, %ebp
.LCFI1:
popl %ebp
ret
.LFE7:
.size _ZN11MyExceptionD1Ev, .-_ZN11MyExceptionD1Ev
_ZN11MyExceptionD1Ev
est MyException :: ~ MyException ()
, le compilateur a donc décidé qu'il fallait une copie non en ligne du destructeur.
.globl __gxx_personality_v0
.globl _Unwind_Resume
.text
.align 2
.p2align 4,,15
.globl _Z20my_catching_functionv
.type _Z20my_catching_functionv, @function
_Z20my_catching_functionv:
.LFB9:
pushl %ebp
.LCFI2:
movl %esp, %ebp
.LCFI3:
pushl %ebx
.LCFI4:
subl $20, %esp
.LCFI5:
movl .section .eh_frame,"a",@progbits
.Lframe1:
.long .LECIE1-.LSCIE1
.LSCIE1:
.long 0x0
.byte 0x1
.string "zPL"
.uleb128 0x1
.sleb128 -4
.byte 0x8
.uleb128 0x6
.byte 0x0
.long __gxx_personality_v0
.byte 0x0
.byte 0xc
.uleb128 0x4
.uleb128 0x4
.byte 0x88
.uleb128 0x1
.align 4
.LECIE1:
.LSFDE3:
.long .LEFDE3-.LASFDE3
.LASFDE3:
.long .LASFDE3-.Lframe1
.long .LFB9
.long .LFE9-.LFB9
.uleb128 0x4
.long .LLSDA9
.byte 0x4
.long .LCFI2-.LFB9
.byte 0xe
.uleb128 0x8
.byte 0x85
.uleb128 0x2
.byte 0x4
.long .LCFI3-.LCFI2
.byte 0xd
.uleb128 0x5
.byte 0x4
.long .LCFI5-.LCFI3
.byte 0x83
.uleb128 0x3
.align 4
.LEFDE3:
.LSFDE5:
.long .LEFDE5-.LASFDE5
.LASFDE5:
.long .LASFDE5-.Lframe1
.long .LFB8
.long .LFE8-.LFB8
.uleb128 0x4
.long 0x0
.byte 0x4
.long .LCFI6-.LFB8
.byte 0xe
.uleb128 0x8
.byte 0x85
.uleb128 0x2
.byte 0x4
.long .LCFI7-.LCFI6
.byte 0xd
.uleb128 0x5
.align 4
.LEFDE5:
.ident "GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
.section .note.GNU-stack,"",@progbits
, (%esp)
.LEHB0:
call _Z3logj
.LEHE0:
movl $1, (%esp)
.LEHB1:
call _Z3logj
call _Z16another_functionv
movl $2, (%esp)
call _Z3logj
.LEHE1:
.L5:
movl $4, (%esp)
.LEHB2:
call _Z3logj
addl $20, %esp
popl %ebx
popl %ebp
ret
.L12:
subl $1, %edx
movl %eax, %ebx
je .L16
.L14:
movl %ebx, (%esp)
call _Unwind_Resume
.LEHE2:
.L16:
.L6:
movl %eax, (%esp)
call __cxa_begin_catch
movl $3, (%esp)
.LEHB3:
call _Z3logj
.LEHE3:
call __cxa_end_catch
.p2align 4,,3
jmp .L5
.L11:
.L8:
movl %eax, %ebx
.p2align 4,,6
call __cxa_end_catch
.p2align 4,,6
jmp .L14
.LFE9:
.size _Z20my_catching_functionv, .-_Z20my_catching_functionv
.section .gcc_except_table,"a",@progbits
.align 4
.LLSDA9:
.byte 0xff
.byte 0x0
.uleb128 .LLSDATT9-.LLSDATTD9
.LLSDATTD9:
.byte 0x1
.uleb128 .LLSDACSE9-.LLSDACSB9
.LLSDACSB9:
.uleb128 .LEHB0-.LFB9
.uleb128 .LEHE0-.LEHB0
.uleb128 0x0
.uleb128 0x0
.uleb128 .LEHB1-.LFB9
.uleb128 .LEHE1-.LEHB1
.uleb128 .L12-.LFB9
.uleb128 0x1
.uleb128 .LEHB2-.LFB9
.uleb128 .LEHE2-.LEHB2
.uleb128 0x0
.uleb128 0x0
.uleb128 .LEHB3-.LFB9
.uleb128 .LEHE3-.LEHB3
.uleb128 .L11-.LFB9
.uleb128 0x0
.LLSDACSE9:
.byte 0x1
.byte 0x0
.align 4
.long _ZTI11MyException
.LLSDATT9:
Surprise! Il n'y a aucune instruction supplémentaire sur le chemin du code normal. À la place, le compilateur a généré des blocs de code de correction hors ligne supplémentaires, référencés via une table à la fin de la fonction (qui est en réalité placé dans une section distincte de l'exécutable). Tout le travail est effectué dans les coulisses par la bibliothèque standard, basée sur ces tables ( _ZTI11MyException
est typeinfo pour MyException
).
OK, ce n’était pas vraiment une surprise pour moi, je savais déjà comment ce compilateur le faisait. Poursuivant avec la sortie de l'assemblage:
<*>
Nous voyons ici le code permettant de lancer une exception. Bien qu'il n'y ait pas de frais généraux supplémentaires simplement parce qu'une exception peut être levée, il y a évidemment beaucoup de frais généraux lors du lancement et de la capture d'une exception. La plupart d’entre elles sont cachées dans __ cxa_throw
, qui doit:
- Parcourez la pile à l'aide des tables d'exceptions jusqu'à ce qu'elle trouve un gestionnaire pour cette exception.
- Détendez la pile jusqu'à ce qu'elle parvienne à ce gestionnaire.
- Appelez réellement le gestionnaire.
Comparez cela au coût de simplement renvoyer une valeur et vous voyez pourquoi les exceptions ne devraient être utilisées que pour des retours exceptionnels.
Pour terminer, le reste du fichier d'assemblage:
<*>
Les données typeinfo.
<*>
Encore plus de tables de traitement des exceptions et d’informations complémentaires.
Donc, conclusion, du moins pour GCC sous Linux: le coût est un espace supplémentaire (pour les gestionnaires et les tables), que des exceptions soient générées ou non, plus le coût supplémentaire lié à l'analyse des tables et à l'exécution des gestionnaires lorsqu'une exception est jeté. Si vous utilisez des exceptions au lieu de codes d'erreur et qu'une erreur est rare, elle peut être plus rapide , car vous n'avez plus la surcharge de rechercher des erreurs.
Si vous souhaitez plus d'informations, en particulier ce que font toutes les fonctions __ cxa _
, consultez la spécification d'origine à l'origine:
ZN11MyExceptionD1Ev, 8(%esp)
movl Au lieu de deviner, j'ai décidé de regarder le code généré avec un petit morceau de code C ++ et une installation un peu ancienne de Linux.
class MyException
{
public:
MyException() { }
~MyException() { }
};
void my_throwing_function(bool throwit)
{
if (throwit)
throw MyException();
}
void another_function();
void log(unsigned count);
void my_catching_function()
{
log(0);
try
{
log(1);
another_function();
log(2);
}
catch (const MyException& e)
{
log(3);
}
log(4);
}
Je l'ai compilé avec g ++ -m32 -W -Wall -O3 -save-temps -c
, puis j'ai examiné le fichier d'assemblage généré.
.file "foo.cpp"
.section .text._ZN11MyExceptionD1Ev,"axG",@progbits,_ZN11MyExceptionD1Ev,comdat
.align 2
.p2align 4,,15
.weak _ZN11MyExceptionD1Ev
.type _ZN11MyExceptionD1Ev, @function
_ZN11MyExceptionD1Ev:
.LFB7:
pushl %ebp
.LCFI0:
movl %esp, %ebp
.LCFI1:
popl %ebp
ret
.LFE7:
.size _ZN11MyExceptionD1Ev, .-_ZN11MyExceptionD1Ev
_ZN11MyExceptionD1Ev
est MyException :: ~ MyException ()
, le compilateur a donc décidé qu'il fallait une copie non en ligne du destructeur.
.globl __gxx_personality_v0
.globl _Unwind_Resume
.text
.align 2
.p2align 4,,15
.globl _Z20my_catching_functionv
.type _Z20my_catching_functionv, @function
_Z20my_catching_functionv:
.LFB9:
pushl %ebp
.LCFI2:
movl %esp, %ebp
.LCFI3:
pushl %ebx
.LCFI4:
subl $20, %esp
.LCFI5:
movl <*>, (%esp)
.LEHB0:
call _Z3logj
.LEHE0:
movl $1, (%esp)
.LEHB1:
call _Z3logj
call _Z16another_functionv
movl $2, (%esp)
call _Z3logj
.LEHE1:
.L5:
movl $4, (%esp)
.LEHB2:
call _Z3logj
addl $20, %esp
popl %ebx
popl %ebp
ret
.L12:
subl $1, %edx
movl %eax, %ebx
je .L16
.L14:
movl %ebx, (%esp)
call _Unwind_Resume
.LEHE2:
.L16:
.L6:
movl %eax, (%esp)
call __cxa_begin_catch
movl $3, (%esp)
.LEHB3:
call _Z3logj
.LEHE3:
call __cxa_end_catch
.p2align 4,,3
jmp .L5
.L11:
.L8:
movl %eax, %ebx
.p2align 4,,6
call __cxa_end_catch
.p2align 4,,6
jmp .L14
.LFE9:
.size _Z20my_catching_functionv, .-_Z20my_catching_functionv
.section .gcc_except_table,"a",@progbits
.align 4
.LLSDA9:
.byte 0xff
.byte 0x0
.uleb128 .LLSDATT9-.LLSDATTD9
.LLSDATTD9:
.byte 0x1
.uleb128 .LLSDACSE9-.LLSDACSB9
.LLSDACSB9:
.uleb128 .LEHB0-.LFB9
.uleb128 .LEHE0-.LEHB0
.uleb128 0x0
.uleb128 0x0
.uleb128 .LEHB1-.LFB9
.uleb128 .LEHE1-.LEHB1
.uleb128 .L12-.LFB9
.uleb128 0x1
.uleb128 .LEHB2-.LFB9
.uleb128 .LEHE2-.LEHB2
.uleb128 0x0
.uleb128 0x0
.uleb128 .LEHB3-.LFB9
.uleb128 .LEHE3-.LEHB3
.uleb128 .L11-.LFB9
.uleb128 0x0
.LLSDACSE9:
.byte 0x1
.byte 0x0
.align 4
.long _ZTI11MyException
.LLSDATT9:
Surprise! Il n'y a aucune instruction supplémentaire sur le chemin du code normal. À la place, le compilateur a généré des blocs de code de correction hors ligne supplémentaires, référencés via une table à la fin de la fonction (qui est en réalité placé dans une section distincte de l'exécutable). Tout le travail est effectué dans les coulisses par la bibliothèque standard, basée sur ces tables ( _ZTI11MyException
est typeinfo pour MyException
).
OK, ce n’était pas vraiment une surprise pour moi, je savais déjà comment ce compilateur le faisait. Poursuivant avec la sortie de l'assemblage:
<*>
Nous voyons ici le code permettant de lancer une exception. Bien qu'il n'y ait pas de frais généraux supplémentaires simplement parce qu'une exception peut être levée, il y a évidemment beaucoup de frais généraux lors du lancement et de la capture d'une exception. La plupart d’entre elles sont cachées dans __ cxa_throw
, qui doit:
- Parcourez la pile à l'aide des tables d'exceptions jusqu'à ce qu'elle trouve un gestionnaire pour cette exception.
- Détendez la pile jusqu'à ce qu'elle parvienne à ce gestionnaire.
- Appelez réellement le gestionnaire.
Comparez cela au coût de simplement renvoyer une valeur et vous voyez pourquoi les exceptions ne devraient être utilisées que pour des retours exceptionnels.
Pour terminer, le reste du fichier d'assemblage:
<*>
Les données typeinfo.
<*>
Encore plus de tables de traitement des exceptions et d’informations complémentaires.
Donc, conclusion, du moins pour GCC sous Linux: le coût est un espace supplémentaire (pour les gestionnaires et les tables), que des exceptions soient générées ou non, plus le coût supplémentaire lié à l'analyse des tables et à l'exécution des gestionnaires lorsqu'une exception est jeté. Si vous utilisez des exceptions au lieu de codes d'erreur et qu'une erreur est rare, elle peut être plus rapide , car vous n'avez plus la surcharge de rechercher des erreurs.
Si vous souhaitez plus d'informations, en particulier ce que font toutes les fonctions __ cxa _
, consultez la spécification d'origine à l'origine:
ZTI11MyException, 4(%esp)
movl %eax, (%esp)
call __cxa_throw
.LFE8:
.size _Z20my_throwing_functionb, .-_Z20my_throwing_functionb
, (%esp)
.LEHB0:
call _Z3logj
.LEHE0:
movl $1, (%esp)
.LEHB1:
call _Z3logj
call _Z16another_functionv
movl $2, (%esp)
call _Z3logj
.LEHE1:
.L5:
movl $4, (%esp)
.LEHB2:
call _Z3logj
addl $20, %esp
popl %ebx
popl %ebp
ret
.L12:
subl $1, %edx
movl %eax, %ebx
je .L16
.L14:
movl %ebx, (%esp)
call _Unwind_Resume
.LEHE2:
.L16:
.L6:
movl %eax, (%esp)
call __cxa_begin_catch
movl $3, (%esp)
.LEHB3:
call _Z3logj
.LEHE3:
call __cxa_end_catch
.p2align 4,,3
jmp .L5
.L11:
.L8:
movl %eax, %ebx
.p2align 4,,6
call __cxa_end_catch
.p2align 4,,6
jmp .L14
.LFE9:
.size _Z20my_catching_functionv, .-_Z20my_catching_functionv
.section .gcc_except_table,"a",@progbits
.align 4
.LLSDA9:
.byte 0xff
.byte 0x0
.uleb128 .LLSDATT9-.LLSDATTD9
.LLSDATTD9:
.byte 0x1
.uleb128 .LLSDACSE9-.LLSDACSB9
.LLSDACSB9:
.uleb128 .LEHB0-.LFB9
.uleb128 .LEHE0-.LEHB0
.uleb128 0x0
.uleb128 0x0
.uleb128 .LEHB1-.LFB9
.uleb128 .LEHE1-.LEHB1
.uleb128 .L12-.LFB9
.uleb128 0x1
.uleb128 .LEHB2-.LFB9
.uleb128 .LEHE2-.LEHB2
.uleb128 0x0
.uleb128 0x0
.uleb128 .LEHB3-.LFB9
.uleb128 .LEHE3-.LEHB3
.uleb128 .L11-.LFB9
.uleb128 0x0
.LLSDACSE9:
.byte 0x1
.byte 0x0
.align 4
.long _ZTI11MyException
.LLSDATT9:
Surprise! Il n'y a aucune instruction supplémentaire sur le chemin du code normal. À la place, le compilateur a généré des blocs de code de correction hors ligne supplémentaires, référencés via une table à la fin de la fonction (qui est en réalité placé dans une section distincte de l'exécutable). Tout le travail est effectué dans les coulisses par la bibliothèque standard, basée sur ces tables ( _ZTI11MyException
est typeinfo pour MyException
).
OK, ce n’était pas vraiment une surprise pour moi, je savais déjà comment ce compilateur le faisait. Poursuivant avec la sortie de l'assemblage:
<*> Nous voyons ici le code permettant de lancer une exception. Bien qu'il n'y ait pas de frais généraux supplémentaires simplement parce qu'une exception peut être levée, il y a évidemment beaucoup de frais généraux lors du lancement et de la capture d'une exception. La plupart d’entre elles sont cachées dans __ cxa_throw
, qui doit:
- Parcourez la pile à l'aide des tables d'exceptions jusqu'à ce qu'elle trouve un gestionnaire pour cette exception.
- Détendez la pile jusqu'à ce qu'elle parvienne à ce gestionnaire.
- Appelez réellement le gestionnaire.
Comparez cela au coût de simplement renvoyer une valeur et vous voyez pourquoi les exceptions ne devraient être utilisées que pour des retours exceptionnels.
Pour terminer, le reste du fichier d'assemblage:
<*>Les données typeinfo.
<*>Encore plus de tables de traitement des exceptions et d’informations complémentaires.
Donc, conclusion, du moins pour GCC sous Linux: le coût est un espace supplémentaire (pour les gestionnaires et les tables), que des exceptions soient générées ou non, plus le coût supplémentaire lié à l'analyse des tables et à l'exécution des gestionnaires lorsqu'une exception est jeté. Si vous utilisez des exceptions au lieu de codes d'erreur et qu'une erreur est rare, elle peut être plus rapide , car vous n'avez plus la surcharge de rechercher des erreurs.
Si vous souhaitez plus d'informations, en particulier ce que font toutes les fonctions __ cxa _
, consultez la spécification d'origine à l'origine:
Autres conseils
Les exceptions étant lentes était vrai dans le passé.
Dans la plupart des compilateurs modernes, cela n’est plus vrai.
Remarque: le fait que nous ayons des exceptions ne signifie pas que nous n'utilisons pas non plus les codes d'erreur. Quand une erreur peut être gérée localement, utilisez des codes d'erreur. Lorsque les erreurs nécessitent plus de contexte pour les exceptions d’utilisation de la correction: je l’ai écrit beaucoup plus éloquemment ici: Quels sont les principes qui guident votre politique de gestion des exceptions?
Le coût du code de traitement des exceptions lorsqu'aucune exception n'est utilisée est pratiquement nul.
Quand une exception est levée, du travail est fait.
Mais vous devez comparer cela au coût de la restitution des codes d'erreur et de leur vérification jusqu'au point où l'erreur peut être traitée. Les deux prennent plus de temps pour écrire et maintenir.
De plus, il existe un marché pour les novices:
Bien que les objets Exception soient supposés être petits, certaines personnes y mettent beaucoup de choses. Ensuite, vous avez le coût de la copie de l'objet exception. La solution est double:
- Ne mettez pas d'objets supplémentaires dans votre exception.
- Capture par référence de const.
À mon avis, je parierais que le même code avec des exceptions est plus efficace ou au moins aussi comparable que le code sans les exceptions (mais dispose de tout le code supplémentaire pour vérifier les résultats d'erreur de fonction). N'oubliez pas que vous n'obtenez rien gratuitement. Le compilateur génère le code que vous auriez dû écrire pour vérifier les codes d'erreur (et le compilateur est généralement beaucoup plus efficace qu'un humain).
Vous pouvez implémenter des exceptions de différentes manières, mais elles reposent généralement sur une prise en charge sous-jacente du système d'exploitation. Sous Windows, il s’agit du mécanisme structuré de gestion des exceptions.
Il existe une discussion intéressante sur les détails du projet de code: Comment un compilateur C ++ implémente la gestion des exceptions
La surcharge des exceptions se produit parce que le compilateur doit générer du code pour savoir quels objets doivent être détruits dans chaque cadre de pile (ou plus précisément, portée) si une exception se propage en dehors de cette étendue. Si une fonction ne comporte pas de variables locales sur la pile nécessitant l’appel de destructeurs, elle ne doit pas bénéficier d’une gestion des exceptions de pénalité de performance.
L'utilisation d'un code de retour ne peut dérouler qu'un seul niveau de la pile à la fois, alors qu'un mécanisme de traitement des exceptions peut sauter beaucoup plus loin dans la pile en une seule opération s'il n'y a rien à faire dans les cadres de pile intermédiaires.
Matt Pietrek a écrit un excellent article sur le Gestion des exceptions structurées Win32 . Bien que cet article ait été écrit en 1997, il s’applique toujours aujourd’hui (mais bien sûr, s’applique uniquement à Windows).
Cet article examine le problème et constate que, dans la pratique, il existe une analyse coût en temps aux exceptions, bien que le coût soit assez faible si l'exception n'est pas levée. Bon article, recommandé.
Un de mes amis a écrit un peu comment Visual C ++ gère les exceptions il y a quelques années.
Toutes les bonnes réponses.
Pensez également à quel point il est plus facile de déboguer du code qui "vérifie si" en tant que portes au sommet des méthodes au lieu de permettre au code de générer des exceptions.
Ma devise est qu’il est facile d’écrire du code qui fonctionne. Le plus important est d'écrire le code pour la prochaine personne qui le verra. Dans certains cas, c'est vous dans 9 mois et vous ne voulez pas maudire votre nom!