Неожиданные результаты при выборе перегрузки оператора преобразования?

Этот вопрос основан на предыдущем обсуждении SO (на которое повлияли несоответствующие требованиям компиляторы). Поэтому я использую последние версии gcc/clang/MSVC, выпущенные на C++23.

Вот простой тест с перегрузкой оператора преобразования и передачей его варианту:

struct C
{
    template <typename T> operator T () {return 0.5;}
    operator int () {return 1;}
    operator std::string () { return "";}
};

int main ()
{
    C c;
    std::cout << std::variant<int, double, std::string>{c}.index() << "\n"; // selects template
    std::cout << std::variant<int, std::string, double>{c}.index() << "\n"; // selects template
    // demand to use string, MSVC fails
    std::cout << std::variant<int, std::string, double>{std::in_place_index<1>, c}.index() << "\n";
}

Я ожидал, что либо код потерпит неудачу из-за неоднозначности, либо выберите тип int, если variant выполняет вычет индекса в порядке объявления. Но он всегда выбирает оператор шаблона, независимо от порядка, в котором упоминаются типы variant. Правы ли компиляторы? Каково объяснение? Или, возможно, есть УБ. И это ошибка в том, что MSVC не может скомпилировать последнюю строку?

Демоверсия Code Explorer уже здесь

@user12002570 user12002570 - Для ясности: ни один из них не является дубликатом, и на самом деле первая ссылка находится прямо здесь, в этом вопросе, и, что самое худшее, все эти «дубликаты» неверны.

Gene 11.07.2024 08:56
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Вы ожидаете вызвать конструктор формы

template< class T >
constexpr variant( T&& t ) noexcept(/*...*/);

построить объекты std::variant. Но этого не происходит. Этот конструктор нежизнеспособен ни в одном из случаев, поскольку T будет выведено из C. Но конструктор исключается из разрешения перегрузки, если было бы неоднозначно, какой тип элемента должен быть создан из T, который сейчас является C. Именно эта двусмысленность и происходит в вашей конструкции: изображение перегруженной функции F с перегрузками.

void F(int);
void F(double);
void F(std::string);

вызов F(std::forward<T>(t)) там, где T находится C, был бы неправильным, поскольку разрешение перегрузки неоднозначно, поскольку все три кандидата жизнеспособны, но с разными вариантами функции преобразования в аргументе.

Вместо этого вы выбираете конструктор перемещения std::variant</*...*/>. Этот конструктор жизнеспособен, поскольку шаблонную функцию преобразования C можно использовать для преобразования c в std::variant</*...*/> путем приведения T к этому типу.

Затем внутри шаблонной функции преобразования вы копируете и инициализируете экземпляр std::variant</*...*/>, возвращаемый функцией преобразования из операнда оператора return. Тип операнда — double, поэтому будет создан тип элемента double варианта.


MSVC также правильно отклоняет последний случай: конструктор, который вы пытаетесь использовать, исключается из разрешения перегрузки, если только std::string не является конструируемым из c, т. е. если объявление формы

std::string _(c);

был бы хорошо сформирован.

Однако у std::string также есть несколько конструкторов, например:

basic_string( const basic_string& other );
basic_string( basic_string&& other ) noexcept;
explicit basic_string( const Allocator& alloc );
basic_string( const CharT* s, const Allocator& alloc = Allocator() );
basic_string( std::initializer_list<CharT> ilist,
          const Allocator& alloc = Allocator() );
basic_string( std::nullptr_t ) = delete;

Первые два вполне жизнеспособны с вашей функцией преобразования std::string. Другие жизнеспособны с вашей шаблонной функцией преобразования. Следовательно, опять же, у вас есть несколько перегрузок с последовательностями преобразования, которые считаются неотличимыми, и ничего больше в них не делает одного кандидата лучшим, чем другой. Разрешение перегрузки для конструкции будет неверным.

Также существует один конструктор для std::string формы

template< class StringViewLike >
explicit basic_string( const StringViewLike& t,
                       const Allocator& alloc = Allocator() );

Этот вариант был бы предпочтительнее других, если бы он не был исключен из разрешения перегрузки, поскольку StringViewLike можно вывести из C, так что последовательность преобразования будет последовательностью преобразования идентичности. Тогда вопрос в том, исключен ли конструктор из разрешения перегрузки. Он участвует в разрешении перегрузки, только если std::is_convertible_v<const StringViewLike&, std::basic_string_view<CharT, Traits>> истинно, а std::is_convertible_v<const StringViewLike&, const CharT*> ложно.

В вашем случае второе условие, безусловно, true, потому что шаблонный оператор преобразования (единственно) обеспечивает преобразование. Поэтому этот конструктор не участвует и не может сделать построение std::string однозначным.


GCC и Clang технически ошибаются, не диагностируя неправильно сформированный последний вызов как таковой, однако у них есть веская причина отличаться от требований стандарта.

В декларации формы

std::string _(c);

где c можно преобразовать в std::string с помощью функции преобразования, не должно быть никаких оснований рассматривать конструкторы std::string, которые, даже если конструктор перемещения был бы выбран однозначно, всегда вызывали бы избыточный ход. Вместо этого функция преобразования может напрямую инициализировать _ без участия конструктора перемещения.

В настоящее время это все еще открытый вопрос CWG 2327. GCC и Clang, похоже, реализуют решение этой проблемы, в котором функция преобразования предпочитается конструкторам.


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

вау, я многого не знал об этом варианте, спасибо

Gene 10.07.2024 23:35

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