постоянный член и оператор присваивания.Как избежать неопределенного поведения? - PullRequest
33 голосов
/ 09 ноября 2010

I ответил на вопрос о std :: vector объектов и 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
}; 

Я хочу иметь оператор присваивания, но не хочу использовать const_cast, как в следующем коде из одного из ответов:

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

Мое решение

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

Есть ли у меня неопределенное поведение (UB)?

Каким было бы решение без UB?

Ответы [ 6 ]

38 голосов
/ 09 ноября 2010

Ваш код вызывает неопределенное поведение.

Не просто "неопределено, если A используется в качестве базового класса и того, и того, или другого".На самом деле не определено, всегда.return *this уже является UB, потому что this не гарантирует обращения к новому объекту.

В частности, рассмотрим 3.8 / 7:

Если по истечении времени жизниобъект завершился, и до того, как хранилище, которое занимал объект, используется повторно или освобождено, в месте хранения, которое занимал исходный объект, создается новый объект, указатель, который указывал на исходный объект, ссылка, ссылающаяся на исходный объект, илиимя исходного объекта будет автоматически ссылаться на новый объект и после запуска времени жизни нового объекта может использоваться для управления новым объектом, если:

...

- тип исходного объекта не является константно-квалифицированным и, если тип класса, не содержит какого-либо нестатического члена данных, тип которого является константно-квалифицированным или ссылочным типом,

Теперь, «после того, как время жизни объекта закончилось и до того, как хранилище, которое занимал объект, используется повторно или освобождено, новый объект создается в хранилищевозраст, который занимал исходный объект "- это именно то, что вы делаете.

Ваш объект относится к типу класса, и он содержит содержит нестатический элемент данных, тип которого является константно-квалифицированным,Поэтому после запуска вашего оператора присваивания указатели, ссылки и имена, ссылающиеся на старый объект, гарантированно не ссылаются на новый объект и могут использоваться для манипулирования им.

Какконкретный пример того, что может пойти не так, рассмотрим:

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

Ожидайте этот вывод?

1
2

Неправильно!Вероятно, вы могли бы получить этот вывод, но причина, по которой члены const являются исключением из правила, изложенного в 3.8 / 7, заключается в том, что компилятор может обрабатывать x.c как объект const, который, как он утверждает, является.Другими словами, компилятору разрешено обрабатывать этот код так, как если бы он был:

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

Поскольку (неофициально) объекты const не изменяют свои значения .Потенциальное значение этой гарантии при оптимизации кода с использованием константных объектов должно быть очевидным.Чтобы существовал какой-либо способ изменить x.c без , вызывающего UB, эта гарантия должна быть удалена.Так что, пока стандартные авторы выполнили свою работу без ошибок, нет способа делать то, что вы хотите.

[*] На самом деле у меня есть сомнения относительно использования this в качестве аргумента для размещенияnew - возможно, вам следовало сначала скопировать его в void* и использовать его.Но меня не беспокоит, является ли это UB, поскольку это не спасет функцию в целом.

23 голосов
/ 09 ноября 2010

Первое: когда вы делаете элемент данных const, вы сообщаете компилятору и всему миру, что этот элемент данных никогда не изменяется .Конечно, тогда вы не можете присвоить ему , и вы определенно не должны обманывать компилятор в принятии кода, который это делает, независимо от того, какхитрый прием.
Вы можете иметь const элемент данных или оператор присваивания, назначающий всем элементам данных. Вы не можете иметь и то и другое.

Что касается вашего "решения" проблемы:
Я полагаю, что вызывает деструктор объектавнутри функции-члена, вызываемой для этих объектов, сразу вызовет UB . Вызов конструктора для неинициализированных необработанных данных для создания объекта из функции-члена, которая была вызвана для объекта, который находился там, где теперь вызывается конструктор для необработанных данных ... также очень для меня это звучит как UB .(Черт, только из-за того, что это произнесено, мои ноги на ногах скручиваются.) И нет, у меня нет главы и стиха стандарта для этого.Я ненавижу читать стандарт.Я думаю, что не могу вынести его метр.

Однако, если оставить в стороне технические детали, я признаю, что вы можете использовать свое «решение» практически на каждой платформе , если код остается таким же простым, как в вашем примере .Тем не менее, это не делает это хорошим решением.На самом деле, я бы сказал, что это даже не приемлемое решение, потому что код IME никогда не бывает таким простым.С годами он будет расширяться, изменяться, видоизменяться и искажаться, а затем он будет молча терпеть неудачу и потребовать ошеломляющей 36-часовой смены отладки, чтобы найти проблему.Я не знаю о вас, но всякий раз, когда я нахожу кусок кода, подобного этому, ответственного за отладку веселья в течение 36 часов, я хочу задушить жалкого глупца, который сделал это со мной.

Херб Саттер, в его GotW # 23 , рассекает эту идею по частям и, наконец, приходит к выводу, что она "равна полный ловушек , это часто неправильно , и это делает жизнь живым адом для авторов производных классов ... никогда не используйте хитрость реализации назначения копирования с точки зрения создания копии с использованием явного деструктора с последующим размещением нового , даже если этот прием срабатывает каждые три месяца в группах новостей "(подчеркнитемой).

9 голосов
/ 09 ноября 2010

Как вы можете присвоить A, если у него есть постоянный член?Вы пытаетесь сделать что-то, что принципиально невозможно.Ваше решение не имеет нового поведения по сравнению с оригиналом, которое не обязательно UB, но ваше, безусловно, таково.

Простой факт заключается в том, что вы меняете члена const.Вам нужно либо отменить свой член, либо отказаться от оператора присваивания.Нет решения вашей проблемы - это полное противоречие.

Правка для большей ясности:

Константное приведение не всегда приводит к неопределенному поведению.Вы, однако, наверняка сделали.Помимо всего прочего, не определено не вызывать все деструкторы - и вы даже не назвали правильный - до того, как поместили в него, если не знали наверняка, что T - это класс POD.Кроме того, существует неопределенное поведение времени сов, связанное с различными формами наследования.

Вы вызываете неопределенное поведение, и вы можете избежать этого, не пытаясь назначить объект const.

1 голос
/ 09 ноября 2010

Если вы определенно хотите иметь неизменный (но присваиваемый) член, то без UB вы можете расположить вещи так:

#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';
}
0 голосов
/ 09 ноября 2010

В отсутствие других (не const) членов это не имеет никакого смысла вообще, независимо от неопределенного поведения или нет.

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

AFAIK, это не неопределенное поведение, происходящеездесь, потому что c не является экземпляром static const, или вы не можете вызвать оператор копирования-назначения.Однако const_cast должен позвонить в колокольчик и сказать, что что-то не так.const_cast в первую очередь был разработан для работы с не const -корректными API, и здесь это не так.

Кроме того, в следующем фрагменте:

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

У вас есть два основных риска , 1-й из которых уже указан.

  1. При наличии оба являются экземпляром производного класса A и виртуальный деструктор, это приведет только к частичной реконструкции исходного экземпляра.
  2. Если вызов конструктора в new(this) A(right); выдает исключение, ваш объект будет уничтожен дважды.В данном конкретном случае это не будет проблемой, но если вам случится провести значительную очистку, вы пожалеете об этом.

Редактировать : если ваш классимеет этот const член, который не считается "состоянием" в вашем объекте (т.е. это какой-то идентификатор, используемый для отслеживания экземпляров и не является частью сравнений в operator== и т. п.), тогда может иметь смысл следующее:

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

    return *this;
}
0 голосов
/ 09 ноября 2010

Прочитайте эту ссылку:

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

В частности ...

Этот трюк якобы предотвращает код редупликация. Тем не менее, он имеет некоторые серьезные недостатки. Для того, чтобы работать, C's деструктор должен назначить NULLify каждый указатель, который он удалил, потому что последующий вызов конструктора копирования может снова удалить те же указатели когда он переназначает новое значение на char массивы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...