C ++ обрабатывает переменные пользовательских типов с семантикой значений . Это означает, что объекты неявно копируются в различных контекстах, и мы должны понимать, что на самом деле означает «копирование объекта».
Давайте рассмотрим простой пример:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age) : name(name), age(age)
{
}
};
int main()
{
person a("Bjarne Stroustrup", 60);
person b(a); // What happens here?
b = a; // And here?
}
(Если вы озадачены частью name(name), age(age)
, это называется список инициализатора члена .)
Что означает копирование объекта person
? Функция main
показывает два разных сценария копирования. Инициализация person b(a);
выполняется конструктором копирования . Его работа заключается в создании нового объекта на основе состояния существующего объекта. Назначение b = a
выполняется оператором копирования . Его работа обычно немного сложнее, потому что целевой объект уже находится в каком-то допустимом состоянии, с которым нужно иметь дело.
Поскольку мы сами не объявили ни конструктор копирования, ни оператор присваивания (ни деструктор), они неявно определены для нас. Цитата из стандарта:
Конструктор [...] копирования и оператор присваивания копии, [...] и деструктор являются специальными функциями-членами. [ Примечание : Реализация будет неявно объявлять эти функции-члены для некоторых типов классов, когда программа явно не объявляет их. Реализация будет неявно определять их, если они используются. [...] конец примечания ] [n3126.pdf раздел 12 §1]
По умолчанию копирование объекта означает копирование его членов:
Неявно определенный конструктор копирования для класса X, не являющегося объединением, выполняет пошаговую копию своих подобъектов. [n3126.pdf раздел 12.8 §16]
Неявно определенный оператор присваивания копии для класса X, не являющегося объединением, выполняет присваивание для каждого элемента подобласти. [n3126.pdf раздел 12.8 §30]
Неявно определенные специальные функции-члены для person
выглядят следующим образом:
// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}
// 2. copy assignment operator
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}
// 3. destructor
~person()
{
}
Копирование по элементам - это именно то, что нам нужно в этом случае: name
и age
копируются, поэтому мы получаем автономный, независимый объект person
. Неявно определенный деструктор всегда пуст. Это также хорошо в этом случае, так как мы не получили никаких ресурсов в конструкторе. Деструкторы членов неявно вызываются после завершения деструктора person
:
После выполнения тела деструктора и уничтожения любых автоматических объектов, размещенных в теле, деструктор для класса X вызывает деструкторы для прямых [...] членов X [n3126.pdf 12.4 §6]
Итак, когда мы должны объявить эти специальные функции-члены явно? Когда наш класс управляет ресурсом , то есть когда объект класса отвечает за за этот ресурс. Это обычно означает, что ресурс получен в конструкторе (или передан в конструктор) и выпущен в деструкторе.
Давайте вернемся назад к предварительному стандарту C ++. Не было такого понятия, как std::string
, и программисты были влюблены в указатели. Класс person
мог бы выглядеть так:
class person
{
char* name;
int age;
public:
// the constructor acquires a resource:
// in this case, dynamic memory obtained via new[]
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}
// the destructor must release this resource via delete[]
~person()
{
delete[] name;
}
};
Даже сегодня люди все еще пишут классы в этом стиле и попадают в неприятности: « Я толкнул человека в вектор, и теперь я получаю сумасшедшие ошибки памяти! "Помните, что по умолчанию копирование объекта означает копирование его членов, но копирование элемента name
просто копирует указатель, а не массив символов, на который он указывает! Это имеет несколько неприятных эффектов:
a
можно наблюдать через b
. b
уничтожено, a.name
является висящим указателем. a
уничтожено, удаление висящего указателя приводит к неопределенному поведению . name
до присвоения, Рано или поздно у вас появятся утечки памяти повсюду. Поскольку копирование по элементам не дает желаемого эффекта, мы должны явно определить конструктор копирования и оператор назначения копирования, чтобы сделать глубокие копии массива символов:
// 1. copy constructor
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
// 2. copy assignment operator
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
// This is a dangerous point in the flow of execution!
// We have temporarily invalidated the class invariants,
// and the next statement might throw an exception,
// leaving the object in an invalid state :(
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
return *this;
}
Обратите внимание на разницу между инициализацией и назначением: мы должны разрушить старое состояние перед назначением name
, чтобы предотвратить утечки памяти. Также мы должны защищаться от самостоятельного присвоения формы x = x
. Без этой проверки delete[] name
удалит массив, содержащий строку source , потому что когда вы пишете x = x
, и this->name
, и that.name
содержат один и тот же указатель.
К сожалению, это решение не будет выполнено, если new char[...]
сгенерирует исключение из-за исчерпания памяти. Одно из возможных решений - ввести локальную переменную и переупорядочить операторы:
// 2. copy assignment operator
person& operator=(const person& that)
{
char* local_name = new char[strlen(that.name) + 1];
// If the above statement throws,
// the object is still in the same state as before.
// None of the following statements will throw an exception :)
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
}
Это также заботится о самостоятельном назначении без явной проверки. Еще более надежным решением этой проблемы является идиома копирования и замены , но я не буду вдаваться здесь в детали безопасности исключений. Я упомянул только исключения, чтобы подчеркнуть следующее: Написание классов, управляющих ресурсами, является трудным делом.
Некоторые ресурсы не могут или не должны копироваться, например, дескрипторы файлов или мьютексы. В этом случае просто объявите конструктор копирования и оператор присваивания копии как private
, не задавая определение:
private:
person(const person& that);
person& operator=(const person& that);
В качестве альтернативы, вы можете наследовать от boost::noncopyable
или объявить их как удаленные (в C ++ 11 и выше):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
Иногда вам нужно реализовать класс, который управляет ресурсом. (Никогда не управляйте несколькими ресурсами в одном классе, это только приведет к боли.) В этом случае помните правило из трех :
Если вам нужно явно объявить либо деструктору, конструктору копирования или оператору присваивания копии, вам, вероятно, нужно явно объявить все три из них.
(К сожалению, это «правило» не применяется стандартом C ++ или любым известным мне компилятором.)
Из C ++ 11 на, объект имеет 2 дополнительные специальные функции-члены: конструктор перемещения и назначение перемещения. Правило пяти государств для реализации этих функций также.
Пример с подписями:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // Copy Ctor
person(person &&) noexcept = default; // Move Ctor
person& operator=(const person &) = default; // Copy Assignment
person& operator=(person &&) noexcept = default; // Move Assignment
~person() noexcept = default; // Dtor
};
Правило 3/5 также упоминается как правило 0/3/5. Нулевая часть правила гласит, что вы не можете писать какие-либо специальные функции-члены при создании вашего класса.
В большинстве случаев вам не нужно самостоятельно управлять ресурсом, потому что существующий класс, такой как std::string
, уже делает это за вас. Просто сравните простой код, использующий член std::string
, с замысловатой и подверженной ошибкам альтернативой, использующей char*
, и вы должны убедиться. Пока вы держитесь подальше от необработанных членов-указателей, правило трех вряд ли будет касаться вашего собственного кода.
Я полагаю, это потому, что ваш метод Book # editorial_string всегда будет возвращать "". Можно упростить до следующего:
def editorial_string
editorial ? editorial.name : ""
end
Обновление на основе комментария:
Похоже, вы хотите создавать вложенные формы. (См. accept_nested_attributes_for в документации api ). Обратите внимание, что это новинка в Rails 2.3.
Итак, если вы обновите свой класс Book
class Book < ActiveRecord::Base
accepts_nested_attributes_for :editorial
...
end
(теперь вы также можете удалить методы editorial_string =, editorial_string)
И обновите свои формы примерно так:
...
<% f.fields_for :editorial do |editorial_form| %>
<%= editorial_form.label :name, 'Editorial:' %>
<%= editorial_form.text_field :name %>
<% end %>
...
Первая проблема заключается в том, что
def editorial_string
self.editorial.name unless editorial.nil?
""
end
всегда будет возвращать "", потому что это последняя строка.
def editorial_string
return self.editorial.name if editorial
""
end
исправит эту проблему. Что касается того, почему проверки не проходят, я не знаю, что вы делаете в контроллере? Какие ошибки проверки вы получаете?
Взгляните на этот подкаст http://railscasts.com/episodes/167-more-on-virtual-attributes . Я думаю, вам следует переместить свой find_or_create из editorial_string = (input) метод обратного вызова после сохранения.