Сначала нам нужно вернуться к тому, что значит передавать по значению и по ссылке.
Для языков, таких как Java и SML, передача по значению является простой (и нет передачи по ссылке), так же, как копирование значения переменной, так как все переменные являются просто скалярами и имеют встроенную семантику копирования: они либо считаются арифметическими. введите C ++ или «ссылки» (указатели с другим именем и синтаксисом).
В Си у нас есть скалярные и пользовательские типы:
- Скаляры имеют числовое или абстрактное значение (указатели не являются числами, они имеют абстрактное значение), которое копируется.
- Для агрегатных типов скопированы все их инициализированные элементы:
- для типов продуктов (массивов и структур): рекурсивно копируются все члены структур и элементов массивов (синтаксис функции C не позволяет напрямую передавать массивы по значению, только массивы членов структуры, но это деталь ).
- для типов сумм (объединений): значение «активного члена» сохраняется; очевидно, что член за членом не в порядке, так как не все члены могут быть инициализированы.
В C ++ пользовательские типы могут иметь пользовательскую семантику копирования, которая позволяет действительно «объектно-ориентированное» программирование с объектами, владеющими их ресурсами и операциями «глубокого копирования». В таком случае операция копирования на самом деле является вызовом функции, которая может почти выполнять произвольные операции.
Для структур C, скомпилированных как C ++, «копирование» по-прежнему определяется как вызов пользовательской операции копирования (либо конструктора, либо оператора присваивания), которая неявно генерируется компилятором. Это означает, что семантика программы общего подмножества C / C ++ отличается в C и C ++: в C копируется целый тип агрегата, в C ++ вызывается неявно сгенерированная функция копирования для копирования каждого члена; конечный результат заключается в том, что в любом случае каждый член копируется.
(Думаю, есть исключение, когда копируется структура внутри объединения.)
Таким образом, для типа класса единственный способ (вне объединенных копий) создать новый экземпляр - через конструктор (даже для тех, у кого есть тривиальные конструкторы, сгенерированные компилятором).
Вы не можете получить адрес rvalue через унарный оператор, &
но это не значит, что объекта rvalue нет; и объект по определению имеет адрес ; и этот адрес даже представлен синтаксической конструкцией: объект типа класса может быть создан только конструктором, и он имеет this
указатель; но для тривиальных типов пользовательского конструктора не существует, поэтому нет места для размещения this
до тех пор, пока копия не будет сконструирована и не будет названа.
Для скалярного типа значение объекта - это значение объекта, чистое математическое значение, хранящееся в объекте.
Для типа класса единственным понятием значения объекта является другая копия объекта, которая может быть сделана только конструктором копирования, реальной функцией (хотя для тривиальных типов эта функция настолько тривиальна, иногда они могут быть создается без вызова конструктора). Это означает, что значение объекта является результатом изменения глобального состояния программы при выполнении . Это не доступ математически.
Так что передача по значению на самом деле не вещь: это передача по вызову конструктора копирования , что менее привлекательно. Предполагается, что конструктор копирования будет выполнять разумную операцию «копирования» в соответствии с надлежащей семантикой типа объекта с учетом его внутренних инвариантов (которые являются абстрактными пользовательскими свойствами, а не внутренними свойствами C ++).
Передача по значению объекта класса означает:
- создать другой экземпляр
- затем заставьте вызванную функцию действовать в этом случае.
Обратите внимание, что проблема не имеет отношения к тому, является ли сама копия объектом с адресом: все параметры функции являются объектами и имеют адрес (на уровне семантики языка).
Вопрос заключается в следующем:
- копия - это новый объект, инициализированный чистым математическим значением (true pure rvalue) исходного объекта, как со скалярами;
- или копия является значением оригинального объекта , как с классами.
В случае тривиального типа класса вы все равно можете определить член-копию оригинала, поэтому вы можете определить чистое значение оригинала из-за тривиальности операций копирования (конструктор копирования и присваивание). С произвольными специальными пользовательскими функциями это не так: значение оригинала должно быть составной копией.
Объекты класса должны быть созданы вызывающей стороной; у конструктора формально есть 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 определяется в терминах классов программ, а не в конкретных случаях, которые могут быть оптимизированы.
Возможная будущая работа:
Является ли аннотация чистоты достаточно полезной для обобщения и стандартизации?