Почему шаблон с выведенным типом возврата не перегружен другими версиями?

Почему следующие два шаблона несовместимы и не могут быть перегружены?

#include <vector>

template<typename T>
auto f(T t) { return t.size(); }
template<typename T>
auto f(T t) { return t.foobar(); }

int main() {
   f(std::vector<int>());   
}

Я думаю, что они (более или менее) эквивалентны следующему, который компилируется отлично (поскольку мы не можем сделать decltype auto(t.size()), я не могу дать точный эквивалент без какого-либо шума..).

template<typename T>
auto f(T t) -> decltype(t.size() /* plus some decay */) { return t.size(); }

template<typename T>
auto f(T t) -> decltype(t.foobar() /* plus some decay */) { return t.foobar(); }

Clang и GCC жалуются main.cpp:6:16: error: redefinition of 'f', если я не могу оставить возвращаемый тип.

(Обратите внимание, что это вопрос rationale. Я не ищу место в Стандарте, которое определяет это поведение, которое вы можете включить в свой ответ, если хотите, но для объяснения почему это поведение желательно или статус-кво).

Ответ 1

Выведенный тип возврата может явно не быть частью подписи. Тем не менее, выведение выражения, которое определяет тип возврата (и участвует в SFINAE) из операторов return, имеет некоторые проблемы. Предположим, что мы должны были взять первое выражение выражения return и вставить его в некоторый скорректированный, виртуальный тип trailing-return-type:

  • Что делать, если возвращаемое выражение зависит от локальных объявлений? Это не обязательно останавливает нас, но это кричит правила чрезвычайно. Не забывайте, что мы не можем использовать имена объявленных объектов; Это потенциально может скомпенсировать наш высокий уровень возвращаемого типа с высокой вероятностью для потенциально никакой пользы.

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

  • подпись декларации не может быть определена, если она также не является определением, представляя целый ряд других проблем. Самое простое решение - полностью запретить (не определяющие) объявления таких шаблонов функций, что почти смешно.

К счастью автор N3386 стремился сохранить правила (и реализацию!) простыми. Я не могу себе представить, как не нужно писать тип trailing-return-type в некоторых угловых случаях, заслуживает таких тщательных правил.

Ответ 2

Я думаю, что это может быть коммит-мисс, но предыстория, я считаю, такова:

  • Вы не можете перегружать возвращаемый тип функции. Это означает, что в объявлении

    template<typename T>
    auto f(T t) { return t.size(); }
    

    Значение auto неинтересно компилятору на самом деле до экземпляра функции. Очевидно, что компилятор не добавляет некоторую проверку SFINAE в тело функции, чтобы проверить, существует ли T::size, поскольку это не во всех других случаях, когда T используется внутри тела функции

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

    В первом случае компилятор получит smth как

    [template typename T] f(T)
    [template typename T] f(T)
    

    Это точный эквивалент

    Во втором случае, однако, как указано в decltype, он будет добавлен в аргументы шаблона, чтобы вы получили

    [template typename T, typename = typeof(T::size())] f(T)
    [template typename T, typename = typeof(T::size())] f(T)
    

    Это не точные эквиваленты.

    Таким образом, компилятор откажется от первого случая, а второй может быть ОК при замене реального типа вместо T.

Ответ 3

Глядя на символы, созданные моим компилятором:

[[email protected] ~]$ cat test1.cc

#include <vector>

template<typename T>
auto JSchaubStackOverflow(T t) { return t.size(); }

// template<typename T>
// auto f(T t) { return t.foobar(); }

int do_something() {
       JSchaubStackOverflow(std::vector<int>());
       return 4;
}
[[email protected] ~]$ c++ -std=c++14 -pedantic test1.cc -c -o test1.o
[[email protected] ~]$ nm test1.o | grep JScha
0000000000000000 W _Z20JSchaubStackOverflowISt6vectorIiSaIiEEEDaT_
[[email protected] ~]$ nm -C test1.o | grep JScha
0000000000000000 W auto JSchaubStackOverflow<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >)
[[email protected] ~]$ cat test2.cc

#include <vector>

template<typename T>
auto JSchaubStackOverflow(T t) -> decltype(t.size() /* plus some decay */) { return t.size(); }

template<typename T>
auto JSchaubStackOverflow(T t) -> decltype(t.foobar() /* plus some decay */) { return t.foobar(); }
struct Metallica
{

    Metallica* foobar() const
    {
        return nullptr;
    }
};


int do_something() {
       JSchaubStackOverflow(std::vector<int>());
       JSchaubStackOverflow(Metallica());
       return 4;
}
[[email protected] ~]$ c++ -std=c++14 -pedantic test2.cc -c -o test2.o
[[email protected] ~]$ nm test2.o | grep JScha
0000000000000000 W _Z20JSchaubStackOverflowI9MetallicaEDTcldtfp_6foobarEET_
0000000000000000 W _Z20JSchaubStackOverflowISt6vectorIiSaIiEEEDTcldtfp_4sizeEET_
[[email protected] ~]$ nm -C test2.o | grep JScha
0000000000000000 W decltype (({parm#1}.foobar)()) JSchaubStackOverflow<Metallica>(Metallica)
0000000000000000 W decltype (({parm#1}.size)()) JSchaubStackOverflow<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >)

То, что вы можете видеть из этого, - это тип decltype (независимо), который может помочь нам различать символы, это часть подписи. Но "авто" не помогает нам... Поэтому, если у вектора есть метод foobar и size, обе перегрузки JSchaubStackOverflow будут искажены как Z20JSchaubStackOverflowISt6vectorIiSaIiEEEDaT Теперь я оставлю кому-то еще найти соответствующий раздел в ISO о подписях функций шаблона.

- EDIT-- Я знаю, что у него уже есть принятый ответ, но только для записи, вот техническая сложность - объявления без определений:

[[email protected] ~]$ cat test2.cc

#include <vector>

template<typename T>
auto JSchaubStackOverflow(T t) -> decltype(t.size());

template<typename T>
auto JSchaubStackOverflow(T t) -> decltype(t.foobar());

struct Metallica
{

    Metallica* foobar() const
    {
        return nullptr;
    }
};


int do_something() {
       JSchaubStackOverflow(std::vector<int>());
       JSchaubStackOverflow(Metallica());
       return 4;
}
[[email protected] ~]$ c++ -std=c++14 -pedantic test2.cc -c -o test2.o
[[email protected] ~]$ nm -C test2.o | grep JScha
                 U decltype (({parm#1}.foobar)()) JSchaubStackOverflow<Metallica>(Metallica)
                 U decltype (({parm#1}.size)()) JSchaubStackOverflow<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >)

Это означает, что вы можете делать все это без функциональных тел. Специализации шаблонов будут даны в другой единицы перевода, но для этого линкеру необходимо их найти... таким образом, нельзя перегрузить тело функции.

Ответ 4

"Только ошибки в типах и выражениях в непосредственном контексте типа функции или ее типов параметров шаблона являются ошибками SFINAE.

Если оценка замещенного типа/выражения вызывает побочный эффект, такой как создание некоторой специализированной специализации, генерация неявно определенной функции-члена и т.д., ошибки в этих побочных эффектах рассматриваются как жесткие ошибки. " источник

Ваше первое объявление вызывает неявное замещение возвращаемого типа и, следовательно, не соответствует SFINAE