Как устранить эту конструкцию в шаблоном конверсионном операторе?
После путаницы, почему мой код дал мне ошибку двусмысленности в GCC, но никаких ошибок в Clang, я упростил код. Это видно ниже.
struct Foo
{
// Foo(Foo&&) = delete;
// Foo(const Foo&) = delete;
Foo(int*) {}
};
struct Bar
{
template<typename T>
operator T()
{
return Foo{nullptr};
}
};
int main() { Foo f{Bar{}}; }
Ошибки заключаются в следующем.
main.cpp:17:18: error: call to constructor of 'Foo' is ambiguous
int main() { Foo f{Bar{}}; }
^~~~~~~~
main.cpp:1:8: note: candidate is the implicit move constructor
struct Foo
^
main.cpp:1:8: note: candidate is the implicit copy constructor
main.cpp:5:1: note: candidate constructor
Foo(int*) {}
^
На этот раз я не смог сделать это успешно для Clang, поэтому я полагаю, что это была ошибка Clang, и это предполагаемое поведение.
Когда я явно удаляю копии и перемещаю конструкторы (т.е. раскомментирую верхние две строки кода), я вместо этого получаю
note: candidate constructor has been explicitly deleted
но все еще ошибка. Тогда как я могу пойти по поводу рассогласования строительства здесь?
Обратите внимание, что я специально добавил Foo{nullptr}
вместо просто nullptr
, но нет никакой разницы. То же самое с маркировкой Foo
ctor. Эта ошибка неоднозначность возникает только тогда, когда Bar
оператор преобразования шаблонных.
Я могу добавить SFINAE в оператор преобразования, но я не уверен, что я исключу. Например, это заставило бы его работать:
template<typename T, std::enable_if_t<std::is_same<T, Foo>{}>* = nullptr>
Это еще один, который я нашел, и это может быть моим ответом:
template<typename T, std::enable_if_t<!std::is_same<T, int*>{}>* = nullptr>
Ответы
Ответ 1
Чтобы устранить двусмысленность, добавьте explicit
выражение в выражение оператора преобразования:
struct Bar
{
template<typename T>
explicit operator T()
{
return Foo{nullptr};
}
};
Почему это необходимо? Поскольку Foo
имеет конструктор, принимающий int*
, и поэтому operator int*()
экземпляр operator T()
шаблон рассматриваются как часть разрешения перегрузки для инициализации f
. См. Раздел [over.match.copy]:
1 [...] Предполагая, что " cv1 T
" - это тип инициализированного объекта, с типом типа T
, выбранные функции выбираются следующим образом:
-
(1.1) Преобразующие конструкторы T
являются функциями-кандидатами.
-
(1.2) Когда тип выражения инициализатора является типом класса " cv S
", рассматриваются неявные функции преобразования S
и его базовые классы. При инициализации временного объекта ([class.mem]) привязывается к первому параметру конструктора, где параметр имеет тип "ссылка на возможно сгенерированный T
" T
и конструктор вызывается с одним аргументом в контексте прямая инициализация объекта типа " cv2 T
", также рассматриваются явные функции преобразования.
Из (1.2) следует, что для инициализации рассматриваются только функции неявного преобразования, поэтому двусмысленность - поскольку компилятор не может решить между построением f
с использованием ссылки на Foo
или, как уже упоминалось, с использованием int*
(полученного путем copy-initializing) из возвращаемого значения operator int*
. Однако, когда выражение инициализатора является временным объектом, мы также рассматриваем явные преобразования - но только если они соответствуют конструктору, ссылающемуся на Foo
, наш "возможно, с квалификацией T
", то есть на наши конструкторы копирования и перемещения. Все это поведение согласуется с [class.conv.fct¶2]:
Функция преобразования может быть явной ([dcl.fct.spec]), и в этом случае она рассматривается только как пользовательское преобразование для прямой инициализации ([dcl.init]). В противном случае пользовательские преобразования не ограничены для использования в назначениях и инициализациях.
И так, говоря то же самое здесь в третий раз: если он не помечен как explicit
, ничего не мешает компилятору попытаться скопировать-инициализировать int*
который будет использоваться для построения.
Ответ 2
Мое лучшее предположение после некоторого копания: я получаю ту же ошибку со следующим кодом:
struct Foo { Foo(int*) {} };
struct Bar {
operator Foo(); // { return Foo{nullptr}; }
/* explicit */ operator int*();
};
int main() { Foo f{Bar{}}; }
И, когда я раскомментирую прокомментированный код, проблема исчезает. Мне кажется, что в оригинальной шаблонной версии OP, когда неявное преобразование требуется от Bar
to Foo
, GCC только "создает экземпляры" деклараций операторов преобразования, а затем решает перегрузку перед созданием экземпляров своих тел.
Что касается того, почему explicit
помогает, это происходит потому, что во втором случае требуется еще одно требуемое преобразование (Bar
→ int*
а затем int*
→ Foo
).