J'aurai besoin d'une confirmation, car pour une fois, le compilateur ne me trouve pas de bug, alors que pour moi, il y en a un! (C'est bien la première fois, d'habitude c'est le contraire!)
Lors de tests unitaires, d'une class qui a été modifié et re modifié, je me suis retrouvé dans le cas suivant (code grandement simplifié pour le forum):
Et là, non seulement ça compile, mais en plus, ça affiche "1" comme initialement (avant la création de la classe truc_t) !
Hors je n'ai pas de constructeur de "doubleTruc_t" avec comme parametre int ! Mais c'est comme si, il avait automatiquement appelé le constructeur de "doubleTruc_t" avec l'objet résultant du constructeur de "truc_t" avec le parametre int 1.
Alors, comme c'était merveilleux (pour une fois!), je l'ai fait avec un niveau de plus :
Pour résoudre le problème, il suffit de mettre le type explicitement.
class A {
public:
A(int)
{
}
};
class B {
private:
A a_;
public:
B(const A& a)
: a_(a)
{
}
};
class C {
private:
B b_;
public:
C(const B& b)
: b_(b)
{
}
};
int main() {
B b(1);
// C c(123); no matching function for call to C::C(int)
C c{B{123}};
return 0;
}
Dans ton cas, je pense que ça fonctionnera si tu remplaces tripleTruc_t t{1} par tripleTruc_t t{doubleTruc_t{1}};
- Edité par markand 23 août 2019 à 9:10:19
git is great because Linus did it, mercurial is better because he didn't.
La mauvaise, c'est que je ne comprends pas vos réponses: Les conversions entre short/int/long/unsigned int/float ... Ok, c'est classique. Mais là, il s'agit de constructeur: Je crée un "truc_t" a partir d'un argument, je ne le "change" pas sa représentation! ....
Ou alors, à l'inverse, je n'avais peut-être jamais compris les conversions: quand on fait une conversion de type (de int en float, par exemple), ça voudrait dire qu'on appel le constructeur du nouveau type (float, dans l'exemple) avec un argument d'un autre type (int dans l'exemple). C'est ça que vous voulez me dire ?
Ton dernier message n'est pas super clair. Si je comprends bien, tu pensais que seuls les types primitifs pouvaient être convertis implicitement entre eux?
Pre C++11, tout constructeur avec un seul argument et sans le mot clé "explicit" définit une conversion implicite du type de son argument vers le type de sa classe. Et à partir du C++11, c'est même le cas des constructeurs avec plusieurs arguments.
La mauvaise, c'est que je ne comprends pas vos réponses: Les conversions entre short/int/long/unsigned int/float ... Ok, c'est classique. Mais là, il s'agit de constructeur: Je crée un "truc_t" a partir d'un argument, je ne le "change" pas sa représentation! ....
Oui, il faut dire que ce n'est pas forcément des plus intuitifs
Car nous sommes bien d'accord que le rapport entre un int et une classe qui prendrait la forme de
class Truc {
public:
Truc (int i);
};
est loin d'être évident, n'est-ce pas ? Et, pourtant, il y en a un, à cause du constructeur qui prend un entier comme paramètre va -- justement -- agir exactement à la manière un opérateur de conversion implicite.
C'est assez surprenant, mais on peut tout à fait écrire un code proche de
int main(){
Truc a = 3;
std::cout<<a.i<<"\n";
}
qui affichera 3
Et pour bien te montrer que ce n'est pas du au hasard, on peut faire la même chose en disposant d'une variable de type int:
int main(){
int i = 10;
Truc b = i;
std::cout<<b.i<<"\n";
}
qui affichera 10 cette fois
C'est exactement le même principe que celui qui te permet de convertir un short en int:
int main(){
short s = 3;
int i = s; // sa marche!
}
Le ... truc, c'est que C++ met quand même une limite au nombre de conversions implicites qui peuvent être effectuées: on n'a droit qu'à UNE et UNE SEULE conversion implicite, ni plus, ni moins :
class Truc{
public:
Truc(int i):i{i}{}
int i;
};
class Brol{
public:
Brol(Truc const & t):t{t}{
}
Truc t;
};
int main(){
Truc t = 1; // une conversion
Brol b = t; // une conversion
/* MAIS MAIS MAIS
Brol b2 = 3; sera refusé : deux conversions seraient
nécessaires ( int->Truc->Brol )
*/
}
Alors, bien sur, le gros problème avec les "comportements par défaut", c'est que l'on va toujours trouver des situations dans lesquelles... on aurait préféré qu'ils agissent autrement
Et donc, si tu ne veux pas que la conversion puisse se faire automatique, et il y a pas mal de raisons qui pourraient plaider en ce sens, tu peux toujours déclarer ton constructeur comme explicit :
class NoConversion{
public:
explicit NoConversion(int i):i{i}{
}
int i;
};
int main(){
/* autorisé : on fait appel au constructeur directement */
NoConversion nc{1};
/* MAIS MAIS MAIS */
NoConversion erreur = 1; //sera refusé : conversion impossible
}
Est-ce que cela te parait un peu plus clair, maintenant?
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
Ton dernier message n'est pas super clair. Si je comprends bien, tu pensais que seuls les types primitifs pouvaient être convertis implicitement entre eux?
Oui, je viens du monde du C.
Merci SpaceIn pour l'explication et le raccourcie, et Merci au long long message de Koala01, (qui lui de plus est en français ) C'est maintenant clair! On peut pas faire mieux!
Juste encore une question, (et si j'ai bien compris, vous allez me répondre oui!): Quand on fait un cast (c'est à dire une conversion explicite), c'est pareil en fait, on appel le constucteur du type destination ?
Quand on fait un cast (c'est à dire une conversion explicite), c'est pareil en fait, on appel le constucteur du type destination ?
Non...
Pour un cast explicite "à la C" (du genre de (autretype) maVariable), on indique simplement au compilateur qu'il doit considérer maVariable (qui est d'un type donné) est en réalité d'un autre type. En gros, on ment au compilateur.
En C++, les choses vont encore plus loin, car on déconseille le cast "à la C", pour une raison toute bête : on dispose de quatre cast différents:
reinterpret_cast : qui agit exactement comme le cast du C:
char word[4];
reinterpret_cast<int *>(word)=33444666; // on demande au compilateur
// de considérer que
// word est un int
le static_cast : qui demande au compilateur de vérifier si la donnée est d'un type compatible avec le type que l'on veut prétendre qu'elle est
le dynamic_cast : qui demande au compilateur de placer une vérification à l'exécution du type de la variable, par exemple
class Base{
/* ... */
};
class Derivee : public Base{
void specific();
};
class Autre : public Base{
};
int main(){
Base * b= new Derivee;
/* le cast renverra nullptr si b n'est pas de type
* Derivee
*/
Derivee cast =dynamic_cast<Derivee*>(b);
if(cast)
cast->specific();
}
et le const_cast : qui permet de changer la constance d'une donnée (non const --> const ou const --> non const). Note que l'on peut toujours "rajouter la constance si elle n'est pas présente:
int main(){
int i=15; // non constant
int const & ref = i; // constant, aucun problème
}
mais que l'on ne peut pas retirer de la constance si elle est présente
int main(){
int const i = 15;
int & ref = i; // refusé... l'alternative
const_cast<int &>(i) = 32; // accepté, mais très dangereux
}
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
Ce que dit koala01 sur les cast est vrai, mais uniquement pour les références et pointeur. Dans le cas d'un type plein (c.-à-d. hors référence et pointeur), c'est le constructeur qui est utilisé.
La raison est simple, les casts avec pointeur/référence ne modifie pas l'objet, juste la représentation du type utilisé par le compilateur. Les 2 pointeurs font référence à la même chose et modifier la valeur pointée de l'un modifie forcément le contenu des 2 pointés puisqu'ils sont identiques. Dans le cas d'un type plein, cela fait une conversion et donc des appels aux constructeurs ou opérateurs de cast.
En effet Koala01, je n'étais pas assez précis quand je parlais de cast. En fait je parlais de static_cast (comme j'ai mis tous les warning dans les options de compilation, il est courant que j'ai des warning sur des modifications de type en particulier de int --> char, ou autre. Aussi j'utilise le static_cast pour les supprimer !).
Je ne m'étais pas encore posé la question du "cast" des références ni des pointeurs. Cool d'avoir ouvert le sujet! et mon intérêt
Et pour les "types pleins", Jo_link_noir, tu confirmes ce que je supposais. (Je n'ai pas encore le réflexe de chercher sur cppreference.com, ... il faudra à l'avenir! ... Mais c'est un peu compliqué de faire le lien entre son cas pratique et un mot clef dans cppreference.com !)
En tous cas merci d'avoir pris le temps de répondre. ("Je serait moins bête ce soir!").
Hors si je ne me trompe pas "", c'est un litéral de type char *; donc quand je fais un truc_t {""}, je fais 2 conversions implicites de suite: char * --> std::string et std::string --> truc_t. ("Isn't it ?" comme ils disent chez les mangeurs de sauce à la menthe!)
Peut-être qu'il y a encore des subtilités avec un littéral chaîne de caractères.
(PS et là, cppreference.com ne m'a pas donné la réponse ... )
Tu te trompes: en écrivant truc_t t0{""}, tu fais explicitement appel au constructeur de la classe, si bien que tu as
une conversion char * ->std::string
un appel au constructeur
Ce qui est tout bon
Par contre, truc_t t1= ""; aurait été refusé, car tu aurais effectivement eu deux conversions en suivant : char * ->std::string -> truc_t.
Notes d'ailleurs que, depuisC++14 (c'est moins vieuw que ce que je ne le croyais), on peut utiliser l'opérateur s qui se trouve dans l'espace de noms std::literals, et qui nous permet de créer directement une std::string. ce qui fait qu'un code proche de
int main(){
using namespace std::litterals;
/* divisons pour mieux régner: */
auto str = ""s;
truc_t t2= str;
}
serait parfaitement été accepté, car str serait de type std::string (il aurait aussi été accepté sous la forme de truc_t t2 = ""s;, pour la même raison ), et il n'y aurait du coup eu qu'une seule conversion .
Et, du coup, le code
int main(){
using namespace std::literals;
double_truc_t dt{""s};
}
serait lui aussi accepté, toujours pour les même raisons : tu as une (et une seule) conversion de std::string -> truc_t, et un appel explicite au constructeur de double_truc_t
- Edité par koala01 26 août 2019 à 12:13:10
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
En fait non, c'est un tableau constant de caractère qui contient un zéro terminal: const char [1]. "abc" -> const char[4]{'a','b','c','\0'}. Mais lorsqu'on le passe en paramètre de fonction, il devient silencieusement un char const*.
Ok, Ca y est ... Eurêka! J'ai compris ... (Enfin, jusqu'à la prochaine fois ! il faut pas croire que j'ai tous compris ...)
Donc on peut appeler un constructeur qui lui fait une conversion implicite ... Ok!
<---------------------------->
Edit: J'avais pas vu la réponse de Jo_Link_Noir .... Ok, c'est vrais, mais ... bon, quand on le passe en argument il devient un char * (enfin, sauf si c'est différent du C ...)
Ok, Ca y est ... Eurêka! J'ai compris ... (Enfin, jusqu'à la prochaine fois ! il faut pas croire que j'ai tous compris ...)
Donc on peut appeler un constructeur qui lui fait une conversion implicite ... Ok!
Voilà, c'est aussi simple que cela
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 sur Constructeur"
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
git is great because Linus did it, mercurial is better because he didn't.