09 января 2008

Function overloading vs template specialization

Перегрузка функций работает отлично со времен plain C. Пример:

Foo.h:
void Foo(bool);
void Foo(int);
void Foo(const char*);

Foo.cpp:
void Foo(bool) { ... }
void Foo(int) { ... }
void Foo(const char*) { ... }

Test.cpp:
#include "Foo.h"

void Test()
{
bool b(true);
int i(10);
const char *c("hello");

Foo(b);
Foo(i);
Foo(c);

Foo(*c); // хм... компилируется!

shared_ptr<int> p;
Foo(p); // тоже компилируется
}

Неприятность доставляет тип bool, к которому конвертируются многие другие типы. Например, можно забыть разыменовать [умный] указатель, и компилятор нам ничего не подскажет.

Все было бы замечательно, если бы можно было написать void Foo(explicit bool);, но ведь в случае параметра функции explicit не прокатит...

Выход - вместо перегрузки функций использовать специализацию шаблонов. Правим Foo.h:
template <typename T> void Foo(T);
template <> void Foo(bool);
template <> void Foo(int);
template <> void Foo(const char*);

Кстати, у меня под Visual C++ 2005 не пришлось даже менять и/или перекомпилировать Foo.cpp - сингатуры функций в объектных файлах для template <> void Foo(xxx) и void Foo(xxx) видимо совпадают.

Теперь Foo(*c) и Foo(p) из приведенного выше примера не пройдут - линкер скажет, что не нашел определений нужных символов. Можно попросить ругаться не линкер, а компилятор, добавив немного кода в первую строчку Foo.h:
template <typename T> void Foo(T) { typename T::IncorrectParameterType; }


Из минусов такого подхода - все вызовы Foo() требуют теперь точного указания типа параметра:
class ConvertableToInt
{
public:
operator int() { return 0; }
};
Foo(ConvertableToInt()) // теперь так не получится :(
Foo(static_cast<int>(ConvertableToInt())) // только так;

Если такие минусы не устраивают - придется идти на более радикальные меры, например разделить реализации bool и не-bool на функции с разными именами. Интерфейс при этом останется тот же - Foo(x):

Новый Foo.h:
template <typename T> void Foo(T t) { FooImpl(t); }
void Foo(bool);
void FooImpl(int);
void FooImpl(const char*);

Новый Foo.cpp:
void Foo(bool) { ... }
void FooImpl(int) { ... }
void FooImpl(const char*) { ... }

5 комментариев:

Yuriy Volkov комментирует...

А Foo(0); что вызовет Foo(const char*); или Foo(int);? :-) Указатель тоже ведь нулем можно инициализировать. VS 2005 вызывает Foo(int); у меня. Однако я почему то сомневаюсь в правильности такого поведения.

Raider комментирует...

Даже Foo(NULL) вызывает Foo(int). В новом стандарте обещают отдельный nullptr, видимо чтобы избегать путаницы 0 (он же NULL) с нулевым указателем.

Yuriy Volkov комментирует...

gcc version 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)
тоже вызывает Foo(int) по точному соответствию типов (все правильно, зря я сомневался - все как в стандарте). Выходит просто в случае с указателями у комитета неувязочка вышла, которую и призван решить null_ptr

CTpaHHoe комментирует...

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

CTpaHHoe комментирует...

>> Перегрузка функций работает отлично со времен plain C.

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