Стандарт С++ гарантирует, что возвращаемое значение функции имеет постоянный адрес?
Рассмотрим эту программу:
#include <stdio.h>
struct S {
S() { print(); }
void print() { printf("%p\n", (void *) this); }
};
S f() { return {}; }
int main() { f().print(); }
Насколько я могу судить, существует только один объект S
, построенный здесь. Отсутствует копия: нет копии, которую нужно исключить в первую очередь, и действительно, если я явно удалю конструктор копирования и/или перемещения, компиляторы продолжают принимать программу.
Однако я вижу два разных значения указателя. Это происходит из-за того, что моя платформа ABI возвращает тривиально-скопируемые типы, такие как эта, в регистры процессора, поэтому нет возможности с этим ABI избежать копирования. clang сохраняет это поведение даже при полной оптимизации вызова функции. Если я даю S
нетривиальный конструктор копии, даже если он недоступен, тогда я вижу одно и то же значение, напечатанное дважды.
Первоначальный вызов print()
происходит во время построения, который находится до начала жизненного цикла объекта, но использование this
внутри конструктора обычно действует до тех пор, пока оно не используется таким образом, что требуется конструкция чтобы закончить - например, литье в производный класс, и, насколько я знаю, печать или сохранение его значения не требует завершения строительства.
Позволяет ли эта стандартная программа печатать два разных значения указателя?
Примечание. Я знаю, что стандарт позволяет этой программе печатать два разных представления одного и того же значения указателя, и технически я этого не исключал. Я мог бы создать другую программу, которая позволяет избежать сравнения представлений указателей, но было бы труднее понять, поэтому я хотел бы избежать этого, если это возможно.
Ответы
Ответ 1
T.C. указал в комментариях, что это дефект в стандарте. Это проблема основного языка 1590. Это тонкая проблема, чем мой пример, но одна и та же самая причина:
Некоторые ABI требуют, чтобы объект определенных типов классов передавался в регистр [...]. Стандарт следует изменить, чтобы разрешить это использование.
текущая предлагаемая формулировка будет охватывать это, добавив новое правило в стандарт:
Когда объект типа класса X
передается или возвращается из функции, если каждый конструктор копирования, перемещение конструктора и деструктор X
является тривиальным или удаленным, а X
имеет по крайней мере один не- -deleted copy или move constructor, реализациям разрешено создавать временный объект для хранения функционального параметра или объекта результата. [...]
По большей части это позволит использовать текущее поведение GCC/clang.
Существует небольшой угловой случай: в настоящее время, когда тип имеет только удаленный экземпляр или механизм перемещения, который был бы тривиальным, если по умолчанию, по текущим правилам стандарта, этот конструктор по-прежнему является тривиальным, если он удален:
12.8 Копирование и перемещение объектов класса [class.copy]
12 Конструктор копирования/перемещения для класса X
является тривиальным, если он не предоставляется пользователем [...]
Конструктор удаленной копии не предоставляется пользователем, и ничто из следующего не сделает такой конструктор копирования нетривиальным. Так, как указано стандартом, такой конструктор тривиален и как указано моей платформой ABI, из-за тривиального конструктора, GCC и clang create дополнительная копия в этом случае тоже. Однострочное дополнение к моей тестовой программе демонстрирует это:
#include <stdio.h>
struct S {
S() { print(); }
S(const S &) = delete;
void print() { printf("%p\n", (void *) this); }
};
S f() { return {}; }
int main() { f().print(); }
Это печатает два разных адреса как с GCC, так и с clang, хотя даже предлагаемое разрешение требует, чтобы один и тот же адрес печатался дважды. Это, по-видимому, предполагает, что, хотя мы получим обновление стандарта, чтобы не требовать радикально несовместимого ABI, нам все равно нужно получить обновление для ABI для обработки углового футляра в соответствии с тем, что потребует стандарт.
Ответ 2
Это не ответ, а скорее заметка о различном поведении g++ и clang в этом случае, в зависимости от флага оптимизации -O
.
Рассмотрим следующий код:
#include <stdio.h>
struct S {
int i;
S(int _i): i(_i) {
int* p = print("from ctor");
printf("about to put 5 in %p\n", (void *)&i);
*p = 5;
}
int* print(const char* s) {
printf("%s: %p %d %p\n", s, (void *) this, i, (void *)&i);
return &i;
}
};
S f() { return {3}; }
int main() {
f().print("from main");
}
Мы видим, что clang (3.8) и g++ (6.1) принимают его немного по-другому, но оба имеют правильный ответ.
clang (для no -O, -O1, -O2) и g++ (для no -O, -O1)
from ctor: 0x7fff9d5e86b8 3 0x7fff9d5e86b8
about to put 5 in 0x7fff9d5e86b8
from main: 0x7fff9d5e86b0 5 0x7fff9d5e86b0
g++ (для -O2)
from ctor: 0x7fff52a36010 3 0x7fff52a36010
about to put 5 in 0x7fff52a36010
from main: 0x7fff52a36010 5 0x7fff52a36010
Кажется, что оба они делают это правильно в обоих случаях - когда они решают пропустить оптимизацию регистров (g++ -O2) и когда они идут с оптимизацией регистров, но копируют значение до фактического я во времени (все остальные случаи).