Как объявить член в базовом классе шаблона, где тип зависит от производного класса?

Учитывая базовый класс, использующий CRTP, я рассматриваю объявление члена в базовом классе шаблона, где тип зависит от производного класса.

В то время как следующее работает по назначению:

template <class T> class BaseTraits;
template <class T> class Base {
    using TypeId = typename BaseTraits<T>::TypeId;
    TypeId id;
 public:
    Base() { id = 123; }
    TypeId getId() { return id; }
};

class Derived;
template <> class BaseTraits<Derived> {
public:
    using TypeId = int;
};

class Derived : public Base<Derived> {};

int main(int argc, char ** argv) {
     Derived foo;
     return foo.getId();
}

Интересно, смогу ли я упростить реализацию. Я мог бы добавить второй параметр шаблона в шаблон Base и сделать BaseTraits проще или даже избавиться от него. Однако приведенный выше фрагмент уже является попыткой удалить второй параметр шаблона. Я ищу решения, которые не включают второй параметр шаблона для Base.

Я пробовал что-то вроде следующего, но он не компилируется:

ошибка: неправильное использование неполного типа «Производный класс»

template <class T> class Base {
    using TypeId = typename T::TypeId;
    TypeId id;
 public:
    Base() { id = 123; }
    TypeId getId() { return id; }
};

class Derived : public Base<Derived> {
public:
    using TypeId = int;
};

int main(int argc, char ** argv) {
     Derived foo;
     return foo.getId();
}

ОБНОВЛЕНИЕ:

  • Я ограничен С++ 14.
  • Base должен быть шаблоном.
  • Производительность обязательна.

person Flyer    schedule 10.12.2017    source источник
comment
Ну, ваш второй пример не работает, потому что вы не можете использовать класс в качестве параметра шаблона до того, как он будет полностью объявлен. Чего вы хотите добиться с помощью этих классов? Вы уверены, что они вообще должны быть шаблонными?   -  person Maroš Beťko    schedule 10.12.2017
comment
@MarošBeťko Вы очень можете, на данный момент это неполный класс, и вы можете использовать неполный класс в качестве параметра шаблона.   -  person n. 1.8e9-where's-my-share m.    schedule 11.12.2017
comment
@MarošBeťko Я бы хотел, чтобы Base объявил член, тип которого известен только Derived. Да, мне нужно использовать шаблоны, по крайней мере, для Base.   -  person Flyer    schedule 11.12.2017
comment
Вам действительно нужно, чтобы id был членом Base? С тем же успехом вы могли бы сделать его членом Derived...   -  person n. 1.8e9-where's-my-share m.    schedule 11.12.2017
comment
К сожалению, да. На самом деле мне нужен массив из них.   -  person Flyer    schedule 11.12.2017
comment
Непонятно, зачем он вам там нужен. У вас может быть функция, которая возвращает (ссылку/указатель) массив id в Base, в то время как сам массив находится в Derived.   -  person n. 1.8e9-where's-my-share m.    schedule 11.12.2017


Ответы (6)


Можно ли сделать тип члена напрямую зависимым от производного класса? Принимая во внимание тип результата функции-члена, объявленной с помощью auto (выведенный тип возвращаемого значения), это невозможно.

Таким образом, использование типа, как вы делаете в вашем решении, является лучшим и единственным решением.

Причина в том, что базовый класс должен быть полным типом при определении производного класса: компилятор должен сначала создать экземпляр и проанализировать определение базового класса, прежде чем анализировать определение производного класса, Стандарт C++ N4140 [derived.class]/2 (жирный шрифт мой):

Тип, обозначенный спецификатором базового типа, должен быть типом класса, который не является не полностью определенным классом;[...]

person Oliv    schedule 12.12.2017
comment
Вы заметили это бессмысленное двойное отрицание не и inполностью! - person Oliv; 12.12.2017

Как насчет такого:

template <typename T, typename TypeId> class Base 
{
private:
    TypeId id;
public:
    Base() { id = 123; }
    TypeId getId() {return id;}
};

class Derived : public Base<Derived, int> {};
person Killzone Kid    schedule 10.12.2017
comment
Спасибо @killzone-ребенок. Да, это было бы проще, однако я ищу решения, которые не включают второй параметр шаблона для Base. Собственно это то, что у меня есть сегодня. Моя цель — сократить количество параметров шаблона для Base до одного. - person Flyer; 11.12.2017
comment
Как видите, первый параметр здесь не используется, я оставил его, потому что он был у вас изначально, и я подумал, что он может понадобиться вам позже. Вы можете просто template <typename TypeId> class Base и class Derived : public Base<int> {};, если хотите. - person Killzone Kid; 11.12.2017
comment
Я извлек суть реального кода, чтобы сосредоточиться на проблеме. Производные должны наследоваться от Базы. - person Flyer; 11.12.2017

Это немного упрощено, но вы платите за это определенную цену.

#include <any>

template <class T> class Base {
    std::any id; // expensive, but cannot have T::TypeId here
 public:
    Base() : id(123) {}
    auto getId() { 
         return std::any_cast<typename T::TypeId>(id); 
    } // T::TypeId is OK inside a member function
};

class Derived : public Base<Derived> {
public:
    using TypeId = int;
};
person n. 1.8e9-where's-my-share m.    schedule 10.12.2017
comment
Спасибо @n.m. Ваше решение хорошее, однако я не могу использовать std::any, потому что в моей текущей среде я ограничен С++ 14, и это критическое для производительности приложение. - person Flyer; 11.12.2017
comment
Вы можете написать свой собственный упрощенный any или использовать boost::any (или просто использовать void* в крайнем случае), но производительность в любом случае пострадает. - person n. 1.8e9-where's-my-share m.; 11.12.2017

Почему бы не изменить иерархию классов?

template <class T>
class Base : T {
    using TypeId = typename T::TypeId;
    TypeId id;
 public:
    Base() { id = 123; }
    TypeId getId() { return id; }
};

struct BasicDerived {
    using TypeId = int;
};


using Derived = Base<BasicDerived>;
person Guillaume Racicot    schedule 10.12.2017
comment
Я не уверен, что следую. Derived должен быть дочерним классом Base, использующим CRTP. - person Flyer; 11.12.2017
comment
@Flyer да, это больше не CRTP. Derived теперь является базовым классом, а Base — производным классом. - person Guillaume Racicot; 11.12.2017
comment
У меня есть несколько производных классов. База должна оставаться родительским классом. Хотя это было интересное размышление :) - person Flyer; 11.12.2017
comment
@Flyer, если вам нужно, чтобы Base оставался родительским классом, это действительно не применимо. - person Guillaume Racicot; 11.12.2017
comment
@Flyer Я предлагаю вам проверить дизайн класса на основе агента на cppcon 2017 talk. Речь идет о составлении интерфейса и реализации класса. - person Guillaume Racicot; 11.12.2017

На самом деле, я подумал еще немного... это не так уж неприятно:
У вас может быть структура связывания, которую можно даже написать в виде макроса, объявленного непосредственно перед реальным классом.
Структура связывания определяет перечисление. и неполное определение типа для реального класса.
Шаблон определен до всего этого, но использует имя типа для отсрочки своей зависимости, но он создается реальным классом и зависит только от структуры привязки.

template <class ThatClassWrapper>
class MyBase
{
protected:
    typedef typename ThatClassWrapper::TypeId TypeId;
    typedef typename ThatClassWrapper::RealClass ThatClass;
    TypeId typeIdValue;
    TypeId  GetTypeId() {   return typeIdValue; }
    std::vector<ThatClass*> storage;
};

class SomeClass;
namespace TypeIdBinding
{
    struct SomeClass
    {
        enum TypeId
        {
            hello, world
        };
        typedef ::SomeClass RealClass;
    };
}
class SomeClass: public MyBase<TypeIdBinding::SomeClass>
{
public:
    bool CheckValue(TypeId id)
    {   return id == typeIdValue;   }
};

Обратите внимание, что реальный класс использует TypeId, как определено в базе шаблонов, и именованные члены не видны напрямую. Вы можете исправить это, если шаблон Base будет получен из структуры привязки (подтверждено, что он компилируется таким образом). хотя мне на самом деле нравится, что в С++ 11 вы можете экспортировать или typedef только имя типа перечисления из другого пространства имен и использовать это имя типа в качестве префикса для членов перечисления, помогая избежать загрязнения имен.

person Gem Taylor    schedule 11.12.2017
comment
Спасибо за это другое предложение. Вы очень креативны :) Я согласен с тем, что это не так уж неприятно, но это уже не CRTP, и он теряет наследование Derived › Base. Как вы сказали, кажется, что я наткнулся на стену круговых зависимостей, и я думаю, что @oliv прав. - person Flyer; 13.12.2017

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

Не могли бы вы объявить фиктивный класс шаблона, который принимает Derived и TypeID? Хотя я не думаю, что это вам что-то даст.

Является ли TypeID: производным сопоставлением 1: 1? Не будет ли лучше представить это сопоставление 1: 1 с другим вспомогательным шаблоном для обратного поиска, полученного из TypeID? Обратите внимание, что для этого необходимо определить TypeID вне класса Derived.
Действительно ли TypeID нужно определять внутри класса? Может ли он использовать определение, переданное в Base, для поддержки существующего использования внутреннего typedef?

Можете ли вы включить двойное включение? Разделить или макросить ваше определение производного, чтобы typeid находился в определении базового класса, которое можно включить перед шаблоном? Этот DerivedBase может быть объявлен в пространстве имен и содержать ссылку typedef на полный класс Derived, чтобы Base мог найти его для ссылок.

person Gem Taylor    schedule 11.12.2017
comment
Спасибо за ваше предложение. Действительно, я мог бы сохранить второй параметр шаблона. Является ли TypeID: производным сопоставлением 1: 1? Да Не будет ли лучше представить это сопоставление 1:1 другим вспомогательным шаблоном для резервного поиска, полученного из TypeID? Вы имеете в виду, как это делает шаблон BaseTraits? Я не знаю. Это не кажется намного проще, чем второй параметр шаблона. Действительно ли TypeID нужно определять внутри класса? Нет. Я не понимаю дополнительный вопрос. Двойное включение или разделение? Это интересно. Что вы подразумеваете под обратной ссылкой typedef? - person Flyer; 11.12.2017