Спершу нам потрібно повернутися до того, що означає переходити за значенням та за посиланням.
Для таких мов, як Java та SML, передача за значенням є простою (і немає проходу за посиланням), так само як і копіювання змінної величини, оскільки всі змінні є просто скалярами і мають вбудовану семантичну копію: вони є або тими, що вважаються арифметичними. введіть C ++ або "посилання" (вказівники з різною назвою та синтаксисом).
У C у нас є скалярні та визначені користувачем типи:
- Скаляри мають числове або абстрактне значення (покажчики не є числами, вони мають абстрактне значення), яке копіюється.
- Типи сукупних даних скопіювали всі можливі ініціалізовані члени:
- для типів продуктів (масиви та структури): рекурсивно всі члени структур та елементів масивів копіюються (синтаксис функції C не дає можливості передавати масиви за значенням безпосередньо, лише масиви членів структури, але це деталізація ).
- для типів суми (союзи): значення "активного члена" зберігається; Очевидно, що копія член на член не є для того, що не всі члени можуть бути ініціалізовані.
У визначених користувачем типів C ++ можуть бути визначені користувачем семантичні копії, які дозволяють по-справжньому "об'єктно-орієнтоване" програмування з об'єктами, що володіють своїми ресурсами та операціями "глибокої копії". У такому випадку операція копіювання - це дійсно виклик функції, яка майже може робити довільні операції.
Для структур C, складених як C ++, "копіювання" все ще визначається як виклик операції копіювання, визначеної користувачем (конструктором або оператором призначення), які неявно генеруються компілятором. Це означає, що семантика загальної програми підмножини C / C ++ різна в C і C ++: у C копіюється цілий тип агрегату, у C ++ неявно створена функція копіювання викликається для копіювання кожного члена; кінцевим результатом є те, що в будь-якому випадку кожен член копіюється.
(Я думаю, є виняток, коли структура всередині союзу копіюється.)
Отже, для типу класу єдиний спосіб (за допомогою зовнішніх копій) зробити новий екземпляр - це через конструктор (навіть для тих, у кого тривіальні конструктори, створені компілятором).
Ви не можете прийняти адресу rvalue через оператора unry, &
але це не означає, що немає об’єкта rvalue; а об’єкт за визначенням має адресу ; і ця адреса навіть представлена синтаксичною конструкцією: об’єкт типу класу може створювати лише конструктор, і він має this
вказівник; але для тривіальних типів не існує конструктора, написаного користувачем, тому не можна розміщувати його, this
поки не буде створена копія та названа.
Для скалярного типу значення об'єкта - це значення, що є об'єктом, чисте математичне значення, що зберігається в об'єкті.
Для типу класу єдине поняття значення об'єкта - це інша копія об'єкта, яку може створити лише конструктор копій, реальна функція (хоча для тривіальних типів ця функція настільки тривіальна, вони іноді можуть бути створений без виклику конструктора). Це означає, що значення об'єкта є результатом зміни глобального стану програми шляхом виконання . Він не має доступу математично.
Тож передача за значенням насправді не є річчю: це передача за допомогою конструктора копій , що менш красиво. Очікується, що конструктор копій виконає розумну операцію "копіювання" відповідно до належної семантики типу об'єкта, зважаючи на його внутрішні інваріанти (це абстрактні користувацькі властивості, а не властиві C ++ властивості).
Перейти за значенням об'єкта класу означає:
- створити інший екземпляр
- потім зробіть виклик функції дії в цьому екземплярі.
Зауважте, що проблема не має нічого спільного з тим, чи є сама копія об'єктом з адресою: всі параметри функції є об'єктами та мають адресу (на мовному семантичному рівні).
Питання в тому, чи:
- копія - це новий об'єкт, ініціалізований з чистою математичною величиною (справжньою чистою оцінкою) оригінального об'єкта, як у скалярів;
- або копія - це значення оригінального об'єкта , як і для класів.
У випадку тривіального типу класу ви все одно можете визначити копію оригіналу члена-члена, тому ви можете визначити чисту оцінку оригіналу через тривіальність операцій копіювання (конструктор копіювання та призначення). Не так, як для довільних спеціальних функцій користувача: значення оригіналу повинно бути сконструйованою копією.
Об'єкти класу повинні бути побудовані абонентом; у конструктора формально є this
вказівник, але формалізм тут не має значення: усі об'єкти формально мають адресу, але тільки ті, які фактично використовують свою адресу, використовуються не суто локальними способами (на відміну від *&i = 1;
чисто локального використання адреси), повинні мати чітко визначені адреса.
Об'єкт повинен бути абсолютно пройденим за адресою, якщо він повинен мати адресу в обох цих двох окремо складених функціях:
void callee(int &i) {
something(&i);
}
void caller() {
int i;
callee(i);
something(&i);
}
Тут навіть якщо something(address)
це чиста функція або макрос або що-небудь (як printf("%p",arg)
), яке не може зберігати адресу або спілкуватися з іншою сутністю, у нас є вимога перейти за адресою, оскільки адреса повинна бути чітко визначена для унікального об'єкта, int
який має унікальний особистість.
Ми не знаємо, чи буде зовнішня функція "чистою" в плані переданих їй адрес.
Тут потенціал для реального використання адреси або в нетривіальному конструкторі, або в деструкторі на стороні виклику , ймовірно, є причиною прийняття безпечного, спрощеного маршруту та надання об'єкту ідентичності у виклику та передачі його адреси, як це робиться. впевнений, що будь-яке нетривіальне використання його адреси в конструкторі, після побудови та в деструкторі є послідовним : this
повинно бути однаковим щодо існування об'єкта.
Нетривіальний конструктор або деструктор, як і будь-яка інша функція, може використовувати this
вказівник таким чином, щоб вимагати узгодженості його значення, навіть якщо якийсь об'єкт з нетривіальними матеріалами може не:
struct file_handler { // don't use that class!
file_handler () { this->fileno = -1; }
file_handler (int f) { this->fileno = f; }
file_handler (const file_handler& rhs) {
if (this->fileno != -1)
this->fileno = dup(rhs.fileno);
else
this->fileno = -1;
}
~file_handler () {
if (this->fileno != -1)
close(this->fileno);
}
file_handler &operator= (const file_handler& rhs);
};
Зауважте, що в цьому випадку, незважаючи на явне використання вказівника (явного синтаксису this->
), ідентичність об'єкта не має значення: компілятор цілком може використовувати побітову копію об'єкта навколо, щоб перемістити його та зробити "копіювати елісію". Це ґрунтується на рівні "чистоти" використання this
спеціальних функцій-членів (адреса не залишається).
Але чистота не є атрибутом, доступним на стандартному рівні декларування (існують розширення компілятора, які додають опис чистоти до не вбудованого оголошення функції), тому ви не можете визначити ABI на основі чистоти коду, який може бути недоступним (код може може не бути вбудованим і доступним для аналізу).
Чистота вимірюється як «безумовно чиста» або «нечиста або невідома». Спільна основа, або верхня межа семантики (фактично максимум), або LCM (Найменше спільне множина) - "невідомо". Так ABI опиняється на невідомому.
Підсумок:
- Деякі конструкції вимагають від компілятора для визначення ідентичності об'єкта.
- ABI визначається термінами класів програм, а не конкретних випадків, які можуть бути оптимізовані.
Можлива майбутня робота:
Чи достатньо корисна примітка про чистоту, щоб її узагальнити та стандартизувати?