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?

2
Fantastic Mr Fox 19 Apr. 2018 im 13:27

4 Antworten

Beste Antwort

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.

1
Quentin 19 Apr. 2018 im 12:59

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
}
1
llllllllll 19 Apr. 2018 im 13:01

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.

0
Giulio Franco 19 Apr. 2018 im 11:27

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.

0
Gem Taylor 19 Apr. 2018 im 12:51