пятница, 12 ноября 2010 г.

Шаблонные конструкторы против обычных

Про шаблонные конструкторы нужно помнить как минимум две вещи:

1) если он не имеет аргументов, то его нельзя вызвать или специализировать (синтаксис не позволяет)
2) он "сильнее" конструктора по умолчанию, но "слабее" copy- и move- конструкторов

Если мы в своем классе Foo объявили шаблонный конструктор без параметров, то происходят следующие вещи:

struct Foo {
   int a;
   template<class T> Foo() { a = 1; }
};

/* 1 */ Foo f;

Во-первых, конструктор по умолчанию теперь не генерируется. Т.е. на строке 1 компилятор выдаст сообщение об ошибке. В этом смысле шаблонный конструктор сильнее дефолтного.

Во-вторых, наш шаблонный конструктор мы вызвать не сможем, ибо синтаксис не позволяет нам писать параметр T возле имени конструктора:

Foo<T> f1; /* тут шаблонным стал класс, а не конструктор */

Foo pf = new Foo<int>(); /* тут то же самое, хотя такую запись можно было бы и разрешить: в левой части нет параметра T, значит, это не шаблонный класс, а конструктор */

Foo f2<T>; /* а так писать вообще нельзя :) */ 

/* у Вас есть ещё идеи? */

В-третьих, мы не можем написать специализацию для шаблонного конструктора без аргументов:

template<> Foo::Foo<int>() { a = 3; }

Строка выше синтаксически некорректна: нельзя писать угловые скобки после имени конструктора (они воспринимаются как параметры шаблонного класса, но у нас нет никакого шаблонного класса, только конструктор).

Итак, шаблонный конструктор можно специализировать и вызвать только если у него есть аргументы, тогда будет работать механизм type inference. Наиболее интересен случай одного аргумента:

struct Bar {
   int a;
   template<class T> Bar(T) { cout << "ок"; }
};

/* 1 */ Bar b1(5);
/* 2 */ Bar pb = new Bar('a');
/* 3 */ Bar b2(b1);

В строках 1 и 2 на экран будет выведено "ок". Вопрос: что будет в строке 3? Ответ: на экране ничего нового не появится!
Казалось бы, в качестве T может вполне себе выступать тип const Bar&, т.е. конструктор копирования описывается семейством конструкторов Bar(T t), тогда в строке 3 должен вызываться именно наш шаблонный конструктор. Ан нет!
Язык C++ требует, чтобы конструктор копирования (в C++0x также и move-конструктор) были объявлены явно, а не выведены в результате инстанцировния. Поэтому в нашем примере конструктор копирования будет сгенерирован компилятором.

Нашему горю не поможет даже явная специализация

template<>
Bar::Bar(const Bar &) { cout << "OK"; }

Всё равно конструктор копирования (и move-конструктор в C++0x) будет сгенерирован компилятором.

Обратите внимание на синтаксис специализации: без угловых скобочек. Вариант с угловыми скобочками после имени конструктора Bar<const Bar &>, как мы помним, синтаксически недопустим.
 
Налицо еще одна асимметрия языка C++... Остается только горевать.

четверг, 11 ноября 2010 г.

Шаблонные методы без аргументов и неизвестный смысл слова template

Язык С++ не перестаёт удивлять меня. Изучаю его давно, тем не менее, периодически обнаруживаются  неизведанные уголки. Есть ощущение, что C++ так же неисчерпаем, как и атом.

Как известно, внутри классов можно объявлять шаблонные статические методы:

struct Foo {
   template <class T>
   static int method() { return 0; };
};

Теперь можно написать шаблонную функцию f, которая для заданного параметра T зовет соответствующий method<T> из класса Foo:

template<class T>
int f() { return Foo::method<T>(); }

Попробуем теперь параметризовать эту функцию еще классом S, из которого зовется метод, и подставить Foo вместо S:

template<class S, class T>
int f() { return S::method<T>(); }

...

int i = f<Foo, int>();

Что же мы видим? Наш любимый g++ выдает ошибку:

test.cpp: error: expected primary-expression before '>' token
test.cpp: error: expected primary-expression before ')' token

Оказывается, нельзя вызывать шаблонный статический метод с явным указанием типа, когда класс-владелец метода сам является шаблонным параметром.
Собственно, можно даже в функции f отказаться от параметра T, положив его равным, скажем, int:

template<class S>
int f() { return S::method<int>(); }

Получаем то же самое сообщение от компилятора: ему не нравятся именно угловые скобки при явном указании типа для шаблонного метода.
Проблема исчезает, если снабдить метод хотя бы одним аргуметном типа T и отказаться от явного указания типа, полагаясь на механизм type inference:

struct Foo {
   template <class T>
   static int method(T t) { return 0; };
};

template<class S, class T>
int f() { T t; return S::method(t); }

В этом фрагменте уже всё нормально, компилируется. Но нам отнюдь не всегда нужны аргументы в методе. Не передавать же их только для того, чтобы заставить компилятор нормально работать...
Проблема также исчезает, когда мы зовем метод не из самого класса-параметра S, а из какого-то другого класса Traits, зависящего от S:

template<class S> 
struct Traits {
   template <class T>
   static int method() { return 0; };
};

template<class S, class T>
int f() { return Traits<S>::method<T>(); }

Собственно, это один из способов преодолеть трудность - использовать зависимый класс Traits<S> вместо S для хранения метода, зависящего от T.

Аналогичная проблема проявляется при обращении к вложенному шаблонному классу:

struct Foo {
   template<class T>
   struct Bar {};
};

template <class T>
void g() { Foo::Bar<T> bar; }

Это компилируется. А следующий пример - нет:

template <class S, class T>
void g() { typename S::Bar<T> bar; }

Даже более простой случай без T не компилируется:

template <class S>
void g() { typename S::Bar<int> bar; }

Ошибка та же, но сообщение об ошибке уже немного другое:

test.cpp: error: expected primary-expression before '>' token
test.cpp: error: expected primary-expression before ')' token
... (дописать)

Кстати, именно этот вариант сообщения об ошибке навел меня на "правильное" решение проблемы.

Итак, внимание, правильный ответ. Нужно использовать ключевое слово template в непривычном для нас месте: 

template<class S, class T>
int f() { return S:: template method<T>(); }

template<class S, class T>
void g() { typename S:: template Bar<T> bar; }

Надо отметить, что в VisualStudio 2008 указанная проблема не наблюдается - там слово template можно писать, а можно нет, всё и так компилируется. (Вопрос: может, там и typename писать не надо?)

"Когда такое нужно?" - спросите Вы. Ну, напрмер, мы имеем дело с различными потоками ввода-вывода, где статический шаблонный метод сообщает нам размер буфера, отводимого в этом потоке при чтении записей заданного типа:

struct MyStream {
   template <class Record>
   static int bufferSize() { return 1024; }
};

template<class Stream, class Record>
int streamBufferSize() { return Stream::template bufferSize<T>(); }

...

int *p = new char[ streamBufferSize<MyStream, int>() ];

Либо можно прибегнуть к вспомогательному классу Traits:

template <class Stream, class Record>
struct Traits {
   static int bufferSize() { return 1024; }
};


...

int *p = new char[ Traits<MyStream, int>::bufferSize() ];

Но этот (универсальный) способ требует дополнительных телодвижений и усложняет код.

Какие есть теоретические обоснования рассмотренному тут феномену, мне на данный момент неизвестно.