В следующем коде функция-член set() вызывается для model, который является нулевым указателем. Это было бы неопределенным поведением. Однако параметр функции-члена является результатом вызова другой функции, которая проверяет, является ли model нулевым указателем, и в этом случае выдает исключение. Гарантируется ли, что estimate() всегда будет вызываться до того, как будет осуществлен доступ к model, или это все еще неопределенное поведение (UB)?
#include <iostream>
#include <memory>
#include <vector>
struct Model
{
void set(int x)
{
v.resize(x);
}
std::vector<double> v;
};
int estimate(std::shared_ptr<Model> m)
{
return m ? 3 : throw std::runtime_error("Model is not set");
}
int main()
{
try
{
std::shared_ptr<Model> model; // null pointer here
model->set(estimate(model));
}
catch (const std::runtime_error& e)
{
std::cout << e.what();
}
return 0;
}
Ваш вопрос чисто из любопытства или он основан на реальном коде? Я бы не стал организовывать свой код, полагаясь на порядок оценки...
Если model является аргументом для estimate, как вы хотите, чтобы функция вызывалась до доступа к аргументу? Или что вы имеете в виду под "доступом"?
Я думаю, что вполне разумно писать код, основанный на том факте, что аргументы функции будут оцениваться до самой функции.
@ MickaëlC.Guimarães Он основан на реальном коде. Я не хотел разбивать вызов на две линии, и мне стало любопытно, безопасно ли это.
@mentalmushroom Это безопасно. Языки, которые откладывают вычисление до последнего возможного момента (так называемые ленивые вычисления), существуют, но C++ не входит в их число.
Доступ @YvesDaoust к model в порядке, он действителен shared_ptr, только разыменование его не в порядке, потому что он не указывает на действительный указатель
@ 463035818_is_not_a_number: отсюда и мой комментарий.
@YvesDaoust Я согласен, что вопрос можно было бы сформулировать лучше. Текст, ссылающийся на model, как если бы это был необработанный указатель, также немного сбивает с толку.
@ 463035818_is_not_a_number Была бы разница, если бы был необработанный указатель?
FWIW, дезинфицирующее средство адресов говорит, что -> оценивается в любом случае godbolt.org/z/n13P18M3G . Кроме того, использование необработанных указателей (что в основном должно быть той же проблемой здесь) дает тот же диагноз godbolt.org/z/TPT7Y89cG
@mentalmushroom Думаю, нет, но это часть ответа. Если бы ваш код был проблематичным, было бы важно, если бы model был необработанным указателем. edit: только сейчас я вижу ответ. Это имеет (крошечную) разницу
@john: если бы model->set() был вызовом виртуальной функции, нужно было бы выполнить некоторую работу по разыменованию, чтобы добраться до фактического указателя функции. Конечно, совмещение этой работы с настройкой аргументов по-прежнему возможно в соответствии с правилом «как если» в тех случаях, когда установка аргументов не может вызывать исключение, даже если стандарт определяет, что сначала оцениваются аргументы. Таким образом, фактическая формулировка в стандарте кажется несколько отсталой в создании этого UB и небезопасной на бумаге. Возможно, они думали о случаях, когда постфиксное выражение нетривиально, например, fptr[++i]( 4*i ) для перебора массива указателей на функции.





Это все еще неопределенное поведение (UB) согласно expr.compound:
Постфиксное-выражение располагается перед каждым выражением в списке-выражений и любым аргументом по умолчанию. Инициализация параметра, включая каждое вычисление связанного значения и побочный эффект, неопределенно упорядочена по отношению к любому другому параметру.
(выделено мной)
Это означает, что постфиксное выражение model->set располагается перед выражением estimate(model) в списке выражений. А поскольку model является нулевым указателем, предварительное условие std::shared_ptr::operator-> нарушается, и, следовательно, это приводит к UB.
Так что, если я правильно понимаю, поскольку model-> эквивалентно (*model)., это означает, что model все еще разыменован, и поэтому это все еще неопределенное поведение.
Не совсем понятно, как из этого утверждения следует УБ. Выражение постфикса перед стрелкой оценивается, что приводит к нулевому указателю. Вместе с id-выражением он определяет результат всего выражения. Хорошо, но разыменовывает ли он нулевой указатель, чтобы определить его?
@mentalmushroom, если set равно virtual, указатель должен быть фактически разыменован, чтобы отправить вызов на основе динамического типа объекта. Этот случай не выделен в стандарте (хотя не-virtual функция может быть отправлена только на основе статического типа model), поэтому общий случай заключается в том, что указатель должен быть разыменован.
@mentalmushroom Ваше использование std::shared_ptr делает это довольно четким, потому что перегрузка std::shared_ptr::operator-> имеет предварительное условие для спецификации библиотеки, что указатель не равен нулю. Таким образом, вызов этой функции уже имеет неопределенное поведение, независимо от оценки ->set результата. Другой вопрос, будет ли то же самое с необработанным нулевым указателем.
@user17732522 user17732522 Я добавил в свой ответ вторую часть, в которой используется expr.ref. В основном, насколько я понимаю [expr.ref]: «постфиксное выражение model перед оценкой стрелки, и результат этой оценки (то есть nullptr) вместе с id-выражением operator-> используется для определения результата всего постфиксного выражения ." Это приводит к UB, так как предусловие нарушено. Верен ли вывод и анализ с использованием [expr.ref]?
@user17732522 user17732522 Это правда, но действительно ли это вызывается в данном случае? Правило гласит: «постфиксное выражение ПЕРЕД оценкой стрелки». Неясно, оценивается ли выражение id. Или слово «определяет» подразумевает, что оно оценивается?
@mentalmushroom Постфиксное выражение в приведенной выше цитате применительно к вашему примеру будет постфиксным выражением выражения вызова функции (см. контекст цитаты), так что это все model->set. Согласно eel.is/c++draft/over.match.oper#2 часть model-> эквивалентна model.operator->(), которая уже представляет собой вызов operator->() как часть оценки model->set. Как связана фактическая функция-член set, не имеет значения.
@Jason То, что написано в [expr.compound], явно относится только к встроенным операторам, если не указано иное (eel.is/c++draft/expr#pre-3.sentence-1). Я думаю, вам следует цитировать [over.match.oper] вместо [expr.ref], если вы хотите описать поведение перегруженного ->.
@mentalmushroom Я имел в виду model.operator->()-> выше, а также я должен сослаться на eel.is/c++draft/over.match.oper#12.
@ user17732522 Я согласен с тем, что [expr.compound] предназначен для встроенных операторов, если не указано иное. Но [expr.pre] также говорит, что: «Перегруженные операторы подчиняются правилам синтаксиса и порядка вычисления, указанным в [expr.compound]». И поскольку [expr.ref # 2], который я цитировал во второй части своего ответа, говорит об оценке и поэтому должен применяться, не так ли?
@ Джейсон Это записка. Нормативная часть находится в eel.is/c++draft/over.match.oper#2.sentence-4. Он ссылается на [expr.compound] только для порядка вычисления операндов. То, что a в (a).operator->( ) оценивается (посредством применения [expr.ref] к .), на мой взгляд, не очень важно. UB происходит из-за выражения вызова postfix-expression(), где postfix-expression есть (a).operator->().
@ user17732522 Думаю, вы пропустили -> в конце своего последнего комментария. В частности, postfix-expression() должно было быть postfix-expression->() . По сути, поскольку model->set(estimate(model)) совпадает с model.operator->()->set(estimate(model)), поэтому, согласно [expr.compound], постфиксное выражение model.operator->()->set ставится перед выражением в списке выражений. Это приводит к UB из-за нарушения предусловия.
@ Джейсон Я имел в виду «где постфиксное выражение (a).operator->», но я не думаю, что мы действительно в чем-то расходимся.
Насколько я понимаю, это поведение undefined по крайней мере из C++ 17:
- В выражении вызова функции выражение, именующее функцию, располагается перед каждым выражением аргумента и каждым аргументом по умолчанию.
Как я это интерпретирую, это на самом деле гарантирует, что model->set оценивается перед любым аргументом и, таким образом, вызывает неопределенное поведение. Неважно, является ли model необработанным указателем.
Но model->set называет функцию-член, а не функцию, поэтому это, похоже, не применимо. Название вопроса также явно предполагает, что OP интересуется случаем функции-члена и уже знает о случае функции, не являющейся членом.
@Val Значит, функция-член не является функцией? На чем вы основываете это утверждение?
Вызов функции и вызов функции-члена — это две разные вещи.
@Val Это cppreference, а не стандарт. Этот текст написан для удобочитаемости, а не для максимальной точности. Фактическое правило гласит, что постфиксное выражение упорядочено до инициализации аргумента. Здесь применимо правило, приведенное в cppreference.
@Val Даже в Стандарте я считаю, что слово «функция» охватывает функции-члены, а также функции, не являющиеся членами, и, когда это применимо, делается явное различие. Вообще говоря, "вызов функции-члена" является частным случаем "вызова функции".
«модель-> набор оценивается»: это, очевидно, UB для случая std::shared_ptr, как объяснено в другом ответе, но неочевидно, является ли это UB для случая необработанного указателя. Как правило, разыменование самого нулевого указателя должно быть разрешено, и я не вижу ничего очевидного в eel.is/c++draft/expr.ref, что сделало бы его UB. Есть открытые вопросы CWG по точным ограничениям.
@user17732522 user17732522 «разыменование нулевого указателя» дает вам недопустимое lvalue. Если вы попытаетесь получить адрес, вы должны безопасно получить нулевой указатель. Почти все остальное (в частности, преобразование lvalue в rvalue, а также доступ к членам, назначение и т. д.) является UB.
@BenVoigt Это должна быть идея, но eel.is/c++draft/expr.unary.op#1.sentence-3 по-прежнему не обрабатывает этот особый случай и поэтому не определяет результат косвенность вообще, и лучшее, что я мог найти в [expr.ref] о том, будет ли model->set иметь неопределенное поведение, это eel.is/c++draft/expr.ref#8, что не является на самом деле охватывает случай, если читать буквально. Помимо этого, если предположить, что косвенное обращение разрешено, я не являюсь тем, кто заставит члена обращаться к самому себе UB. Вызов, безусловно, будет, но в примере OP он никогда не оценивается.
@user17732522: Ужасная формулировка в eel.is/c++draft/expr.prim.id.general#3.1 может быть частью проблемы. «выражение относится к классу члена» — бессмысленный мусор. Если бы он сказал либо «статический тип выражения», либо «динамический тип объекта, на который ссылается выражение», это было бы намного яснее.
@user17732522 user17732522 Вы правы, в Стандарте об этом ничего не ясно. Это заслуживает (недавнего) вопроса само по себе, но вкратце: результат model->set, когда model является нулевым указателем, на самом деле не определен в стандарте и, следовательно, подпадает под действие eel.is/c++draft/defns.undefined . Другой способ увидеть это: если компилятор реализует произвольное поведение в этом случае, можно ли из-за этого сказать, что он не соответствует требованиям?
@nielsen Я согласен с выводом. Просто я думаю, что именно эта часть была неочевидной частью, в которой интересовался OP, даже если они допустили ошибку, также введя UB через нарушение предварительного условия std::shared_ptr::operator->.
Постфиксное-выражение располагается перед каждым выражением в списке-выражений и любым аргументом по умолчанию.
В данном случае это означает, что model->set оценивается перед estimate(model).
Поскольку model является shared_ptr<Model>, model->set использует перегруженный shared_ptroperator->, который имеет следующее предварительное условие ([util.smartptr.shared.obs]/5):
Условия:
get() != nullptr.
Нарушение этого предварительного условия приводит к неопределенному поведению ([structure.specifications]/3.3).
Ну, вызов
estimateгарантированно будет вызван первым. Это означает, что исключение будет сгенерировано до фактического разыменования нулевого указателя. Является ли код все еще UB, даже если код, вызывающий UB, не выполняется? Это интересный вопрос... :)