Что (не) делать в конструкторе - PullRequest
40 голосов
/ 11 октября 2010

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

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

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

Что здесь за и против?

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

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

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

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

Ответы [ 13 ]

28 голосов
/ 11 октября 2010

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

например:.

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

23 голосов
/ 11 октября 2010

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

6 голосов
/ 11 октября 2010

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

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

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

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

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

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

4 голосов
/ 11 октября 2010

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

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

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

4 голосов
/ 11 октября 2010

Я бы предпочел:

What all to do in the constructor?

, и все, что не описано выше, является ответом на вопрос ОП.

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

  1. инициализируют все переменные-члены для известного состояния и

  2. выделяют ресурсы (если применимо).

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

4 голосов
/ 11 октября 2010

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

3 голосов
/ 11 октября 2010

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

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

2 голосов
/ 11 октября 2010

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

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

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

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

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

...