- Это на самом деле требование ABI или, может быть, это просто некоторая пессимизация в определенных сценариях?
Один из примеров - Двоичный интерфейс приложения System V AMD64Дополнение к архитектуре процессора . Этот ABI предназначен для 64-битных x86-совместимых процессоров (архитектура Linux x86_64). За ним следуют в Solaris, Linux, FreeBSD, macOS, подсистеме Windows для Linux:
Если объект C ++ имеет нетривиальный конструктор копирования или нетривиальный деструктор, он передается невидимымссылка (объект заменяется в списке параметров указателем, имеющим класс INTEGER).
Объект с нетривиальным конструктором копирования или нетривиальным деструктором не может быть передан по значению, поскольку такие объекты должныиметь четко определенные адреса. Аналогичные проблемы возникают при возврате объекта из функции.
Обратите внимание, что только 2 регистра общего назначения могут использоваться для передачи 1 объекта с помощью конструктора тривиального копирования и тривиального деструктора, т.е. только значений объектовс sizeof
в регистры нельзя передавать больше 16. См. Соглашения о вызовах от Agner Fog для подробного рассмотрения соглашений о вызовах, в частности §7.1 Передача и возврат объектов. Существуют отдельные соглашения о вызовах для передачи типов SIMD в регистры.
Существуют различные ABI для других архитектур ЦП.
Почему ABI такой? То есть, если поля структуры / класса вписываются в регистры или даже в один регистр - почему мы не можем передать его в этот регистр?
Это реализацияподробно, но когда обрабатывается исключение, во время разматывания стека уничтожаемые объекты с автоматической продолжительностью хранения должны быть адресуемыми относительно фрейма стека функций, поскольку к этому времени регистры были засорены. Коду раскрутки стека требуются адреса объектов для вызова их деструкторов, но объекты в регистрах не имеют адреса.
Педантически, деструкторы работают с объектами :
AnОбъект занимает область хранения в период его строительства ([class.cdtor]), в течение всего времени его существования и в период его разрушения.
, и объект не может существовать в C ++, если нет адресуемое хранилище выделено для него, потому что идентификатор объекта является его адресом .
Когда требуется адрес объекта с тривиальным конструктором копирования, который хранится в регистрах, компилятор может просто сохранитьОбъект в память и получить адрес. Если конструктор копирования нетривиален, с другой стороны, компилятор не может просто сохранить его в памяти, ему скорее нужно вызвать конструктор копирования, который берет ссылку и, следовательно, требует адрес объекта в регистрах. Соглашение о вызовах, вероятно, не может зависеть от того, был ли конструктор копирования встроен в вызываемый объект или нет.
Еще один способ подумать об этом заключается в том, что для тривиально копируемых типов компилятор передает значение объект в регистрах, из которого объект может быть восстановлен обычными хранилищами памяти при необходимости. Например:
void f(long*);
void g(long a) { f(&a); }
в x86_64 с System V ABI компилируется в:
g(long): // Argument a is in rdi.
push rax // Align stack, faster sub rsp, 8.
mov qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
mov rdi, rsp // Load the address of the object on the stack into rdi.
call f(long*) // Call f with the address in rdi.
pop rax // Faster add rsp, 8.
ret // The destructor of the stack object is trivial, no code to emit.
В своем наводящем на размышлениях выступлении Чендлер Кэррут упоминает , что разрывИзменение ABI может быть необходимо (среди прочего), чтобы осуществить разрушительное движение, которое могло улучшить вещи. IMO, изменение ABI могло бы быть неразрывным, если функции, использующие новый ABI, явно соглашаются иметь новую другую связь, например объявляют их в блоке extern "C++20" {}
(возможно, в новом встроенном пространстве имен для переноса существующих API). Так что только код, скомпилированный с новыми объявлениями функций с новой связью, может использовать новый ABI.
Обратите внимание, что ABI не применяется, когда вызванная функция была встроена. Как и при генерации кода во время компиляции, компилятор может встроить функции, определенные в других единицах перевода, или использовать пользовательские соглашения о вызовах.