Ответ 3
После некоторых я пришел с решением, которое работает событие для частных членов шаблонных производных классов. Он не решает проблему не предоставления всем членам производного класса базы, поскольку он использует объявление friend
для всего класса. С другой стороны, для простого случая это не требует повторения базового имени и параметров шаблона и всегда будет работать.
Сначала простой случай, когда производное не является шаблоном. База принимает дополнительный параметр шаблона void
, чтобы показать, что все еще работает в случае дополнительных параметров шаблона базы. Единственный необходимый, согласно CRTP, это typename Derived
.
//Templated variadic base
template <typename Derived, typename...>
struct Interface
{
using CRTP = Interface; //Magic!
void f() { static_cast<Derived*>(this)->f(); }
};
//Simple usage of the base with extra types
//This can only be used when the derived is NON templated
class A : public Interface<A, void>
{
friend CRTP;
void f() {}
};
Единственное, что нужно для этого, - это объявление using CRTP = Interface;
в базе и объявление friend CRTP;
в производном.
Для случая, когда производная сама является шаблонной, ситуация сложнее. Мне понадобилось некоторое время, чтобы прийти к решению, и я уверен, что оно все еще не идеально.
Большая часть магии происходит внутри этих шаблонов:
namespace CRTP
{
template <template <typename, typename...> class _Base, typename _Derived, typename... _BaseArgs>
struct Friend { using Base = _Base<_Derived, _BaseArgs...>; };
template <template <typename, typename...> class _Base, typename ..._BaseArgs>
struct Base
{
template <template <typename...> class _Derived, typename... _DerivedArgs>
struct Derived : public _Base<_Derived<_DerivedArgs...>, _BaseArgs...> {};
};
}
Их использование более или менее просто. Для двух использованных выше шаблонов необходимо выполнить несколько шагов.
Во-первых, при наследовании в производном классе необходимо указать базовый класс наследуемого и его необязательные параметры. Это делается с помощью CRTP::Base<MyBase, BaseOptional....>
, где MyBase
- это имя класса, используемого для CRTP, а BaseOptional...
- это параметры шаблона, которые передаются в базовый класс как есть, непосредственно после передачи нашего производного класса, который предоставляется на следующем этапе. Если базовый класс не принимает никаких дополнительных параметров шаблона, они могут быть полностью опущены: CRTP::Base<MyBase>
.
Следующим шагом является представление производного класса (весь смысл CRTP). Это можно сделать, следуя приведенному выше CRTP::Base<...>
с помощью ::Derived<ThisDerived, DerivedOptional...>
. Где ThisDerived
- это класс, в котором он определен, а DerivedOptional...
- все параметры шаблона, объявленные в объявлении этого класса template
. Необязательные параметры должны быть указаны точно, как они указаны в объявлении класса template
.
Последний шаг - объявить базовый класс как friend
. Это делается путем объявления friend typename CRTP::Friend<MyBase, ThisDerived, BaseOptional...>::Base
где-то в классе. Шаблонные параметры BaseOptional...
должны повторяться в точности так, как они появляются в CRTP::Base<MyBase, BaseOptional...>
, который унаследован от.
Ниже приведен пример использования шаблонного производного, когда основание не зависит от шаблонных типов (но оно все еще может принимать другие параметры шаблона, void
в этом примере).
//Templated derived with extra, non-dependant types, passed to the base
//The arguments passed to CRTP::Base::Derived<, ARGS> must exactly match
// the template
template <typename T, typename... Args>
class B : public CRTP::Base<Interface, void>::Derived<B, T, Args...>
{
friend typename CRTP::Friend<Interface, B, void>::Base;
void f() {}
};
Далее приведен пример, когда база зависит от параметров шаблона производного. Единственное отличие от предыдущего примера - ключевое слово template
. Эксперимент показывает, что, если ключевое слово указано для предыдущего, независимого случая, код также соответствует требованиям.
//Templated derived with extra dependant types passed to the base
//Notice the addition of the "template" keyword
template <typename... Args>
class C : public CRTP::Base<Interface, Args...>::template Derived<C, Args...>
{
friend typename CRTP::Friend<Interface, C, Args...>::Base;
void f() {}
};
Обратите внимание, что эти шаблоны не работают для производных классов без шаблонов. Я обновлю этот ответ, когда найду решение, чтобы единый синтаксис мог использоваться для всех случаев. Самое близкое, что можно сделать, это просто использовать какой-то поддельный параметр шаблона. Обратите внимание, что он все еще должен быть назван и передан в механизм CRTP. Например:
template <typename Fake = void>
class D : public CRTP::Base<Interface>::Derived<D, Fake>
{
friend typename CRTP::Friend<Interface, D>::Base;
void f() {}
};
Обратите внимание, что A
, B
, C
и D
объявляется как class
. То есть все их участники являются частными.
Ниже приведен код, который использует перечисленные выше классы.
template <typename... Args>
void invoke(Interface<Args...> & base)
{
base.f();
}
int main(int, char *[])
{
{
A derived;
//Direct invocation through cast to base (derived.f() is private)
static_cast<A::CRTP &>(derived).f();
//Invocation through template function accepting the base
invoke(derived);
}
{
B<int> derived;
static_cast<B<int>::CRTP &>(derived).f();
invoke(derived);
}
{
C<void> derived;
static_cast<C<void>::CRTP &>(derived).f();
invoke(derived);
}
{
D<void> derived;
static_cast<D<>::CRTP &>(derived).f();
invoke(derived);
}
return 0;
}
Автономная шаблонная функция invoke
работает для любого класса, полученного из базы.
Также показано, как привести производное к базе без необходимости фактически указывать имя базы.
Удивительно, но это не зависит от системных заголовков.
Полный код доступен здесь: https://gist.github.com/equilibr/b27524468a0519aad37abc060cb8bc2b
Комментарии и исправления приветствуются.