В качестве упражнения я работал над некоторым кодом.
template <typename T> const T& larger(const T& a, const T& b) {
std::cout << "Calling larger(const T&, const T&)" << std::endl;
return a > b ? a : b;
}
template <typename T> const T* larger(const T* a, const T* b) {
std::cout << "Calling larger(const T*, const T*)" << std::endl;
return *a > *b ? a : b;
}
int main() {
int a { 1 }, b { 2 };
std::cout << larger(a, b) << std::endl;
int *c { new int { 5 } }, *d { new int { 4 } };
std::cout << *larger<int>(c, d) << std::endl;
return 0;
}
Есть несколько вещей, которые я не понимаю. Прежде всего, почему компилятор разрешает оба вызова функции larger()
в main шаблону, который принимает ссылочные параметры. Разве во втором вызове более крупного типа выведенные типы не должны быть int*
и, следовательно, вместо этого вызывать функцию, которая принимает параметры указателя?
Я знаю, что могу «исправить» свою проблему, вызвав вторую функцию с явным параметром типа larger<int>(c, d)
(это работает, но я не знаю почему). Однако меня больше интересует понимание правил вывода типов для этого сценария?
ОТ: Здесь нет необходимости в динамическом распределении. Просто используйте оператор указателя &
, чтобы получить указатель. Ставьте лайк &a
, который будет иметь тип int*
.
@Swift-FridayPie Однако для константных указателей это будет соответствовать варианту указателя. Так что отборочные имеют значение, верно?
Что именно ты имеешь ввиду? larger(&a, &b)
приведет к тому же варианту вызова ссылки, обратите внимание: вы можете привязать неконстантную ссылку или указатель на неконстантную ссылку к константной ссылке или указатель на константу. Дело в том, что вы получаете const int *& larger(const int *& ,const int *&);
Коллекция ценностей имеет значение. Если вы имеете в виду, что если аргументы являются указателями на const (const int *a
), то да, вторая версия предпочтительнее, потому что, как работает перегрузка, она будет считать ее вариантом с меньшим количеством преобразований. В исходном случае вызовы приводят к преобразованию lvalue int*
в const int*&
вместо const int*
. Это не проблема шаблонов. Удалите код шаблона и создайте функцию, используя int
вместо T
. тот же результат.
На самом деле это вообще не проблема, а то, как обязывающий аргумент при копировании. Lvalue или xvalue типа T могут быть связаны с константой T& перед T. Xvalue предпочтет T&& перед этим. prvalue предпочел бы T
Сначала посмотрите, что выводится, когда вы передаете указатель lvalue в int.
Первым становится
template<T=int*>
int * const& larger(int* const&, int*const&);
второй становится
template<T=int>
int const* larger(int const*, int const*);
(Я использую стиль East-const единообразно, потому что он более понятен: часть типа, изменяемая с помощью const, находится слева от ключевого слова const.)
При выборе между const& и точным соответствием типа и неявным преобразованием в более константный указатель побеждает ссылка на const с точным соответствием типа.
Вы можете избежать этого несколькими способами. Вы можете заблокировать T как указатель в первой перегрузке шаблона, используя либо предложение require, либо sfinae. Кроме того, вы могли бы обеспечить, чтобы второе было строгим ограничением для первого.
Последняя неприятная проблема с вашим кодом заключается в том, что если вы передаете prvalues, вы получаете висячую ссылку.
Лично я бы разделил больше, чем сравниваю. Тогда делай
template<class T>
T larger(T&& a,T&& b){
if (compare(a,b))
return std::forward<T>(a);
else
return std::forward<T>(b);
}
теперь функция сравнения должна беспокоиться о сравнении указателей и не-указателей, а не о возврате висячих ссылок или нет.
(Более привлекательный вариант обрабатывает один из них как prvalue, а другой не использует что-то вроде стандартного общего типа).
Для первых вызовов larger(a,b)
жизнеспособна только первая версия larger(const T& a, const T& b)
, поэтому выбирается/используется. То есть для первого вызова вторая версия большего размера (const T* a, const T* b) даже нежизнеспособна, поскольку она более специализирована для указателей, но аргументы a
и b
не являются указателями.
Для второго вызова larger<int>(c, d)
жизнеспособна только вторая версия larger(const T* a, const T* b)
. То есть здесь первая версия большего размера(const T& a, const T& b) даже нежизнеспособна.
Прежде всего, почему компилятор разрешает оба вызова функции
larger()
вmain
шаблону, который принимает ссылочные параметры?
В larger(a, b)
очевидно: из данных const T*
нельзя вывести int
; они не являются указателями, поэтому жизнеспособна только первая перегрузка.
В реальном larger<int>(c, d)
первая перегрузка будет иметь const int&
параметры после замены int
, и вы не можете привязать const int&
к указателям, поэтому жизнеспособна только вторая перегрузка (с параметрами int*
).
В гипотетическом larger(c, d)
параметры в первой перегрузке равны int * const &
(ссылка на const
, указатель на int
), и этот тип совместим по ссылке с заданными int*
.
Таким образом, ссылка привязывается напрямую согласно [dcl.init.ref] p5.1.1
без каких-либо неявных преобразований.
С другой стороны, преобразование int*
в const int*
для второй перегрузки потребует преобразования квалификации , что усугубляет эту перегрузку.
При разрешении перегрузки побеждает перегрузка с лучшей последовательностью преобразования, и здесь Идентичность лучше, чем Квалификационная корректировка (см. [tab:over.ics.scs]).
Есть несколько способов обойти эту дилемму:
if constexpr (std::is_pointer_v<T>) ...
, который обрабатывает оба случая.requires !std::is_pointer_v<T>
.
T может быть указателем или любым другим составным типом, вы не можете перегружать его без ограничений или SFINAE.