2010-10-11 4 views
37

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

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

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

Что такое PRO и CON здесь?

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

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

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

Возможно, вы можете мне помочь?

+6

http://gotw.ca/gotw/066.htm – DumbCoder

+13

Я думаю, что это плохая идея добавлять в классы такие вещи, как 'init()' и 'cleanup()'. Это кричит об ошибках. Вы не можете быть уверены, что класс был 'init()' ed. Вы можете добавить функции для проверки этого, но это делает его еще более сложным. – jwueller

+0

@ DumbCoder: Извините, но я не совсем понял. Насколько я понимаю, статья, которую вы связали, говорит об исключениях. – tyrondis

ответ

22

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

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

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; 
} 

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

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

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

+0

+1 для хранения простых механизмов в конструкторах. Но предположить, что использование RAII может стоить того же, хотя и не так просто. –

+2

Проблема с вызовами методов init() на ленивой основе заключается в том, что вы должны абсолютно уверены, что они вызываются перед использованием. –

+0

@ Давид: Я согласен, и чем проще, тем лучше. Я предложил его в основном для полноты. –

2

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

4

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

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

-1: Нет, делать более сложные вещи ** не ** хорошо. Использование полиморфизма в конструкторе или деструкторе часто связано с ** сбоем **. См. Мой пример ниже. –

+3

«Не называть' delete this' в конструкторе »- разве вы не слышали о том, что Resource Assquisition Is Invalidation? – Potatoswatter

+0

+1. Однако я не согласен с «no' init() ». Это по существу противоречит любому кодовому факторингу в конструкторах. 'init' не должен заменять список инициализаторов членов, но одна из любимых функций Stroustrup C++ 0x, нестатических инициализаторов членов, получает лучшее из обоих миров. – Potatoswatter

27

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

например.:

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

+1

+1 Вы не можете подчеркнуть, что достаточно сложно! – helpermethod

+0

Вы имели в виду inherit B от A, как в 'class B: public A'? – Arun

+0

@ArunSaha, да, вы правы ;-), спасибо за замечание, я отредактировал. –

4

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

What all to do in the constructor? 

и ничего не охватывается выше является ответом на вопрос ФП в.

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

  1. инициализировать все переменные члены в известном состоянии, и

  2. распределять ресурсы (если это применимо).

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

9

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

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

Для примера рассмотрим:

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

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

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

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

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

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

4

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

Вы должны, однако, никогда не выбрасывать из деструктора.

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

1

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

+1

Очень расплывчатый ответ, я не могу сделать себе ни поддержки, ни нисходящего. –

1

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

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

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

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

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

3

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

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

+0

+1 для «если по какой-то причине вы не можете использовать исключения» - это не редкость во встроенных средах реального времени, где другие преимущества C++ over C делают C++ первым выбором, несмотря на отсутствие исключений. –

6

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

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

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

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

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

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

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