2016-07-08 5 views
0

Это несколько широкий вопрос, который, кажется, не имеет ни одного истинного ответа.Инициализация исходного объекта

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

Как же мне нужно инициализировать объекты, сложенные объектами?

Это способ, которым я бы попытаться инициализацию с помощью того, что я узнал в школе:

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

На самом деле это плохая практика? Я часто нахожу времена, когда мне нужно инициализировать объект-член отдельно от его владельца, что делает автоматические объекты непривлекательными. Кроме того, я решил позволить более крупным объектам строить свои собственные объекты-члены. Это, по-видимому, имеет больше смысла с точки зрения безопасности, но меньше смысла с точки зрения ответственности объекта.

+1

's/appealing/appalling /' – juanchopanza

+1

Школа должна преподавать вам о 'std :: unique_ptr' и' std :: shared_ptr'. – kfsone

+0

@juanchopanza Ах, я имел в виду обращение ко мне. Исправлена. –

ответ

0

Я официально преподается на поставку геттеры и сеттеры для всех данных членов и в пользу сырых указателей на объекты вместо автоматических объектов

Лично у меня нет никаких проблем с наличием сеттеров и добытчиками для все члены данных. Хорошая практика - иметь и сэкономить много горя, особенно если вы займетесь нитями. Фактически, многие инструменты UML автоматически генерируют их для вас. Вам просто нужно знать, что вернуть. В этом конкретном примере не возвращайте необработанный указатель на SmallObject1 *. Верните SmallObject1 * const.

Вторая часть о

сырых указателях

делаются для образовательных целей.


Для вашего основного вопроса: способ структурирования хранения объектов зависит от более крупного дизайна. Является ли BigObject единственным классом, который когда-либо будет использовать SmallObject? Тогда я бы поставил их полностью внутри BigObject в качестве частных членов и выполнял там все управление памятью. Если SmallObject разделяются между разными объектами, и необязательно из класса BigObject, тогда я бы сделал то, что вы сделали. Тем не менее, я бы сохранил ссылки или указатели на const для них, а не удалял их в деструкторе класса BigObject - BigObject не выделял их, поэтому его не следует удалять.

+0

Спасибо, это проясняет некоторую путаницу. Я думаю, что мне нужно сделать: A) читать на интеллектуальных указателях и 2) больше думать о том, кто управляет какой памятью и как она ее хранит. –

0

Я официально преподается на поставку геттеры и сеттеры для всех данных членов и в пользу сырых указателей на объекты вместо автоматических объектов

К сожалению, вы учили неправильно.

Там нет абсолютно никаких оснований благосклонности сырья указатели над любыми стандартными конструкциями библиотеки как std::vector<>, std::array<>, или если вам нужно std::unique_ptr<>, std::shared_ptr<>.

Самый распространенный преступник в багги-программном обеспечении - это то, что (сворачивает собственное) управление памятью выявляет недостатки, а еще хуже, что их обычно трудно отлаживать.

+0

И почему бы не автоматические объекты? – juanchopanza

+0

@juanchopanza Я считаю, что OP хочет, чтобы мы экстраполировали для больших серий 'SmallObject ' в классе. –

+0

@ πάνταῥεῖ Я согласен, что геттеры и сеттеры просто ошибаются. ИМХО, потому что они противоречат инкапсуляции и скрытию данных. Для каждого getter/setter поляX задайте себе 2 вопроса: «is fieldX в неправильном объекте?» или «это функциональность с использованием поляX, закодированного в неправильном месте?» Оба/оба указывают на необходимость рефакторинга. –

0

Рассмотрим следующий код:

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 создаются,
  • косвенность требуется для доступа к элементам этого жизненно важного объекта,
  • мы должны проверить нулевые указатели, если мы хотим, чтобы поддержать ваше дело конструктор по умолчанию,
  • разрушение требует дальнейшего динамического вызова памяти,

Один из аргументов против использования автоматических объектов - это , стоимость которых составляет, передавая их по значению.

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

Вот -O3 реализация GCC по 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 

Выделенные (; <<) линии компилятор делает конструкцию в своем месте, и это 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» в конструкторе? И каково ваше ожидание владения «Базой данных» в B? Вы собираетесь создавать уникальное соединение с базой данных для каждого создаваемого вами B?

Для дальнейшего ответа на ваш вопрос в пользу сырьевых указателей, мы должны смотреть не дальше, чем стандарт 2011 C++, который ввел понятие 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); 

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

0

Другие описали причины оптимизации, теперь я просматриваю его с точки зрения типа/функциональности. По словам Страустрапа, «каждому конструктору задается определение инварианта класса». Каков ваш классовый инвариант здесь? Важно знать (и определять!), Иначе вы будете загрязнять свои функции-члены с помощью if s, чтобы проверить, действительно ли операция - это не намного лучше, чем вообще не иметь типов. В 90-е годы у нас были такие классы, но в настоящее время мы действительно придерживаемся определений инвариантов и хотим, чтобы объекты находились в правильном состоянии все время. (Функциональное программирование идет на шаге дальше и пытается извлечь переменные состояния из объектов, поэтому объекты могут быть константными.)

  • Если ваш класс является действительным тогда и только тогда у вас есть эти суб-объекты, то есть их как члены, период.
  • Если вы хотите share SmallObjects среди BigObjects, то вам нужны указатели.
  • Если у вас нет данного SmallObject, но вам не нужно делиться, вы можете рассмотреть std::optional<SmallObject> пользователей. Необязательно, как правило, выделяется локально (по сравнению с кучей), поэтому вы можете использовать локальную сеть.
  • Если вам трудно построить такой объект, например, слишком много параметров конструктора, тогда у вас есть две ортогональные проблемы: конструкция и члены класса. Решите проблемы с конструкцией, введя класс строителя (шаблон Builder). Обычно приемлемым решением является просто наличие всех параметров всех конструкторов в качестве необязательных членов.

Обратите внимание, что многие из нас, кто предпочитает функциональный стиль, рассматривают строитель как анти-шаблон и используют его только для десериализации, если вообще. Причина в том, что очень сложно рассуждать о буйлере (что получится, будет ли это удачным, какой конструктор получит призывы). Если у вас есть два ints, это просто: два ints. Лучше всего просто держать их в отдельных переменных, тогда компилятор должен делать все виды оптимизации. Я не был бы удивлен, если бы части чудесным образом просто упали в кусок, и ваши ints были бы построены «на месте», поэтому копия не потребуется позже.

OTOH, если вы обнаружите, что одни и те же параметры «получают ограниченные» (получить их значение) во многих местах заранее, другие, то вы можете ввести для них тип. В этом случае ваши два ints будут типами (предпочтительно структурой). Вы можете решить, хотите ли вы сделать его базовым классом BigObject, членом или просто отдельным классом (вам нужно будет выбрать третье, если у вас есть несколько заказов на привязку) - в любом случае ваш конструктор теперь возьмет новый класс вместо двух ints. Вы даже можете подумать об отказе от своего другого конструктора (тот, который принимает два ints), как 1. новый объект можно легко построить, 2. он может быть общим (например, при создании элементов в цикле). Если вы хотите сохранить старый конструктор, сделайте один из них делегатом для другого.

Смежные вопросы