Почему шаблон с выведенным типом возврата не перегружен другими версиями?
Почему следующие два шаблона несовместимы и не могут быть перегружены?
#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