Странный случай разрешения перегрузки C 11

#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}) , так что он будет вызывать? На уровне здравого смысла нет ничего сложного в том, что он не должен пытаться вызвать удаленную функцию… но тогда компиляторы никогда не были сильны в здравом смысле, не так ли? = P

4. Я чувствую, что есть какая-то скобка / скобка, которая решила бы эту проблему.

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} the 1, 2 идеально подходит для списка аргументов, тогда как для построения C{1, 2} требуется неявное преобразование из the 1 в S объект? Я думал, что это S{1, 2} улучшит перегрузку.

3. @Chris: Деннис имеет в виду, что не шаблонный конструктор C (конструктор копирования) предпочтительнее шаблонного конструктора C.

4. @High: Честно говоря, я начинаю приходить к выводу, что ваш компилятор не смог даже рассмотреть конструктор копирования, потому что для этого потребовалось два определяемых пользователем преобразования в последовательности [из 1-> S и из {S, 2}-> C] . Кроме того, это не пример инициализации копирования, поэтому конструктор копирования не должен был рассматриваться в первую очередь.

5. Но, если бы это было допустимо, то для обоих потребовалось бы неявное преобразование. Либо из {S, 2}-> C, либо из {1, 2}-> S, что делает оба одинаково жизнеспособными. И поскольку одна из них не была шаблонной функцией, она была выбрана.