Аргумент this оценивается до или после других аргументов функции-члена?

В следующем коде функция-член 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;
}

Ну, вызов estimate гарантированно будет вызван первым. Это означает, что исключение будет сгенерировано до фактического разыменования нулевого указателя. Является ли код все еще UB, даже если код, вызывающий UB, не выполняется? Это интересный вопрос... :)

Some programmer dude 09.05.2023 10:33

Ваш вопрос чисто из любопытства или он основан на реальном коде? Я бы не стал организовывать свой код, полагаясь на порядок оценки...

Mickaël C. Guimarães 09.05.2023 10:34

Если model является аргументом для estimate, как вы хотите, чтобы функция вызывалась до доступа к аргументу? Или что вы имеете в виду под "доступом"?

Yves Daoust 09.05.2023 10:36

Я думаю, что вполне разумно писать код, основанный на том факте, что аргументы функции будут оцениваться до самой функции.

john 09.05.2023 10:36

@ MickaëlC.Guimarães Он основан на реальном коде. Я не хотел разбивать вызов на две линии, и мне стало любопытно, безопасно ли это.

mentalmushroom 09.05.2023 10:36

@mentalmushroom Это безопасно. Языки, которые откладывают вычисление до последнего возможного момента (так называемые ленивые вычисления), существуют, но C++ не входит в их число.

john 09.05.2023 10:38

Доступ @YvesDaoust к model в порядке, он действителен shared_ptr, только разыменование его не в порядке, потому что он не указывает на действительный указатель

463035818_is_not_a_number 09.05.2023 10:40

@ 463035818_is_not_a_number: отсюда и мой комментарий.

Yves Daoust 09.05.2023 10:41

@YvesDaoust Я согласен, что вопрос можно было бы сформулировать лучше. Текст, ссылающийся на model, как если бы это был необработанный указатель, также немного сбивает с толку.

463035818_is_not_a_number 09.05.2023 10:42

@ 463035818_is_not_a_number Была бы разница, если бы был необработанный указатель?

mentalmushroom 09.05.2023 10:57

@mentalmushroom Думаю, нет, но это часть ответа. Если бы ваш код был проблематичным, было бы важно, если бы model был необработанным указателем. edit: только сейчас я вижу ответ. Это имеет (крошечную) разницу

463035818_is_not_a_number 09.05.2023 11:29

@john: если бы model->set() был вызовом виртуальной функции, нужно было бы выполнить некоторую работу по разыменованию, чтобы добраться до фактического указателя функции. Конечно, совмещение этой работы с настройкой аргументов по-прежнему возможно в соответствии с правилом «как если» в тех случаях, когда установка аргументов не может вызывать исключение, даже если стандарт определяет, что сначала оцениваются аргументы. Таким образом, фактическая формулировка в стандарте кажется несколько отсталой в создании этого UB и небезопасной на бумаге. Возможно, они думали о случаях, когда постфиксное выражение нетривиально, например, fptr[++i]( 4*i ) для перебора массива указателей на функции.

Peter Cordes 10.05.2023 05:17
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
33
13
2 211
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Это все еще неопределенное поведение (UB) согласно expr.compound:

Постфиксное-выражение располагается перед каждым выражением в списке-выражений и любым аргументом по умолчанию. Инициализация параметра, включая каждое вычисление связанного значения и побочный эффект, неопределенно упорядочена по отношению к любому другому параметру.

(выделено мной)

Это означает, что постфиксное выражение model->set располагается перед выражением estimate(model) в списке выражений. А поскольку model является нулевым указателем, предварительное условие std::shared_ptr::operator-> нарушается, и, следовательно, это приводит к UB.


Так что, если я правильно понимаю, поскольку model-> эквивалентно (*model)., это означает, что model все еще разыменован, и поэтому это все еще неопределенное поведение.

Val 09.05.2023 11:27

Не совсем понятно, как из этого утверждения следует УБ. Выражение постфикса перед стрелкой оценивается, что приводит к нулевому указателю. Вместе с id-выражением он определяет результат всего выражения. Хорошо, но разыменовывает ли он нулевой указатель, чтобы определить его?

mentalmushroom 09.05.2023 12:25

@mentalmushroom, если set равно virtual, указатель должен быть фактически разыменован, чтобы отправить вызов на основе динамического типа объекта. Этот случай не выделен в стандарте (хотя не-virtual функция может быть отправлена ​​только на основе статического типа model), поэтому общий случай заключается в том, что указатель должен быть разыменован.

Quentin 09.05.2023 15:45

@mentalmushroom Ваше использование std::shared_ptr делает это довольно четким, потому что перегрузка std::shared_ptr::operator-> имеет предварительное условие для спецификации библиотеки, что указатель не равен нулю. Таким образом, вызов этой функции уже имеет неопределенное поведение, независимо от оценки ->set результата. Другой вопрос, будет ли то же самое с необработанным нулевым указателем.

user17732522 09.05.2023 16:46

@user17732522 user17732522 Я добавил в свой ответ вторую часть, в которой используется expr.ref. В основном, насколько я понимаю [expr.ref]: «постфиксное выражение model перед оценкой стрелки, и результат этой оценки (то есть nullptr) вместе с id-выражением operator-> используется для определения результата всего постфиксного выражения ." Это приводит к UB, так как предусловие нарушено. Верен ли вывод и анализ с использованием [expr.ref]?

Jason 09.05.2023 17:05

@user17732522 user17732522 Это правда, но действительно ли это вызывается в данном случае? Правило гласит: «постфиксное выражение ПЕРЕД оценкой стрелки». Неясно, оценивается ли выражение id. Или слово «определяет» подразумевает, что оно оценивается?

mentalmushroom 09.05.2023 19:13

@mentalmushroom Постфиксное выражение в приведенной выше цитате применительно к вашему примеру будет постфиксным выражением выражения вызова функции (см. контекст цитаты), так что это все model->set. Согласно eel.is/c++draft/over.match.oper#2 часть model-> эквивалентна model.operator->(), которая уже представляет собой вызов operator->() как часть оценки model->set. Как связана фактическая функция-член set, не имеет значения.

user17732522 09.05.2023 19:29

@Jason То, что написано в [expr.compound], явно относится только к встроенным операторам, если не указано иное (eel.is/c++draft/expr#pre-3.sentence-1). Я думаю, вам следует цитировать [over.match.oper] вместо [expr.ref], если вы хотите описать поведение перегруженного ->.

user17732522 09.05.2023 19:35

@mentalmushroom Я имел в виду model.operator->()-> выше, а также я должен сослаться на eel.is/c++draft/over.match.oper#12.

user17732522 09.05.2023 19:39

@ user17732522 Я согласен с тем, что [expr.compound] предназначен для встроенных операторов, если не указано иное. Но [expr.pre] также говорит, что: «Перегруженные операторы подчиняются правилам синтаксиса и порядка вычисления, указанным в [expr.compound]». И поскольку [expr.ref # 2], который я цитировал во второй части своего ответа, говорит об оценке и поэтому должен применяться, не так ли?

Jason 10.05.2023 06:10

@ Джейсон Это записка. Нормативная часть находится в 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 10.05.2023 11:22

@ user17732522 Думаю, вы пропустили -> в конце своего последнего комментария. В частности, postfix-expression() должно было быть postfix-expression->() . По сути, поскольку model->set(estimate(model)) совпадает с model.operator->()->set(estimate(model)), поэтому, согласно [expr.compound], постфиксное выражение model.operator->()->set ставится перед выражением в списке выражений. Это приводит к UB из-за нарушения предусловия.

Jason 10.05.2023 13:45

@ Джейсон Я имел в виду «где постфиксное выражение (a).operator->», но я не думаю, что мы действительно в чем-то расходимся.

user17732522 10.05.2023 16:23

Насколько я понимаю, это поведение undefined по крайней мере из C++ 17:

  1. В выражении вызова функции выражение, именующее функцию, располагается перед каждым выражением аргумента и каждым аргументом по умолчанию.

Как я это интерпретирую, это на самом деле гарантирует, что model->set оценивается перед любым аргументом и, таким образом, вызывает неопределенное поведение. Неважно, является ли model необработанным указателем.

Но model->set называет функцию-член, а не функцию, поэтому это, похоже, не применимо. Название вопроса также явно предполагает, что OP интересуется случаем функции-члена и уже знает о случае функции, не являющейся членом.

Val 09.05.2023 11:32

@Val Значит, функция-член не является функцией? На чем вы основываете это утверждение?

nielsen 09.05.2023 11:36

Вызов функции и вызов функции-члена — это две разные вещи.

Val 09.05.2023 11:39

@Val Это cppreference, а не стандарт. Этот текст написан для удобочитаемости, а не для максимальной точности. Фактическое правило гласит, что постфиксное выражение упорядочено до инициализации аргумента. Здесь применимо правило, приведенное в cppreference.

j6t 09.05.2023 12:18

@Val Даже в Стандарте я считаю, что слово «функция» охватывает функции-члены, а также функции, не являющиеся членами, и, когда это применимо, делается явное различие. Вообще говоря, "вызов функции-члена" является частным случаем "вызова функции".

nielsen 09.05.2023 13:01

«модель-> набор оценивается»: это, очевидно, UB для случая std::shared_ptr, как объяснено в другом ответе, но неочевидно, является ли это UB для случая необработанного указателя. Как правило, разыменование самого нулевого указателя должно быть разрешено, и я не вижу ничего очевидного в eel.is/c++draft/expr.ref, что сделало бы его UB. Есть открытые вопросы CWG по точным ограничениям.

user17732522 09.05.2023 16:57

@user17732522 user17732522 «разыменование нулевого указателя» дает вам недопустимое lvalue. Если вы попытаетесь получить адрес, вы должны безопасно получить нулевой указатель. Почти все остальное (в частности, преобразование lvalue в rvalue, а также доступ к членам, назначение и т. д.) является UB.

Ben Voigt 09.05.2023 23:52

@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 09.05.2023 23:59

@user17732522: Ужасная формулировка в eel.is/c++draft/expr.prim.id.general#3.1 может быть частью проблемы. «выражение относится к классу члена» — бессмысленный мусор. Если бы он сказал либо «статический тип выражения», либо «динамический тип объекта, на который ссылается выражение», это было бы намного яснее.

Ben Voigt 10.05.2023 00:10

@user17732522 user17732522 Вы правы, в Стандарте об этом ничего не ясно. Это заслуживает (недавнего) вопроса само по себе, но вкратце: результат model->set, когда model является нулевым указателем, на самом деле не определен в стандарте и, следовательно, подпадает под действие eel.is/c++draft/defns.undefined . Другой способ увидеть это: если компилятор реализует произвольное поведение в этом случае, можно ли из-за этого сказать, что он не соответствует требованиям?

nielsen 10.05.2023 09:49

@nielsen Я согласен с выводом. Просто я думаю, что именно эта часть была неочевидной частью, в которой интересовался OP, даже если они допустили ошибку, также введя UB через нарушение предварительного условия std::shared_ptr::operator->.

user17732522 10.05.2023 11:10

[вызов]/7:

Постфиксное-выражение располагается перед каждым выражением в списке-выражений и любым аргументом по умолчанию.

В данном случае это означает, что model->set оценивается перед estimate(model).

Поскольку model является shared_ptr<Model>, model->set использует перегруженный shared_ptroperator->, который имеет следующее предварительное условие ([util.smartptr.shared.obs]/5):

Условия: get() != nullptr.

Нарушение этого предварительного условия приводит к неопределенному поведению ([structure.specifications]/3.3).

Другие вопросы по теме