Извините за довольно неудачное название, но я не знаю, как кратко описать проблему.
Рассмотрим следующую настройку:
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; };
};
@Тед Люнгмо Спасибо :)
Я думаю, что clang здесь неправильный.
Инициализация списка поддерживает исключение скобок. Существует механизм, с помощью которого вы определяете, ссылается ли инициализатор на текущий элемент или на подэлемент этого элемента. Это правило [dcl.init.aggr]/14:
Говорят, что каждое предложение-инициализатора в списке инициализаторов, заключенном в фигурные скобки, принадлежит элементу инициализируемого агрегата или элементу одного из его подагрегатов. Учитывая последовательность предложений-инициализаторов и последовательность элементов агрегата, первоначально сформированную как последовательность элементов агрегата, инициализируемого и потенциально модифицируемого, как описано ниже, каждое предложение-инициализатор принадлежит соответствующему агрегатному элементу, если
- агрегатный элемент не является агрегатом, или
- предложение инициализатора начинается с левой скобки или
- предложение-инициализатора является выражением, и может быть сформирована неявная последовательность преобразования, которая преобразует выражение в тип агрегатного элемента, или
- агрегатный элемент — это агрегат, который сам по себе не имеет агрегатных элементов. В противном случае агрегатный элемент является агрегатом, и этот подагрегат заменяется в списке агрегатных элементов последовательностью своих собственных агрегатных элементов, и анализ принадлежности возобновляется с первым таким элементом и тем же предложением-инициализатором.
Поэтому, когда вы, например, делаете 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, удаление функции неявного преобразования не препятствует формированию последовательности неявного преобразования. И мне кажется, что это согласуется с использованием «последовательности неявного преобразования» при разрешении перегрузки.
Однако приведенная формулировка является достаточно новой. В старой формулировке считалось «Если выражение-присваивание может инициализировать элемент», что мне кажется гораздо менее ясным.
@Барри Я часто просматриваю новые вопросы SO по C++, потому что из них часто можно узнать что-то новое, и я заметил, что чаще всего вы первый, кто дает очень краткий и ясный ответ. Поэтому я просто хотел воспользоваться этой возможностью, чтобы поблагодарить вас за ваши постоянные усилия в этом сообществе! Я думаю, что из ваших ответов здесь на SO и сообщений в блоге я узнал больше о C++, чем из любого учебника. Поэтому я определенно учту ваше понимание данной формулировки.
Но я также должен согласиться с @user17732522 в том, что поведение MSVC и gcc кажется более совместимым с остальным языком и для меня более интуитивно понятным. Кстати, я надеялся, что clang здесь ошибся, поскольку я мог бы использовать его для определения того, является ли элемент массивом C, и использовать его для правильного подсчета членов агрегата с элементом массива C. (кому интересно: godbolt.org/z/h5hsezYso)
@ user17732522 Да, ты прав.
@Velocirobtor Спасибо, я очень ценю это!
ОТ: Я думаю, что название вопроса очень хорошо сжимает вопрос.