Bonjour Nal,
Si tu es comme moi, tu détestes Go malgré quelques fonctionnalités géniales et un écosystème vaste et grandissant, qui en font un choix à considérer malgré tes préférences personnelles.
Si tu es comme moi, tu préfères certainement le C++ surtout depuis le C++11. Tu fais d'ailleurs surement du C++20 histoire d'être moderne.
Parmi les fonctionnalités de Go que tu apprécies, il y a le mot clé defer
:
func example() error {
rsrc1, err := CreateResource()
if err != nil {
return err
}
defer rsrc1.Destroy()
rsrc2, err := CreateResource()
if err != nil {
return err
}
defer rsrc2.Destroy()
DoSomething(rsrc1, rsrc2)
return nil
}
TL;DR : Il permet d'exécuter du code lorsque l'on quitte cette dernière.
Je veux la même chose en C++. Et en vrai, cela se fait assez simplement. On va juste profiter du concept de destructeur :
#include <functional>
#include <vector>
#include <algorithm>
class defer_frame {
public:
using function = std::function<void(void)>;
private:
std::vector<function> m_funcs;
public:
~defer_frame() {
std::for_each(m_funcs.rbegin(), m_funcs.rend(), [](auto &f) {
f();
});
}
void defer(function fn) {
m_funcs.push_back(fn);
}
};
Mais que fait cette classe ? Elle stocke tout simplement un ensemble de fonctions, et lorsque l'instance de la classe est détruite, elle appelle ces fonctions dans l'ordre inverse de leur déclaration.
Quand est-ce qu'est appelé le destructeur d'une classe ? Lorsque l'on quitte la scope actuelle, c'est à dire dans l'un des cas suivants :
- un a atteint la fin du scope avec
}
- on quitte la boucle avec
break
- on quitte la fonction avec
return
- on lève une exception avec
throw
Ce qui donne :
void example() {
defer_frame _;
auto rsrc1 = create_resource(); // throws on failure
_.defer([&]() { destroy_resource(rsrc1); });
auto rsrc2 = create_resource();
_.defer([&]() { destroy_resource(rsrc2); });
do_something(rsrc1, rsrc2);
}
La gestion d'erreur est tout de suite un peu plus propre :)
Histoire de taper un peu moins de code, on peut se créer 2 petites macros :
#define new_defer_frame() defer_frame _
#define defer(code_block) _.defer([&]() code_block)
Un petit exemple avec la SDL pour la route :
#include <SDL.h>
#include <stdexcept>
void abort_sdl() {
throw std::runtime_error(SDL_GetError());
}
void run_sdl_program() {
new_defer_frame();
if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
abort_sdl();
}
defer({ SDL_Quit(); });
auto win = SDL_CreateWindow(
"example",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
800, 600,
SDL_WINDOW_SHOWN
);
if (win == nullptr) {
abort_sdl();
}
defer({ SDL_DestroyWindow(win); });
auto renderer = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
if (renderer == nullptr) {
abort_sdl();
}
defer({ SDL_DestroyRenderer(renderer); });
bool running = true;
while (running) {
SDL_Event evt;
while (SDL_PollEvent(&evt)) {
switch (evt.type) {
case:
running = false;
break;
default:
break;
}
}
}
}
# Coquille
Posté par David Delassus (site web personnel) . Évalué à 3.
Et voilà, après 5 relectures, je laisse
ensemble de fonction
au lieu deensemble de fonctions
.Quand est-ce que je le vois ? Juste après avoir cliqué sur "Poster".
Si les modos passent par là…
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Coquille
Posté par gUI (Mastodon) . Évalué à 3.
Corrigé, merci
En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.
# le defer est pavé de bonnes exceptions
Posté par devnewton 🍺 (site web personnel) . Évalué à 10.
Pourquoi vouloir defer quand on a du RAII ?
Le post ci-dessus est une grosse connerie, ne le lisez pas sérieusement.
[^] # Re: le defer est pavé de bonnes exceptions
Posté par Thomas Douillard . Évalué à 5.
Quand tu veux pas écrire une classe avec un destructeur dédié à un boulot de nettoyage très particulier ?
[^] # Re: le defer est pavé de bonnes exceptions
Posté par David Delassus (site web personnel) . Évalué à 7.
Prend l'exemple de la SDL.
Tu serais obligé de créer des
unique_ptr<SDL_Window>
etunique_ptr<SDL_Renderer>
avec les deleters appropriés. Tu devras aussi encapsuler dans une classe l'appel àSDL_Init
avec son destructeur qui appelleSDL_Quit
. Tout plein de code qui complexifie une fonction pourtant toute bête.Je t'avouerai en plus ne pas connaître suffisamment la spec pour te garantir dans quel ordre les destructeurs seront appelés. Ici, au moins, c'est explicite.
RAII c'est un design pattern parmi d'autres. Si tu utilises une lib C en C++ (SDL, lua, …), c'est parfois plus simple et plus clair d'avoir un simple
defer
.https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: le defer est pavé de bonnes exceptions
Posté par devnewton 🍺 (site web personnel) . Évalué à 2.
J'aurais fait une classe Game avec les pointeurs SDL_Window et SDL_Renderer comme membres :-)
Le post ci-dessus est une grosse connerie, ne le lisez pas sérieusement.
[^] # Re: le defer est pavé de bonnes exceptions
Posté par David Delassus (site web personnel) . Évalué à 4.
Et donc tu appelles quand le SDL_DestroyRenderer/Window et SQL_Quit ?
Si tu me dis que tu les appelles pas et que tu quittes tout simplement le programme sans t'en préoccuper, je te répondrais :
Et tu fais quoi si tu as plusieurs fenêtres (cf ceci) ?
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: le defer est pavé de bonnes exceptions
Posté par David Delassus (site web personnel) . Évalué à 4.
Un autre exemple qui vaut le coup d'oeil :
Dans cet exemple, j'ai 2 frames, une au niveau de la classe qui sera appelée par le destructeur, m'évitant ainsi de devoir maintenir une liste des textures créées. Et une au niveau de la fonction pour détruire la surface qui n'est que temporaire.
Dans ma fonction
run_sdl_program()
, j'ai plus qu'à faire unnew
et "defer" ledelete
de montexture_manager
.https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: le defer est pavé de bonnes exceptions
Posté par rewind (Mastodon) . Évalué à 8.
Moi, un étudiant me rend un code comme ça, je le pends par les pieds. Parce que là, ça ne demande qu'à péter à la gueule de celui qui va l'utiliser.
Il suffit de faire une copie du
texture_manager
et là, je vais avoir des doubles appels àDestroyTexture
, ça va être très fun. Et le problème vient du fait qu'à la base, on peut copier le defer_frame alors que ça n'a aucun sens.Moi, je suis comme devnewton, je suis partisan du RAII, c'est pas fait pour les chiens et au moins, c'est safe.
[^] # Re: le defer est pavé de bonnes exceptions
Posté par small_duck (site web personnel) . Évalué à 9.
J'aime beaucoup le defer, mais j'aime aussi beaucoup le unique_ptr, et je le préfère pour gérer la création et la destruction de ressources à travers des API C, comme pour SDL, justement.
est pour moi plus clair et plus sûr, car dans la même commande on gère à la fois la création de la ressource, et on prépare sa destruction. Et impossible de s'emmêler les pinceaux et de détruire la mauvaise ressource. Cerise sur le compilo, il est même possible d'utiliser le std::move et donc d'expliciter un transfert de la ressource.
Je vois donc le defer comme un plan B, lorsque la sémantique est différente: afficher un message de log en fin de fonction, par exemple, ou encore aller écrire un fichier.
Notons que ce dernier cas est assez mal géré par la RAII au cas où l'opération jette une exception (je n'entre pas dans les détails, mais en gros lancer une exception depuis un destructeur, c'est le mal). Ici, le defer nous donne une opportunité pour gérer ce cas d'erreur plus proprement.
[^] # Re: le defer est pavé de bonnes exceptions
Posté par Misc (site web personnel) . Évalué à 10.
les RAII defer, c'est en effet plus solide, c'est pour ça que les programmes roulent plus vite ?
[^] # Re: le defer est pavé de bonnes exceptions
Posté par groumly . Évalué à 8.
Parce que le block du defer n’est pas forcément lié à la destruction d’un objet.
Tu peux vouloir logger quelque chose, émettre un événement analytics, fermer une fenêtre/modal, arrêter un activity indicator etc.
Le concept c’est “qu’importe comment cette function termine, je veux toujours faire ceci”, pas “quand cet objet mort, fait ceci”.
Linuxfr, le portail francais du logiciel libre et du neo nazisme.
# Le diable est l'ennemi du détail (ou un truc comme ça)
Posté par barmic 🦦 . Évalué à 3.
Je ne fais ni de c++ ni de go, mais il me semble que l'ordre de libération est inverse. Ce serait plutôt une pile qu'il faut utiliser.
D'ailleurs il me semble que la gestion des scopes doit pouvoir devenir pigeux :
En go, j'imagine que ça :
Devra se traduire par :
C'est piégeux de devoir gérer manuellement le contexte (la frame).
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Le diable est l'ennemi du détail (ou un truc comme ça)
Posté par David Delassus (site web personnel) . Évalué à 3.
C'est le cas, j'ai utilisé
rbegin
etrend
, qui itère sur lestd::vector
depuis la fin vers le début.En Go
defer
exécute du code à la fin de la fonction, pas à la fin du scope, donc tondefer
à l'intérieur duif
ne sera pas exécuté au bon moment.Donc ma version C++ permet une gestion plus fine. Le fait de déclarer une seconde frame dans le if (qui repose sur le variable shadowing) va justement te permettre d'avoir le comportement souhaité.
https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Le diable est l'ennemi du détail (ou un truc comme ça)
Posté par barmic 🦦 . Évalué à 3.
Ah oui effectivement je suis allé trop vite.
Ok je n'étais pas sûr.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
# Les scope guards en D
Posté par raphj . Évalué à 10. Dernière modification le 07 février 2022 à 19:58.
En D (pour celles et ceux qui ne connaissent pas, c'est un langage à ramasse-miettes compilé nativement qui se veut un remplaçant de C++ avec 90% de sa puissance et 10% de sa complexité), cette fonctionnalité existe sous la forme de
scope
guards : il suffit de préfixer une instruction (ou un bloc d'instructions) parscope(exit)
et elle sera exécutée à la fin du bloc de code actuel (pas de la fonction, sauf s'il s'agit du bloc principal de la fonction bien sûr).Il y a aussi
scope(success)
: l'instruction ne sera exécutée que s'il n'y a pas eu d'exception levée, etscope(failure)
que s'il y a eu une exception levée avant la fin de l'exécution du bloc courant.La doc vend cette fonctionnalité comme un remplacement de RAII en C++ :
[^] # Re: Les scope guards en D
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 3.
Fais gaffe, beaucoup pense que c'est du gc façon java alers que c'est en mieux je trouve
C'est vraiment beau les scopes :)
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
# Allocs
Posté par Julien Jorge (site web personnel) . Évalué à 5.
C'est bien mais le premier truc que je relèverai est que les
std::function
vont faire des allocations dynamiques, de même pour lestd::vector
.Exercices pour le lecteur, et l'auteur :
std::function
ne fera-t-elle pas d'allocation dynamique ? Est-ce garanti ?Et sur un autre sujet, tu ne devrais pas utiliser
__
pour tes variables, c'est réservé pour le compilateur :)[^] # Re: Allocs
Posté par David Delassus (site web personnel) . Évalué à 1.
Quand elle est initialisée à partir d'un pointeur de fonction, et oui c'est garanti.
Pour les lambdas, je suis pas sûr.
A partir du moment ou je veux pouvoir "defer" des fonctions, lambdas, des méthodes d'objet, et pouvoir jouer avec
std::bind
. Je pense pas que cela soit possible.https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg
[^] # Re: Allocs
Posté par amdg . Évalué à 7.
Les lambdas "decay" en simple pointeur de fonction quand elles n'ont pas d'état. Je crois que la plupart du temps on souhaite que ces fonctions de nettoyage capturent tout par référence. Donc ça risque de pas être tip-top.
Voilà ma tentative d'implémentation, il faut au minimum C++14, et ça nécessite simplement d'avoir accès à
std::move
.Avec le lien godbolt qui va bien pour montrer que ça optimise pas trop mal: https://godbolt.org/z/7bK7q8Phe
[^] # Re: Allocs
Posté par Cyrille Pontvieux (site web personnel, Mastodon) . Évalué à 4. Dernière modification le 08 février 2022 à 16:07.
Je suis pas au fait du C++.
Est-ce que tu pourrais m’expliquer :
- d’où vient
__COUNTER__
, c’est le compilo qui te garanti que pour chaque appel de macro du as un compteur ?- j’ai compris l’astuce de surcharge de l’opérateur
+
qui permet d’éviter d’avoir une macro où on construit l’objet avec les parenthèse avant et après la fonction. Ça permet également de planquer la fonction en ayant l’impression d’avoir juste un block. Très beau. Mais je ne comprends pas à quoi sert letemplate
(je suis rouillé du C++)- à quoi sert le
std::move
?Désolé si c’est trivial…
[^] # Re: Allocs
Posté par amdg . Évalué à 4.
Il n'y a pas de mauvaises questions ;)
__COUNTER__
c'est une macro qui s'incrémente toute seule à chaque fois que qu'elle est utilisé. C'est souvent utilisé comme__LINE__
pour générer des noms uniques. Mais attention ça n'est pas standard, c'est une extension implémentée par MSVC, Clang et GCC.template<typename TCallback>
est nécessaire pour pouvoir passer la lambda. Tu ne peux pas préciser le type de la lambda, et l'objectif est d'éviter l'usage destd::function
, donc template.move
n'apporte rien. En revanche si la lambda capturait par copie lemove
pourrait être bénéfique si les objets capturés contiennent des allocations sur le tas.[^] # Re: Allocs
Posté par Cyrille Pontvieux (site web personnel, Mastodon) . Évalué à 3.
Merci, c’est très clair maintenant.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.