#c #templates #inheritance #typedef #crtp
#c #шаблоны #наследование #typedef #crtp
Вопрос:
Я прочитал статью в Википедии о любопытно повторяющемся шаблоне шаблонов в C для выполнения статического (читай: во время компиляции) полиморфизма. Я хотел обобщить это, чтобы я мог изменять возвращаемые типы функций на основе производного типа. (Кажется, что это должно быть возможно, поскольку базовый тип знает производный тип из параметра шаблона). К сожалению, следующий код не будет компилироваться с использованием MSVC 2010 (у меня сейчас нет простого доступа к gcc, поэтому я еще не пробовал). Кто-нибудь знает почему?
template <typename derived_t>
class base {
public:
typedef typename derived_t::value_type value_type;
value_type foo() {
return static_cast<derived_t*>(this)->foo();
}
};
template <typename T>
class derived : public base<derived<T> > {
public:
typedef T value_type;
value_type foo() {
return T(); //return some T object (assumes T is default constructable)
}
};
int main() {
derived<int> a;
}
Кстати, у меня есть обходной путь с использованием дополнительных параметров шаблона, но мне это не нравится — это станет очень подробным при передаче многих типов вверх по цепочке наследования.
template <typename derived_t, typename value_type>
class base { ... };
template <typename T>
class derived : public base<derived<T>,T> { ... };
Редактировать:
Сообщение об ошибке, которое выдает MSVC 2010 в этой ситуации, является error C2039: 'value_type' : is not a member of 'derived<T>'
g 4.1.2 (через codepad.org ) говорит error: no type named 'value_type' in 'class derived<int>'
Комментарии:
1. Просто чтобы вы знали, codepad.org могу скомпилировать и запустить код для вас, и я полагаю, что он использует gcc / g . Таким образом, вы никогда не будете вне досягаемости g 🙂
2. не могли бы вы добавить, какую ошибку вы получаете, чтобы я был полезен для читателей.
3. @Seth: Ideone наверняка использует gcc, так что это еще один вариант 🙂
4. @Seth: спасибо за совет о codepad.org ! @Sriram: Хорошее решение. Я добавил их.
Ответ №1:
derived
является неполным, когда вы используете его в качестве аргумента шаблона для base
в списке базовых классов.
Распространенным обходным путем является использование шаблона класса признаков. Вот ваш пример, обозначенный признаками. Это показывает, как вы можете использовать оба типа и функции из производного класса с помощью признаков.
// Declare a base_traits traits class template:
template <typename derived_t>
struct base_traits;
// Define the base class that uses the traits:
template <typename derived_t>
struct base {
typedef typename base_traits<derived_t>::value_type value_type;
value_type base_foo() {
return base_traits<derived_t>::call_foo(static_cast<derived_t*>(this));
}
};
// Define the derived class; it can use the traits too:
template <typename T>
struct derived : base<derived<T> > {
typedef typename base_traits<derived>::value_type value_type;
value_type derived_foo() {
return value_type();
}
};
// Declare and define a base_traits specialization for derived:
template <typename T>
struct base_traits<derived<T> > {
typedef T value_type;
static value_type call_foo(derived<T>* x) {
return x->derived_foo();
}
};
Вам просто нужно специализироваться base_traits
на любых типах, которые вы используете для аргумента шаблона derived_t
из base
, и убедиться, что каждая специализация предоставляет все элементы, которые base
требуются.
Комментарии:
1. Я создал один тестовый файл и скомпилировал код OP с установкой main () в Ubuntu g 4.4.1, он работает нормально. Объявление объекта типа
Derived<int>
выдает ошибку, я пытаюсь выяснить, почему?2. @iammilind Это потому, что в пустом
main()
экземпляре шаблона не выполняется. Определенные ошибки возникают только тогда, когда компилятор пытается создать экземпляр шаблона и использовать его.3. Правильно; вы должны создать экземпляр шаблона, чтобы здесь не было никаких ошибок; если вы добавите
int main() { derived<int>().base_foo(); }
в конец моего примера черт (это принудительно создает экземпляры всего), он должен скомпилироваться с Visual C 2010, g 4.5.1 и последними сборками Clang.4. не могли бы вы подробнее объяснить, что вы подразумеваете под этим «производный является неполным, когда вы используете его в качестве аргумента шаблона для base в его списке базовых классов». каким образом это неполно?
5. @JamesMcNellis и Sriram, я чувствую, что возможна путаница. Слегка упрощенная версия с производным, не являющимся шаблоном:
struct derived : base<derived> {...};
делает базовые признаки не определенными.
Ответ №2:
Одним из небольших недостатков использования признаков является то, что вам приходится объявлять по одному для каждого производного класса. Вы можете написать менее подробный и повторный обходной путь, подобный этому :
template <template <typename> class Derived, typename T>
class base {
public:
typedef T value_type;
value_type foo() {
return static_cast<Derived<T>*>(this)->foo();
}
};
template <typename T>
class Derived : public base<Derived, T> {
public:
typedef T value_type;
value_type foo() {
return T(); //return some T object (assumes T is default constructable)
}
};
int main() {
Derived<int> a;
}
Комментарии:
1. в чем здесь особенность?
2. Это выглядит интересно, но я не совсем понимаю, не могли бы вы объяснить это немного подробнее?
3. @BenFarmer Вместо того, чтобы полагаться на производный тип для извлечения value_type в базе, мы напрямую передаем его в базовый класс. Базовый класс теперь имеет два параметра шаблона. Это очень практично, если у вас есть только или два типа для передачи, но если у вас есть, это неудобно.
4. @BaptisteWicht Действительно, вы могли бы использовать переменные шаблоны и кортежи для немного большей гибкости.
5. Это не является избыточным, поскольку вы передаете не тип производного класса, а его шаблон в качестве аргумента шаблона базового класса. (Обратите внимание, что в производном классе typedef можно удалить)
Ответ №3:
В C 14 вы могли бы удалить typedef
и использовать функцию auto
, возвращающую вывод типа:
template <typename derived_t>
class base {
public:
auto foo() {
return static_cast<derived_t*>(this)->foo();
}
};
Это работает, потому что вывод возвращаемого типа base::foo
откладывается до derived_t
завершения.
Комментарии:
1. Ограничивает ли этот метод возвращаемое значение? Работает ли это с полем или параметром функции? Поможет ли C 17 еще больше?
Ответ №4:
Альтернативой признакам типа, которая требует меньше шаблонности, является вложение вашего производного класса в класс-оболочку, который содержит ваши typedefs (или using), и передача оболочки в качестве аргумента шаблона вашему базовому классу.
template <typename Outer>
struct base {
using derived = typename Outer::derived;
using value_type = typename Outer::value_type;
value_type base_func(int x) {
return static_cast<derived *>(this)->derived_func(x);
}
};
// outer holds our typedefs, derived does the rest
template <typename T>
struct outer {
using value_type = T;
struct derived : public base<outer> { // outer is now complete
value_type derived_func(int x) { return 5 * x; }
};
};
// If you want you can give it a better name
template <typename T>
using NicerName = typename outer<T>::derived;
int main() {
NicerName<long long> obj;
return obj.base_func(5);
}
Комментарии:
1. Умно, но это имеет побочные эффекты. Например,
T
не смогут быть выведены, например, в табличных файлах функцийtemplate<class T> f(outer<T>::derived x)
(как указано вtemplate<class T> f(derived2<T> x)
.2. Вы можете написать
template <class C, class T = typename C::value_type> auto f(C x)
. Если вы беспокоитесь о принятии произвольных типов, вы можете добавитьclass = std::enable_if_t< std::is_same_v< C, NicerName<T> >, int>
в список параметров шаблона, но это становится довольно длинным.
Ответ №5:
Я знаю, что это в основном обходной путь, который вы нашли и который вам не нравится, но я хотел задокументировать это, а также сказать, что это в основном текущее решение этой проблемы.
Я некоторое время искал способ сделать это и так и не нашел хорошего решения. Тот факт, что это невозможно, является причиной, по которой в конечном итоге для таких вещей, как boost::iterator_facade<Self, different_type, value_type, ...>
требуется много параметров.
Конечно, мы хотели бы, чтобы что-то вроде этого работало:
template<class CRTP>
struct incrementable{
void operator (){static_cast<CRTPamp;>(*this).increment();}
using ptr_type = typename CRTP::value_type*; // doesn't work, A is incomplete
};
template<class T>
struct A : incrementable<A<T>>{
void increment(){}
using value_type = T;
value_type f() const{return value_type{};}
};
int main(){A<double> a; a;}
Если бы это было возможно, все свойства производного класса могли бы быть переданы неявно базовому классу. Найденная мной идиома для получения того же эффекта заключается в полной передаче признаков базовому классу.
template<class CRTP, class ValueType>
struct incrementable{
void operator (){static_cast<CRTPamp;>(*this).increment();}
using value_type = ValueType;
using ptr_type = value_type*;
};
template<class T>
struct A : incrementable<A<T>, T>{
void increment(){}
typename A::value_type f() const{return typename A::value_type{};}
// using value_type = typename A::value_type;
// value_type f() const{return value_type{};}
};
int main(){A<double> a; a;}
Недостатком является то, что доступ к элементу в производном классе должен осуществляться с помощью квалифицированного typename
или повторно включенного через using
.