Имеет ли следующая программа неопределенное поведение в С++ 17 и более поздних версиях?
struct A {
void f(int) { /* Assume there is no access to *this here */ }
};
int main() {
auto a = new A;
a->f((a->~A(), 0));
}
С++ 17 гарантирует, что a->f
вычисляется для функции-члена объекта A
до оценки аргумента вызова. Следовательно, направление от ->
четко определено. Но до того, как вызов функции введен, аргумент оценивается и заканчивает время жизни объекта A
(см. однако правки ниже). У вызова все еще есть неопределенное поведение? Можно ли таким способом вызывать функцию-член объекта вне его времени жизни?
Категория значений a->f
является prvalue для [expr.ref]/6.3.2, а [basic.life]/7 запрещает только вызовы нестатических функций-членов для glvalues ссылаясь на объект после жизни. Означает ли это, что вызов действителен? (Изменение: как обсуждалось в комментариях, я, вероятно, неправильно понимаю [basic.life]/7, и это, вероятно, применимо здесь.)
Изменится ли ответ, если я заменю вызов деструктора a->~A()
на delete a
или new(a) A
(на #include<new>
)?
Некоторые уточнения и уточнения по моему вопросу:
Если бы я разделил вызов функции-члена и деструктор /delete/place-new на два оператора, я думаю, что ответы ясны:
a->A(); a->f(0)
: UB из-за нестатического вызова члена наa
вне его времени жизни. (см. правку ниже)delete a; a->f(0)
: то же, что и вышеnew(a) A; a->f(0)
: четко определено, вызов нового объекта
Однако во всех этих случаях a->f
секвенируется после первого соответствующего оператора, в то время как в моем первоначальном примере этот порядок обратный. У меня вопрос: позволяет ли это изменение изменить ответы?
Для стандартов до С++ 17 я изначально думал, что все три случая вызывают неопределенное поведение, потому что оценка a->f
зависит от значения a
, но не является последовательной по отношению к оценке аргумента, который вызывает сторону -эффект на a
. Тем не менее, это неопределенное поведение, только если есть фактический побочный эффект от скалярного значения, например запись в скалярный объект. Однако ни один скалярный объект не записан, потому что A
тривиален, и поэтому мне также было бы интересно узнать, какое именно ограничение нарушается в случае стандартов до С++ 17. В частности, сейчас мне неясен случай с новым размещением.
Я только что понял, что формулировка о времени жизни объектов изменилась между С++ 17 и текущим проектом. В n4659 (черновик С++ 17) [basic.life]/1 говорит:
Время жизни объекта o типа T заканчивается, когда:
- если Т является классом тип с нетривиальным деструктором (15.4), вызов деструктора начинается
[...]
в то время как текущий черновик гласит:
Время жизни объекта o типа T заканчивается, когда:
[...]
- если T является типом класса, начинается вызов деструктора или
[...]
Поэтому я полагаю, что мой пример имеет четко определенное поведение в С++ 17, но не в текущем (С++ 20) черновике, потому что вызов деструктора тривиален, а время жизни объекта A
не заканчивается. Буду признателен за разъяснения по этому вопросу. Мой оригинальный вопрос все еще стоит даже для С++ 17 в случае замены вызова деструктора выражением delete или place-new.
Если f
обращается к *this
в своем теле, то может быть неопределенное поведение для случаев вызова деструктора и выражения удаления, однако в этом вопросе я хочу сосредоточиться на том, действителен ли сам вызов или нет.
Однако обратите внимание, что вариант моего вопроса с Placement-New потенциально не будет иметь проблемы с доступом к члену в f
, в зависимости от того, является ли сам вызов неопределенным поведением или нет. Но в этом случае может возникнуть дополнительный вопрос, особенно для случая размещения нового, потому что мне неясно, будет ли this
в функции всегда автоматически ссылаться на новый объект или может понадобиться потенциально быть std::launder
ред (в зависимости от того, какие члены A
имеют).
В то время как A
действительно имеет тривиальный деструктор, более интересным случаем, вероятно, является то, что он имеет некоторый побочный эффект, относительно которого компилятор может захотеть сделать предположения для целей оптимизации. (Я не знаю, использует ли какой-либо компилятор что-то вроде этого.) Поэтому я приветствую ответы для случая, когда A
также имеет нетривиальный деструктор, особенно если ответ отличается в двух случаях.
Кроме того, с практической точки зрения, тривиальный вызов деструктора, вероятно, не влияет на сгенерированный код и (вряд ли?) На оптимизацию, основанную на неопределенных предположениях о поведении, за исключением того, что все примеры кода, скорее всего, сгенерируют код, который выполняется, как ожидается, на большинстве компиляторов. Меня больше интересует теоретическая, а не практическая перспектива.
Этот вопрос призван лучше понять детали языка. Я не призываю никого писать такой код.