web-dev-qa-db-fra.com

Pourquoi C++ n'a-t-il pas de constructeur const?

( Edit: changement important car l'exemple précédent était défectueux, ce qui peut donner l'impression que certaines réponses/commentaires semblent étranges)

Cela peut être un peu trop artificiel, mais ce qui suit est légal à cause du manque de constructeur const:

class Cheater
{
public:
    Cheater(int avalue) 
       : cheaterPtr(this) //conceptually odd legality in const Cheater ctor
       , value(avalue) 
    {}

    Cheater& getCheaterPtr() const {return *cheaterPtr;}
    int value;

private:
    Cheater * cheaterPtr;
};

int main()
{
    const Cheater cheater(7); //Initialize the value to 7

    cheater.value                 = 4;    //good, illegal
    cheater.getCheaterPtr().value = 4;    //oops, legal

    return 0;
}

Il semble que fournir à un constructeur const une chose serait aussi simple techniquement que les méthodes const, et serait analogue à une surcharge const.

Remarque: je ne cherche pas 'Image( const Data & data ) const' mais plutôt 'const Image( const Data & data) const'

Alors:

  • Pourquoi le constructeur const est-il absent en C++?

Voici quelques éléments liés au contexte:

29
Catskul

Ce ne serait pas une méthode const elle-même

Si ce constructeur n'était pas une méthode const en soi, les pointeurs internes et autres ne seraient pas non plus const. Par conséquent, il n'a pas pu définir les valeurs const dans ces membres non -const.

La seule façon de le faire fonctionner de manière syntaxique est que ce constructeur exige une initialisation de membre pour tous les membres non -mutable. Essentiellement, tout membre non déclaré mutable serait implicitement déclaré const lors de l'utilisation de ce constructeur. Ce qui équivaut à faire du constructeur une méthode const; seuls les initialiseurs peuvent initialiser les membres. Le corps du constructeur ne peut rien faire avec des membres non mutables, car ces membres seraient const à ce moment-là.

Ce que vous demandez est syntaxiquement douteux. Vous essayez essentiellement de tromper l'API en stockant des données constantes dans un objet conçu pour des données mutables (c'est pourquoi vous n'avez pas déclaré le pointeur du membre comme étant const). Si vous souhaitez un comportement différent pour un objet, vous devez déclarer l'objet pour qu'il ait ce comportement spécifique.

5
Nicol Bolas

Le fait que Image soit const dans votre constructeur imaginaire ne signifie pas que ce que m_data désigne est. Vous finirez par être en mesure d'affecter un "pointeur à const" à un "pointeur const à non-const" dans votre classe, ce qui supprimerait la constance sans transtypage. Cela vous permettrait évidemment de violer les invariants et ne pourrait pas être autorisé.

Autant que je sache, tous les ensembles de constantes spécifiques nécessaires peuvent être spécifiés de manière précise et complète dans la norme actuelle.

Une autre façon de voir les choses est que const signifie que la méthode ne mute pas l'état de votre objet. Le seul but d'un constructeur est d'initialiser l'état d'un objet pour qu'il soit valide (enfin, espérons-le, tous les constructeurs ayant des effets secondaires devraient être ... soigneusement évalués).

EDIT: En C++, la constness s'applique aux deux membres, et pour les pointeurs et les références, à la constness accessible de l'objet référencé. C++ a délibérément pris la décision de séparer ces deux constantes différentes. Tout d'abord, sommes-nous d'accord pour dire que ce code démontrant la différence devrait compiler et afficher "non-const"?

#include <iostream>

struct Data
{
    void non_const() { std::cout << "non-const" << std::endl; }
};

struct Image
{
     Image(             Data & data ) : m_data( data ) {}

     void check() const { m_data.non_const(); }
     Data & m_data;
};

int main()
{
    Data data;
    const Image img(data);
    img.check();

    return 0;
}

Donc, pour obtenir le comportement où il pourrait accepter un const-ref et le stocker en tant que const-ref, la déclaration effective de la référence devrait être changée en const. Cela signifierait alors qu'il s'agirait d'un type complètement distinct, PAS d'une version const du type d'origine (puisque deux types dont les membres diffèrent par qualification const sont traités comme deux types distincts en C++). Ainsi, soit le compilateur doit être capable de faire une magie excessive en coulisse pour convertir ces choses en arrière, en se souvenant de la constance des membres, soit il doit le traiter comme un type séparé qui ne pourrait pas ensuite être utilisé à la place du type normal.

Je pense que ce que vous essayez d’obtenir est un objet referencee_const, un concept qui n’existe que dans C++ en tant que classe distincte (qui, je suppose, pourrait être implémenté avec une utilisation judicieuse de modèles bien que je n’ai pas tenté de le faire).

S'agit-il d'une question strictement théorique (réponse: C++ a décidé de scinder l'objet et la constance de référence) ou existe-t-il un problème concret et non contredit que vous essayez de résoudre?

10
Mark B

Mark B passe en revue les considérations fondamentales, mais notez que vous pouvez faire quelque chose de similaire en C++ pur. Considérer:

struct Data { };

class ConstImage {
protected:
  const Data *const_data;
public:
  ConstImage (const Data *cd) : const_data(cd) { }
  int getFoo() const { return const_data->getFoo(); }
};

class Image : public ConstImage {
protected:
  Data *data() { return const_cast<Data *>(const_data); }
public:
  Image(Data *d) : const_data(d) { }
  void frob() { data()->frob(); }
};

Au lieu d'utiliser const Image *, utilisez ConstImage *, et voilà. Vous pouvez aussi simplement définir un pseudo-constructeur de fonction statique:

const Image *Image::newConstImage(const Data *d) {
  return new Image(const_cast<Data*>(d));
}

Ceci, bien sûr, repose sur le programmeur pour s'assurer qu'il n'y a pas de fonction const qui pourrait en quelque sorte muter l'état de Data pointé.

Vous pouvez également combiner ces techniques:

class Image {
protected:
  const Data *const_data;
  Data *data() { return const_cast<Data *>(const_data); }
public:
  void frob() { data()->frob(); }
  int getFoo() const { return const_data->getFoo(); }

  Image(Data *d) : const_data(d) { }

  static const Image *newConst(const Data *cd) {
    return new Image(const_cast<Data *>(cd));
  }
};

Cela donne le meilleur des deux mondes; étant donné que data() est un membre non-const, vous devez effectuer une vérification statique de la mutation de la valeur pointée. Cependant, vous avez également un constructeur const, et vous pouvez transtyper directement entre Image * et const Image * (en d’autres termes, vous pouvez supprimer la constness si vous savez qu’elle est sûre).

Vous pouvez également faire abstraction de la séparation des pointeurs:

template<typename T>
class ConstPropPointer {
private:
  T *ptr;
public:
  ConstPropPointer(T *ptr_) : ptr(ptr_) { }
  T &operator*() { return *ptr; }
  const T &operator*() const { return *ptr; }
  T *operator->() { return ptr; }
  const T *operator->() const { return ptr; }
};


class Image {
protected:
  ConstPropPointer<Data> data;
public:
  void frob() { data->frob(); }
  int getFoo() const { return data->getFoo(); }

  Image(Data *d) : data(d) { }

  static const Image *newConst(const Data *cd) {
    return new Image(const_cast<Data *>(cd));
  }
};

Maintenant, si this est const, data devient const, en le propageant également dans *data. Assez bon pour vous? :)

Je suppose que la réponse finale est probablement la suivante: Pour qu'un constructeur de const soit utile et sûr, nous avons besoin de quelque chose comme la variable ConstPropPointer que vous voyez intégrée dans le langage. Les constructeurs de const seraient alors autorisés à affecter de const T * à constprop T *. C'est plus complexe qu'il n'y paraît, par exemple, comment cela interagit-il avec les classes de modèle telles que vector?

Il s’agit donc d’un changement assez complexe, mais le problème ne semble pas trop se poser. Plus important encore, il existe une solution de contournement simple ici (la ConstPropPointer peut être librarisée, et le pseudo-constructeur statique est assez simple à ajouter). Donc, le comité C++ a probablement passé le relais pour des choses plus importantes, si cela avait même été proposé.

4
bdonlan

À mon avis, le fait que les opérateurs n'aient pas spécifié de type retour est ce qui échoue ici. Toute autre syntaxe imaginable comme par exemple

class A
{
    const A& ctor(...);
}

serait, à mon humble avis, très précieux. Par exemple, imaginez une telle situation d’appel d’une méthode avec prototype

void my_method(const my_own_string_class& z);

Si my_own_string_class contient un ctor de char *, le compilateur pourrait choisir Ce ctor, mais comme ce ctor n'est pas autorisé à retourner un objet const, il doit allouer et copier ... Si le type de retour const était autorisé, on pourrait faire

class my_own_string_class
{
    char *_ptr;
    public:
    const my_own_string_class& ctor(char *txt)
    : _ptr(txt)
    { return *this;}
 }

à condition que cette construction spéciale soit limitée à la création d'instances temporelles. (Et dtor doit être mutable;)).

0
Mel Viso Martinez