Перегрузка и нестабильность

Как это будет работать?

struct S {};

void foo(volatile S&);
void foo(S);

int main() {
    volatile S v;
    foo(v);
}

Компиляторы расходятся во мнениях: MSVC принимает код, а Clang и GCC говорят, что вызов неоднозначен (демо).

Мы можем доказать, что только 1-й кандидат может принять v, если закомментируем одного из кандидатов (и в этом случае составители соглашаются):

  • Если мы оставим только первый, код компилируется.
  • Если оставить только второй, то получим ошибку (у S нет копии из volatile S&).

Обновлено: Поскольку вопрос был опубликован, люди придумали более простой пример без volatile.

Когда вы говорите о доказательстве того, что вызов не является двусмысленным, вы имеете в виду эксперимент с закомментированием одной перегрузки? Обратите внимание, что запись const вместо этого и удаление конструктора копирования S приводит к тому же результату.

Abstraction 20.05.2024 15:14

И привязка к ссылке, и преобразование lvalue в rvalue имеют одинаковый ранг: timsong-cpp.github.io/cppwp/over.match.best#tab:over.ics.scs

NathanOliver 20.05.2024 15:20

@Abstraction: Я не понимаю. Покажите мне демо.

Dr. Gut 20.05.2024 15:26

Здесь: godbolt.org/z/Wvf8Ka69M

Abstraction 20.05.2024 15:29
foo(v) одинаково хорошо подходит для обеих перегрузок foo(), отсюда и неоднозначность. Проверка возможности передачи аргумента (например, foo(S) требует доступного конструктора копирования) происходит ПОСЛЕ того, как выбирается один «лучший» кандидат на перегрузку. MSVC, похоже, преждевременно игнорирует кандидатов. Одна из причин, по которой стандарт делает это таким образом, состоит в том, чтобы избежать обстоятельств, когда добавление перегрузки незаметно меняет функцию, вызываемую существующим кодом (в противном случае такие изменения могли бы быть неожиданными и вызвать ошибки, которые трудно обнаружить).
Peter 20.05.2024 15:46

@NathanOliver: Где lvalue преобразуется в rvalue? Я не вижу здесь никаких значений. 1-й кандидат связывает регерентность, 2-й кандидат вызывает копировщика. (не перемещать актера), существовал бы он. Где значение?

Dr. Gut 20.05.2024 15:55

При вызове передачи по значению (случай void foo(S);) на месте вызова foo(v); создается копия v. Эта копия представляет собой значение r, которое будет привязано к параметру void foo(S);, который в данном случае не имеет имени в объявлении).

Eljay 20.05.2024 16:50

@Eljay: Давайте приведем полный пример с именами. В void foo(S par); и вызове S s; foo(s);, par (lvalue) создается путем копирования из s (также lvalue). Где значение? Как вы думаете, s сначала копируется в prvalue, а затем par создается из него с помощью перемещения? Я думаю, этого не происходит. Это было бы слишком медленно и в любом случае ненужно.

Dr. Gut 20.05.2024 18:59

Значение r находится в месте вызова и будет привязано к параметру void foo(S). В большинстве реализаций, когда вы вызываете функцию, аргументы помещаются в стек (или, возможно, в регистры, в зависимости от соглашения о вызове), а затем вызывается функция. Эти аргументы являются локальными параметрами функции. (Это не то, как это описывает стандарт C++, поскольку стандарт описывает абстрактную машину C++, которая не определяет детали реализации стека или кучи.) Значения на сайте вызова, которые помещаются в стек, не имеют имени; это ценности.

Eljay 20.05.2024 19:17

@Eljay: Я вижу только, что par — это lvalue, которое не «привязано к rvalue», а создано путем копирования из s. И оба являются значениями. Это рассуждение ошибочно?

Dr. Gut 20.05.2024 19:36

Аргумент передается по значению на месте вызова. На месте вызова значение, передаваемое по значению, созданное путем копирования, не имеет имени, это rvalue. Место вызова не знает, что параметр будет назван или останется безымянным вызванной функцией.

Eljay 20.05.2024 20:37
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
11
234
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Clang и GCC правы, это неоднозначный вызов.

Обе перегрузки f являются подходящими функциями для этого вызова, поскольку одна не требует преобразования, а другая представляет собой преобразование lvalue в rvalue. Ни то, ни другое преобразование не является лучшим, поскольку lvalue-to-rvalue имеет тот же ранг, что и идентификатор.

Отсутствие конструктора копирования, который принимает volatile S &, не имеет значения для перегрузки разрешения там, где есть точное совпадение по типу.

Вы можете увидеть аналогичную двусмысленность с конструктором удаленной копии, однако на этот раз MSVC согласен с GCC и Clang:

struct S {
    S() {}
    S(const S&) = delete;
};

Этот случай показывает, почему правила разрешения перегрузки не требуют, чтобы преобразование было правильно сформированным. Когда вы = delete перегружаете функцию, вы хотите, чтобы она была выбрана, а затем оказалась неправильно сформированной.

Это не лучший пример, поскольку определение функции как удаленной игнорируется при разрешении перегрузки (у S есть конструктор с сигнатурой S(const S&), он просто удаляется). Если вы полностью удалите конструктор S(const S&), MSVC все равно ошибочно примет его: godbolt.org/z/Wede9xP1o

Artyer 20.05.2024 15:34

@Артьер, верно, msvc ошибается, это тот случай, когда ошибка более очевидна.

Caleth 20.05.2024 15:38

@Caleth: Пожалуйста, исправьте пример, чтобы людям не приходилось читать комментарий Артьера (который затем можно было бы удалить). Спасибо.

Dr. Gut 20.05.2024 16:15

@Dr.Gut, пример неправильный. Артьер указывает на другой случай.

Caleth 20.05.2024 16:57

Где должно происходить преобразование lvalue в rvalue?

user17732522 20.05.2024 17:28

@Caleth: Поскольку разрешение перегрузки находится на стадии анализа, пример должен повлиять на разрешение перегрузки. У тебя нет. Из комментария Артьера: при разрешении перегрузки игнорируется, определена ли функция как удаленная. Это имеет значение только в том случае, если разрешение перегрузки выбирает удаленную функцию.

Dr. Gut 20.05.2024 17:29

@user17732522 foo(v), для foo(S)

Caleth 20.05.2024 17:30

@Dr.Gut Вы неправильно понимаете. Артьер говорит, что удаление функции не влияет на разрешение перегрузки, а не то, что удаленные функции игнорируются во время разрешения перегрузки.

Caleth 20.05.2024 17:31

@Caleth: foo(v) для foo(S) будет называться копирующим, а не движущимся. Никакие значения здесь не задействованы. Что ты имеешь в виду? || Пример должен влиять на разрешение перегрузки, поскольку речь идет о разрешении перегрузки.

Dr. Gut 20.05.2024 17:35

@Dr.Gut Копирование с помощью конструктора копирования представляет собой преобразование lvalue в rvalue. Удаленная функция участвует в разрешении перегрузки. Шаблон ограниченной функции, ограничение которого не соблюдается, игнорируется при разрешении перегрузки.

Caleth 20.05.2024 17:37

@Caleth Но при разрешении перегрузки нет преобразования rvalue в lvalue, хотя эффект аналогичен. Как процитировал Артьер в своем ответе, преобразование считается преобразованием идентичности (и это независимо от cv-квалификаций, как поясняет стандартный абзац перед цитатой). И в самой инициализации также не будет инициализации lvalue-to-rvalue, потому что eel.is/c++draft/dcl.init#general-16.6.2 утверждает, что конструкторы рассматриваются сразу, а не такие применяется преобразование.

user17732522 20.05.2024 17:37

@user17732522 user17732522 Артьер ошибается, это преобразование lvalue в rvalue. S — это отдельный объект для v, это значение rvalue.

Caleth 20.05.2024 17:38

Если бы такое преобразование имело место, оно имело бы непредвиденные последствия, например. использование в константных выражениях, где подобные преобразования lvalue в rvalue могут быть запрещены. Как правило, за исключением некоторых объединений, преобразования lvalue в rvalue должны быть возможны только для скалярных типов.

user17732522 20.05.2024 17:38

@Caleth Так должно ли struct A {} a; constexpr A b = a; быть неправильно сформированным для использования преобразования lvalue в rvalue?

user17732522 20.05.2024 17:39

@ user17732522 нет. Это конкретное преобразование lvalue в rvalue неправильно оформлено, поскольку v является изменчивым. В общем, типы классов имеют преобразования lvalue в rvalue

Caleth 20.05.2024 17:40

Рассмотрим эту заметку [over.best.ics.general]p2:

[Примечание 1: [...]. Таким образом, хотя последовательность неявного преобразования может быть определена для данной пары аргумент-параметр, преобразование аргумента в параметр в конечном итоге все равно может оказаться некорректным. — последнее примечание]

Из [over.best.ics.general]p7:

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

Мы знаем, что ICS для первого аргумента второй перегрузки (void foo(S);) — это преобразование идентификаторов (хотя смоделированная инициализация копирования невозможна).

volatileS lvalue -> volatile S& для первого аргумента другой перегрузки также является тождественным преобразованием (записанным в [over.ics.ref]p(1.2)).

Поскольку обе ICS идентичны, обе перегрузки жизнеспособны, но ни одна из них не является лучшей, что делает ее неоднозначной.

Как это связано с: eel.is/c++draft/class.copy.ctor#footnote-91This implies that the reference parameter of the implicitly-declared copy constructor cannot bind to a volatile? Не означает ли это, что S нельзя составить из volatile S и соответствующее преобразование недоступно?

Marek R 20.05.2024 15:47

@MarekR Он не может быть привязан, но это не означает, что кандидат нежизнеспособен при разрешении перегрузки. Тот факт, что он не может быть обязательным, станет актуальным только в том случае, если кандидат действительно будет выбран. См. также ссылки в моем предыдущем комментарии.

user17732522 20.05.2024 16:51

@user17732522 user17732522 Это не два преобразования личности, но они все равно одного ранга.

Caleth 20.05.2024 16:58

Не обращайте внимания на мои предыдущие удаленные комментарии. Я думаю, что вы правы.

user17732522 20.05.2024 17:22

Таблица категорий конверсий из C++23: Таблица 19.

Dr. Gut 20.05.2024 19:55

Насколько я помню, не может быть двух перегрузок одной и той же функции, отличающихся только параметрами ссылки и значения. Это была одна из основных причин (не единственная) введения ссылок rvalue в C++. Следовательно, MSVC в этом случае кажется неправильным. Но поскольку volatile перегрузки случаются редко, это, должно быть, им не сшло с рук. Кто-то должен отправить отчет об ошибке в Microsoft.

Red.Wave 20.05.2024 20:36
Ответ принят как подходящий

Формулировка, которая сейчас есть в стандарте о последовательностях неявного преобразования, не очень хороша, и, вероятно, именно поэтому вы видите это расхождение в реализации.

Рассмотрим [over.best.ics.general] пункт 1 и первую половину пункта 6:

Последовательность неявного преобразования — это последовательность преобразований, используемая для преобразования аргумента при вызове функции в тип соответствующего параметра вызываемой функции. Последовательность преобразований представляет собой неявное преобразование, определенное в [conv], что означает, что она регулируется правилами инициализации объекта или ссылки с помощью одного выражения ([dcl.init], [dcl.init.ref]).

[...]

Если тип параметра не является ссылкой, последовательность неявного преобразования моделирует инициализацию копирования параметра из выражения аргумента. Последовательность неявного преобразования необходима для преобразования выражения аргумента в prvalue типа параметра.

Это говорит о том, что последовательность неявного преобразования из v (lvalue типа volatile S) в тип параметра S определяется правилами копирования-инициализации объекта S из lvalue volatile S. Это регулируется [dcl.init.general]/16.6.2, поскольку неквалифицированная cv версия типа инициализатора совпадает с типом класса инициализируемого объекта. Поскольку разрешение перегрузки не удается (у S нет летательного конструктора копирования), применяется подпункт 3, и инициализация имеет неверный формат.

Однако когда в [dcl.init] говорится, что инициализация неправильно сформирована, и это гипотетическая инициализация с целью формирования последовательности неявного преобразования, иногда это означает, что неявной последовательности преобразования не существует, а иногда это означает неявную последовательность преобразований. Последовательность преобразования существует, но на самом деле ее использование в вызове было бы неправильно, и вам просто нужно знать, что она означает. (См. CWG2525.)

Это важно, потому что если нет неявной последовательности преобразования, то должна быть выбрана другая перегрузка (та, у которой тип параметра volatile S&), но если существует неявная последовательность преобразования, которая будет неправильно сформирована, если ее использовать для вызова, тогда разрешение перегрузки неоднозначно.

Пример во второй половине [over.best.ics.general] параграфа 6 слабо намекает на то, что последняя интерпретация верна:

Параметр типа A может быть инициализирован аргументом типа const A. Последовательность неявного преобразования в этом случае — это идентификационная последовательность; он не содержит «преобразования» из const A в A.

A теоретически может быть типом класса, конструктор копирования которого имеет форму A(A&) (а не A(const A&)), однако в примере утверждается, что последовательность неявного преобразования всегда является тождественным преобразованием.

Пункт 7 тоже является слабым намеком:

Если параметр имеет тип класса и выражение аргумента имеет тот же тип, последовательность неявного преобразования представляет собой преобразование идентификаторов. Если параметр имеет тип класса, а выражение аргумента имеет тип производного класса, неявная последовательность преобразования представляет собой преобразование производного класса в базовый из производного класса в базовый класс. Преобразование производного в базовое имеет рейтинг преобразования ([over.ics.scs]).

Первое предложение неприменимо, поскольку тип параметра S не совпадает с типом аргумента volatile S. Но второе предложение намекает, что даже если, например, инициализация копирования типа параметра из типа аргумента не будет простым вызовом конструктора копирования типа параметра (но на самом деле может включать в себя другие конкурирующие конструкторы и может быть неоднозначной), она не влияет на формирование последовательности неявного преобразования . Таким образом, мы вынуждены думать, что формирование последовательности неявного преобразования в Base из cv Derived всегда завершается успешно. Если это так, то это должно быть еще более справедливо для преобразования в S из cv S (и, возможно, первое предложение следует изменить, включив в него регистр с указанием cv).

Итак, я думаю, что Clang и GCC делают то, что означает стандарт, но, честно говоря, это не так ясно.

Смысл этого мне неясен. Зачем создавать язык таким образом? Я бы предпочел путь MSVC и предпочел бы исправить стандарт Clang и GCC, чтобы он соответствовал MSVC.

Dr. Gut 21.05.2024 19:01

@Dr.Gut В определенных ситуациях желательно, чтобы существовала последовательность неявного преобразования, даже если она будет неправильно сформирована для выполнения фактического преобразования. Очевидным примером из C++11 и более поздних версий является случай удаления конструктора или функции преобразования.

Brian Bi 21.05.2024 22:40

@Dr.Gut В контексте C++03 учтите тот факт, что volatile S настолько похож на S (так же, как const S очень похож на S), что есть некоторая логика в идее о том, что это всегда считается точным соответствовать. Что, если бы вторая перегрузка имела тип параметра int (и предположим, что существует функция преобразования из S в int)? В этом случае может быть очень неожиданно вызвать перегрузку int.

Brian Bi 21.05.2024 22:41

Аргумент об удаленных функциях недействителен. Мы рассматриваем (даже в настоящее время) удаленные определения после завершения разрешения перегрузки. Они обычно участвуют в разрешении перегрузки и могут победить. █ В вашем примере C++03, возможно, есть ошибка (вы хотели заменить 1-го кандидата, а не 2-го), но я понял. Демо. Я согласен, что в обоих случаях есть сюрприз, но все же утверждаю, что путь MSVC легче понять, предсказать или обучить.

Dr. Gut 23.05.2024 02:23

@Dr.Gut Если удаленная функция необходима для формирования неявной последовательности преобразования во время разрешения перегрузки, перегрузка, для которой требуется удаленная функция, все равно рассматривается, и общий результат может оказаться неоднозначным.

Brian Bi 23.05.2024 14:52

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

До C++11 «правило одного определения» нарушалось при инициализации членов класса нестатических и неконстантных переменных. Почему?
Что произойдет, если я вызову allocate_at_least(0) согласно стандарту C++23?
Почему изменяемая лямбда преобразуется в указатель на функцию вместо вызова оператора()?
Могут ли `::` и `*`, образующие тип указателя-члена, происходить из разных расширений макроса или они должны появляться как один токен?
Определено ли вычисление смещений указателей байтов между элементами композиции, не являющимися массивами?
Как проверить, является ли конструктор явно дефолтным
Требуется ли создание экземпляра для неиспользуемого, но инициализированного статического элемента данных const int шаблона класса?
Когда типы замыканий наконец стали структурными типами?
Почему существует разница между оператором и соответствующей функцией-членом?
Каковы фактические правила для ожидания Final_suspend в сопрограммах C++?