Этот вопрос основан на предыдущем обсуждении 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 не может скомпилировать последнюю строку?
Вы ожидаете вызвать конструктор формы
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, похоже, реализуют решение этой проблемы, в котором функция преобразования предпочитается конструкторам.
Я думаю, что вывод из этого заключается в том, чтобы по возможности избегать функций преобразования, особенно шаблонных.
вау, я многого не знал об этом варианте, спасибо
@user12002570 user12002570 - Для ясности: ни один из них не является дубликатом, и на самом деле первая ссылка находится прямо здесь, в этом вопросе, и, что самое худшее, все эти «дубликаты» неверны.