Еще один WTF в языке C++. Но, может быть, как обычно, есть какое-то (псевдо)рациональное объяснение для этого?
Когда я пишу f(B), я жду, что вызовется f(A), потому что B это тоже A ("is a"). Даже если не брать это очевидное разумное обоснование, все-таки, перегрузка с производным классом как-то гораздо специфичнее, чем шаблонная перегрузка, принимающая тип без ограничений.
#include <iostream>
using namespace std;
class A {};
class B : public A {};
void f(const A& a) {
cout << "f(A);" << endl;
}
template <class T>
void f(const T& t) {
cout << "f(T);" << endl;
}
int main() {
A a;
f(a);
B b;
f(b);
return 0;
}
Здравствуйте, Eternity, Вы писали:
E>Еще один WTF в языке C++. Но, может быть, как обычно, есть какое-то (псевдо)рациональное объяснение для этого?
E>Когда я пишу f(B), я жду, что вызовется f(A), потому что B это тоже A ("is a"). Даже если не брать это очевидное разумное обоснование, все-таки, перегрузка с производным классом как-то гораздо специфичнее, чем шаблонная перегрузка, принимающая тип без ограничений.
Наоборот, шаблон способен предоставить полное попадание void f(const B& t), в то время как f(A) потребует преобразования аргумента B->A и поэтому проиграет при разрешении перегрузки.
Здравствуйте, jazzer, Вы писали:
J>Наоборот, шаблон способен предоставить полное попадание void f(const B& t), в то время как f(A) потребует преобразования аргумента B->A и поэтому проиграет при разрешении перегрузки.
Странно, что мне это не приходило в голову. Видимо, таким объяснением и руководствовались разработчики языка.
С другой стороны, оно несколько алогичное. Ведь шаблонная перегрузка обеспечивает точное попадание и для f(a) тоже. Если руководствоваться точным попаданием, тогда нужно как-то обосновать, почему мы выбираем f(const A&), а не f(const T&) для вызова f(a).
Я почему-то всегда думал о шаблонной перегрузке как о "наименее специфичной". Это как если бы все наследовалось от класса Object.
class A {};
class B : public A {};
void f(const A& a);
void f(const B& a);
void f(const Object& object);
J>ЗЫ В следующий раз пиши осмысленные сабжи, плиз.
Здравствуйте, Eternity, Вы писали:
E>С другой стороны, оно несколько алогичное. Ведь шаблонная перегрузка обеспечивает точное попадание и для f(a) тоже. Если руководствоваться точным попаданием, тогда нужно как-то обосновать, почему мы выбираем f(const A&), а не f(const T&) для вызова f(a).
На эту тему есть есть примеры поприкольнее:
#include <iostream>
struct C
{
C()
{
std::cout << "default C()" << std::endl;
}
template<typename T> C(const T& t)
{
std::cout << "C(T)" << std::endl;
}
};
class D {};
int main()
{
C c1;
C c2(c1);
D d;
C c3(d);
std::cout << "end of test" << std::endl;
}
Здравствуйте, Eternity, Вы писали:
E>С другой стороны, оно несколько алогичное. Ведь шаблонная перегрузка обеспечивает точное попадание и для f(a) тоже. Если руководствоваться точным попаданием, тогда нужно как-то обосновать, почему мы выбираем f(const A&), а не f(const T&) для вызова f(a).
Для A (в отличие от B) обе перегрузки обеспечивают точное попадание. Только шаблонная перегрузка способна принять множество разных типов, а нешаблонная только один — тот, для которого она написана. Так которая из них будет более специфичной по-твоему?
E>Я почему-то всегда думал о шаблонной перегрузке как о "наименее специфичной". Это как если бы все наследовалось от класса Object.
Если шаблонная перегрузка дает лучшее совпадение типов параметров, то рассуждения о специфичности неуместны.
--
Не можешь достичь желаемого — пожелай достигнутого.
Здравствуйте, Eternity, Вы писали:
J>>Наоборот, шаблон способен предоставить полное попадание void f(const B& t), в то время как f(A) потребует преобразования аргумента B->A и поэтому проиграет при разрешении перегрузки. E>Странно, что мне это не приходило в голову. Видимо, таким объяснением и руководствовались разработчики языка. E>С другой стороны, оно несколько алогичное. Ведь шаблонная перегрузка обеспечивает точное попадание и для f(a) тоже.
При равных условиях т.е. без промежуточных преобразований, нешаблонная функция имеет приоритет над шаблонной, т.к. она как-бы более специализирована под параметр. Запомнить просто, тут всё зависит от количества промежуточных преобразований, компилятор выбирает ту функцию, где нужно меньше пребразований делать, чтобы её вызвать.
[In theory there is no difference between theory and practice. In
practice there is.]
[Даю очевидные ответы на риторические вопросы]
Здравствуйте, wvoquine, Вы писали:
W>Здравствуйте, Eternity, Вы писали:
E>>С другой стороны, оно несколько алогичное. Ведь шаблонная перегрузка обеспечивает точное попадание и для f(a) тоже. Если руководствоваться точным попаданием, тогда нужно как-то обосновать, почему мы выбираем f(const A&), а не f(const T&) для вызова f(a).
W>На эту тему есть есть примеры поприкольнее:
+1.
Причем иногда их можно использовать во благо: http://rsdn.ru/forum/cpp.applied/5255987.1
Неправильно вопрос ставишь. Надо не WTF писать, а сказать следующее:
"Пишу семейство функций, охватывающее одну или несколько иерархий классов. Хочу, чтобы это семейство работало по таким же законам, как и функции-члены — т.е. с наследованием, плюс мне нужна функция-ловушка для всего остального".
Сделать это можно несколькими способами.
1) Объединить иерарархии классов: унаследовать все базы от одной супер-базы-заглушки, а дальше просто, f(Object&)
2) Написать в функции-ловушке великое отрицание:
void f(ARoot const&);
void f(ADerived const&);
void f(AnotherRoot const&);
void f(int); // целые числа тоже, в некотором роде, можно трактовать как иерархию...template<class T>
disable_if<
is_base_and_derived<ARoot, T>::value
|| is_base_and_derived<AnotherRoot, T>::value
|| is_integral<T>
, void>::type
f(T const&) {.....}
Можно было сделать изящнее — написать отдельно предикат-дискриминатор, и отдельно на enable_if/disable_if диспетчер, — и это работало бы даже на С++98. Но мне сейчас немножко лень.
Перекуём баги на фичи!
Re[2]: Как вы думаете, является ли данное поведение перегрузки дебильным?
К>Неправильно вопрос ставишь. Надо не WTF писать, а сказать следующее: К>"Пишу семейство функций, охватывающее одну или несколько иерархий классов. Хочу, чтобы это семейство работало по таким же законам, как и функции-члены — т.е. с наследованием, плюс мне нужна функция-ловушка для всего остального".
Кодт.. скажи чесно, сколько у вас на работе человек которые так вот пишут ?
Re[3]: Как вы думаете, является ли данное поведение перегрузки дебильным?
Здравствуйте, Кодт, Вы писали:
К>Неправильно вопрос ставишь. Надо не WTF писать, а сказать следующее: К>"Пишу семейство функций, охватывающее одну или несколько иерархий классов. Хочу, чтобы это семейство работало по таким же законам, как и функции-члены — т.е. с наследованием, плюс мне нужна функция-ловушка для всего остального".
Вообще-то нет, я спросил именно о дизайне языка, так как собираюсь ради самообразования разработать очередной диалект C++. То, что решить можно, понятно. На практике можно просто перегрузки делать через enable_if. Меня интересует вопрос идеологии.
Здравствуйте, rg45, Вы писали:
R>Для A (в отличие от B) обе перегрузки обеспечивают точное попадание. Только шаблонная перегрузка способна принять множество разных типов, а нешаблонная только один — тот, для которого она написана. Так которая из них будет более специфичной по-твоему?
Ну вот видишь, мы тут возвращаемся к понятию специфичности. Если говорить о специфичности как о количестве принимаемых типов, то перегрузка f(A) уж точно способна принять меньше типов, чем f(T).
Принцип работы C++ в этом месте я прекрасно понимаю: перегрузка без преобразования предпочитается перегрузке с преобразованием. Идея понятна, но, по-моему, это просто не совсем корректный подход для решения задачи о выборе перегрузок. Это классическое решение от реализации, а не от использования, как программист делает юзабилити программы таким, как ему проще и понятнее реализовать, а не так, как пользователю понятнее и удобнее.
Но тут спорный момент еще и в том, правильно ли считать приведение к базовому классу преобразованием. Потому что идеологически тип-наследник не нуждается в преобразовании. Класс B это тоже класс A. Стол это мебель, стол не нужно преобразовывать в мебель. Класс B это не класс, который преобразуется в класс A, класс B это уже и есть A, несмотря на то, что технически, "под капотом", преобразование необходимо.
E>>Я почему-то всегда думал о шаблонной перегрузке как о "наименее специфичной". Это как если бы все наследовалось от класса Object.
R>Если шаблонная перегрузка дает лучшее совпадение типов параметров, то рассуждения о специфичности неуместны.
.
Как я сказал выше, принцип работы C++ в этом месте мне понятен. Но с точки зрения использования языка и здравого смысла эти рассуждения о совпадении выглядят не очень связно. Функция f(T) принимает любой тип, она вообще никак не указывает что это за тип. Так почему ты говоришь о "лучшем совпадении"? Совпадении с чем? Функция f(A) принимает базовый тип для B, но ты говоришь, что оно "хуже совпадает", чем функция f(T), которая принимает какой-то тип. По-моему, f(A) лучше совпадает с B.
Здравствуйте, Vain, Вы писали:
V>При равных условиях т.е. без промежуточных преобразований, нешаблонная функция имеет приоритет над шаблонной, т.к. она как-бы более специализирована под параметр. Запомнить просто, тут всё зависит от количества промежуточных преобразований, компилятор выбирает ту функцию, где нужно меньше пребразований делать, чтобы её вызвать.
Здравствуйте, Eternity, Вы писали:
E>Но тут спорный момент еще и в том, правильно ли считать приведение к базовому классу преобразованием. Потому что идеологически тип-наследник не нуждается в преобразовании. Класс B это тоже класс A. Стол это мебель, стол не нужно преобразовывать в мебель. Класс B это не класс, который преобразуется в класс A, класс B это уже и есть A, несмотря на то, что технически, "под капотом", преобразование необходимо.
По-моему, по этой логике следующий код должен ломаться на компиляции:
class A {};
class B : public A {};
void f(const A& a) {}
void f(const B& a) {}
int main()
{
B b;
f(b);
return 0;
}
Здравствуйте, wvoquine, Вы писали: E>>Но тут спорный момент еще и в том, правильно ли считать приведение к базовому классу преобразованием. Потому что идеологически тип-наследник не нуждается в преобразовании. Класс B это тоже класс A. Стол это мебель, стол не нужно преобразовывать в мебель. Класс B это не класс, который преобразуется в класс A, класс B это уже и есть A, несмотря на то, что технически, "под капотом", преобразование необходимо. W>По-моему, по этой логике следующий код должен ломаться на компиляции:
Скрытый текст
W>
W>class A {};
W>class B : public A {};
W>void f(const A& a) {}
W>void f(const B& a) {}
W>int main()
W>{
W> B b;
W> f(b);
W> return 0;
W>}
W>
Здрасьте, Новый год. "По этой логике" этот код должен ломаться не более, чем вот этот код:
#include <iostream>
using namespace std;
class A {};
class B : public A {};
void f(const A& a) {
cout << "f(A);" << endl;
}
template <class T>
void f(const T& t) {
cout << "f(T);" << endl;
}
int main() {
A a;
f(a);
return 0;
}
(Так как здесь типы тоже "точно совпадают".)
Re[5]: Как вы думаете, является ли данное поведение перегрузки дебильным?
E>. E>Как я сказал выше, принцип работы C++ в этом месте мне понятен. Но с точки зрения использования языка и здравого смысла эти рассуждения о совпадении выглядят не очень связно. Функция f(T) принимает любой тип, она вообще никак не указывает что это за тип. Так почему ты говоришь о "лучшем совпадении"? Совпадении с чем? Функция f(A) принимает базовый тип для B, но ты говоришь, что оно "хуже совпадает", чем функция f(T), которая принимает какой-то тип. По-моему, f(A) лучше совпадает с B.
В том то и дело, что она не принимает какой-то тип — какого-то типа не существует. Шаблон функции — это, говоря языком формальной логики, схема функций, и аргументы у него схематичны. То есть нет этого какого-то аргумента, а есть схемы, в которые мы можем подставить какие-то аргументы, и схема инстанцируется в нечто конкретное, с конкретным типом аргумента. То есть для каждого подставленного типа аргумента генерируется отдельная функция, и это не просто деталь реализации, это и есть схема по своему смыслу.
Потому и получается точное совпадение — с инстанцированной по типу схемой.
Здравствуйте, Eternity, Вы писали:
E>Здравствуйте, wvoquine, Вы писали:
E>Здрасьте, Новый год. "По этой логике" этот код должен ломаться не более, чем вот этот код:
E>
E>#include <iostream>
E>using namespace std;
E>class A {};
E>class B : public A {};
E>void f(const A& a) {
E> cout << "f(A);" << endl;
E>}
E>template <class T>
E>void f(const T& t) {
E> cout << "f(T);" << endl;
E>}
E>int main() {
E> A a;
E> f(a);
E> return 0;
E>}
Как я и писал, не нужно путать шаблонные аргументы с конкретными базовыми типами.
E>(Так как здесь типы тоже "точно совпадают".)
Вот тут как раз точно совпадают в отличие от того случая (где требовалось преобразование). И выходом из этого было при точном совпадении (а шаблон только такое и дает) сделать нешаблонные функции более приоритетными.