Как это будет работать?
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.
И привязка к ссылке, и преобразование lvalue в rvalue имеют одинаковый ранг: timsong-cpp.github.io/cppwp/over.match.best#tab:over.ics.scs
@Abstraction: Я не понимаю. Покажите мне демо.
Здесь: godbolt.org/z/Wvf8Ka69M
foo(v) одинаково хорошо подходит для обеих перегрузок foo(), отсюда и неоднозначность. Проверка возможности передачи аргумента (например, foo(S) требует доступного конструктора копирования) происходит ПОСЛЕ того, как выбирается один «лучший» кандидат на перегрузку. MSVC, похоже, преждевременно игнорирует кандидатов. Одна из причин, по которой стандарт делает это таким образом, состоит в том, чтобы избежать обстоятельств, когда добавление перегрузки незаметно меняет функцию, вызываемую существующим кодом (в противном случае такие изменения могли бы быть неожиданными и вызвать ошибки, которые трудно обнаружить).
@NathanOliver: Где lvalue преобразуется в rvalue? Я не вижу здесь никаких значений. 1-й кандидат связывает регерентность, 2-й кандидат вызывает копировщика. (не перемещать актера), существовал бы он. Где значение?
При вызове передачи по значению (случай void foo(S);) на месте вызова foo(v); создается копия v. Эта копия представляет собой значение r, которое будет привязано к параметру void foo(S);, который в данном случае не имеет имени в объявлении).
@Eljay: Давайте приведем полный пример с именами. В void foo(S par); и вызове S s; foo(s);, par (lvalue) создается путем копирования из s (также lvalue). Где значение? Как вы думаете, s сначала копируется в prvalue, а затем par создается из него с помощью перемещения? Я думаю, этого не происходит. Это было бы слишком медленно и в любом случае ненужно.
Значение r находится в месте вызова и будет привязано к параметру void foo(S). В большинстве реализаций, когда вы вызываете функцию, аргументы помещаются в стек (или, возможно, в регистры, в зависимости от соглашения о вызове), а затем вызывается функция. Эти аргументы являются локальными параметрами функции. (Это не то, как это описывает стандарт C++, поскольку стандарт описывает абстрактную машину C++, которая не определяет детали реализации стека или кучи.) Значения на сайте вызова, которые помещаются в стек, не имеют имени; это ценности.
@Eljay: Я вижу только, что par — это lvalue, которое не «привязано к rvalue», а создано путем копирования из s. И оба являются значениями. Это рассуждение ошибочно?
Аргумент передается по значению на месте вызова. На месте вызова значение, передаваемое по значению, созданное путем копирования, не имеет имени, это rvalue. Место вызова не знает, что параметр будет назван или останется безымянным вызванной функцией.





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
@Артьер, верно, msvc ошибается, это тот случай, когда ошибка более очевидна.
@Caleth: Пожалуйста, исправьте пример, чтобы людям не приходилось читать комментарий Артьера (который затем можно было бы удалить). Спасибо.
@Dr.Gut, пример неправильный. Артьер указывает на другой случай.
Где должно происходить преобразование lvalue в rvalue?
@Caleth: Поскольку разрешение перегрузки находится на стадии анализа, пример должен повлиять на разрешение перегрузки. У тебя нет. Из комментария Артьера: при разрешении перегрузки игнорируется, определена ли функция как удаленная. Это имеет значение только в том случае, если разрешение перегрузки выбирает удаленную функцию.
@user17732522 foo(v), для foo(S)
@Dr.Gut Вы неправильно понимаете. Артьер говорит, что удаление функции не влияет на разрешение перегрузки, а не то, что удаленные функции игнорируются во время разрешения перегрузки.
@Caleth: foo(v) для foo(S) будет называться копирующим, а не движущимся. Никакие значения здесь не задействованы. Что ты имеешь в виду? || Пример должен влиять на разрешение перегрузки, поскольку речь идет о разрешении перегрузки.
@Dr.Gut Копирование с помощью конструктора копирования представляет собой преобразование lvalue в rvalue. Удаленная функция участвует в разрешении перегрузки. Шаблон ограниченной функции, ограничение которого не соблюдается, игнорируется при разрешении перегрузки.
@Caleth Но при разрешении перегрузки нет преобразования rvalue в lvalue, хотя эффект аналогичен. Как процитировал Артьер в своем ответе, преобразование считается преобразованием идентичности (и это независимо от cv-квалификаций, как поясняет стандартный абзац перед цитатой). И в самой инициализации также не будет инициализации lvalue-to-rvalue, потому что eel.is/c++draft/dcl.init#general-16.6.2 утверждает, что конструкторы рассматриваются сразу, а не такие применяется преобразование.
@user17732522 user17732522 Артьер ошибается, это преобразование lvalue в rvalue. S — это отдельный объект для v, это значение rvalue.
Если бы такое преобразование имело место, оно имело бы непредвиденные последствия, например. использование в константных выражениях, где подобные преобразования lvalue в rvalue могут быть запрещены. Как правило, за исключением некоторых объединений, преобразования lvalue в rvalue должны быть возможны только для скалярных типов.
@Caleth Так должно ли struct A {} a; constexpr A b = a; быть неправильно сформированным для использования преобразования lvalue в rvalue?
@ user17732522 нет. Это конкретное преобразование lvalue в rvalue неправильно оформлено, поскольку v является изменчивым. В общем, типы классов имеют преобразования lvalue в rvalue
Рассмотрим эту заметку [over.best.ics.general]p2:
[Примечание 1: [...]. Таким образом, хотя последовательность неявного преобразования может быть определена для данной пары аргумент-параметр, преобразование аргумента в параметр в конечном итоге все равно может оказаться некорректным. — последнее примечание]
Если параметр имеет тип класса и выражение аргумента имеет тот же тип, последовательность неявного преобразования представляет собой преобразование идентификаторов.
Мы знаем, что 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 и соответствующее преобразование недоступно?
@MarekR Он не может быть привязан, но это не означает, что кандидат нежизнеспособен при разрешении перегрузки. Тот факт, что он не может быть обязательным, станет актуальным только в том случае, если кандидат действительно будет выбран. См. также ссылки в моем предыдущем комментарии.
@user17732522 user17732522 Это не два преобразования личности, но они все равно одного ранга.
Не обращайте внимания на мои предыдущие удаленные комментарии. Я думаю, что вы правы.
Таблица категорий конверсий из C++23: Таблица 19.
Насколько я помню, не может быть двух перегрузок одной и той же функции, отличающихся только параметрами ссылки и значения. Это была одна из основных причин (не единственная) введения ссылок rvalue в C++. Следовательно, MSVC в этом случае кажется неправильным. Но поскольку volatile перегрузки случаются редко, это, должно быть, им не сшло с рук. Кто-то должен отправить отчет об ошибке в Microsoft.
Формулировка, которая сейчас есть в стандарте о последовательностях неявного преобразования, не очень хороша, и, вероятно, именно поэтому вы видите это расхождение в реализации.
Рассмотрим [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 В определенных ситуациях желательно, чтобы существовала последовательность неявного преобразования, даже если она будет неправильно сформирована для выполнения фактического преобразования. Очевидным примером из C++11 и более поздних версий является случай удаления конструктора или функции преобразования.
@Dr.Gut В контексте C++03 учтите тот факт, что volatile S настолько похож на S (так же, как const S очень похож на S), что есть некоторая логика в идее о том, что это всегда считается точным соответствовать. Что, если бы вторая перегрузка имела тип параметра int (и предположим, что существует функция преобразования из S в int)? В этом случае может быть очень неожиданно вызвать перегрузку int.
Аргумент об удаленных функциях недействителен. Мы рассматриваем (даже в настоящее время) удаленные определения после завершения разрешения перегрузки. Они обычно участвуют в разрешении перегрузки и могут победить. █ В вашем примере C++03, возможно, есть ошибка (вы хотели заменить 1-го кандидата, а не 2-го), но я понял. Демо. Я согласен, что в обоих случаях есть сюрприз, но все же утверждаю, что путь MSVC легче понять, предсказать или обучить.
@Dr.Gut Если удаленная функция необходима для формирования неявной последовательности преобразования во время разрешения перегрузки, перегрузка, для которой требуется удаленная функция, все равно рассматривается, и общий результат может оказаться неоднозначным.
Когда вы говорите о доказательстве того, что вызов не является двусмысленным, вы имеете в виду эксперимент с закомментированием одной перегрузки? Обратите внимание, что запись
constвместо этого и удаление конструктора копированияSприводит к тому же результату.