Приоритет оператора неявного преобразования агрегатной инициализации

Извините за довольно неудачное название, но я не знаю, как кратко описать проблему.

Рассмотрим следующую настройку:

struct MyInt {
    template<class T>
    operator T() = delete;

    operator int(){ return 42; }
};

struct TestCArr {
    int arr[2];
};

struct TestStdArr {
    std::array<int, 2> arr;
};

MyInt здесь неявно конвертируется в int, но все остальные преобразования удаляются.

Следующее компилируется, как и ожидалось, со всеми основными компиляторами, использующими агрегатную инициализацию для инициализации arr[0] с помощью MyInt{} через operator int():

TestCArr{ MyInt{} };

Но каково ожидаемое поведение следующего утверждения?

TestStdArr{ MyInt{} };

Насколько я понимаю, это не должно компилироваться: У MyInt есть оператор преобразования в std::array<int, 2>, так что он подойдет лучше всего. Но этот оператор удален, поэтому я ожидаю ошибку во время компиляции. (Отличие от предыдущего состоит в том, что невозможно иметь оператор преобразования в int[2], поэтому используется преобразование в int.)

MSVC и gcc, похоже, со мной согласны, но clang компилирует оператор, выбирая оператор преобразования int, а агрегат инициализирует первый член arr (см. https://godbolt.org/z/dK4ronrGv).

Правильно ли я понимаю, что operator std::array<int, 2>() подходит лучше, чем operator int() и, следовательно, имеет более высокий приоритет, даже если он удален?

P.S. Если я удалю удаленный operator T() из MyInt, приведенный выше оператор скомпилируется всеми тремя основными компиляторами, каждый из которых выберет operator int() для выполнения агрегатной инициализации: https://godbolt.org/z/sYr7ff793

P.P.S Если я использую следующее определение MyInt, поведение всех компиляторов останется прежним (удаляя аргументы, касающиеся шаблонов и нешаблонов и более ограниченных и менее ограниченных): https://godbolt.org/z/cvhMj9xPq

struct MyInt {
    template<class T>
      requires (!std::integral<T>)
    operator T() = delete;


    template<class T>
      requires std::integral<T>
    operator T() { return 42; };
};

ОТ: Я думаю, что название вопроса очень хорошо сжимает вопрос.

Ted Lyngmo 06.09.2024 18:53

@Тед Люнгмо Спасибо :)

Velocirobtor 06.09.2024 22:38
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
2
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Я думаю, что clang здесь неправильный.

Инициализация списка поддерживает исключение скобок. Существует механизм, с помощью которого вы определяете, ссылается ли инициализатор на текущий элемент или на подэлемент этого элемента. Это правило [dcl.init.aggr]/14:

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

  1. агрегатный элемент не является агрегатом, или
  2. предложение инициализатора начинается с левой скобки или
  3. предложение-инициализатора является выражением, и может быть сформирована неявная последовательность преобразования, которая преобразует выражение в тип агрегатного элемента, или
  4. агрегатный элемент — это агрегат, который сам по себе не имеет агрегатных элементов. В противном случае агрегатный элемент является агрегатом, и этот подагрегат заменяется в списке агрегатных элементов последовательностью своих собственных агрегатных элементов, и анализ принадлежности возобновляется с первым таким элементом и тем же предложением-инициализатором.

Поэтому, когда вы, например, делаете TestCArr{ MyInt{} }, мы сначала пытаемся увидеть, принадлежит ли MyInt{} к int arr[2]. И это не так. (1) это агрегат, (2) наше предложение инициализатора не начинается с левой скобки, (3) предложение инициализатора является выражением, но мы не можем сформировать последовательность преобразования в int[2], и (4) int[2] не является т пусто. Итак, мы рекурсивно рассматриваем первый элемент int[2], который инициализируется.

Но когда вы это делаете TestStdArr{ MyInt{} }, логика уже не та. В (3) мы можем сформировать неявную последовательность преобразования в std::array<int, 2>. Эта функция преобразования удалена, но она по-прежнему считается неявной последовательностью преобразования (что важно, потому что в противном случае deleted не будет работать так, как задумано...). Таким образом, мы не обращаемся к подагрегатам std::array<int, 2>, а напрямую инициализируем std::array. Что должно потерпеть неудачу.

gcc и msvc понимают это правильно.

«Мы все еще не можем сформировать неявную последовательность преобразования в (3), потому что этот оператор преобразования удален».: Согласно примечанию в eel.is/c++draft/over.best.ics#general-2, удаление функции неявного преобразования не препятствует формированию последовательности неявного преобразования. И мне кажется, что это согласуется с использованием «последовательности неявного преобразования» при разрешении перегрузки.

user17732522 06.09.2024 19:11

Однако приведенная формулировка является достаточно новой. В старой формулировке считалось «Если выражение-присваивание может инициализировать элемент», что мне кажется гораздо менее ясным.

user17732522 06.09.2024 19:12

@Барри Я часто просматриваю новые вопросы SO по C++, потому что из них часто можно узнать что-то новое, и я заметил, что чаще всего вы первый, кто дает очень краткий и ясный ответ. Поэтому я просто хотел воспользоваться этой возможностью, чтобы поблагодарить вас за ваши постоянные усилия в этом сообществе! Я думаю, что из ваших ответов здесь на SO и сообщений в блоге я узнал больше о C++, чем из любого учебника. Поэтому я определенно учту ваше понимание данной формулировки.

Velocirobtor 06.09.2024 20:19

Но я также должен согласиться с @user17732522 в том, что поведение MSVC и gcc кажется более совместимым с остальным языком и для меня более интуитивно понятным. Кстати, я надеялся, что clang здесь ошибся, поскольку я мог бы использовать его для определения того, является ли элемент массивом C, и использовать его для правильного подсчета членов агрегата с элементом массива C. (кому интересно: godbolt.org/z/h5hsezYso)

Velocirobtor 06.09.2024 20:20

@ user17732522 Да, ты прав.

Barry 06.09.2024 21:09

@Velocirobtor Спасибо, я очень ценю это!

Barry 06.09.2024 21:17

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