Как устранить эту конструкцию в шаблоном конверсионном операторе?

После путаницы, почему мой код дал мне ошибку двусмысленности в 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 помогает, это происходит потому, что во втором случае требуется еще одно требуемое преобразование (Barint* а затем int*Foo).