#c #c 11 #overload-resolution
#c #c 11 #перегрузка-разрешение
Вопрос:
Сегодня я столкнулся с довольно странным случаем разрешения перегрузки. Я сократил его до следующего:
struct S
{
S(int, int = 0);
};
class C
{
public:
template <typename... Args>
C(S, Args... args);
C(const Camp;) = delete;
};
int main()
{
C c({1, 2});
}
Я полностью ожидал C c({1, 2})
, что он будет соответствовать первому конструктору C
с числом переменных аргументов, равным нулю, и {1, 2}
будет рассматриваться как построение списка инициализаторов S
объекта.
Однако я получаю ошибку компилятора, которая указывает, что вместо этого она соответствует удаленному конструктору копирования C!
test.cpp: In function 'int main()':
test.cpp:17:15: error: use of deleted function 'C(const C amp;)'
test.cpp:12:5: error: declared here
Я могу понять, как это может работать — {1, 2}
может быть истолковано как допустимый инициализатор для C, при 1
этом инициализатор является инициализатором для S
(который неявно конструируется из int, поскольку второй аргумент его конструктора имеет значение по умолчанию), а параметр 2
является переменным аргументом… но я не понимаю, почему это было бы лучшим совпадением, особенно учитывая, что рассматриваемый конструктор копирования удален.
Не мог бы кто-нибудь, пожалуйста, объяснить правила разрешения перегрузки, которые здесь действуют, и сказать, существует ли обходной путь, который не предполагает упоминания имени S в вызове конструктора?
РЕДАКТИРОВАТЬ: поскольку кто-то упомянул, что фрагмент компилируется с другим компилятором, я должен уточнить, что я получил вышеуказанную ошибку с помощью GCC 4.6.1.
РЕДАКТИРОВАТЬ 2: я еще больше упростил фрагмент, чтобы получить еще более тревожный сбой:
struct S
{
S(int, int = 0);
};
struct C
{
C(S);
};
int main()
{
C c({1});
}
Ошибки:
test.cpp: In function 'int main()':
test.cpp:13:12: error: call of overloaded 'C(<brace-enclosed initializer list>)' is ambiguous
test.cpp:13:12: note: candidates are:
test.cpp:8:5: note: C::C(S)
test.cpp:6:8: note: constexpr C::C(const Camp;)
test.cpp:6:8: note: constexpr C::C(Camp;amp;)
И на этот раз GCC 4.5.1 также выдает ту же ошибку (за вычетом constexpr
s и конструктора перемещения, которые он не генерирует неявно).
Мне очень трудно поверить, что это то, что задумали разработчики языка…
Комментарии:
1. Компилятор не рассматривает вопрос «могу ли я вызвать это?» до тех пор, пока он не решит, «что я должен вызвать». Таким образом, удаляемый конструктор копирования, к сожалению, не имеет никакого отношения к этому вопросу..
2. @Dennis: разве SFINAE не является контрпримером к этому?
3. @bdonlan: подумайте об этом так: Если бы я написал
C c(C{1, 2})
, у него не было бы выбора, кроме как вызвать конструктор копирования. Если бы я написалC c(S{1, 2})
, у него не было бы выбора, кроме как вызвать первый конструктор. Но я написалC c({1, 2})
, так что он будет вызывать? На уровне здравого смысла нет ничего сложного в том, что он не должен пытаться вызвать удаленную функцию… но тогда компиляторы никогда не были сильны в здравом смысле, не так ли? = P4. Я чувствую, что есть какая-то скобка / скобка, которая решила бы эту проблему.
5. @HighCommander4: вызов и создание экземпляра шаблона — это разные вещи — в этом случае создание экземпляра шаблона даже не предпринимается
Ответ №1:
Для C c({1, 2});
вас есть два конструктора, которые можно использовать. Итак, происходит разрешение перегрузки и выясняется, какую функцию использовать
C(S, Args...)
C(const Camp;)
Args
будет выведено к нулю, как вы выяснили. Таким образом, компилятор сравнивает построение S
с построением C
временного out of {1, 2}
. Построение S
из {1, 2}
является прямым и использует ваш объявленный конструктор S
. Построение C
из {1, 2}
также является прямым и использует ваш шаблон конструктора (конструктор копирования нежизнеспособен, потому что он имеет только один параметр, но передаются два аргумента — 1
и 2
— are). Эти две последовательности преобразования несопоставимы. Таким образом, два конструктора были бы неоднозначными, если бы не тот факт, что первый является шаблоном. Таким образом, GCC предпочтет не-шаблон, выбрав конструктор удаленной копии и выдаст вам диагностику.
Теперь для вашего C c({1});
testcase можно использовать три конструктора
C(S)
C(C constamp;)
C(C amp;amp;)
Для двух последних компилятор предпочтет третий, потому что он связывает rvalue с rvalue . Но если вы считаете C(S)
против, C(Camp;amp;)
вы не найдете победителя между двумя типами параметров, потому что for C(S)
вы можете создать S
из a, {1}
а for C(Camp;amp;)
вы можете инициализировать C
временный из a {1}
, используя C(S)
конструктор (стандарт явно запрещает определяемые пользователем преобразования для параметра конструктора перемещения или копирования, которые можно использовать для инициализацииобъекта класса C
from {...}
, поскольку это может привести к нежелательным неоднозначностям; вот почему преобразование 1
в Camp;amp;
здесь не рассматривается, а только преобразование из 1
в S
). Но на этот раз, в отличие от вашего первого тестового примера, ни один из конструкторов не является шаблоном, поэтому в итоге возникает двусмысленность.
Это полностью то, как все должно работать. Инициализация в C странная, поэтому, к сожалению, получить все «интуитивно понятное» для всех будет невозможно. Даже простой пример, приведенный выше, быстро усложняется. Когда я написал этот ответ и через час случайно просмотрел его снова, я заметил, что что-то упустил, и мне пришлось исправить ответ.
Комментарии:
1. Я не следую этой части: «Стандарт явно запрещает определенные пользователем преобразования для параметра конструктора перемещения или копирования, которые можно использовать для инициализации объекта класса C из {…}, поскольку это может привести к нежелательным неоднозначностям» — разве это не означает, что компилятор не должен рассматриватьскопировать конструктор в качестве кандидата, потому что для этого требуется определяемое пользователем преобразование?
2. @High пожалуйста, проверьте еще раз. У меня была мысль выше.
3. @High нет, потому что вы не инициализируете объект класса C. С помощью
{ ... }
. Вы инициализируете его с помощью( ... )
. Если вы сделаетеC c{1};
то, что я сказал, применимо. Но вы заключили скобки в скобки, что приводит к тому, что это больше не инициализация списка, а обычный вызов набора функций во время прямой инициализации (в данном случае конструкторов).4. Вот часть, с которой я не согласен: «Построение C из {1, 2} также является прямым и использует ваш шаблон конструктора» — как это так же просто, как S {1, 2}, когда C {1, 2} требует дополнительного преобразования из 1 вS?
5. Рассмотрим
struct B; struct A { operator int(); operator B(); }; struct B { B(int); }; A a; B b{a};
. Объявлениеb
правильно сформировано,B(Bamp;amp;)
конструктор не используется, поскольку для этого потребуется определяемое пользователем преобразование изA
вB
. Если вы скажетеB b(a);
илиB b({a});
код станет неоднозначным.
Ответ №2:
Возможно, вы правы в своей интерпретации того, почему он может создать a C
из этого списка инициализаторов. ideone с радостью компилирует ваш пример кода, и оба компилятора не могут быть корректными. Однако предполагается, что создание копии допустимо…
Итак, с точки зрения компилятора, у него есть два варианта: создать новый S{1,2}
и использовать шаблонный конструктор или создать новый C{1,2}
и использовать конструктор копирования. Как правило, не шаблонные функции предпочтительнее шаблонных, поэтому выбирается конструктор копирования. Затем он проверяет, можно ли вызвать функцию … она не может, поэтому выдает ошибку.
Для SFINAE требуется ошибка другого типа… они возникают на первом шаге, когда проверяется, какие функции являются возможными совпадениями. Если простое создание функции приводит к ошибке, эта ошибка игнорируется, и функция не рассматривается как возможная перегрузка. После перечисления возможных перегрузок подавление ошибок отключается, и вы зависаете с тем, что получаете.
Комментарии:
1. Это шаблонный конструктор C. Почему он предпочитает шаблонную функцию над не шаблонной?
2. Как насчет того факта, что при построении
S{1, 2}
the1, 2
идеально подходит для списка аргументов, тогда как для построенияC{1, 2}
требуется неявное преобразование из the1
вS
объект? Я думал, что этоS{1, 2}
улучшит перегрузку.3. @Chris: Деннис имеет в виду, что не шаблонный конструктор C (конструктор копирования) предпочтительнее шаблонного конструктора C.
4. @High: Честно говоря, я начинаю приходить к выводу, что ваш компилятор не смог даже рассмотреть конструктор копирования, потому что для этого потребовалось два определяемых пользователем преобразования в последовательности [из 1-> S и из {S, 2}-> C] . Кроме того, это не пример инициализации копирования, поэтому конструктор копирования не должен был рассматриваться в первую очередь.
5. Но, если бы это было допустимо, то для обоих потребовалось бы неявное преобразование. Либо из {S, 2}-> C, либо из {1, 2}-> S, что делает оба одинаково жизнеспособными. И поскольку одна из них не была шаблонной функцией, она была выбрана.