Разрешение перегрузки функции для параметров типа шаблона относительно ссылок и указателей

В качестве упражнения я работал над некоторым кодом.

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) (это работает, но я не знаю почему). Однако меня больше интересует понимание правил вывода типов для этого сценария?

T может быть указателем или любым другим составным типом, вы не можете перегружать его без ограничений или SFINAE.

Swift - Friday Pie 04.05.2024 03:00

ОТ: Здесь нет необходимости в динамическом распределении. Просто используйте оператор указателя &, чтобы получить указатель. Ставьте лайк &a, который будет иметь тип int*.

Some programmer dude 04.05.2024 03:00

@Swift-FridayPie Однако для константных указателей это будет соответствовать варианту указателя. Так что отборочные имеют значение, верно?

Mutating Algorithm 04.05.2024 03:08

Что именно ты имеешь ввиду? larger(&a, &b) приведет к тому же варианту вызова ссылки, обратите внимание: вы можете привязать неконстантную ссылку или указатель на неконстантную ссылку к константной ссылке или указатель на константу. Дело в том, что вы получаете const int *& larger(const int *& ,const int *&);

Swift - Friday Pie 04.05.2024 03:27

Коллекция ценностей имеет значение. Если вы имеете в виду, что если аргументы являются указателями на const (const int *a), то да, вторая версия предпочтительнее, потому что, как работает перегрузка, она будет считать ее вариантом с меньшим количеством преобразований. В исходном случае вызовы приводят к преобразованию lvalue int* в const int*& вместо const int*. Это не проблема шаблонов. Удалите код шаблона и создайте функцию, используя int вместо T. тот же результат.

Swift - Friday Pie 04.05.2024 03:39

На самом деле это вообще не проблема, а то, как обязывающий аргумент при копировании. Lvalue или xvalue типа T могут быть связаны с константой T& перед T. Xvalue предпочтет T&& перед этим. prvalue предпочел бы T

Swift - Friday Pie 04.05.2024 03:50
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
70
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Сначала посмотрите, что выводится, когда вы передаете указатель 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, а другой не использует что-то вроде стандартного общего типа).

Дело 1

Для первых вызовов larger(a,b) жизнеспособна только первая версия larger(const T& a, const T& b), поэтому выбирается/используется. То есть для первого вызова вторая версия большего размера (const T* a, const T* b) даже нежизнеспособна, поскольку она более специализирована для указателей, но аргументы a и b не являются указателями.

Случай 2

Для второго вызова 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>.
  • ...

Другие вопросы по теме