2014-02-11 7 views
14

Es gibt ein Muster in einigen Legacy-Test-Framework-Code, der auf Visual C++ gebrochen zweiphasigen Look-up, verursacht Kopfschmerzen bei der Portierung auf andere Compiler. Es gibt zahlreiche Lösungen, die ich kenne, um das Problem zu beheben, aber alle erfordern "umfangreiche" strukturelle Änderungen.zwang unqualifizierte Namen zu abhängigen Werten

Während ich mir ziemlich sicher bin, dass es nicht gibt, bin ich neugierig, ob es einen "einfachen" Hack geben kann, der das gewünschte Verhalten in standardkonformen Compilern mit einem sehr kleinen Satz notwendiger Änderungen bringt.

Das Muster wird in diesem Beispiel zu sehen:

#include <cstdio> 

// global "system" function to test; generally something like `fopen` in a real test 
const char* GetString() { return "GLOBAL"; } 

// provides no overrides of the standard system functions being tested 
struct NoOverrides {}; 

// set of functions overriding the system functions being tested 
struct TestOverrides { 
    // if this were `fopen` this might be a version that always fails 
    static const char* GetString() { return "OVERRIDE"; } 
}; 

// test case 
template <typename Overrides> 
struct Test : private Overrides { 
    void Run() { 
    // call to GetString is not dependent on Overrides 
    printf("%s\n", GetString()); 
    } 
}; 

int main() { 
    // test with no overrides; use the system functions 
    Test<NoOverrides> test1; 
    test1.Run(); 

    // test with overrides; use test case version of system functions 
    Test<TestOverrides> test2; 
    test2.Run(); 
} 

Die Idee ist, dass es globale Funktionen, in der Regel etwas in einem System-Header definiert (wie eine ANSI-C-Funktion oder OS bereitgestellte Funktion). Es gibt dann einen Typ, der eine Reihe alternativer Versionen davon als statische Elementfunktionen definiert. Ein Test kann entweder vom Typ mit diesen alternativen Versionen oder von einem Typ ohne Alternativen geerbt werden.

Mit der zweiphasigen Suche von Visual C++ werden die unqualifizierten Aufrufe der zu testenden Systemfunktionen bis zur Instanziierung der Vorlage verzögert. Wenn der Überschreibtyp TestOverrides ein Basistyp des Typs Test ist, wird die statische Elementversion von GetString gefunden. Bei anderen Compilern, die Zweiphasen-Lookup ordnungsgemäß implementieren, wird die Version der freien Funktion während der ersten Analyse gefunden und ist bereits zu dem Zeitpunkt aufgelöst, zu dem die Vorlage instanziiert wird.

Mir sind einige relativ intrusive Lösungen für dieses Problem bekannt. Eine wäre, die NoOverrides Art tatsächlich Wrapper zu haben, die die freien Funktionen aufrufen und dann den Anruf zu GetString qualifizieren, um den Overrides Vorlagenparameter, der mein erster Instinkt ist. Beispiel:

#include <cstdio> 

const char* GetString() { return "GLOBAL"; } 

// wrappers to invoke the system function versions 
struct NoOverrides { 
    static const char* GetString() { return ::GetString(); } 
}; 

struct TestOverrides { 
    static const char* GetString() { return "OVERRIDE"; } 
}; 

template <typename Overrides> 
struct Test { 
    void Run() { 
    // call to GetString is made dependent on template type Overrides 
    printf("%s\n", Overrides::GetString()); 
    } 
}; 

int main() { 
    // test with the default overrides; use the system functions 
    Test<NoOverrides> test1; 
    test1.Run(); 

    Test<TestOverrides> test2; 
    test2.Run(); 
} 

Offensichtlich gibt es funktionierende Lösungen für die Zweiphasen-Suche. Eine gute Anzahl dieser Tests kann ziemlich komplex sein und würde eine Menge Arbeit erfordern, um eine Struktur wie die, die ich gerade bereitgestellt habe, zu verwenden. Ich bin neugierig, ob es eine andere Lösung gibt, die weniger strukturelle Änderungen an dem Code erfordert, an den ich nicht denke.

+2

Was SFINAE? Z.B. Fügen Sie in 'Test' etwas hinzu, wie' template decltype (T :: GetString()) GetString (int); declltype (:: GetString()) GetString (kurz); ' – dyp

+1

@dyp: Ja, das könnte funktionieren. Obwohl ich es als eine noch aufdringlichere Änderung betrachten würde, als nur die statischen Wrapper in 'NoOverrides' hinzuzufügen, vor allem angesichts der Tatsache, wie unbequem der durchschnittliche C++ - Programmierer (meiner Erfahrung nach) mit Konzepten wie SFINAE ist. –

+0

habe ich sogar ausprobiert: '-fms-extensions' [mit clang] (http://clang.llvm.org/docs/UsersManual.html#microsoft-extensions) aber leider haben sie das falsch verstanden (es druckt' GLOBAL' zweimal mit Ihrem Testprogramm). Also ja, Portierung wird ein Problem sein. –

Antwort

2

Hier ist meine am wenigsten intrusive Version ohne SFINAE. Getestet mit Apple LLVM 5.1 und g ++ 4.8.2. Dies ist nur eine Realisierung der allgemeinen Idee, die unter dem Code beschrieben wird.

#include <cstdio> 
// global "system" function to test; generally something like `fopen` in a real test 
const char* GetString() { return "GLOBAL"; } 

// list all global functions that could possibly be overridden by function pointers 
// (initialize them with the original function) and provide template overriding function. 
struct PossibleOverridesList 
{ 
    decltype(&::GetString) GetString = &::GetString; 

    template<typename O> 
    void setOverrides() { 
     O::setMyOverrides(this); 
    }; 

}; 

// provides no overrides of the standard system functions being tested 
// (setOverrides method does nothing) 
struct NoOverrides { static void setMyOverrides(PossibleOverridesList* ol){}; }; 

// set of functions overriding the system functions being tested 
// (setOverrides method sets the desired pointers to the static member functions) 
struct TestOverrides { 
    // if this were `fopen` this might be a version that always fails 
    static const char* GetString() { return "OVERRIDE"; } 
    static void setMyOverrides(PossibleOverridesList* ol) { ol->GetString = &GetString; }; 
}; 

// test case (inheritance doesn't depend on template parameters, so it gets included in the lookup) 
struct Test : PossibleOverridesList { 
    void Run() { 
    printf("%s\n", GetString()); 
    } 
}; 

int main() { 
    // test with no overrides; use the system functions 
    Test test1; 
    test1.setOverrides<NoOverrides>(); 
    test1.Run(); 

    // test with overrides; use test case version of system functions 
    Test test2; 
    test2.setOverrides<TestOverrides>(); 
    test2.Run(); 
} 

Meine Idee ist folgende: Da mein Verständnis ist, dass Sie wollen innerhalb Run() den Code nicht ändern, gibt es keine Möglichkeit, den nicht modifizierten Namen von einem Template-Klasse zu suchen. Daher müssen einige (aufrufbare) Platzhalterobjekte in Geltungsbereich sein, wenn GetString nachgeschlagen wird, innerhalb derer wir noch entscheiden können, welche Funktion aufgerufen werden soll (wenn GetString() innerhalb einmal an die globale GetString() bindet, können wir später nichts tun, um sie zu ändern). Dies könnte eine Funktion im umgebenden Namensraum oder ein Funktions-/Funktionsobjekt-/Funktionszeiger in einer (Nicht-Vorlage-) Basisklasse sein. Ich wähle das letztere hier (mit der Klasse PossibleOverridesList), um so nah wie möglich an Ihrem ursprünglichen Code zu bleiben. Dieser Platzhalter ruft standardmäßig die ursprüngliche Version der Funktion auf, kann jedoch durch ähnliche Klassen wie Ihre Override-Klassen geändert werden. Der einzige Unterschied besteht darin, dass diese Override-Klassen die zusätzliche Methode setMyOverrides(PossibleOverridesList*) benötigen, die die Funktionszeiger in der Platzhalterklasse entsprechend setzt.

Die Änderungen in main sind nicht unbedingt notwendig. In meinem Code müssen Sie setOverrides<...OverriderClass...>() aufrufen, anstatt die OverriderClass in der Deklaration anzugeben.Es sollte jedoch einfach sein, eine Wrapper-Vorlagenklasse zu schreiben, die von Test erbt und die ursprüngliche Syntax durch internes Aufrufen der setOverrides-Methode mit ihrem Vorlagenargument während der Konstruktion beibehält.

Der obige Code mehrere Vorteile gegenüber der Lösung der Bereitstellung Wrapper in der NoOverrides Klasse (Ihr Vorschlag die Frage) hat:

  • Mit Wrapper, wenn Sie viele Aufschalten Klassen mit vielen überschriebenen Funktionen haben, würden Sie müssen immer Wrapper für alle Funktionen bereitstellen, die Sie nicht überschreiben möchten. Hier gibt es nur eine globale Liste von Funktionen, die überschrieben werden könnten, PossibleOverridesList. Es ist leicht zu erkennen, wenn Sie einen vergessen haben, da der Versuch, dieses Mitglied in der setMyOverrides-Methode der entsprechenden Override-Klasse festzulegen, nicht kompiliert wird. Es gibt also nur zwei Stellen, an denen Sie etwas ändern können, wenn Sie eine andere überschriebene Methode in einer Override-Klasse hinzufügen.
  • Sie müssen die Funktionssignatur für den Wrapper der Systemfunktion nicht reproduzieren.
  • Sie müssen den Code innerhalb Run() nicht ändern, um die Funktionen der geerbten Klasse zu verwenden.
  • Ich weiß nicht, ob in Ihrem ursprünglichen komplexen Code für VC++ Test tatsächlich von mehreren Override-Klassen erben kann. Aber wenn es möglich ist, wird die Umhüllungslösung nicht funktionieren, da Sie nicht wissen, wie Sie die Methode qualifizieren sollen (Sie wissen nicht, aus welcher geerbten Klasse sie stammt). Hier können Sie die Methode setOverrides() auf dem Testobjekt mehrmals aufrufen, um bestimmte Funktionen nacheinander zu ersetzen.

Natürlich gibt es auch Einschränkungen, zum Beispiel, wenn die ursprüngliche Systemfunktion nicht im :: Namespace ist, aber vielleicht sind sie nicht strenger als für die Wrapper-Lösung. Wie ich oben sagte, gibt es wahrscheinlich sehr unterschiedliche Implementierungen, die das gleiche Konzept verwenden, aber ich denke, dass eine Art von Standardplatzhalter für die überschriebenen Methoden unvermeidlich ist (obwohl ich gerne eine Lösung ohne sie sehen würde).


EDIT: Ich kam gerade mit noch weniger aufdringlich Version, die nur die zusätzlichen PossibleOverridesList Klasse benötigt, aber keine Änderungen an Run(), main(). Am wichtigsten ist, gibt es keine Notwendigkeit für die setOverrides Methoden und die Test<Override> Vorlagenvererbung aus dem ursprünglichen Code ist erhalten!

Der Trick besteht darin, virtuelle Methoden anstelle von statischen zu verwenden und virtuelle Vererbung auszunutzen. Auf diese Weise kann der Funktionsaufruf an GetString() in Run() an die virtuelle Funktion in PossibleOverridesList binden (da diese Vererbung nicht von dem Vorlagenparameter abhängt). Zur Laufzeit wird der Aufruf dann an die am weitesten abgeleitete Klasse, d. H. In die Überschreibungsklasse, gesendet. Dies ist nur aufgrund der virtuellen Vererbung eindeutig, so dass tatsächlich nur ein PossibleOverridesList Objekt in der Klasse vorhanden ist.

So in der Zusammenfassung sind die minimalen Änderungen in dieser Version:

  • definieren PossibleOverridesList mit allen Funktionen, die (als virtuelle Member-Funktionen) außer Kraft gesetzt werden könnten, auf die ursprüngliche Systemfunktion umgeleitet wird.
  • Ändern Sie Member-Methoden in den Override-Klassen von static bis virtual.
  • lassen Sie alle überschreiben und die Test Klassen erben virtuell von PossibleOverridesList zusätzlich zu jeder anderen Vererbung. Für NoOverride ist es nicht unbedingt notwendig, aber nett für ein einheitliches Muster.

Hier ist der Code (wieder getestet mit Apple LLVM 5.1 und g ++ 4.8.2):

#include <cstdio> 
// global "system" function to test; generally something like `fopen` in a real test 
const char* GetString() { return "GLOBAL"; } 

// list all global functions that could possibly be overridden by function pointers 
struct PossibleOverridesList 
{ 
    virtual const char* GetString() {return ::GetString();}; 
}; 

// provides no overrides of the standard system functions being tested 
struct NoOverrides : virtual PossibleOverridesList { }; 

// set of functions overriding the system functions being tested 
struct TestOverrides : virtual PossibleOverridesList { 
    // if this were `fopen` this might be a version that always fails 
    virtual const char* GetString() { return "OVERRIDE"; } 
}; 

// test case (inheritance from first class doesn't depend on template parameter, so it gets included in the lookup) 
template <typename Override> 
struct Test : virtual PossibleOverridesList, Override { 
    void Run() { 
    printf("%s\n", GetString()); 
    } 
}; 

int main() { 
    // test with no overrides; use the system functions 
    Test<NoOverrides> test1; 
    test1.Run(); 

    // test with overrides; use test case version of system functions 
    Test<TestOverrides> test2; 
    test2.Run(); 
} 
+0

Ich habe gerade eine andere Lösung hinzugefügt, die noch mehr von der ursprünglichen Code-Struktur erhält: 'Test' erbt nun wieder von der im Template-Parameter angegebenen Override-Klasse, und es gibt keine zusätzlichen Methoden vor' Run() ', etc. Du bist immer noch interessiert. :) – Oguk

Verwandte Themen