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

Если у нас есть функция шаблона, которая принимает непиковый параметр типа int или short, компилятор жалуется на неоднозначность следующего вызова:

// Definition
template <int I>   void foo() { std::cout << "I: " << I << '\n'; }
template <short S> void foo() { std::cout << "S: " << S << '\n'; }

// Usage
foo<0>(); // Ambiguous, int or short?

Сначала я не был удивлен этим поведением, буквальный 0 мог быть int или short, но если мы попробуем это:

// Definition
void foo(int i)   { std::cout << "i: " << i << '\n'; }
void foo(short s) { std::cout << "s: " << s << '\n'; }

// Usage
foo(0); // "i: 0"!

Вызов foo однозначен! он принимает перегрузку int (даже если версия шаблона не была). Ну, немного подумав, это не удивительное поведение, ведь нет способа указать литерал short, поэтому компилятор считает, что 0 - это int (это поведение по умолчанию AFAIK), в чтобы однозначно вызвать версию short не templated foo, мы можем явно создать экземпляр short:

foo(0);        // "i: 0"
foo(short{0}); // "s: 0"

Итак, я думал, что это однозначно определит шаблонную версию, но это не так:

foo<int{0}>();   // Ambiguous call, candidates are int and short versions
foo<short{0}>(); // Ambiguous call, candidates are int and short versions
call of overloaded 'foo()' is ambiguous
 foo<int{0}>();
note: candidates are:
void foo() [with int I = 0]
void foo() [with short int S = 0]

call of overloaded 'foo()' is ambiguous
 foo<short{0}>();
note: candidates are:
void foo() [with int I = 0]
void foo() [with short int S = 0]

Последнее, что я пробовал, это использовать экземпляры вместо литералов:

template <int I>   void foo() { std::cout << "I: " << I << '\n'; }
template <short S> void foo() { std::cout << "S: " << S << '\n'; }

void foo(int i)   { std::cout << "i: " << i << '\n'; }
void foo(short s) { std::cout << "s: " << s << '\n'; }

constexpr int i{1};
constexpr short s{5};

int main()
{
    foo(i); // "i: 1"
    foo(s); // "s: 5"
    foo<i>(); // Ambiguous! (expected "I: 1")
    foo<s>(); // Ambiguous! (expected "S: 5")
    return 0;
}

Без успеха, как вы можете... Итак, какой вопрос?

  • Почему вызов templated foo неоднозначен? (обратите внимание, что шаблоны foo не имеют версии int, поэтому недвусмысленны).
  • Почему вызов templated foo остается неоднозначным даже после указания типа вызова? (обратите внимание, что шаблонный foo не работает нормально).

Спасибо.

Ответы

Ответ 1

Вот что происходит, когда вы пишете f<0>().

  • Компилятор просматривает f и находит две объявления шаблонов функций:

    template <int I>   void foo();
    template <short S> void foo();
    
  • Компилятор видит список аргументов явного шаблона и пытается заменить его на каждое объявление шаблона функции:

    template <int I>   void foo(); // with I = 0
    template <short S> void foo(); // with S = 0
    

    Замена завершается в обоих случаях, потому что 0 является int и может быть преобразован в short, и преобразование является разрешенным преобразованием в этом контексте.

  • После подстановки создаются две специализированные специализации. Оба они жизнеспособны. Затем выполняется разрешение перегрузки - и поскольку подпись идентична и не применяется тай-брейк, разрешение перегрузки выходит из строя, а вызов неоднозначен.

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

Ответ 2

Сообщение об ошибке компилятора *. Вы бы интуитивно считали, что это означает, что "вызов функции неоднозначен!", Но на самом деле компиляция не выполняется на более ранней стадии, определение специализированной функции даже не генерируется в этой точке.

На самом деле это означает: "Специализация функции неоднозначна!"

Давайте посмотрим, как это компилируется:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

Первым шагом компиляции является специализированная специализация.

Шаг 1. Компилятор понимает, что foo<0> является специализированным шаблоном и соответственно генерирует объявление функции:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }

template<>
void foo<0>();

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

Шаг 2. Компилятор переопределяет функцию, которая фактически вызывается (это кажется очевидным в этом случае, но это менее очевидно, если у вас есть шаблон шаблона.) и генерирует определение:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }

template<>
void foo<0>();

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

template<>
void foo<0>(){
    std::cout << "S: " << 0 << '\n';
}

Шаг 3: Теперь у вас есть вызываемая функция, компиляция продолжается нормально.

Давайте попробуем выполнить те же шаги в вашем случае:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }
template <int I>   void foo() { std::cout << "I: " << I << '\n'; }

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

Шаг 1: создание объявления функции:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }
template <int I>   void foo() { std::cout << "I: " << I << '\n'; }

template<>
void foo<0>();

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

И на данный момент компиляция терпит неудачу, потому что специализированное объявление foo неоднозначно. Если вы хотите получить доказательство, попробуйте выполнить компиляцию этого кода:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }
template <int I>   void foo() { std::cout << "I: " << I << '\n'; }

template<>
void foo<0>();

int main(int argc, char* argv[])
{
    return 0;
}

Вы получите то же сообщение об ошибке без вызова функции!

Обновление

Таким образом, выгода заключается в том, что все переводится в специализированные объявления функций. Поэтому независимо от того, пишете ли вы foo<int{0}> из foo<short{0}>, компилятор будет генерировать template<> void foo<0>(); для обоих. Явные типы будут проигнорированы. (Вот почему его действительно важно, чтобы они были constexpr -s.)

Обновление Как T.C. в своем комментарии, в стандарте (413-я страница в формате PDF) есть очень похожий пример:

[Пример: В следующем примере предполагается, что подписанный char не может представляют значение 1000, сужение преобразования (8.5.4) будет требуется преобразовать шаблон-аргумент типа int в подписанный char, поэтому замена для второго шаблона (14.3.2) не выполняется.

 template <int> int f(int);
 template <signed char> int f(int);
 int i1 = f<1000>(0); // OK
 int i2 = f<1>(0); // ambiguous; not narrowing

-end пример]


* Сообщение об ошибке полностью верно. Не может быть особенно интуитивным, но он отражает процедуру, указанную в стандарте. - T.C.

Ответ 3

Функция шаблона здесь параметризована по значению и только по значению, а не по типу! обновление: теперь я не уверен. С другой стороны, не templated версия параметризуется по типу (и можно пользоваться полиморфными вызовами).

обновление: Ну, похоже, что запущенные функции искаженное имя фактически зависит от типа числового параметра шаблона.