membre const et opérateur d'affectation. Comment éviter le comportement indéfini?

I a répondu à la question sur std:: vector des objets et const-correctif et a obtenu undeserved downvote et un commentaire sur le comportement indéfini. Je ne suis pas d'accord et j'ai donc une question.

Considérez la classe avec le membre const:

class A { 
public: 
    const int c; // must not be modified! 
    A(int c) : c(c) {} 
    A(const A& copy) : c(copy.c) { }     
    // No assignment operator
}; 

Je veux avoir un opérateur d'affectation, mais je ne souhaite pas utiliser const_cast, comme dans le code suivant à partir de l'une des réponses:

A& operator=(const A& assign) 
{ 
    *const_cast<int*> (&c)= assign.c;  // very very bad, IMHO, it is UB
    return *this; 
} 

Ma solution est

A& operator=(const A& right)  
{  
    if (this == &right) return *this;  
    this->~A() 
    new (this) A(right); 
    return *this;  
}  

Vraiment comportement indéfini?

S'il vous plait, votre solution sans UB.

29
demandé sur Community 2010-11-09 19:42:45

6 réponses

Votre code provoque un comportement indéfini.

Pas seulement "undefined si A est utilisé comme classe de base et ceci, cela ou l'autre". En fait indéfini, toujours. return *this est déjà UB, car this n'est pas garanti pour faire référence au nouvel objet.

Plus Précisément, considérons 3.8/7:

Si, après la durée de vie d'un objet a fin et avant le stockage l'objet occupé est réutilisé ou libéré, un nouvel objet est créé à l'emplacement de stockage qui l' objet original occupé, un pointeur qui pointait à l'objet d'origine, un référence visée à l' objet d'origine, ou le nom de l' objet original sera automatiquement reportez-vous au nouvel objet et, une fois le durée de vie du nouvel objet a commencé, peut être utilisé pour manipuler les nouvel objet, si:

...

- le type de l'objet d'origine est non qualifié const, et, si une classe type, ne contient pas de non-statique membre de données dont le type est const qualifié ou un type de référence,

Maintenant, "après la fin de la durée de vie d'un objet et avant que le stockage occupé par l'objet ne soit réutilisé ou libéré, un nouvel objet est créé à l'emplacement de stockage occupé par l'objet d'origine" est exactement ce que vous faites.

Votre objet est de type class, et contient un membre de données non statique dont le type est qualifié const. Par conséquent, après l'exécution de votre opérateur d'affectation, les pointeurs, les références et les noms se référant à l'ancien objet sontpas garantis pour se référer au nouvel objet et être utilisables pour le manipuler.

Comme exemple concret de ce qui pourrait mal tourner, considérez:

A x(1);
B y(2);
std::cout << x.c << "\n";
x = y;
std::cout << x.c << "\n";

Attendez-vous à cette sortie?

1
2

Faux! Il est plausible que vous obteniez cette sortie, mais la raison pour laquelle les membres const sont une exception à la règle indiquée dans 3.8/7, est que le compilateur peut traiter x.c comme l'objet const qu'il prétend être. En d'autres termes, le compilateur est autorisé pour traiter ce code comme si c'était:

A x(1);
B y(2);
int tmp = x.c
std::cout << tmp << "\n";
x = y;
std::cout << tmp << "\n";

Parce que (de manière informelle) Les objets const ne changent pas leurs valeurs. La valeur potentielle de cette garantie lors de l'optimisation du code impliquant des objets const devrait être évidente. Pour qu'il y ait un moyen de modifier x.c sans invoquer UB, cette garantie devrait être supprimée. Donc, tant que les rédacteurs standard ont fait leur travail sans erreurs, il n'y a aucun moyen de faire ce que vous voulez.

[*] En fait, j'ai des doutes sur en utilisant this comme argument pour placer new-peut-être que vous auriez dû le copier dans un void* en premier, et l'utiliser. Mais je ne suis pas dérangé si c'est spécifiquement UB, car cela ne sauvegarderait pas la fonction dans son ensemble.

33
répondu Steve Jessop 2010-11-09 18:27:35

Premièrement: lorsque vous créez un membre de données const, Vous dites au compilateur et au monde entier que ce membre de données ne change jamais. Bien sûr alors vous ne pouvez pas lui attribuer et vous certainement ne doit pas tromper le compilateur en acceptant le code qui le fait, peu importe à quel point l'astuce est intelligente.
Vous pouvez avoir un const membre de données ou un opérateur d'affectation l'affectation à tous les membres de données. Vous ne pouvez pas avoir les deux.

Quant à votre "solution" au problème:
Je suppose que appelant le destructeur sur un objet dans une fonction membre appelée pour que les objets invoqueraient UB tout de suite. invoquer un constructeur sur des données brutes non initialisées pour créer un objet à partir d'une fonction membre qui a été invoquée pour un objet qui résidait où maintenant le constructeur est appelé sur des données brutes... aussi très beaucoup de sons comme UB pour moi. (L'enfer, juste l'orthographe fait que mes ongles se courbent.) Et, non, je n'ai pas de chapitre et le verset de la norme. Je déteste lire la norme. Je pense que je ne peux pas supporter son compteur.

Cependant, les aspects techniques mis à part, j'admets que vous pourriez vous en tirer avec votre "solution" sur à peu près toutes les plates-formestant que le code reste aussi simple que dans votre exemple. Pourtant, cela n'en fait pas une bonne solution . En fait, je dirais que ce n'est même pas une solution acceptable , parce que le code IME ne reste jamais aussi simple que cela. Au fil des ans, il sera étendu, changé, muté et tordu, puis il échouera silencieusement et nécessitera un changement de débogage de 36 heures pour trouver le problème. Je ne sais pas pour vous, mais chaque fois que je trouve un morceau de code comme celui-ci responsable de 36 heures de débogage amusant, je veux étrangler le misérable idiot qui m'a fait ça.

Herb Sutter, dans son GotW #23, dissèque cette idée pièce par pièce et conclut qu'il "est plein de pièges, c'est souvent à tort, et il rend la vie un enfer pour les auteurs de classes dérivées... n'utilisez jamais l'astuce d'implémenter l'affectation de copie en termes de construction de copie en utilisant un destructeur explicite suivi du placement new , même si cette astuce apparaît tous les trois mois sur les newsgroups "(soulignez le mien).

21
répondu sbi 2010-11-09 18:14:42

Comment Pouvez-vous attribuer à un A s'il a un membre const? Vous essayez d'accomplir quelque chose qui est fondamentalement impossible. Votre solution n'a pas de nouveau comportement par rapport à l'original, ce qui n'est pas nécessairement UB mais le vôtre l'est certainement.

Le simple fait est que vous changez de membre const. Vous devez soit détacher votre membre, soit abandonner l'opérateur d'affectation. Il n'y a pas de solution à votre problème - c'est une contradiction totale.

Modifier pour plus clarté:

Const cast n'introduit pas toujours un comportement indéfini. Vous, cependant, très certainement fait. En dehors de toute autre chose, il est indéfini de ne pas appeler tous les destructeurs - et vous n'avez même pas appelé le bon - avant de le placer à moins que vous ne sachiez avec certitude que T est une classe POD. En outre, il y a des comportements indéfini owch-time impliqués dans diverses formes d'héritage.

Vous invoquez un comportement indéfini, et vous pouvez éviter cela en n'essayant pas d'assigner à un objet const.

9
répondu Puppy 2010-11-09 17:10:12

Si vous voulez vraiment avoir un membre immuable (mais assignable), alors sans UB vous pouvez disposer les choses comme ceci:

#include <iostream>

class ConstC
{
    int c;
protected:
    ConstC(int n): c(n) {}
    int get() const { return c; }
};

class A: private ConstC
{
public:
    A(int n): ConstC(n) {}
    friend std::ostream& operator<< (std::ostream& os, const A& a)
    {
        return os << a.get();
    }
};

int main()
{
    A first(10);
    A second(20);
    std::cout << first << ' ' << second << '\n';
    first = second;
    std::cout << first << ' ' << second << '\n';
}
1
répondu UncleBens 2010-11-09 17:56:13

Avoir une lecture de ce lien:

Http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=368

En particulier...

Cette astuce empêche le code la réduplication. Cependant, il a quelques de graves lacunes. Pour le travail, C est destructor doit assigner annuler chaque pointeur qu'il a supprimé parce que l'appel du constructeur de copie suivant peut supprimer les mêmes pointeurs à nouveau quand il réaffecte une nouvelle valeur à char tableau.

0
répondu Roddy 2010-11-09 17:04:07

En l'absence d'autres membres (non-const), cela n'a aucun sens, quel que soit le comportement indéfini ou non.

A& operator=(const A& assign) 
{ 
    *const_cast<int*> (&c)= assign.c;  // very very bad, IMHO, it is UB
    return *this; 
}

AFAIK, ce n'est pas un comportement indéfini qui se passe ici parce que c n'est pas une instance static const, ou vous ne pouvez pas appeler l'opérateur d'affectation de copie. Cependant, {[6] } devrait sonner une cloche et vous dire que quelque chose ne va pas. const_cast a été principalement conçu pour contourner les API non const correctes, et cela ne semble pas être le cas ici.

Aussi, dans le extrait suivant:

A& operator=(const A& right)  
{  
    if (this == &right) return *this;  
    this->~A() 
    new (this) A(right); 
    return *this;  
}

Vous avez deux risques majeurs, le 1er qui a déjà été souligné.

  1. En présence de deux une instance de la classe dérivée de A et un destructeur virtuel, cela ne mènerait qu'à la reconstruction partielle de l'instance d'origine.
  2. Si l'appel du constructeur dans new(this) A(right); lève une exception, votre objet sera détruit deux fois. Dans ce cas particulier, ce ne sera pas un problème, mais si vous arrivez à avoir un nettoyage important, vous allez le regretter.

Edit : si votre classe A ce membre const qui n'est pas considéré comme "état" dans votre objet (c'est-à-dire qu'il s'agit d'une sorte D'ID utilisé pour le suivi des instances et ne fait pas partie des comparaisons dans operator== et autres), alors ce qui suit peut avoir du sens:

A& operator=(const A& assign) 
{ 
    // Copy all but `const` member `c`.
    // ...

    return *this;
}
0
répondu André Caron 2010-11-09 17:10:01