Подтвердить что ты не робот

Const и оператор присваивания. Как избежать поведения undefined?

Я ответил на вопрос о 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?

4b9b3361

Ответ 1

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

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

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

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

...

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

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

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

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

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 не изменяют свои значения. Потенциальная ценность этой гарантии при оптимизации кода с участием объектов const должна быть очевидной. Чтобы какой-либо способ изменить x.c без вызова UB, эта гарантия должна быть удалена. Таким образом, до тех пор, пока стандартные авторы выполнили свою работу без ошибок, вы не сможете делать то, что хотите.

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

Ответ 2

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

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

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

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

Ответ 3

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

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

Изменить для большей ясности:

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

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

Ответ 4

Если вы определенно хотите иметь неизменяемый (но назначаемый) элемент, то без 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';
}

Ответ 5

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

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

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

Этот трюк якобы предотвращает код редупликация. Однако он имеет некоторые серьезные недостатки. Чтобы работать, Cs деструктор должен назначать NULLify каждый указатель, который он удалил, потому что последующий вызов конструктора копии могут снова удалить те же указатели когда он переназначает новое значение charМассивы.

Ответ 6

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

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

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

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

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

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

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

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

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

    return *this;
}