web-dev-qa-db-fra.com

Pourquoi appeler explicitement un constructeur en C++

Je sais que nous pouvons appeler explicitement le constructeur d’une classe en C++ en utilisant l’opérateur de résolution de portée, c’est-à-dire className::className(). Je me demandais exactement où aurais-je besoin de faire un tel appel.

21
Arnkrishn

Le plus souvent, dans un constructeur de classe enfant nécessitant certains paramètres:

class BaseClass
{
public:
    BaseClass( const std::string& name ) : m_name( name ) { }

    const std::string& getName() const { return m_name; }

private:

    const std::string m_name;

//...

};


class DerivedClass : public BaseClass
{
public:

    DerivedClass( const std::string& name ) : BaseClass( name ) { }

// ...
};

class TestClass : 
{
public:
    TestClass( int testValue ); //...
};

class UniqueTestClass 
     : public BaseClass
     , public TestClass
{
public:
    UniqueTestClass() 
       : BaseClass( "UniqueTest" ) 
       , TestClass( 42 )
    { }

// ...
};

... par exemple.

À part ça, je ne vois pas l'utilitaire. Je n'ai appelé le constructeur dans un autre code que lorsque j'étais trop jeune pour savoir ce que je faisais vraiment ...

13
Klaim

Vous utilisez parfois aussi explicitement un constructeur pour créer un temporaire. Par exemple, si vous avez une classe avec un constructeur:

class Foo
{
    Foo(char* c, int i);
};

et une fonction

void Bar(Foo foo);

mais vous n'avez pas de Foo, vous pouvez le faire

Bar(Foo("hello", 5));

C'est comme un casting. En effet, si vous avez un constructeur qui ne prend qu'un paramètre, le compilateur C++ utilisera ce constructeur pour effectuer des conversions implicites.

Il est pas légal d'appeler un constructeur sur un objet déjà existant. C'est, vous ne pouvez pas faire

Foo foo;
foo.Foo();  // compile error!

peut importe ce que vous faites. Mais vous pouvez invoquer un constructeur sans allouer de mémoire - c’est la raison pour laquelle placement new est destiné.

char buffer[sizeof(Foo)];      // a bit of memory
Foo* foo = new(buffer) Foo();  // construct a Foo inside buffer

Vous donnez de la mémoire à new, qui construit l'objet à cet endroit au lieu d'allouer une nouvelle mémoire. Cet usage est considéré comme diabolique et est rare dans la plupart des types de code, mais il est courant dans le code incorporé et le code de structure de données. 

Par exemple, std::vector::Push_back utilise cette technique pour appeler le constructeur de copie. De cette manière, il suffit de faire une copie au lieu de créer un objet vide et d’utiliser l’opérateur d’affectation.

44
Charlie

Je pense que le message d'erreur relatif à l'erreur de compilation C2585 fournit la meilleure raison pour laquelle vous auriez réellement besoin d'utiliser l'opérateur de résolution d'étendue sur le constructeur, et il le fait avec la réponse de Charlie:

Conversion à partir d'une classe ou d'un type de structure basé sur plusieurs héritages. Si le type hérite plusieurs fois de la même classe de base, la fonction ou l'opérateur de conversion doit utiliser la résolution de portée (: :) pour spécifier laquelle des classes héritées doit être utilisée dans la conversion.

Alors imaginez que BaseClass et BaseClassA et BaseClassB héritent toutes deux de BaseClass, puis que DerivedClass hérite à la fois de BaseClassA et de BaseClassB.

Si vous effectuez une conversion ou une surcharge d'opérateur pour convertir DerivedClass en BaseClassA ou BaseClassB, vous devrez identifier le constructeur à utiliser (par exemple, un constructeur de copie, IIRC) à utiliser dans la conversion.

3

En général, vous n'appelez pas directement le constructeur. L'opérateur new l'appelle pour vous ou une sous-classe appelle les constructeurs de la classe parente. En C++, il est garanti que la classe de base sera entièrement construite avant le démarrage du constructeur de la classe dérivée.

Le seul moment où vous appelez directement un constructeur est le cas extrêmement rare où vous gérez de la mémoire sans utiliser new. Et même alors, vous ne devriez pas le faire. Au lieu de cela, vous devez utiliser la forme de placement de l'opérateur new.

2
jmucchiello

Il existe des cas d'utilisation valides dans lesquels vous souhaitez exposer des constructeurs de classes. Si vous souhaitez effectuer votre propre gestion de la mémoire avec un allocateur d'arène, par exemple, vous aurez besoin d'une construction en deux phases comprenant l'allocation et l'initialisation des objets. 

L’approche que j’adopte est semblable à celle de nombreuses autres langues. Je mets simplement mon code de construction dans des méthodes publiques bien connues ( Construct () , init () quelque chose comme ça) et les appelle directement si nécessaire. 

Vous pouvez créer des surcharges de ces méthodes correspondant à vos constructeurs; vos constructeurs habituels appellent juste en eux. Mettez de gros commentaires dans le code pour avertir les autres que vous le faites afin qu'ils n'ajoutent pas de code de construction important au mauvais endroit.

N'oubliez pas qu'il n'existe qu'une seule méthode de destruction, quelle que soit la surcharge de construction utilisée. Par conséquent, assurez-vous que vos destructeurs sont robustes pour les membres non initialisés. 

Je déconseille d'essayer d'écrire des initialiseurs pouvant se réinitialiser. Il est difficile de déterminer le cas où vous observez un objet contenant des données erronées en raison d'une mémoire non initialisée par rapport à la détention de données réelles.

Le problème le plus difficile vient des classes qui ont des méthodes virtuelles. Dans ce cas, le compilateur connecte normalement le pointeur de la table des fonctions vtable en tant que champ masqué au début de la classe. Vous pouvez initialiser manuellement ce pointeur, mais vous dépendez essentiellement du comportement spécifique du compilateur et vos collègues vont probablement vous regarder de façon amusante. 

Le placement nouveau est cassé à bien des égards; dans la construction/destruction de tableaux est un cas donc j'ai tendance à ne pas l'utiliser.

0
jls

Je ne pense pas que vous utiliseriez généralement cela pour le constructeur, du moins pas comme vous le décrivez. Vous en auriez cependant besoin si vous avez deux classes dans des espaces de noms différents. Par exemple, pour spécifier la différence entre ces deux classes composées, Xml::Element et Chemistry::Element.

Généralement, le nom de la classe est utilisé avec l'opérateur de résolution d'étendue pour appeler une fonction sur le parent d'une classe héritée. Ainsi, si vous avez une classe Dog héritant de Animal et que ces deux classes définissent la fonction Eat () différemment, il peut arriver que vous souhaitiez utiliser la version Animal de manger sur un objet Dog appelé "someDog". Ma syntaxe C++ est un peu rouillée, mais je pense que dans ce cas, vous diriez someDog.Animal::Eat().

0

Considérez le programme suivant.

template<class T>
double GetAverage(T tArray[], int nElements)
{
T tSum = T(); // tSum = 0

for (int nIndex = 0; nIndex < nElements; ++nIndex)
{
    tSum += tArray[nIndex];
}

// Whatever type of T is, convert to double
return double(tSum) / nElements;
}

Cela appellera explicitement un constructeur par défaut pour initialiser la variable.

0
Devesh Agrawal