2016-07-02 21 views
1

In einen DSL-Gestaltung (die in C kompiliert ++), fand ich es bequem, eine Wrapper-Klasse, die, uppon Zerstörung zu definieren, eine .free() Methode auf dem enthaltenen Klasse aufrufen würde:Implizit Guss Hüllklasse in templated Aufruf supperclass

Der Wrapper ist vollständig transparent: Alle Methoden, Überladungen und Konstruktoren sind geerbt von T (zumindest meines Wissens), aber wenn sie in den Wrapper enthalten ist, wird die free() -Methode aufgerufen Zerstörung. Beachten Sie, dass ich die Verwendung des Destruktors T explizit vermeiden möchte, da T::free() und ~T() unterschiedliche Semantik haben können!

All dies funktioniert einwandfrei, bis eine umbrochene Klasse als Mitglied für einen nicht referenzierten Aufruf verwendet wird. An diesem Punkt wird freeOnDestroy instanziiert, wobei das umschlossene Objekt frei aufgerufen wird. Was ich würde wie passieren, ist für die templated Methode, T anstelle von freeOnDestroy<T> zu verwenden, und den Parameter implizit in die Supperclass zu werfen. Das folgende Codebeispiel veranschaulicht dieses Problem:

// First class that has a free (and will be used in foo) 
class C{ 
    int * arr; 
public: 
    C(int size){ 
     arr = new int[size]; 
     for (int i = 0; i < size; i++) arr[i] = i; 
    } 
    int operator[] (int idx) { return arr[idx]; } 
    void free(){ cout << "free called!\n"; delete []arr; } 
}; 

// Second class that has a free (and is also used in foo) 
class V{ 
    int cval; 
public: 
    V(int cval) : cval(cval) {} 
    int operator[] (int idx) { return cval; } 
    void free(){} 
}; 

// Foo: in this case, accepts anything with operator[int] 
// Foo cannot be assumed to be written as T &in! 
// Foo in actuality may have many differently-templated parameters, not just one 
template<typename T> 
void foo(T in){ 
    for(int i = 0; i < 5; i++) cout << in[i] << ' '; 
    cout << '\n'; 
} 

int main(void){ 
    C c(15); 
    V v(1); 
    freeOnDestroy<C> f_c(15); 
    foo(c); // OK! 
    foo(v); // OK! 
    foo<C>(f_c); // OK, but the base (C) of f_c may not be explicitly known at the call site, for example, if f_c is itself received as a template 
    foo(f_c); // BAD: Creates a new freeOnDestroy<C> by implicit copy constructor, and uppon completion calls C::free, deleting arr! Would prefer it call foo<C> 
    foo(f_c); // OH NO! Tries to print arr, but it has been deleted by previous call! Segmentation fault :(
    return 0; 
} 

Einige nicht-Lösungen sollte ich erwähnen sind:

  • machen freeOnDestroy::freeOnDestroy(const freeOnDestroy &src) expliziten und privat, aber dies scheint T ‚s Konstruktor außer Kraft zu setzen. Ich hatte gehofft, es würde versuchen, es implizit in T konvertieren und das als Vorlage Argument verwenden.
  • Angenommen foo erhält einen Verweis seiner Templat-Argumente (wie in void foo(T &in): Dies ist weder der Fall ist, noch in einigen Fällen wünschenswert
  • immer den Aufruf foo explizit Vorlage, wie in foo<C>(f_c): f_c selbst als Templat werden können, so Es ist schwer zu wissen, foo mit C instanziieren (ja, dies könnte mit mehreren Versionen von foo getan werden, um die Wrapper eins nach dem anderen zu entfernen, aber ich kann keinen Weg finden, dies zu tun, ohne eine andere Überladung für jede Vorlage erstellen Argument foo)

Zusammengefasst ist meine Frage: Gibt es eine saubere (ish) -Methode, um sicherzustellen, dass eine Basisklasse in ihre Superklasse gegossen wird, wenn eine Vorlage aufgelöst wird? Oder, wenn nicht, gibt es eine Möglichkeit, SFINAE zu verwenden, indem ein Substitutionsfehler verursacht wird, wenn das Template-Argument eine Instanz der Wrapper-Klasse ist, und somit gezwungen wird, die implizite Umwandlung in die Wrapped-Klasse zu verwenden (ohne jede foo-like Methodensignatur möglicherweise Dutzende Male)?

Ich habe derzeit eine Arbeit-Umgebung, die Änderungen in der DSL beinhaltet, aber ich bin nicht ganz glücklich damit, und war neugierig, ob es überhaupt möglich war, eine Wrapper-Klasse zu entwerfen, die wie beschrieben funktioniert.

Antwort

0

Eine Lösung, die, wenn auch ein wenig hässlich, zu funktionieren scheint, ist eine überladene Abwickeln Methode zu verwenden, wie zum Beispiel:

int main(void){ 
    C c(15); 
    V v(1); 
    freeOnDestroy<C> f_c(15); 
    foo(freeOnDestroyUnwrapper(c)); 
    foo(freeOnDestroyUnwrapper(v)); 
    foo<C>(freeOnDestroyUnwrapper(f_c)); 
    foo(freeOnDestroyUnwrapper(f_c)); 
    foo(freeOnDestroyUnwrapper(f_c)); 
    return 0; 
} 
:

template<typename T> T freeOnDestroyUnwrapper(const T &in){ return in; } 
template<typename T> T freeOnDestroyUnwrapper(const freeOnDestroy<T> &in){ return in; } 
template<typename T> T freeOnDestroyUnwrapper(const freeOnDestroy<typename std::decay<T>::type> &in){ return in; } 
template<typename T> T& freeOnDestroyUnwrapper(T &in){ return in; } 
template<typename T> T& freeOnDestroyUnwrapper(freeOnDestroy<T> &in){ return in; } 
template<typename T> T& freeOnDestroyUnwrapper(freeOnDestroy<typename std::decay<T>::type> &in){ return in; } 

Dann können Anrufe mit dem Unwrapper gemacht werden

Oder dies weniger ausführlich zu machen, wir foo es so verändern tut dies für uns:

template<typename T> 
void _foo(T in){ 
    for(int i = 0; i < 5; i++) cout << in[i] << ' '; 
    cout << '\n'; 
} 
template<typename... Ts> 
void foo(Ts&&... args){ 
    _foo(freeOnDestroyUnwrapper(args)...); 
} 

Und dann nennt es als normal:

int main(void){ 
    C c(15); 
    V v(1); 
    freeOnDestroy<C> f_c(15); 
    foo(c); 
    foo(v); 
    //foo<C>(f_c); // This now doesn't work! 
    foo(f_c); 
    foo(f_c); 
    return 0; 
} 

Diese für eine beliebige Anzahl von Argumenten zu funktionieren scheinen foo haben (von verschiedenen Vorlagen, falls erforderlich), und in geeigneter Weise zu verhalten scheint, wenn foo s Eingang eine Referenz (das tritt in meinem Kontext nicht auf, wäre aber gut, um diese Lösung generisch zu machen.

Ich bin nicht davon überzeugt, dass dies die beste Lösung ist, oder dass es auf jeden Fall verallgemeinert, plus, die Verdoppelung aller Deklarationen ist ein wenig umständlich und undurchsichtig für die meisten IDEs Autocomplete-Funktionen. Bessere Lösungen und Verbesserungen sind willkommen!

1

Das Problem hier nicht, wenn "Wrapped-Klasse als Mitglied zu einem nicht referenzierten Aufruf verwendet wird".

Das Problem hier ist, dass die Vorlage Wrapper - und wahrscheinlich seine Oberklasse auch - violated the Rule Of Three hat.

Das Übergeben einer Instanz der Klasse als Nicht-Referenzparameter ist nur eine andere Art zu sagen "nach Wert". Bei der Übergabe nach Wert wird eine Kopie der Instanz der Klasse erstellt.Weder Ihre Template-Klasse noch ihre eingepackte Klasse haben einen expliziten Copy-Konstruktor. Daher weiß die kopierte Instanz der Klasse nicht, dass es sich um eine Kopie handelt. Daher tut der Destruktor, was er zu tun glaubt.

Die richtige Lösung hier ist nicht etwas zu hacken, die eine freeOnDestroy<T> Wert vorbei Kopie T, anstatt freeOnDestroy<T> Kopie macht. Die richtige Lösung besteht darin, sowohl der freeOnDestroy Vorlage als auch eventuell einer übergeordneten Klasse, die sie verwendet, einen richtigen Kopierkonstruktor und den Zuweisungsoperator hinzuzufügen, so dass alles der Regel der Drei entspricht.

+0

Hatte nicht an diese Alternative gedacht, und ja, das Hinzufügen einer 'bool isCopy' und das Initialisieren/Überprüfen es passend zu' freeOnDestroy' löst das Problem, obwohl auf Kosten eines extra boolean und eine Verzweigung für jedes Mal die Die aufgerufene Funktion wird zurückgegeben - wahrscheinlich nicht besonders teuer (obwohl ich ein wenig besorgt bin, könnte die zusätzliche Größe Block-Missausrichtung verursachen und den C++ - Compiler-Optimierer verwirren, der dann SEHR teuer sein könnte). Was gutes Design angeht, ist dies eine korrekte Lösung. –

0

Sie einen richtig definiert Detektor und eine sfinae d Funktion verwenden können, wie folgt:

#include<iostream> 
#include<type_traits> 

template<class T> 
class freeOnDestroy : public T { 
    using T::T; 
public: 
    operator T&() const { return *this; } 
    ~freeOnDestroy() { T::free(); } 
}; 

template<typename T> 
struct FreeOnDestroyDetector: std::false_type { }; 

template<typename T> 
struct FreeOnDestroyDetector<freeOnDestroy<T>>: std::true_type { }; 

class C{ 
    int * arr; 
public: 
    C(int size){ 
     arr = new int[size]; 
     for (int i = 0; i < size; i++) arr[i] = i; 
    } 
    int operator[] (int idx) { return arr[idx]; } 
    void free(){ std::cout << "free called!\n"; delete []arr; } 
}; 

class V{ 
    int cval; 
public: 
    V(int cval) : cval(cval) {} 
    int operator[] (int idx) { return cval; } 
    void free(){} 
}; 

template<typename..., typename T> 
std::enable_if_t<not FreeOnDestroyDetector<std::decay_t<T>>::value> 
foo(T in) { 
    std::cout << "here you have not a freeOnDestroy based class" << std::endl; 
} 

template<typename..., typename T> 
std::enable_if_t<FreeOnDestroyDetector<std::decay_t<T>>::value> 
foo(T &in) { 
    std::cout << "here you have a freeOnDestroy based class" << std::endl; 
} 

int main(void){ 
    C c(15); 
    V v(1); 
    freeOnDestroy<C> f_c(15); 
    foo(c); 
    foo(v); 
    foo<C>(f_c); 
    foo(f_c); 
    foo(f_c); 
    return 0; 
} 

Wie Sie, indem Sie das Beispiel sehen können, ist free nur einmal aufgerufen, das heißt für die freeOnDestroy erstellt in der main Funktion.
Wenn Sie auf jeden Fall freeOnDestroy als Parameter verbieten wollen, können Sie eine einzelne Funktion wie die folgenden verwenden:

template<typename..., typename T> 
void foo(T &in) { 
    static_assert(not FreeOnDestroyDetector<std::decay_t<T>>::value, "!"); 
    std::cout << "here you have a freeOnDestroy based class" << std::endl; 
} 

Bitte beachte, dass ich einen variadische Parameter als Wächter hinzugefügt, so dass man nicht mehr foo<C>(f_c); verwenden um zu erzwingen, dass ein Typ verwendet wird.
Entfernen Sie es, wenn Sie einen solchen Ausdruck zulassen möchten. Es war nicht klar aus der Frage.

+0

Es ist nicht wirklich erwünscht, die Verwendung umgebrochener Klassen zu verbieten, da es das Ziel ist, dass sie in Funktionsaufrufen so verwendet werden, als wären sie die eingepackte Klasse "T", nicht um sie vollständig zu verbieten!Wie in der Frage erwähnt, wird durch das Verursachen von "in" einer Referenz, wie etwa "foo (T & in)", das unmittelbare Problem gelöst (selbst ohne den Wrapper explizit zu erkennen!), Aber die Semantik des Pass-by-Value wird verloren erforderlich sein, damit 'foo' richtig funktioniert. Beachten Sie, dass, wie erwartet, das Entfernen des '&' von Ihrer zweiten Definition von foo den Code in das vorherige Verhalten zurückversetzt, indem '.free()' zweimal aufgerufen wird. –

+0

@ AndréHarder Nun, ja, aber im Beispiel gibt es ** zwei ** * sfinae * d 'foo 'Funktionen. Ersteres erhält seinen Parameter nach Bedarf, während letzterer es durch Referenz erhält. Beachten Sie, dass letzteres der ist, der erkennt, ob ein "freeOnDestroy" -Wrapper bereits existiert. – skypjack

+0

Wahr genug, und es ist definitiv besser als mit einem einzigen 'foo (T & in)'! Wir verlieren jedoch immer noch die Wertemethode für den Wert, wenn wir den Wrapper haben. –