J'ai dû mal à me représenter comment je dois utiliser les unique_ptr en C++. Voici un code simple que j'ai implémenté et qui compile mais je ne sais pas si l'implémentation est correcte :
#include <iostream>
#include <memory>
class Vehicule {
public:
Vehicule(const std::string& brand) : brand_(brand) {}
virtual ~Vehicule() = default;
virtual void startEngine() = 0;
protected:
std::string brand_;
};
class Car : public Vehicule {
public:
Car(const std::string& brand) : Vehicule(brand) {}
void startEngine() override {
std::cout << "Car : " << brand_ << " started engine" << std::endl;
}
};
class Motorbike : public Vehicule {
public:
Motorbike(const std::string& brand) : Vehicule(brand) {}
void startEngine() override {
std::cout << "Motorbike : " << brand_ << " started engine" << std::endl;
}
};
class Pilot {
public:
Pilot(std::unique_ptr<Vehicule> vehicule) {
vehicule_ = std::move(vehicule);
}
void startVehiculeEngine() {
vehicule_->startEngine();
}
private:
std::unique_ptr<Vehicule> vehicule_;
};
int main() {
std::unique_ptr<Car> car = std::make_unique<Car>("Nissan");
std::unique_ptr<Motorbike> motorbike = std::make_unique<Motorbike>("Ducati");
Pilot carPilot(std::move(car));
Pilot motorbikePilot(std::move(motorbike));
carPilot.startVehiculeEngine();
motorbikePilot.startVehiculeEngine();
return 0;
}
Je suis perdu sur plusieurs points pourtant très basiques :
Comment dois-je passer le std::unique_ptr en argument de mon constructeur ?
Ce code ne compile pas mais je ne comprends pas pourquoi. Je pensais qu'un passage par référence pouvait fonctionner avec un std::unique_ptr puisqu'il n'y pas de copie du pointeur
Autre question : est-ce qu'il y a un moyen de s'assurer que personne n'utilisera les variables car (l.51) et motorbike (l.52) après avoir utilisé std::move dessus ? J'aime bien le concept des std::unique_ptr qui sont plus légers en mémoire que les std::shared_ptr mais s'ils sont aussi plus dangereux car ils peuvent entraîner des undefined behavior c'est pas génial...
- Edité par JeffWallace 2 septembre 2024 à 18:40:44
Pour la dernière question, c'est au dev de faire attention de ne pas utiliser un pointeur après un move. Normalement, c'est pas une chose qu'on loupe, parce que le move est explicite.
Comment choisir entre l'option 1 et l'option 2 dans ce cas ? (c'est peut-être un détail mais souvent il y a une manière à privilégier plutôt qu'une autre en C++)
Pour l'option 3, pourquoi je ne peux pas passer le std::unique_ptr par valeur dans le constructeur dans ce cas ?
Ce code ne compile pas :
Pilot(std::unique_ptr<Vehicule>& vehicule) {
vehicule_ = std::move(vehicule);
}
...
Pilot carPilot(car);
Dans le dernier cas. on ne peut pas convertir une lvalueunique_ptr<Car> en unique_ptr<Vehicule>&. Les 2 types sont "unrelated".
Dans les premiers cas, un unique_ptr<X> est prévu pour accepter d'être construit à partir d'un unique_ptr<Y>&&, à condition que Y* soit implicitement convertible en X*. C'est intéressant pour la conversion dérivée vers base qui est justement le cas ici.
Si dans le dernier cas, on passait un unique_ptr<Vehicule> ça marcherait, mais passer par l-rvalue reference un unique_ptr pour en faire un move est plutôt source de problème (alors que quand on reçoit par valeur ou par &&, on est sûr que le move ultérieur est sain).
std::unique_ptr<Vehicule> ptr_intermediaire{std::move(car)};
Pilot carPilot( ptr_intermediaire ); // on a fait un passage par référence!
// A cet instant, car ET ptr_intermediaire sont vides.
Dans tous les cas je suis obligé de construire les instances de la classe Pilot avec les std::move. Impossible de passer les variables directement :
Pilot carPilot(std::move(car));
Pilot motorbikePilot(std::move(motorbike));
Désolé je suis vraiment à la ramasse avec les unique_ptr. J'ai du mal à trouver des exemples de code les utilisant. Pourtant j'ai regardé dans dans plein de livres sur le C++.
// Create a (uniquely owned) resource
std::unique_ptr<D> p = std::make_unique<D>();
// Transfer ownership to `pass_through`,
// which in turn transfers ownership back through the return value
std::unique_ptr<D> q = pass_through(std::move(p));
Ok donc il faut bien passer en argument par valeur dans les fonctions puis utiliser std::move lorsque que le passage par copie n'est pas disponible.
Mais du coup ça veut pas dire que le passage par valeur est peu efficace ? Un passage par référence ne serait pas plus approprié ?
- Edité par JeffWallace 2 septembre 2024 à 23:17:22
Tu te prend trop la tête avec le passage d'un unique_ptr. Comme on te l'a dit et comme c'est indiqué dans le code que tu as copié, "a function consuming a unique_ptr can take it by value or by rvalue reference". C'est juste une question de préférence d'écriture, mais le résultat est le même dans ce cas (le compilateur sait qu'il doit optimiser le passage par valeur)
Dans tous les cas, la copie n'est pas utilisable (c'est le principe du "unique").
Et les references non constantes ne sont pas utilisables sur un temporaire.
Ah en fait, l'option 3 ne fonctionnait pas tout simplement parce que les pointeurs n'étaient pas de type Vehicule
Ce code compile :
#include <iostream>
#include <memory>
class Vehicule {
public:
Vehicule(const std::string& brand) : brand_(brand) {}
virtual ~Vehicule() = default;
virtual void startEngine() = 0;
protected:
std::string brand_;
};
class Car : public Vehicule {
public:
Car(const std::string& brand) : Vehicule(brand) {}
void startEngine() override {
std::cout << "Car : " << brand_ << " started engine" << std::endl;
}
};
class Motorbike : public Vehicule {
public:
Motorbike(const std::string& brand) : Vehicule(brand) {}
void startEngine() override {
std::cout << "Motorbike : " << brand_ << " started engine" << std::endl;
}
};
class Pilot {
public:
Pilot(std::unique_ptr<Vehicule>& vehicule) {
vehicule_ = std::move(vehicule);
}
void startVehiculeEngine() {
vehicule_->startEngine();
}
private:
std::unique_ptr<Vehicule> vehicule_;
};
int main() {
std::unique_ptr<Vehicule> car = std::make_unique<Car>("Nissan");
std::unique_ptr<Vehicule> motorbike = std::make_unique<Motorbike>("Ducati");
Pilot carPilot(car);
Pilot motorbikePilot(motorbike);
carPilot.startVehiculeEngine();
motorbikePilot.startVehiculeEngine();
return 0;
}
Après je trouve qu'il y a un vrai sujet sur cette histoire de comment passer les pointeurs à une fonction. Je tombe sur beaucoup de discussions sur internet sur ce sujet.
La différence entre par rvalue et par valeur est surtout si on veut une erreur récupérable.
Par valeur -> l'appelant transfère la propriété, si la fonction échoue (par exemple avec exception), le pointeur est libéré.
Par rvalue -> l'appelé peut récupérer l'ownership au dernier moment et s'il y a une erreur, l'appelant possède toujours le pointeur.
Dans les fait, il y a rarement besoin que l'appelant conserve l'ownership, mais par habitude j'opte pour la rvalues.
> Alors que d'habitude je suis plutôt habitué à voir des passages par référence constante
Il faut bien comprendre qu'un transfère d'owernship n'est pas définition pas quelque chose qui peut être constant.
Je n'ai pas lu l'ensemble de la discussion, je risque donc de répéter quelque chose qui a déjà été dit.
Le problème de ta conception actuelle est qu'elle semble partir du principe que, si un pilote est détruit, le véhicule dont il est propriétaire le sera forcément aussi.
Cela me fait me poser une question idiote : Le pilote d'un véhicule en est-il réellement le propriétaire, celui qui a le droit de le détruire quand bon il lui semble, ou n'est il en définitive qu'un utilisateur du véhicule, à charge pour "quelqu'un d'autre" de choisir le moment où il faut changer de véhicule, voire, qui pourrait décider de fournir le véhicule d'un pilote "disparu" à un autre pilote?
Car le principe même du std::unique_ptr est vraiment d'être "seul propriétaire de la ressource sous-jacente"; d'être le seul à pouvoir décider lorsque cette ressource doit (ou plus souvent peut) être libérée. C'est sa responsabilité absolue.
Seulement, comme std::unique_ptr n'est jamais qu'un "détail d'implémentation" de ta classe Pilot, cette classe agit "aux yeux du monde" comme si ... elle avait pris à son compte la responsabilité du std::unique_ptr.
Or, tout nous indique qu'elle a une autre responsabilité : celle d'utiliser la ressource mise à disposition par le std::unique_ptr. Si bien qu'elle se retrouve désormais avec deux responsabilités : celle d'utiliser la ressource qui lui est propre ET celle de décider du moment opportun de la libérée, et qui vient de std::unique_ptr.
Mais alors, que fait on du SRP (Single Responsibility Principle ou Principe de la Responsabilité unique), qui nous dit que chaque type de donnée, chaque donnée, chaque fonction ne devrait faire qu'une seule chose, pour s'assurer de la faire correctement? On le jette aux ortilles?
** Peut-être ** devrais tu faire en sorte que la propriété effective du véhicule n'échoie pas au pilote, mais à "autre chose" comme un VehicleOwner (un propriétaire effectif et reconnu comme tel), qui sera le seul à pouvoir décider de quand "le véhicule a fait son temps et doit être mis à la casse", ce qui permettrait de clarifier la responsabilité du pilote vu qu'il n'en serait alors plus que ... l'utilisateur.
Toute la question étant alors de savoir comment faire pour fournir un véhicule à un pilote
Je verrais bien l'utilisation d'un std::reference_wrapper dans la classe Pilot pour représenter le véhicule mis à sa disposition. Ce qui permettrait de changer facilement le véhicule mis à disposition du pilote.
Seulement, un std::reference_wrapper doit forcément référence un objet, ce qui obligerait tous les pilotes à avoir un véhicule à leur disposition en permanence. Ce qui n'est sans doute malheureusement pas toujours garanti...
Cependant, nous pourrions résoudre ce problème en créant ce que l'on appelle un "null object": une classe (héritée de la classe Véhicule, dans le cas présent) dont tous les comportement nous font bien comprendre que l'instance utilisée n'est pas un véhicule "valide et cohérent".
Elle pourrait prendre une forme proche de
class NoVehicule : public Vehicule {
public:
NoVehicule(const std::string& brand) : Vehicule("None") {}
void startEngine() override {
std::cout << "No vehicle on which to start engine" << std::endl;
}
};
(histoire de s'adapter à l'existant)
Et comme il nous faudra peut-être une instance de ce "non véhicule" y compris pour créer les pilotes (après tout, rien ne dit que chaque pilote aura son véhicule dés son engagement :D), mais qu'il ne sert à rien d'avoir trente six instances de ce non véhicule en mémoire, nous pouvons en créer une instance "globale" grâce à une fonction proche de
NoVehicule const & noVehicleAvailable(){
static NoVehicule data; // toute la magie sera là
return data;
}
Et bien sur, nous n'oublirons pas de corriger la classe Pilot en conséquence, en lui donnant une forme proche de
class Pilot {
public:
Pilot(Vehicule const & v = noVehicleAviable()):vehicule_(v) {
}
void startVehiculeEngine() {
vehicule_.startEngine();
}
void changeVehicule(Vehicule const & v=noVehicleAviable()){
vehicule_ = std::wrapper(v);
}
private:
std::reference_wrapper<Vehicule> vehicule_;
};
Alors, je sais que j'ai complètement modifié le problème, que je n'ai absolument pas répondu à ta question d'origine, et que je suis sans doute allé beaucoup plus loin que ce à quoi tu t'attendais.
Cependant, ce genre d'approche pourrait te faciliter énormément la vie pour la suite de ton projet
- Edité par koala01 14 septembre 2024 à 2:27:52
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Constructeur qui prend un unique_ptr en argument
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
Discord NaN. Mon site.
Discord NaN. Mon site.
Discord NaN. Mon site.
Discord NaN. Mon site.