Подтвердить что ты не робот

Статический полиморфизм С++ (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>'

4b9b3361

Ответ 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