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

Что (не) делать в конструкторе

Я хочу попросить вас о ваших лучших практиках в отношении конструкторов на С++. Я не совсем уверен, что мне делать в конструкторе, а что нет.

Должен ли я использовать его только для инициализации атрибута, вызова родительских конструкторов и т.д.? Или я мог бы включить в них более сложные функции, такие как чтение и анализ данных конфигурации, настройка внешних библиотек a.s.o.

Или мне нужно писать специальные функции для этого? Соответственно init()/cleanup()?

Что такое PRO и CON?

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

Если я обрабатываю его в конструкторе, мне нужно создать его экземпляр во время выполнения. Тогда мне нужен указатель.

Я действительно не знаю, как решить.

Может быть, вы можете мне помочь?

4b9b3361

Ответ 1

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

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

class Vector
{
public:
  Vector(): mSize(10), mData(new int[mSize]) {}
private:
  size_t mSize;
  int mData[];
};

Это не означает полностью инициализированный объект, вы можете отложить некоторую инициализацию (считайте ленивым), пока пользователь не должен думать об этом.

class Vector
{
public:
  Vector(): mSize(0), mData(0) {}

  // first call to access element should grab memory

private:
  size_t mSize;
  int mData[];
};

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

// in the constructor
Setting::Setting()
{
  // connect
  // retrieve settings
  // close connection (wait, you used RAII right ?)
  // initialize object
}

// Builder method
Setting Setting::Build()
{
  // connect
  // retrieve settings

  Setting setting;
  // initialize object
  return setting;
}

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

Этот метод строителя подразумевает частный конструктор и публичный (или друг) Builder. Обратите внимание: наличие частного конструктора накладывает ряд ограничений на обычаи, которые могут выполняться для класса (например, не может храниться в контейнерах STL), поэтому вам может понадобиться объединить другие шаблоны. Именно поэтому этот метод следует использовать только в исключительных обстоятельствах.

Возможно, вы захотите также изучить, как тестировать такие объекты, если вы зависите от внешней вещи (файл/БД), подумайте об Injection Dependency, это действительно помогает с Unit Testing.

Ответ 2

наиболее распространенная ошибка, выполняемая в конструкторе, а также в деструкторе, заключается в использовании полиморфизма. Полиморфизм часто не работает в конструкторах!

например:.

class A
{
public:
    A(){ doA();} 
    virtual void doA(){};
}

class B : public A
{
public:
    virtual void doA(){ doB();};
    void doB(){};   
}


void testB()
{
    B b; // this WON'T call doB();
}

это связано с тем, что объект B еще не сконструирован при выполнении конструктора материнского класса A... таким образом, невозможно вызвать надёжную версию void doA();

Бен, в комментариях ниже, попросил меня пример, где полиморфизм будет работать в конструкторах.

например:.

class A
{
public: 
    void callAPolymorphicBehaviour()
    {
        doOverridenBehaviour(); 
    }

    virtual void doOverridenBehaviour()
    {
        doA();
    }

    void doA(){}
};

class B : public A
{
public:
    B()
    {
        callAPolymorphicBehaviour();
    }

    virtual void doOverridenBehaviour()
    {
        doB()
    }

    void doB(){}
};

void testB()
{
   B b; // this WILL call doB();
}

На этот раз причина заключается в следующем: во время вызова функции virtual doOverridenBehaviour() объект b уже инициализирован (но еще не сконструирован), это означает, что его виртуальная таблица инициализирована и, следовательно, может выполнять полиморфизм.

Ответ 3

  • Не вызывайте delete this или деструктор в конструкторе.
  • Не используйте элементы init()/cleanup(). Если вам нужно вызывать init() каждый раз, когда вы создаете экземпляр, все в init() должно быть в конструкторе. Конструктор предназначен для того, чтобы привести экземпляр в согласованное состояние, которое позволяет любому публичному члену вызываться с четко определенным поведением. Аналогично для cleanup(), плюс cleanup() убивает RAII. (Однако, когда у вас есть несколько конструкторов, часто полезно иметь частную функцию init(), вызываемую ими.)
  • Выполнение более сложных вещей в конструкторах в порядке, в зависимости от предполагаемого использования классов и вашего общего дизайна. Например, было бы неплохо прочитать файл в конструкторе какого-то класса Integer или Point; пользователи ожидают, что их будет дешево создавать. Также важно учитывать, как конструкторы доступа к файлам будут влиять на вашу способность писать модульные тесты. Лучшим решением, как правило, является конструктор, который просто берет данные, которые ему необходимы для создания членов, и записывает не-членную функцию, которая выполняет синтаксический анализ и возвращает экземпляр.

Ответ 4

Простой ответ: это зависит.

При разработке своего программного обеспечения вы, возможно, захотите запрограммировать с помощью принципа RAII ( "Инициализация ресурсов" ). Это означает, между прочим, что сам объект несет ответственность за свои ресурсы, а не за вызывающего. Кроме того, вы можете ознакомиться с безопасностью исключений (в разной степени).

Например, рассмотрим:

void func() {
    MyFile f("myfile.dat");
    doSomething(f);
}

Если вы создаете класс MyFile таким образом, что перед doSomething(f) вы можете быть уверены, что f инициализирован, вы сохраняете много проблем, проверяя это. Кроме того, если вы отпустите ressources, хранящиеся в f в деструкторе, т.е. Закройте дескриптор файла, вы находитесь в безопасности и прост в использовании.

В этом конкретном случае вы можете использовать специальные свойства конструкторов:

  • Если вы создадите исключение из конструктора во внешний мир, объект не будет создан. Это означает, что деструктор не будет вызван, и память будет немедленно освобождена.
  • Вызывается конструктор must. Вы не можете заставить пользователя использовать любую другую функцию (кроме деструктора), только по соглашению. Итак, если вы хотите заставить пользователя инициализировать ваш объект, почему бы не через конструктор?
  • Если у вас есть методы virtual, , вы не должны вызывать эти внутри конструктора, если вы не знаете, что делаете, - вы (или более поздние пользователи) можете удивиться, почему виртуальный переопределяющий метод не вызывается. Лучше никого не путать.

Конструктор должен оставить ваш объект в состоянии usable. И поскольку для трудно использовать неправильный API, лучше всего сделать , чтобы было легко использовать правильный (sic для Scott Meyers). Выполнение инициализации внутри конструктора должно быть вашей стратегией по умолчанию, но, конечно, всегда есть исключения.

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

Ответ 5

Из языка программирования С++:

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

Я обычно рассматриваю следующее правило при разработке класса: после выполнения конструктора я должен использовать любой класс безопасно. Безопасный здесь означает, что вы всегда можете генерировать исключения, если метод объекта init() не был вызван, но я предпочитаю иметь что-то, что на самом деле можно использовать.

Например, class std::string может не выделять какую-либо память при использовании конструктора по умолчанию, потому что большинство методов (т.е. begin() и end()) будут работать правильно, если оба возвращают нулевые указатели, а c_str() не обязательно возвращаются текущий буфер для других причин проектирования, поэтому он должен быть готов выделить память в любое время. Не выделяя память в этом случае, все же приводит к идеально используемому экземпляру строки.

И наоборот, использование RAII в защищенных оболочках для блокировок mutex является примером конструктора, который может выполняться в течение сколь угодно долгого времени (до тех пор, пока владелец замка не выпустит его), но по-прежнему считается общепринятой практикой.

В любом случае, ленивая инициализация может быть выполнена более безопасными способами, чем использование метода init(). Один из способов - использовать некоторый промежуточный класс, который фиксирует все параметры для конструктора. Другим является использование шаблона построителя.

Ответ 6

Предполагается, что конструктор создаст объект, который можно использовать из слова go. Если по какой-то причине он не может создать полезный объект, он должен сделать исключение и сделать с ним. Таким образом, все вспомогательные методы/функции, необходимые для правильной работы объекта, должны вызываться из конструктора (если вы не хотите иметь ленивые функции загрузки)

Ответ 7

Я бы предпочел спросить:

What all to do in the constructor?

и все, что не описано выше, - это ответ на вопрос OP.

Я думаю, что единственная цель конструктора -

  • инициализировать все переменные-члены в известном состоянии и

  • выделить ресурсы (если применимо).

Элемент №1 звучит так просто, но я вижу, что его нужно забыть/игнорировать на регулярной основе и только напомнить статическому анализу. Никогда не недооценивайте это (каламбур).

Ответ 8

Вы МОЖЕТЕ выбраться из конструктора, и это часто лучший вариант, чем создание объекта зомби, т.е. объекта, который имеет состояние "сбой".

Однако вы не должны выбрасывать деструктора.

Компилятор будет знать, какой порядок создаются объектами-членами - порядок, который они отображаются в заголовке. Однако деструктор не будет вызван, как вы сказали, а это означает, что если вы вызываете новые многократные числа в конструкторе, вы не можете полагаться на свой деструктор, вызывающий удаления для вас. Если вы поместите их в объекты интеллектуального указателя, это не проблема, так как эти объекты будут удалены. Если вы хотите, чтобы они были грубыми указателями, тогда временно помещайте их в объекты auto_ptr, пока не узнаете, что ваш конструктор больше не будет бросать, а затем вызовите release() для всех ваших auto_ptrs.

Ответ 9

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

Отдельные функции init() - хорошая идея, только если по какой-то причине вы не можете использовать исключения.

Ответ 10

Ну, "конструктор" строится, строится, настраивается. Итак, там, где происходит вся инициализация. Всякий раз, когда вы экземпляр класса, используйте конструктор, чтобы убедиться, что все сделано для того, чтобы сделать новый объект работоспособным.

Ответ 11

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

Ответ 12

Я думаю, что самое главное - это немного здравый смысл! Там много разговоров о do и dont - все хорошо и хорошо, но ключевым моментом для рассмотрения является то, как ваш объект будет использоваться. Например,

  • сколько экземпляров этого объекта будет создано? (например, они хранятся в контейнерах?)
  • как часто они создаются и уничтожаются? (Петли?)
  • насколько он велик?

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

Есть преимущества иметь двухфазную нагрузку (или что-то еще), но главный недостаток - забыть назвать это - сколько из нас это сделали?:)

Итак, моя таппенс, не придерживайтесь жесткого и быстрого правила, внимательно изучайте, как ваш объект будет использоваться, а затем создайте его, чтобы он соответствовал!

Ответ 13

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

Вот мой пост в блоге об этом: конструкторы должны быть без кода