Извините за довольно неудачное название, но я не знаю, как кратко описать проблему.
Рассмотрим следующую настройку:
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 Спасибо, я очень ценю это!
ОТ: Я думаю, что название вопроса очень хорошо сжимает вопрос.