Рассмотрим следующий код:
#include <vector>
#define BROKEN
class Var {
public:
#ifdef BROKEN
template <typename T>
Var(T x) : value(x) {}
#else
Var(int x) : value(x) {}
#endif
int value;
};
class Class {
public:
Class(std::vector<Var> arg) : v{arg} {}
std::vector<Var> v;
};
Clang++ (7.0.1) компилирует это без ошибок независимо от того, определен ли BROKEN, но g++ (8.2.1) выдает ошибку, если BROKEN определен:
main.cpp:9:20: error: cannot convert ‘std::vector<Var>’ to ‘int’ in initialization
Var(T x) : value(x) {}
^
Насколько мне известно, юниформ-инициализация, используемая здесь, должна выбирать конструктор std::vector(std::vector&&) в обоих случаях; по-видимому, однако, g++ вместо этого видит {arg} как список инициализаторов и пытается инициализировать первый элемент v с помощью Var, примененного к вектору, что не сработает.
Если BROKEN не определено, g++, по-видимому, достаточно умен, чтобы понять, что перегрузка initializer_list не будет работать.
Какое поведение является правильным или оба разрешены стандартом?
СЛОМАН определенный gcc
BROKEN не определен gcc
СЛОМАН определенный лязг
Еще одна точка данных: godbolt.org/z/gsc48J (вы можете поиграть с разными уровнями оптимизации). По-видимому, gcc вызывает Var::Var<std::vector<Var> >(std::vector<Var>) после вызова конструктора копирования std::vector.
По той же причине, что и эта почта.
@xskxzr Если я правильно понимаю, это означает, что clang считает ошибку типа в списке инициализации (: value(x)) означающей, что конструктор нежизнеспособен, в то время как gcc уже решил, что конструктор жизнеспособен, прежде чем смотреть на список инициализации. Учитывая, что список инициализации, как правило, может находиться в другой единице компиляции, поведение gcc кажется подходящим, что делает clang неверным. Это правда?
@tomsmeding: Clang не учитывает template <typename T> Var(T x), даже если вы удалите тело функции. Сообщение, на которое ссылается @xskxzr, не содержит никаких возможных ошибок типов внутри конструктора или списка инициализаторов.





Вот два способа решить эту проблему:
class Var {
public:
#ifdef BROKEN
template <typename T>
Var(T x, typename std::enable_if<std::is_convertible<T, int>::value>::type* = nullptr) : value(x) {}
#else
Var(int x) : value(x) {}
#endif
int value;
};
Или:
class Class {
public:
Class(std::vector<Var> arg) : v(arg) {}
std::vector<Var> v;
};
Теперь, по-видимому, в случае попытки gcc использовать параметр шаблона в качестве списка инициализации. Когда используются фигурные скобки и определен список инициализации, список инициализации имеет более высокий приоритет, чем конструктор копирования.
Таким образом, добавление правильного enable_if решает проблему, поскольку защищает от создания версии списка инициализации конструктора. В концепциях С++ 20 это лучше решается.
А когда вместо фигурных скобок для инициализации используются v список инициализации не предпочтителен.
Я не уверен, кто прав (ИМХО лязг, но это просто внутреннее чувство). Может кто лучше разбирается в стандарте подскажет.
Как template <typename T> Var(T x); будет аргументом в пользу SFINAE? Я не думаю, что тело функции (или список инициализаторов) имеет значение, тем более что оно может находиться в другом модуле компиляции.
Я не эксперт, но есть интересные данные: если вы пометите ctor
explicit, то g++ понравится больше.