Ich wollte eine Klasse erstellen, die unter einer bestimmten Vorlageninstanziierung eine andere API verfügbar macht. Es hat allgemeine Funktionen, aber einige sollten deaktiviert werden, falls der Benutzer eine bestimmte Instanziierung der Klasse verwendet. Etwas wie das:
VarApi<T1> v1;
v1.common();
v1.funcA1();
// v1.funcA2(); // ERROR
v1.funcA1_2();
VarApi<T2> v2;
v1.common();
// v2.funcA1(); // ERROR
v2.funcA2();
v2.funcA1_2();
VarApi<T3> v3;
v3.common();
// v2.funcA1(); // ERROR
// v2.funcA2(); // ERROR
// v1.funcA1_2(); // ERROR
Ich habe festgestellt, dass Sie dies mit SFINAE
und std::enable_if
wie folgt erreichen können:
enum Type { T1, T2, T3 };
template <Type TType> struct VarApi {
void common() { }
template <Type T = TType,
typename = typename std::enable_if<T == T1>::type>
void funcA1() { }
template <Type T = TType,
typename = typename std::enable_if<T == T2>::type >
void funcA2() { }
template <Type T = TType,
typename = typename std::enable_if<T == T1 || T == T2>::type >
void funcA1_2() { }
template <Type T = TType,
typename = typename std::enable_if<T == T3>::type >
void funcA3() { }
};
Dies funktioniert und erreicht die oben genannte Funktionalität. Das Problem ist, dass der Benutzer dies immer noch überschreiben kann mit:
VarApi<T2> v2;
v2.funcA1<T1>(); // NOT ERROR
Gibt es eine Möglichkeit, diesen Fall zu verhindern?
4 Antworten
Dies funktioniert und erreicht die oben genannte Funktionalität. Das Problem ist, dass der Benutzer dies immer noch überschreiben kann mit:
VarApi<T2> v2; v2.funcA1<T1>(); // NOT ERROR
Gibt es eine Möglichkeit, diesen Fall zu verhindern?
Sicher.
Sie können festlegen, dass T
und TType
vom gleichen Typ sind
template <Type T = TType,
typename = typename std::enable_if<
std::is_same<T, T1>::value
&& std::is_same<T, TType>::value>::type>
void funcA1() { }
Dies verhindert das "Hijacking" der Vorlage.
Sie können die Vererbung ausnutzen, um die gewünschten Funktionen bereitzustellen. Mit CRTP greifen Sie über den self
Zeiger auf die Funktionalität der ursprünglichen Klasse im func_provider
zu.
template<class T, class Derived> struct func_provider;
template<class Derived>
struct func_provider<int, Derived> {
void funcA1() {
auto self = static_cast<Derived*>(this);
// do something with self
}
};
template<class Derived> struct func_provider<double, Derived> { void funcA2() {} };
template<class T>
struct foo : public func_provider<T, foo<T>> {};
int main() {
foo<int> f;
foo<double> g;
f.funcA1();
// f.funcA2(); // Error
g.funcA2();
// g.funcA1(); // Error
}
BEARBEITEN:
Diese Version ermöglicht es dem Benutzer, Funktionen für mehrere Typen an einem Ort zu implementieren. Der Benutzer kann Typen miteinander kombinieren:
template<class... Ts> struct types {};
template<class Types, class T> struct is_in : public std::false_type {};
template<class... Ts, class T>
struct is_in<types<T, Ts...>, T> : public std::true_type {};
template<class... Ts, class T0, class T>
struct is_in<types<T0, Ts...>, T> : public is_in<types<Ts...>, T> {};
template<class Derived, bool B, class T> struct func_provider {};
template<class Derived, class T, class... Ts>
struct func_collector
: public func_provider<Derived, is_in<Ts, T>::value, Ts>...
{};
// implement functions for int
template<class Derived>
struct func_provider<Derived, true, types<int>> {
void funcA1() {
auto self = static_cast<Derived*>(this);
// do something with self
}
};
// implement functions for double
template<class Derived>
struct func_provider<Derived, true, types<double>> { void funcA2() {} };
// implement functions for both int and double
template<class Derived>
struct func_provider<Derived, true, types<int, double>> { void funcA1_2() {} };
template<class T>
struct foo : public func_collector<foo<T>, T,
// pull desired functions
types<int>, types<double>, types<int, double>>
{
void common() {}
};
int main() {
foo<int> f;
foo<double> g;
f.common();
f.funcA1();
f.funcA1_2();
// f.funcA2(); // Error
g.funcA2();
g.funcA1_2();
// g.funcA1(); // Error
}
Lösung 1
Eine Möglichkeit, das zu erreichen, wonach Sie fragen, besteht darin, die optionale Funktionalität mithilfe von Vorlagenspezialisierung und abhängigen Basisklassen anzubieten.
// I'm using E for enum. I find TType a bit misleading, since T usually stands for Type
template< Type EType >
struct VarApiBase { }; // empty by default
template< >
struct VarApiBase<T1> {
void funcA1() { }
};
template< >
struct VarApiBase<T2> {
void funcA2() { }
};
template <Type TType>
struct VarApi : VarApiBase<TType> {
void funcA1_2() { }
};
template <>
struct VarApi<T3> { };
Ich mag diese Lösung nicht besonders. Weil es komplex wird, gemeinsam genutzte Funktionen bereitzustellen (ich habe funcA1_2
in VarApi und nicht in die Basis eingefügt und dann VarApi erneut spezialisiert, um es für T3 zu deaktivieren, aber dies zwingt Sie dazu, sich jedes Mal explizit zu spezialisieren, wenn Sie eine neue hinzufügen EType-Wert. Sie könnten ihn mit einem Enabler für die Spezialisierung umgehen, aber er wird wieder komplex, wenn Sie eine komplexere Freigabe haben.
Wenn Sie es brauchen, können Sie VarApiBase
Zugriff auf VarApi
gewähren, indem Sie es in VarApi
als Freund deklarieren.
Lösung 2
Als billige Alternative zu all dem können Sie einfach ein static_assert
in Ihre Funktionen einfügen:
template <Type ETypeInner = EType >
void funcA1_2() {
static_assert(ETypeInner==EType);
static_assert(EType == T1 || EType == T2);
}
Wenn Sie SFINAE wirklich benötigen, können Sie die Bedingung ==T1 || ==T2
trotzdem in die Vorlage einfügen
template <Type ETypeInner = EType,
typename = typename std::enable_if<ETypeInner == T1 || ETypeInner == T2>::type >
void funcA1_2() {
static_assert(ETypeInner==EType);
}
Beachten Sie jedoch, dass die Kompilierung dadurch langsamer wird.
Lösung 3
Der sauberste Weg wäre wahrscheinlich, explizite Spezialisierungen und Dienstprogrammfunktionen zu haben.
In VarApi.h:
struct VarApiImpl;
template< Type EType >
struct VarApi; // undefined
// Ideally, VarApiCommon shouldn't need to be a template
template< Type EType >
struct VarApiCommon {
// you can put here members and functions which common to all implementations, just for convenience.
void common() { /* ... */ }
private:
// You can do this if you need access to specialization-specific members.
// Ideally, if a function is common, it should only need common members, though.
VarApi<EType> & Derived() { return static_cast<VarApi<EType>&>(*this); }
VarApi<EType> const& Derived() const { return static_cast<VarApi<EType> const&>(*this); }
};
template<>
struct VarApi<T1> : VarApiCommon<T1> {
friend VarApiImpl;
friend VarApiCommon<T1>;
void funcA1();
void funcA1_2();
};
template<>
struct VarApi<T2> : VarApiCommon<T2> {
friend VarApiImpl;
friend VarApiCommon<T2>;
void funcA2();
void funcA1_2();
};
template<>
struct VarApi<T3> : VarApiCommon<T3> {
friend VarApiCommon<T3>;
};
In VarApi.cpp:
struct VarApiImpl final {
// Here go the functions which are only shared by some specializations
template< Type EType >
static void funcA1_2(VarApi<EType>& vapi) {
// Just for sanity. Since this function is private to the .cpp, it should be impossible to call it inappropriately
static_assert(EType==T1 || EType==T2);
// ...
}
};
void VarApi<T1>::funcA1() { /* ... */ }
void VarApi<T1>::funcA1_2() { VarApiImpl::funcA1_2(*this); }
void VarApi<T2>::funcA2() { /* ... */ }
void VarApi<T2>::funcA1_2() { VarApiImpl::funcA1_2(*this); }
Es wird so ausführlich wie C ++ sein kann, aber zumindest haben Sie explizite Schnittstellen, die klar angeben, was angeboten wird und was nicht, ohne eine Reihe von enable_if
lesen zu müssen.
Lösung 4
Letztendlich würde ich Ihnen empfehlen, Ihre Anforderungen genauer zu betrachten, um festzustellen, ob sie nicht als richtige Klassenhierarchie ausgedrückt werden können, basierend auf den Merkmalen, die jeder Aufzählungswert darstellt. C ++ hat sogar eine virtuelle Vererbung, wenn Sie doppelte Basen vermeiden müssen. Zum Beispiel wäre das in Ihrem Beispiel möglich:
struct VarApiCommon {
void common();
};
struct VarApi12 : VarApiCommon {
void funcA1_2();
};
template< Type EType >
struct VarApi; // undefined
template<>
struct VarApi<T1> : VarApi12 {
void funcA1();
};
template<>
struct VarApi<T2> : VarApi12 {
void funcA2();
};
template<>
struct VarApi<T2> : VarApiCommon {
void funcA3();
};
Wenn Sie beispielsweise eine funcA2_3 hatten, können Sie dies möglicherweise immer noch folgendermaßen tun:
struct VarApiCommon {
void common();
};
struct VarApi12 : virtual VarApiCommon {
void funcA1_2();
};
struct VarApi23 : virtual VarApiCommon {
void funcA2_3();
};
template< Type EType >
struct VarApi; // undefined
template<>
struct VarApi<T1> : VarApi12 {
void funcA1();
};
template<>
struct VarApi<T2> : VarApi12, VarApi23 {
void funcA2();
};
template<>
struct VarApi<T2> : VarApi23 {
void funcA3();
};
Viel hängt von den Mitgliedern ab.
Mein Vorschlag basiert darauf, dass Sie die Implementierung bereitstellen können, sie aber ausblenden möchten.
Haben Sie eine Basisimplementierung, die alles implementiert
template <class X> class Base
{
public:
void A();
void B();
void C();
void D();
void E();
};
Haben Sie eine abgeleitete Klasse, die geschützt erbt, dann aber alle gängigen Methoden von der Basis öffentlich veröffentlicht
template <class X> class Mid: protected Base<X>
{
public:
using Base::A;
using Base::B;
using Base::C;
// D & E are contentious
};
Haben Sie die tatsächlich veröffentlichte Klasse, in der jede Variante T1, T2, T3 spezialisiert ist.
Diese Klassen erben alle öffentlich von der zweiten Klasse, aber dann veröffentlicht ein öffentlicher Freund die Methoden, die sie unterstützen.
template <class X> class Top: public Mid<X> {};
template <> class Top<X1>: public Mid<X1>
{
public:
using Base::D;
// Can't get E
};
template <> class Top<X2>: public Mid<X2>
{
public:
// Can't get D
using Base::E;
};
Gewinne: Auf die Methoden, die Sie ausblenden möchten, kann nicht zugegriffen werden. Es gibt keine Magie für Vorlagenfunktionen.
Verluste: Die Regeln für die Veröffentlichung sind willkürlich und werden überhaupt nicht von 'lesbaren' FINAE bestimmt. Sie können die Vererbung auch nicht einfach zum Erstellen von Regeln verwenden, obwohl Sie möglicherweise ein zweites LikeX-Vorlagenargument ausführen können.
Verwandte Fragen
Neue Fragen
c++
C ++ ist eine universelle Programmiersprache. Es wurde ursprünglich als Erweiterung von C entwickelt und hat eine ähnliche Syntax, ist aber jetzt eine völlig andere Sprache. Verwenden Sie dieses Tag für Fragen zu Code, der mit einem C ++ - Compiler kompiliert werden soll. Verwenden Sie ein versionsspezifisches Tag für Fragen zu einer bestimmten Standardversion [C ++ 11], [C ++ 14], [C ++ 17], [C ++ 20] oder [C ++ 23] usw. .