Что такое делегирующий конструктор
Урок №119. Делегирующие конструкторы
Обновл. 13 Сен 2021 |
На этом уроке мы рассмотрим, что такое делегирующие конструкторы в языке С++, зачем они были придуманы и как их использовать.
Проблема
При создании нового объекта класса, компилятор C++ неявно вызывает конструктор этого объекта. Не редкость встретить класс с несколькими конструкторами, которые частично выполняют одно и то же, например:
Здесь есть 2 конструктора: конструктор по умолчанию и конструктор, который принимает целочисленное значение. Поскольку Часть кода X требуется обоим конструкторам, то она дублируется в каждом из них.
А как вы уже могли догадаться, дублирование кода — это то, чего следует избегать, поэтому давайте рассмотрим возможные решения этой проблемы.
Решение в C++11
Неплохо было бы, чтобы конструктор Boo(int) вызывал конструктор Boo() для выполнения Часть кода X :
Однако, если ваш компилятор не совместим с C++11, и вы попытаетесь вызвать один конструктор внутри другого конструктора, то это скомпилируется, но будет работать не так, как вы ожидаете.
До C++11 явный вызов одного конструктора из другого приводит к созданию временного объекта, который затем инициализируется с помощью конструктора этого объекта и отбрасывается, оставляя исходный объект неизменным.
Использование отдельного метода
Конструкторам разрешено вызывать другие методы класса, которые не являются конструкторами. Хотя у вас может возникнуть соблазн скопировать код из первого конструктора во второй конструктор, наличие дублированного кода сделает ваш класс более трудным для понимания и более обременительным для поддержки. Лучшим решением будет создание отдельного метода (не конструктора), который будет выполнять общую инициализацию, и оба конструктора будут вызывать этот метод. Например:
Здесь мы свели дублирование кода к минимуму.
Кроме того, вы можете оказаться в ситуации, когда вам нужно будет написать метод для повторной инициализации класса обратно до значений по умолчанию. Поскольку у вас, вероятно, уже есть конструктор, который это делает, то у вас может возникнуть соблазн попытаться вызвать этот конструктор из вашего метода. Однако это приведет к неожиданным результатам. Многие разработчики просто копируют код из конструктора в функцию инициализации — это сработает, но приведет также к дублированию кода. Лучшим решением будет переместить код из конструктора в вашу новую функцию и заставить конструктор вызывать вашу новую функцию для выполнения инициализации:
Здесь мы подключаем функцию Init() для инициализации переменных-членов обратно значениями по умолчанию, а затем каждый конструктор вызывает функцию Init() перед своим фактическим выполнением. Это сокращает дублирование кода до минимума и позволяет явно вызывать Init() из любого места в программе.
Делегирующие конструкторы в C++11
Начиная с C++11, конструкторам разрешено вызывать другие конструкторы. Этот процесс называется делегированием конструкторов (или «цепочкой конструкторов»). Чтобы один конструктор вызывал другой, нужно просто сделать вызов этого конструктора в списке инициализации членов. Например:
Всё работает как нужно. Убедитесь, что вы вызываете конструктор из списка инициализации членов, а не из тела конструктора.
Вот еще один пример использования делегирующих конструкторов для сокращения дублированного кода:
Этот класс имеет 2 конструктора (один из которых вызывает другой). Таким образом, количество дублированного кода сокращено (нам нужно записать только одно определение конструктора вместо двух).
Несколько заметок о делегирующих конструкторах
Во-первых, конструктору, который вызывает другой конструктор, не разрешается выполнять какую-либо инициализацию членов класса. Поэтому конструкторы могут либо вызывать другие конструкторы, либо выполнять инициализацию, но не всё сразу.
Во-вторых, один конструктор может вызывать другой конструктор, в коде которого может находиться вызов первого конструктора. Это создаст бесконечный цикл и приведет к тому, что память стека закончится и произойдет сбой. Вы можете избежать этого, убедившись, что в конструкторе, который вызывается, нет вызова первого (и вообще любого другого) конструктора. Будьте аккуратны и не используйте вложенные вызовы конструкторов.
RAII и делегирующие конструкторы в C++11
В этом посте пойдет речь об одной интересной фичи в C++11, которая называется делегирующие конструкторы (delegating constructors): почему она интересна, и как ее можно применить для более эффективного управления ресурсами, т.е. реализации идиомы RAII.
Кратко об RAII (ну очень кратко)
Когда нам нужно автоматизировать управление каким-нибудь “голым” ресурсом, мы его “заворачиваем” в отдельный класс. Продемонстрируем это на примере такого ресурса как FILE из стандартной библиотеки C:
Здесь мы создаем FILE ресурс в конструкторе и освобождаем в деструкторе. Теперь ресурс FILE управляется в полном соответствии с идиомой RAII.
Немного более усложненный случай RAII
Допустим теперь, что в дополнение к открытию файла в конструкторе нам нужно провести с ним некоторую операцию. Например, будем в заново открытый файл записывать время последнего открытия, time stamp. Для этого создадим в классе File функцию put_time_stamp, которая в каким-то образом помещает в файл time stamp, а в случае неудачи выбрасывает какое-то исключение.
Реализуется это дело как-то так:
Но как видно, в данной реализации есть небольшая проблема. Конструктор File перестал быть exception safe. Если из put_time_stamp вылетит исключение, то оно не приведет в вызову деструктора объекта File, так как его конструктор еще не завершился. Поэтому ресурс file_ будет потерян.
Как нам решить эту проблему? Тупое решение “в лоб” заключается в оборачивании вызова put_time_stamp в блок try/catch:
Этот подход работает, но он немного некрасив из-за необходимости иметь явный try/catch блок и отдельный метод для явного разрушения объекта, чтобы не дублировать одну и ту же функциональность в catch блоке и в деструкторе.
Мы можем немного улучшить данное решение, если введем дополнительный класс специально для хранения и удаления FILE, FileHandle:
Как видно, теперь явный try/catch блок уже не нужен. Объект file_ будет корректно разрушен, даже если из конструктора класса File вылетит исключение, и ресурс FILE будет освобожден. Но в этом решении все равно есть некоторый недостаток, заключающийся в отдельном классе FileHandle, который разносит создание и освобождение ресурса FILE на два разных класса: FILE создается в классе File, а освобождается в классе FileHandle.
Делегирующие конструкторы
Рассмотрим теперь одну очень полезную фичу из C++11 под названием делегирующие конструкторы, которая позволит нам еще более улучшить предыдущий код класса File. Но для начала, посмотрим, как вообще работают эти делегирующие конструкторы.
Допустим, у нас есть класс с двумя конструкторами: один от параметра типа int, а другой от double. Конструктор для int делает то же самое, что и конструктор для double, только сначала он переводит параметр от типа int к типу double. Т.е. конструктор для int делегирует создание объекта конструктору для double. Вот как это выглядит в коде:
После того, как конструктор для double закончит выполнение, конструктор для int может продолжить выполняться и “доконструировать” объект. Сама по себе это очень полезная фича, без которой в коде выше нам наверняка пришлось бы ввести дополнительную функцию init(double param) для инкапсуляции общего кода по созданию объект от типа double.
Но в дополнение у этой фичи есть один очень интересный побочный эффект. Дело в том, что как только один из конструкторов объекта закончит выполнение, объект считается созданным. И значит, если другой конструктор, из которого произошел делегирующий вызов первого конструктора, завершится с выбросом исключения, для этого объекта все равно будет вызван деструктор. Заметьте критический момент: для объекта теперь может выполниться больше одного конструктора. Но объект считается созданным после выполнения самого первого конструктора.
Продемонстрируем это поведение на следующем примере:
Конструктор MyClass(int) вызывает другой конструктор MyClass(double), после чего сам выбрасывает исключение. Это исключение ловится в catch(. ), и при раскрутке стека вызывается деструктор
MyClass. На консоль при выполнении данного кода выведется следующее:
Делегирующие конструкторы и RAII
Нетрудно заметить, что такое интересное поведение конструкторов при делегировании можно очень эффективно использовать в нашем примере реализации RAII для FILE. Теперь нам не нужно вводить никакой дополнительный класс FileHandle для освобождения ресурса FILE, а тем более не нужен и try/catch. Нужно ввести всего лишь один дополнительный конструктор, которому будет произведена делегация из основного конструктора. То есть:
И это все что нам необходимо. Очень красиво, элегантно и полностью безопасно по отношению к исключениям (exception safe). Вывод: подобная техника существенно облегчит реализацию идиомы RAII в новом коде с использованием делегирующих конструкторов из C++11.
Почему С++ 11 представил делегирующие конструкторы?
Я не понимаю, что такое использование делегирования конструкторов. Просто, чего не может быть достигнуто без делегирования конструкторов?
Он может сделать что-то простое, как это
Но я не вижу, что стоит добавить новую функцию для такой простой вещи? Может быть, я не мог распознать важный момент. Любая идея?
ОТВЕТЫ
Ответ 1
Делегирующие конструкторы предотвращают дублирование кода (и все возможные ошибки и недостатки, которые приходят с ним: увеличение обслуживания, снижение удобочитаемости. ), что хорошо.
Это также единственный способ делегировать список инициализации (для инициализации членов и баз), т.е. вы действительно не можете заменить эту функцию с помощью общего метода Init() для ваших конструкторов.
1) Общая инициализация из предложение N1986:
2) Делегирование с конструктором и конструктором копирования, а также из предложения N1986:
3) MSDN дает этот пример, причем конструкторы, выполняющие проверку аргументов (как прокомментировано, этот дизайн является спорным)
Благодаря делегированию конструкторов он сводится к:
Ссылки:
Ответ 2
В дополнение к превосходному ответу на квант (который у меня есть выше) я хотел также продемонстрировать проблемы безопасности исключений делегирования конструкторов для тех типов, которые должны явно получать несколько ресурсов в конструкторе и явно распоряжаться несколькими ресурсами в своем деструкторе.
В качестве примера я буду использовать простые исходные указатели. Обратите внимание, что этот пример не очень мотивирован, потому что использование интеллектуальных указателей над необработанными указателями решает проблему более аккуратно, чем делегирование конструкторов. Но пример прост. Все еще существуют более сложные примеры, которые не решаются с помощью интеллектуальных указателей.
X получил построенный дважды, но разрушен только один раз. Существует утечка памяти. Существует несколько способов сделать этот конструктор безопасным, один из способов:
Пример программы теперь корректно выводит:
Однако нетрудно заметить, что при добавлении управляемых ресурсов в Z это быстро становится громоздким. Эта проблема решена очень элегантно, делегируя конструкторы:
Этот конструктор сначала делегирует конструктору по умолчанию, который не делает ничего, кроме того, что класс переходит в действительное состояние без ресурса. После завершения конструктора по умолчанию Z теперь считается полностью построенным. Поэтому, если что-то в теле этого конструктора бросает, теперь выполняется
Z() правильно очищает ресурсы, которые уже были построены (и игнорирует те, которые не имеют).
Если вам нужно написать класс, который управляет несколькими ресурсами в своем деструкторе, и по каким-либо причинам вы не можете использовать другие объекты для управления этими ресурсами (например, unique_ptr ), я настоятельно рекомендую эту идиому управлять безопасностью исключений.
Обновление
Возможно, более мотивирующим примером является пользовательский класс контейнера (std:: lib не предоставляет все контейнеры).
Ваш класс контейнера может выглядеть так:
Один из способов реализации конструктора-шаблона-члена:
Но вот как я это сделаю:
Если кто-то в обзоре кода назвал последнюю плохую практику, я бы пошел на коврик на этом.
Ответ 3
Одним из ключевых вариантов делегирования конструкторов, который не просто уменьшает дублирование кода, является получение дополнительных пакетов параметров шаблона, в частности последовательность целых индексов, необходимая для указания инициализатора элемента:
Таким образом, мы можем определить конструктор, который инициализирует массив постоянным значением без первоначальной инициализации каждого элемента по умолчанию. Единственный способ добиться этого, насколько мне известно, потребует приклеивания элемента данных в базовом классе.
Конечно, лучшая поддержка языков для пакетов параметров шаблонов может сделать это ненужным.
Ответ 4
Я описал другое использование делегирующих конструкторов в Overload # 113, которое упрощает решения, описанные в Cassio Neri Сложная логика в списке инициализаторов участников в режиме перегрузки # 112.
В отличие от кода внутри тела функции, когда вы пишете инициализаторы-члены конструктора, вы не можете создать локальную переменную для хранения промежуточного результата, который необходим более чем одному из членов.
Рассмотрим конструктор, подобный этому:
Мы хотим избежать выполнения дорогостоящего вычисления дважды (и в контексте исходной проблемы, описанной Cassio, базовый класс также хочет получить результат вычисления, поэтому вы не можете просто назначить x_ и y_ в тело конструктора).
Трюк, который я описал, заключается в вычислении промежуточного результата и делегировании другому конструктору, который использует этот результат:
Ответ 5
12.8 – Перекрывающиеся и делегирующие конструкторы
Конструкторы с перекрывающимся функционалом
Когда вы создаете экземпляр нового объекта, конструктор объекта вызывается неявно. Нередко у классов бывает несколько конструкторов, которые содержат перекрывающуюся функциональность. Рассмотрим следующий класс:
Этот класс имеет два конструктора: конструктор по умолчанию и конструктор, принимающий целочисленное значение. Поскольку часть конструктора «код для выполнения A» требуется обоим конструкторам, этот код дублируется в каждом конструкторе.
Как вы (надеюсь) уже поняли, дублирования кода следует максимально избегать. Поэтому давайте рассмотрим несколько способов решения этой проблемы.
Очевидное решение не работает
Очевидным решением было бы, чтобы конструктор Foo(int) вызывал конструктор Foo() для выполнения части A.
Делегирующие конструкторы
Конструкторам разрешено вызывать другие конструкторы. Это называется делегирующими конструкторами (или цепочкой конструкторов).
Чтобы один конструктор вызывал другой, просто вызовите конструктор в списке инициализаторов членов. Это тот случай, когда прямой вызов другого конструктора приемлем. Применительно к нашему примеру выше:
Это работает именно так, как вы ожидали. Убедитесь, что вы вызываете конструктор из списка инициализаторов членов, а не в теле конструктора.
Вот еще один пример использования делегирующих конструкторов для уменьшения избыточности кода:
Несколько дополнительных замечаний о делегирующих конструкторах. Во-первых, конструктору, который делегирует выполнение другому конструктору, не разрешается выполнять инициализацию каких-либо членов самостоятельно. Итак, конструкторы могут либо делегировать, либо инициализировать, но не то и другое одновременно.
Во-вторых, один конструктор может делегировать другому конструктору, который делегирует обратно первому конструктору. Это создаст бесконечный цикл и приведет к тому, что ваша программа исчерпает пространство стека и завершится со сбоем. Вы можете избежать этого, убедившись, что все ваши конструкторы вычисляются в конструктор без делегирования.
Лучшая практика
Если у вас есть несколько конструкторов с одинаковой функциональностью, используйте делегирующие конструкторы, чтобы избежать дублирования кода.
Использование отдельной функции
Соответственно, вы можете оказаться в ситуации, когда захотите написать функцию-член, чтобы повторно инициализировать класс до значений по умолчанию. Поскольку у вас, вероятно, уже есть конструктор, который делает это, у вас может возникнуть соблазн попытаться вызвать этот конструктор из вашей функции-члена. Однако попытка вызвать конструктор напрямую обычно приводит к неожиданному поведению, как мы показали выше. Многие разработчики просто копируют код из конструктора в функцию инициализации, что работает, но приводит к дублированию кода. Лучшее решение в этом случае – переместить код из конструктора в новую функцию и заставить конструктор вызывать эту функцию для выполнения работы по «инициализации» данных:
Конструкторам разрешено вызывать функции, не являющиеся конструкторами класса. Просто будьте осторожны, чтобы любые члены, которые использует функция, не являющаяся конструктором, уже были инициализированы. Хотя у вас может возникнуть соблазн скопировать код из первого конструктора во второй конструктор, наличие дублирующего кода затрудняет понимание вашего класса и затрудняет его обслуживание.
Конструкторы и деструктор
Чтобы при создании экземпляра класса присвоить начальные значения полям, необходимо создать метод, имеющий такое же имя, что и название класса. Тип возвращаемого значения не указывается. Такой метод называется конструктором. Конструктор всегда автоматически вызывается при создании объекта.
Конструктор может иметь перегруженные версии, отличающиеся типом параметров или их количеством. Если внутри класса нет конструктора, то автоматически создается конструктор по умолчанию, который не имеет параметров. В этом случае объект объявляется так:
В предыдущих примерах мы как раз пользовались конструктором по умолчанию:
Если внутри класса объявлен пользовательский конструктор, то конструктор по умолчанию не создается. Это означает, что если вы создали конструктор с одним параметром, то при создании объекта обязательно нужно будет указывать значение. Чтобы иметь возможность создания объекта без указания значений, следует дополнительно создать конструктор без параметров. Создание объекта с указанием значений выглядит следующим образом:
Существует также альтернативный вариант создания объекта:
Можно также воспользоваться фигурными скобками, внутри которых перечислить значения через запятую:
Создание класса с несколькими конструкторами и различные способы создания экземпляра класса приведены в листинге 13.4.
Листинг 13.4. Способы создания объектов и перегрузка конструкторов
Как видно из примера (см. создание объекта obj4 ), если конструктор принимает только один параметр, то становится возможным следующий способ создания экземпляра класса:
В этом случае производится неявное преобразование значения в объект класса. Чтобы предотвратить такое преобразование перед конструктором, принимающим один параметр, следует указать ключевое слово explicit (листинг 13.5).
Листинг 13.5. Ключевое слово explicit
Существует еще один способ присваивания начальных значений полям класса, который заключается в указании списка инициализации после двоеточия между списком параметров и телом конструктора. Внутри списка инициализации указывается название поля после которого внутри круглых (или фигурных) скобок задается значение. Следует учитывать, что инициализация полей производится в порядке их объявления внутри класса, а не в порядке перечисления внутри списка инициализации. Пример использования списка инициализации приведен в листинге 13.6.
Листинг 13.6. Использование списка инициализации полей
При отсутствии списка инициализации полей в конструкторе присваивание значений полям объектных типов будет выполнено дважды. Первый раз при инициализации (вызывается конструктор по умолчанию), а второй раз — значениями, которые присваиваются внутри тела конструктора. Поэтому при использовании полей объектных типов лучше создавать список инициализации полей в конструкторе явным образом. Рассмотрим пример:
Создадим объекты внутри функции main() :
Конструктор класса может иметь множество перегруженных версий с разными параметрами. Начиная со стандарта C++11, имеется возможность вызвать один конструктор из другого. Для этого после списка параметров ставится двоеточие, далее указывается название класса и внутри круглых или фигурных скобок через запятую передаются значения в другой конструктор (листинг 13.7). Конструктор, вызывающий другой конструктор называется делегирующим. Обратите внимание, одновременно с вызовом другого конструктора нельзя производить инициализацию полей класса.
Листинг 13.7. Вызов одного конструктора из другого
Если конструктор вызывается при создании объекта, то перед уничтожением объекта автоматически вызывается метод, называемый деструктором. Внутри деструктора можно закрыть ранее открытый файл, освободить динамически выделенную память и др. Название деструктора совпадает с названием класса и конструктора, но перед названием добавляется знак тильда (
). Тип возвращаемого значения не указывается. В качестве примера продемонстрируем последовательность вызова конструкторов и деструкторов при создании и удалении нескольких объектов (листинг 13.8).
Листинг 13.8. Порядок вызова конструкторов и деструкторов
Учебник C++ (Qt Creator и MinGW) в формате PDF
Помощь сайту
ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов