Шаг 20 - Временные объекты. Неявные вызовы конструкторов и их подавление.
Шаг 20 - Временные объекты. Неявные вызовы конструкторов и их подавление.
Не удается углубиться в какую-либо тему. Приходится касаться по верхам, потом переключаться на что-то другое. С другой стороны, может это и правильно, часто достаточно только знать, что есть ТАКОЕ решение, а изучить детально можно и позже, когда сделаешь окончательный выбор. Да и не очень это интересно - что за радость переписать двадцать страниц из учебника, или перевести статью какого-нибудь доктора CS? Объяснения которого в точности так же логичны, как рассказ Ивана Бездомного насчет "…Берлиоза зарезало трамваем, а тот заранее знал про масло, которое Аннушка пролила" - то есть логика и связь есть - но только для него самого.
Чтож, к делу.
А кто такие временные объекты? Локальные переменные с замечательными именами a, a1, a2, a_1, tmp1, tmp2? (Кстати ни за что не берите на работу болванов, которые так именуют переменные; пусть на FoxPro пишут. Думаю написать про это отдельный Шаг - причины для немедленного увольнения.) Вообще-то нет. Временные объекты - это объекты, которые не имеют имен в коде и неявно создаются компилятором. Поскольку неявные "подарки" компилятора иногда бывают очень некстати, лучше заранее знать, чего можно ожидать от него. А зачем он их создает? Первое - при выполнении преобразования типов, для вызова функций. Второе - для возвращения объекта из функции.
Придется немного поэкспериментировать. Поэтому скопируйте себе код небольшого класса:
#include ‹iostream.h›
class CInt {
private:
int m_i;
int m_instance;
static int iCounter;
public:
CInt (int);
CInt (const CInt&);
~CInt ();
CInt operator+ (const CInt&);
CInt& operator+=(const CInt&);
CInt& operator= (const CInt&); // operator int ();
};
int CInt::iCounter = 0;
CInt::CInt (int _i=0): m_i(_i) {
m_instance = ++iCounter;
cout‹‹"defa constr " ‹‹ m_instance ‹‹ " "‹‹ m_i‹‹ endl;
}
CInt::CInt (const CInt& _i): m_i(_i.m_i) {
m_instance = ++iCounter;
cout‹‹"copy constr " ‹‹ m_instance ‹‹ " "‹‹ m_i‹‹ endl;
}
CInt::~CInt () {
iCounter--;
cout ‹‹"~destructor " ‹‹ m_instance ‹‹ " "‹‹ m_i‹‹ endl;
}
CInt& CInt::operator=(const CInt& _i) {
m_i = _i.m_i;
cout ‹‹"assert oper " ‹‹ m_instance ‹‹ " "‹‹ m_i‹‹ endl;
return *this;
}
CInt CInt::operator+(const CInt& _i) {
cout‹‹"addi operat " ‹‹ m_instance ‹‹ " "‹‹ m_i‹‹ endl;
return CInt (m_i + _i.m_i);
}
CInt& CInt::operator+= (const CInt& _i) {
m_i += _i.m_i;
cout‹‹"autoadd ope " ‹‹ m_instance ‹‹ " "‹‹ m_i‹‹ endl;
return *this;
}
/*
CInt::operator int () {
return m_i;
}
*/
int main (void) {
cout ‹‹ "start" ‹‹ endl;
// Позиция 1.
CInt i_test = CInt (2) + CInt (4);
cout ‹‹ "firststop" ‹‹ endl;
{
// Позиция 2.
}
cout ‹‹ "thirdstop" ‹‹ endl;
return 0;
}
Пояснения: класс представляет целые числа. Определены конструктор по умолчанию и копирования, присваивание, пара арифметических операторов, оператор преобразования в int (закомментирован). В функции main отмечены 2 позиции для экспериментов.
Еще момент - вызвала затруднения форма конструктора со списком инициализации, типа этой:
CClass::CClass (int _a, int _b, int _c) : m_a(_a), m_bc(_b, _c) {}
Тут нет ничего такого, просто конструкторы членов-переменных и базовых классов вызываются явно со своими параметрами, это выгоднее чем создавать пустые, а потом в теле конструктора выполнять ПРИСВАИВАНИЕ при помощи оператора operator=().
Попробуем в позицию 1 поставить:
CInt i_test = 1 + 2;
Вызовется только один конструктор - по умолчанию. Это одно и то же:
CInt i_test = 3; ‹=====› CInt i_test(3);
Попробуем так
CInt i_test;
i_test = CInt(1) + CInt(2);
Сначала создается первый объект, потом левый операнд, потом правый, потом результат, потом выполняется присваивание, потом оба операнда и результат удаляются, сразу после использования. Всего четыре объекта. Один - временный.
А если записать в одну строку?
CInt i_test = CInt(1) + CInt(2);
Подумаем немного. Сначала левый операнд, потом правый, потом результат, потом создается объект а при помощи конструктора копирования. Всего четыре. Три по умолчанию, один копирования. Лепота.
ДА НИЧЕГО ТАКОГО! Компилятору плевать на нашу логику. Он берет результат, и превращает его в i_test. Оптимизирует. Три вызова дефолт конструктора, и ни одного временного объекта.
Я встречал этот вопрос на BrainBench и на ProveIt.
А еще давайте сравним два варианта кода:
CInt i_test = CInt(1) + CInt(2) + CInt (4) + CInt(8);
и
CInt i_test = CInt (1);
i_test+=CInt(2);
i_test+=CInt(4);
i_test+=CInt(8);
Видите? В первом варианте конструктор вызывается 7 раз, а во втором 4.
С явными вызовами конструкторов все понятно. А неявные?
CInt i_test = CInt(1) + 2;
Компилятор пытается найти подходящий оператор operator+, но его нет для примитивного int. Тогда он считает, что конструктор CInt(int) - вполне подходящий способ преобразования, и на место двойки ставит CInt(2).
Теперь раскройте оператор operator int. Хочется ожидать разумного поведения компилятора; но увы - в нашем примере этого ожидать не стоит. Есть два способа вычислить последнее выражение - и компилятор не знает что выбрать, и подыхает, как Буриданов осел между двумя кучами сена. Чтобы помочь компилятору, нужно один вариант блокировать. Как?
Не определять оператор преобразования, а определять вместо них функции, типа operator int() ‹-› asInt()
В определении конструктора использовать модификатор explicit для подавления неявных вызовов.
Использовать proxy-object - промежуточный объект наподобие курсора из Шага 16, все назначение которого - быть другим объектом когда нужно, и не быть им, когда не нужно. Словами больно заумно, проще нарисовать код.
// Класс прокси-объекта
class CProxyInt {
friend class CInt;
private:
int m_i;
public:
CProxyInt (int _i): m_i(_i) {}
int getInt () const { return m_i; }
};
// Предыдущий класс инт.
class CInt {
friend class CProxyInt;
private:
int m_i;
int m_instance;
static int iCounter;
public:
// Конструктор по умолчанию изменен
CInt (CProxyInt);
CInt (const CInt&);
~CInt();
CInt operator+(const CInt&);
CInt& operator+=(const CInt&);
CInt& operator= (const CInt&);
// operator int ();
};
int CInt::iCounter = 0;
// Реализация конструктора, вместо инта стоит прокси
CInt::CInt (CProxyInt _i=0): m_i(_i.m_i) {
m_instance = ++iCounter;
cout‹‹"defa constr " ‹‹ m_instance ‹‹ " "‹‹ m_i ‹‹ endl;
}
CInt a(5); // Это компилируется нормально
CInt a = 5; // А это нет. И все неявные вызовы тоже.
Видите, мы используем технику proxy уже второй раз, но совершенно в другом контексте. Общее то, что proxy применяется в том случае, если мы хотим определить свои законы преобразования типов и классов.
В этом смысле smart-указатель несомненно тоже рroxy, (уменьш. ласк. проксятник, проксятничек).
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКЧитайте также
6.4.2. Неявные критерии
6.4.2. Неявные критерии В этом разделе мы рассмотрим неявные критерии, точнее, те критерии, которые подгружаются неявно и становятся доступны, например при указании критерия –protocol tcp. На сегодняшний день существует три автоматически подгружаемых расширения, это TCP
Подавление линий заднего плана
Подавление линий заднего плана Команда HIDE обеспечивает создание рисунка без скрытых линий. Сложные трехмерные модели часто оказываются перегруженными, что затрудняет их чтение и просмотр результатов выполнения какой-либо команды на объекте. Можно устранить эту
R.12.2 Временные объекты
R.12.2 Временные объекты В некоторых ситуациях транслятору бывает необходимо или удобно создавать временные объекты. Использование временных объектов зависит от реализации. Если транслятору понадобился временный объект типа класса с конструктором, он должен обеспечить
Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции
Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции В мире объектно-ориентированного программирования преобладают явные интерфейсы и полиморфизм на этапе исполнения. Например, рассмотрим следующий (бессмысленный) класс:class Widget
5.15. Явные и неявные преобразования чисел
5.15. Явные и неявные преобразования чисел Программисты, только начинающие изучать Ruby, часто удивляются, зачем нужны два метода to_i и to_int (и аналогичные им to_f и to_flt). В общем случае метод с коротким именем применяется для явных преобразований, а метод с длинным именем — для
11.1.1. Применение нескольких конструкторов
11.1.1. Применение нескольких конструкторов В Ruby нет «настоящих» конструкторов, как в C++ или в Java. Сама идея, конечно, никуда не делась, поскольку объекты необходимо создавать и инициализировать, но реализация выглядит иначе.В Ruby каждый класс имеет метод класса new, который
8.3. Использование конструкторов и деструкторов для управления ресурсами (RAII)
8.3. Использование конструкторов и деструкторов для управления ресурсами (RAII) ПроблемаДля класса, представляющего некоторый ресурс, требуется использовать конструктор для получения этого ресурса и деструктор для его освобождения. Эта методика часто называется
Роль конструкторов
Роль конструкторов До сих пор объекты HelloClass строились с помощью конструктора, заданного по умолчанию, который, по определению, не имеет аргументов. Каждый класс C# автоматически снабжается типовым конструктором, который вы можете при необходимости переопределить. Этот
Определение конструкторов типов
Определение конструкторов типов Система CTS (общая система типов) поддерживает конструкторы как уровня экземпляра, так и уровня класса (статические конструкторы). В терминах CIL для конструкторов уровня экземпляра используется лексема .ctor, а для статических конструкторов
Подавление линий заднего плана
Подавление линий заднего плана Команда HIDE обеспечивает создание рисунка без скрытых линий. Сложные трехмерные модели часто оказываются перегруженными, что затрудняет их чтение и просмотр результатов выполнения какой-либо команды на объекте. Можно устранить эту
18.5.3. Порядок вызова конструкторов и деструкторов
18.5.3. Порядок вызова конструкторов и деструкторов Виртуальные базовые классы всегда конструируются перед невиртуальными, вне зависимости от их расположения в иерархии наследования. Например, в приведенной иерархии у класса TeddyBear (плюшевый мишка) есть два виртуальных
Подавление линий заднего плана
Подавление линий заднего плана Команда HIDE обеспечивает создание рисунка без скрытых линий . Сложные трехмерные модели часто оказываются перегруженными, что затрудняет их чтение и просмотр результатов выполнения какой-либо команды на объекте. Можно устранить эту
Подавление линий заднего плана
Подавление линий заднего плана Команда HIDE обеспечивает создание рисунка без скрытых линий. Сложные трехмерные модели часто оказываются перегруженными, что затрудняет их чтение и просмотр результатов выполнения какой-либо команды на объекте. Можно устранить эту
8.4.2. Подавление вывода сообщений об ошибках
8.4.2. Подавление вывода сообщений об ошибках Допустим, вы хотите найти запись пользователя louise в системном файле паролей:$ grep louise /etc/passwdlouise:lxAL6GW9G.ZyY:501:501:Accounts Sect1С:/home/accts/louise:/bin/shHe исключена возможность, что вы забудете, как называется этот файл. В таком случае воспользуйтесь
Наследование конструкторов
Наследование конструкторов Правила наследования конструкторов - достаточно сложные. В разных языках программирования приняты разные решения на этот счет. В частности, в Delphi Object Pascal все конструкторы наследуются. В .NET, напротив, конструкторы не наследуются. Причина такого