Статический полиморфизм С++ (CRTP) и использование typedefs из производных классов
Я прочитал статью Wikipedia о любопытно повторяющемся шаблоне шаблона в С++ для выполнения статического (read: compile-time) полиморфизма. Я хотел обобщить его так, чтобы я мог изменять возвращаемые типы функций на основе производного типа. (Кажется, это должно быть возможно, поскольку базовый тип знает производный тип из параметра шаблона). К сожалению, следующий код не будет компилироваться с использованием 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> { ... };
EDIT:
Сообщение об ошибке, которое 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
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
.
Ответ 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;
}
Ответ 3
В С++ 14 вы можете удалить typedef
и использовать функцию auto
вывод типа возвращаемого типа:
template <typename derived_t>
class base {
public:
auto foo() {
return static_cast<derived_t*>(this)->foo();
}
};
Это работает, потому что вывод возвращаемого типа base::foo
задерживается до завершения derived_t
.
Ответ 4
Альтернативой типу типов, который требует меньше шаблона, является вложение вашего производного класса внутри класса-оболочки, который содержит ваши typedef (или использование) и передает оболочку в качестве аргумента шаблона в ваш базовый класс.
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);
}
Ответ 5
Я знаю, что это в основном обходной путь, который вы нашли и который вам не нравится, но я хотел задокументировать его, а также сказать, что это в основном текущее решение этой проблемы.
Я долго искал способ сделать это и так и не нашел хорошего решения. Тот факт, что это невозможно, является причиной того, что в конечном итоге для таких вещей, как boost::iterator_facade<Self, different_type, value_type,...>
требуется много параметров.
Конечно, мы бы хотели, чтобы что-то вроде этого работало:
template<class CRTP>
struct incrementable{
void operator++(){static_cast<CRTP&>(*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<CRTP&>(*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;}
https://godbolt.org/z/2G4w7d
Недостатком является то, что к признаку в производном классе нужно обращаться с квалифицированным typename
или повторно using
с using
.
Ответ 6
Вы можете избежать передачи 2 аргументов в template
. В CRTP, если у вас есть уверенность, что class base
будет сопряжен с class derived
(а не с class derived_2
), используйте ниже метод:
template <typename T> class derived; // forward declare
template <typename value_type, typename derived_t = derived<value_type> >
class base { // make class derived as default argument
value_type foo();
};
Использование:
template <typename T>
class derived : public base<T> // directly use <T> for base