Это довольно широкий вопрос, на который, похоже, нет единственно верного ответа.
Я был озадачен инициализацией составных объектов в течение достаточно долгого времени. Меня формально учили поставлять геттеры и сеттеры для всех данных-членов и отдавать предпочтение необработанным указателям на объекты вместо автоматических объектов — это, кажется, контрастирует с тем, что многие люди в переполнении стека (такие как этот популярный пост) советую.
Как же тогда мне начинать инициализацию объектно-составных объектов?
Это способ, которым я попытался бы инициализировать, используя то, что я изучил в школе:
class SmallObject1 {
public:
SmallObject1() {};
};
class SmallObject2 {
public:
SmallObject2() {};
};
class BigObject {
private:
SmallObject1 *obj1;
SmallObject2 *obj2;
int field1;
int field2;
public:
BigObject() {}
BigObject(SmallObject1* obj1, SmallObject2* obj2, int field1, int field2) {
// Assign values as you would expect
}
~BigObject() {
delete obj1;
delete obj2;
}
// Apply getters and setters for ALL members here
};
int main() {
// Create data for BigObject object
SmallObject1 *obj1 = new SmallObject1();
SmallObject2 *obj2 = new SmallObject2();
int field1 = 1;
int field2 = 2;
// Using setters
BigObject *bobj1 = new BigObject();
// Set obj1, obj2, field1, field2 using setters
// Using overloaded contructor
BigObject *bobj2 = new BigObject(obj1, obj2, field1, field2);
return 0;
}
Этот дизайн привлекателен, потому что он читабелен (для меня). Дело в том, что BigObject
имеет указатели на свои объекты-члены, что позволяет инициализировать obj1
а также obj2
после инициализации. Тем не менее, динамическая память может сделать программу более сложной и запутанной в будущем, таким образом созревая для утечек памяти. Кроме того, использование методов получения и установки создает помехи в классе и может также сделать данные членов слишком простыми для доступа и изменения.
Это на самом деле плохая практика? Я часто нахожу моменты, когда мне нужно инициализировать объект-член отдельно от его владельца, что делает автоматические объекты непривлекательными. Кроме того, я рассмотрел возможность создания более крупных объектов своими собственными объектами-членами. Кажется, это имеет больше смысла с точки зрения безопасности, но меньше смысла с точки зрения ответственности за объект.
Меня формально учили поставлять геттеры и сеттеры для всех данных-членов и отдавать предпочтение необработанным указателям на объекты вместо автоматических объектов.
Лично у меня нет проблем с установщиками и получателями для всех членов данных. Хорошая практика — иметь и может спасти много горя, особенно если вы решаетесь на темы. На самом деле, многие инструменты UML автоматически генерируют их для вас. Вам просто нужно знать, что вернуть. В этом конкретном примере не возвращайте необработанный указатель на SmallObject1 *
, Вернуть SmallObject1 * const
вместо.
Вторая часть о
сырые указатели
сделано в образовательных целях.
Ваш главный вопрос: как вы структурируете хранилище объектов, зависит от более крупного дизайна. Является BigObject
единственный класс, который когда-либо будет использовать SmallObject
«S? Тогда я бы положил их полностью внутри BigObject
как частные члены и делать все управление памятью там. Если SmallObject
делятся между различными объектами, и не обязательно BigObject
класс, тогда я бы сделал то, что ты сделал. Тем не менее, я буду хранить ссылки или указатели, чтобы соответствовать им, а не удалять их в BigObject
деструктор класса — BigObject
не размещать их, поэтому не следует удалять.
Меня формально учили поставлять геттеры и сеттеры для всех данных-членов и отдавать предпочтение необработанным указателям на объекты вместо автоматических объектов.
К сожалению, вас учили неправильно.
Там нет абсолютно никаких оснований для пользу сырых указателей поверх любых стандартных конструкций библиотеки, таких как std::vector<>
, std::array<>
или, если вам нужно std::unique_ptr<>
, std::shared_ptr<>
,
Самым распространенным преступником в программном обеспечении с ошибками является то, что (собственное) управление памятью обнаруживает недостатки, и, что еще хуже, их обычно трудно отладить.
Рассмотрим следующий код:
class SmallObj {
public:
int i_;
double j_;
SmallObj(int i, double j) : i_(i), j_(j) {}
};
class A {
SmallObj so_;
int x_;
public:
A(SmallObj so, int x) : so_(so), x_(x) {}
int something();
int sox() const { return so_.i_; }
};
class B {
SmallObj* so_;
int x_;
public:
B(SmallObj* so, int x) : so_(so), x_(x) {}
~B() { delete so_; }
int something();
int sox() const { return so_->i_; }
};
int a1() {
A mya(SmallObj(1, 42.), -1.);
mya.something();
return mya.sox();
}
int a2() {
SmallObj so(1, 42.);
A mya(so, -1.);
mya.something();
return mya.sox();
}
int b() {
SmallObj* so = new SmallObj(1, 42.);
B myb(so, -1.);
myb.something();
return myb.sox();
}
Недостатки подхода «А»:
SmallObject
делает нас зависимыми от его определения: мы не можем просто объявить его,SmallObject
уникален для нашего экземпляра (не разделяется), Недостатки подхода «B» несколько:
B
создано,Одним из аргументов против использования автоматических объектов является Стоимость передать их по значению.
Это сомнительно: во многих случаях тривиальных автоматических объектов компилятор может оптимизировать эту ситуацию и инициализировать подобъект в строке. Если конструктор тривиален, он может даже сделать все за одну инициализацию стека.
Вот GCC -O3 реализация a1 ()
_Z2a1v:
.LFB11:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
subq $40, %rsp ; <<
.cfi_def_cfa_offset 48
movabsq $4631107791820423168, %rsi ; <<
movq %rsp, %rdi ; <<
movq %rsi, 8(%rsp) ; <<
movl $1, (%rsp) ; <<
movl $-1, 16(%rsp) ; <<
call _ZN1A9somethingEv
movl (%rsp), %eax
addq $40, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
Выделенный (; <<
Строки) — это компилятор, выполняющий конструкцию A на месте и его подобъекта SmallObj за один раз.
И a2 () оптимизирует очень похоже:
_Z2a2v:
.LFB12:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
subq $40, %rsp
.cfi_def_cfa_offset 48
movabsq $4631107791820423168, %rcx
movq %rsp, %rdi
movq %rcx, 8(%rsp)
movl $1, (%rsp)
movl $-1, 16(%rsp)
call _ZN1A9somethingEv
movl (%rsp), %eax
addq $40, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
И там есть б ():
_Z1bv:
.LFB16:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA16
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movl $16, %edi
subq $16, %rsp
.cfi_def_cfa_offset 32
.LEHB0:
call _Znwm
.LEHE0:
movabsq $4631107791820423168, %rdx
movl $1, (%rax)
movq %rsp, %rdi
movq %rdx, 8(%rax)
movq %rax, (%rsp)
movl $-1, 8(%rsp)
.LEHB1:
call _ZN1B9somethingEv
.LEHE1:
movq (%rsp), %rdi
movl (%rdi), %ebx
call _ZdlPv
addq $16, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 16
movl %ebx, %eax
popq %rbx
.cfi_def_cfa_offset 8
ret
.L6:
.cfi_restore_state
.L3:
movq (%rsp), %rdi
movq %rax, %rbx
call _ZdlPv
movq %rbx, %rdi
.LEHB2:
call _Unwind_Resume
.LEHE2:
.cfi_endproc
Понятно, что в этом случае мы заплатили высокую цену за переход по указателю вместо значения.
Теперь давайте рассмотрим следующий фрагмент кода:
class A {
SmallObj* so_;
public:
A(SmallObj* so);
~A();
};
class B {
Database* db_;
public:
B(Database* db);
~B();
};
Исходя из приведенного выше кода, что вы ожидаете от владения «SmallObj» в конструкторе A? А что вы ожидаете от владения «БД» в Б? Намереваетесь ли вы создать уникальное соединение с базой данных для каждого B, который вы создаете?
Чтобы еще больше ответить на ваш вопрос о предпочтении необработанных указателей, нам нужно взглянуть не дальше, чем стандарт C ++ 2011 года, в котором были представлены концепции std::unique_ptr
а также std::shared_ptr
чтобы помочь разрешить неоднозначность владения, которая существовала со времен Cs strdup()
(возвращает указатель на копию строки, не забудьте освободить).
Перед комитетом по стандартизации есть предложение ввести observer_ptr
в C ++ 17, который не является оберткой вокруг необработанного указателя.
Использование их с вашим предпочтительным подходом вводит много котла:
auto so = std::make_unique<SmallObject>(1, 42.);
A a(std::move(so), -1);
Мы знаем здесь a
имеет право собственности на so
экземпляр мы распределили, так как мы явно предоставляем ему право собственности std::move
, Но все это явно стоит символов. Контраст с:
A a(SmallObject(1, 42.), -1);
или же
SmallObject so(1, 4.2);
A a(so, -1);
Таким образом, я думаю, что в целом очень мало аргументов в пользу использования сырых указателей для небольших объектов для композиции. Вы должны просмотреть свой материал, который приведет вас к выводу, поскольку, вероятно, вы упустили из виду или неправильно поняли факторы в рекомендации о том, когда использовать необработанные указатели.
Другие описали причины оптимизации, я сейчас рассматриваю это с точки зрения типа / функциональности. По словам Страуструпа, «каждый конструктор должен установить инвариант класса». Какой ваш класс инвариант здесь? Важно знать (и определять!), Иначе вы будете загрязнять свои функции-члены if
Проверка правильности операции — это не намного лучше, чем отсутствие типов вообще. В 90-х у нас были подобные классы, но в настоящее время мы действительно придерживаемся инвариантных определений и хотим, чтобы объекты постоянно находились в допустимом состоянии. (Функциональное программирование идет дальше и пытается извлечь переменное состояние из объектов, чтобы объекты могли быть постоянными.)
std::optional<SmallObject>
члены. Необязательный обычно выделяет локально (по сравнению с кучей), таким образом, вы можете извлечь выгоду из локальности кэша.Обратите внимание, что многие из нас, кто предпочитает функциональный стиль, считают построитель антишаблоном и используют его только для десериализации, если вообще используют. Причина в том, что очень сложно рассуждать о сборщике (что получится, получится ли, какой конструктор получит вызовы). Если у вас есть два целых, это всего лишь два целых. Ваша лучшая ставка обычно состоит в том, чтобы просто хранить их в отдельных переменных, тогда компилятор должен выполнить все виды оптимизации. Я не удивлюсь, если кусочки чудесным образом просто упадут на куски, а ваши целые будут построены «на месте», поэтому позже копия не требуется.
OTOH, если вы обнаружите, что одни и те же параметры «ограничены» (получают их значение) во многих местах заранее, чем другие, то вы можете ввести для них тип. В этом случае ваши две целые будут типом (предпочтительно структурой). Вы можете решить, хотите ли вы сделать его базовым классом BigObject
, член или просто отдельный класс (вам придется выбрать третий, если у вас несколько порядков связывания) — в любом случае ваш конструктор теперь будет принимать новый класс вместо двух целых. Вы могли бы даже рассмотреть вопрос об устаревании вашего другого конструктора (тот, который принимает два целых числа) как 1. новый объект может быть легко сконструирован, 2. он может использоваться совместно (например, при создании элементов в цикле). Если вы хотите сохранить старый конструктор, сделайте один из них делегатом для другого.