Статический полиморфизм C (CRTP) и использование определений типов из производных классов

#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;}
  

https://godbolt.org/z/2G4w7d

Недостатком является то, что доступ к элементу в производном классе должен осуществляться с помощью квалифицированного typename или повторно включенного через using .