2010-11-23 10 views
6

Ich habe Code, den ich seit einigen Jahren erfolgreich verwendet, um ein "variant-type object" zu implementieren; das heißt, ein C++ - Objekt, das Werte verschiedener Typen enthalten kann, aber nur so viel Speicher verwendet wie der größte der möglichen Typen. Der Code ähnelt im Wesentlichen einer markierten Union, außer dass er auch Nicht-POD-Datentypen unterstützt. Es erreicht diese Magie, indem es einen Zeichenpuffer verwendet, Placement new/delete und reinterpret_cast <>.Placement-new vs gcc 4.4.3 Strict-Aliasing-Regeln

ich diesen Code unter gcc vor kurzem versucht 4.4.3 kompilieren (mit O3 und -Wall) und bekam viele Warnungen wie folgt aus:

warning: dereferencing type-punned pointer will break strict-aliasing rules 

Von dem, was ich gelesen habe, ist dies ein Zeichen dass der neue Optimierer des GCC 'fehlerhaften' Code erzeugt, den ich offensichtlich vermeiden möchte.

Ich habe unten eine "Spielzeugversion" meines Codes eingefügt; Gibt es irgendetwas, was ich mit meinem Code tun kann, um es unter gcc 4.4.3 sicherer zu machen, während ich immer noch Nicht-POD-Datentypen unterstütze? Ich weiß, dass ich als letzten Ausweg immer den Code mit -no-strict-aliasing kompilieren konnte, aber es wäre schön Code zu haben, der nicht unter Optimierung bricht, also würde ich das lieber nicht machen.

(Beachten Sie, dass ich vermeiden möchte, eine Erhöhung oder C++ 0X-Abhängigkeit in die Codebasis einzuführen, also, während Boost/C++ 0X-Lösungen interessant sind, würde ich etwas etwas mehr altmodisch bevorzugen)

#include <new> 

class Duck 
{ 
public: 
    Duck() : _speed(0.0f), _quacking(false) {/* empty */} 
    virtual ~Duck() {/* empty */} // virtual only to demonstrate that this may not be a POD type 

    float _speed; 
    bool _quacking; 
}; 

class Soup 
{ 
public: 
    Soup() : _size(0), _temperature(0.0f) {/* empty */} 
    virtual ~Soup() {/* empty */} // virtual only to demonstrate that this may not be a POD type 

    int _size; 
    float _temperature; 
}; 

enum { 
    TYPE_UNSET = 0, 
    TYPE_DUCK, 
    TYPE_SOUP 
}; 

/** Tagged-union style variant class, can hold either one Duck or one Soup, but not both at once. */ 
class DuckOrSoup 
{ 
public: 
    DuckOrSoup() : _type(TYPE_UNSET) {/* empty*/} 
    ~DuckOrSoup() {Unset();} 

    void Unset() {ChangeType(TYPE_UNSET);} 
    void SetValueDuck(const Duck & duck) {ChangeType(TYPE_DUCK); reinterpret_cast<Duck*>(_data)[0] = duck;} 
    void SetValueSoup(const Soup & soup) {ChangeType(TYPE_SOUP); reinterpret_cast<Soup*>(_data)[0] = soup;} 

private: 
    void ChangeType(int newType); 

    template <int S1, int S2> struct _maxx {enum {sz = (S1>S2)?S1:S2};}; 
    #define compile_time_max(a,b) (_maxx< (a), (b) >::sz) 
    enum {STORAGE_SIZE = compile_time_max(sizeof(Duck), sizeof(Soup))}; 

    char _data[STORAGE_SIZE]; 
    int _type; // a TYPE_* indicating what type of data we currently hold 
}; 

void DuckOrSoup :: ChangeType(int newType) 
{ 
    if (newType != _type) 
    { 
     switch(_type) 
     { 
     case TYPE_DUCK: (reinterpret_cast<Duck*>(_data))->~Duck(); break; 
     case TYPE_SOUP: (reinterpret_cast<Soup*>(_data))->~Soup(); break; 
     } 
     _type = newType; 
     switch(_type) 
     { 
     case TYPE_DUCK: (void) new (_data) Duck(); break; 
     case TYPE_SOUP: (void) new (_data) Soup(); break; 
     } 
    } 
} 

int main(int argc, char ** argv) 
{ 
    DuckOrSoup dos; 
    dos.SetValueDuck(Duck()); 
    dos.SetValueSoup(Soup()); 
    return 0; 
} 
+0

Das ist Freakish Code ... –

+0

Haben Sie diesen Code an das GCC-Team eingereicht, vielleicht als Fehlerbericht? – curiousguy

Antwort

1

OK, Sie können es tun, wenn Sie bereit sind, eine zusätzliche Lücke zu speichern *. Ich habe Ihre Probe ein wenig umformatiert, so dass es einfacher für mich war, mit ihr zu arbeiten. Schau dir das an und schau, ob es deinen Bedürfnissen entspricht. Beachten Sie auch, dass ich einige Beispiele zur Verfügung gestellt habe, damit Sie einige Vorlagen hinzufügen können, die die Benutzerfreundlichkeit verbessern. Sie können viel mehr verlängert werden, aber das sollte Ihnen eine gute Idee geben.

Es gibt auch einige Ausgaben, die Ihnen helfen zu sehen, was vor sich geht.

Eine weitere Sache, ich nehme an, Sie wissen, dass Sie entsprechende Kopie-Ctor und Zuweisung-Operator bereitstellen müssen, aber das ist nicht der Kern dieses Problems.

Meine g ++ Version Info:

g ++ --version g ++ (SUSE Linux) 4.5.0 20100604 [gcc-4_5-Zweigrevisions 160292]

#include <new> 
#include <iostream> 

class Duck 
{ 
public: 
    Duck(float s = 0.0f, bool q = false) : _speed(s), _quacking(q) 
    { 
    std::cout << "Duck::Duck()" << std::endl; 
    } 
    virtual ~Duck() // virtual only to demonstrate that this may not be a POD type 
    { 
    std::cout << "Duck::~Duck()" << std::endl; 
    } 

    float _speed; 
    bool _quacking; 
}; 

class Soup 
{ 
public: 
    Soup(int s = 0, float t = 0.0f) : _size(s), _temperature(t) 
    { 
    std::cout << "Soup::Soup()" << std::endl; 
    } 
    virtual ~Soup() // virtual only to demonstrate that this may not be a POD type 
    { 
    std::cout << "Soup::~Soup()" << std::endl; 
    } 

    int _size; 
    float _temperature; 
}; 

enum TypeEnum { 
    TYPE_UNSET = 0, 
    TYPE_DUCK, 
    TYPE_SOUP 
}; 
template < class T > TypeEnum type_enum_for(); 
template < > TypeEnum type_enum_for<Duck>() { return TYPE_DUCK; } 
template < > TypeEnum type_enum_for<Soup>() { return TYPE_SOUP; } 

/** Tagged-union style variant class, can hold either one Duck or one Soup, but not both at once. */ 
class DuckOrSoup 
{ 
public: 
    DuckOrSoup() : _type(TYPE_UNSET), _data_ptr(_data) {/* empty*/} 
    ~DuckOrSoup() {Unset();} 

    void Unset() {ChangeType(TYPE_UNSET);} 
    void SetValueDuck(const Duck & duck) 
    { 
    ChangeType(TYPE_DUCK); 
    reinterpret_cast<Duck*>(_data_ptr)[0] = duck; 
    } 
    void SetValueSoup(const Soup & soup) 
    { 
    ChangeType(TYPE_SOUP); 
    reinterpret_cast<Soup*>(_data_ptr)[0] = soup; 
    } 

    template < class T > 
    void set(T const & t) 
    { 
    ChangeType(type_enum_for<T>()); 
    reinterpret_cast< T * >(_data_ptr)[0] = t; 
    } 

    template < class T > 
    T & get() 
    { 
    ChangeType(type_enum_for<T>()); 
    return reinterpret_cast< T * >(_data_ptr)[0]; 
    } 

    template < class T > 
    T const & get_const() 
    { 
    ChangeType(type_enum_for<T>()); 
    return reinterpret_cast< T const * >(_data_ptr)[0]; 
    } 

private: 
    void ChangeType(int newType); 

    template <int S1, int S2> struct _maxx {enum {sz = (S1>S2)?S1:S2};}; 
    #define compile_time_max(a,b) (_maxx< (a), (b) >::sz) 
    enum {STORAGE_SIZE = compile_time_max(sizeof(Duck), sizeof(Soup))}; 

    char _data[STORAGE_SIZE]; 
    int _type; // a TYPE_* indicating what type of data we currently hold 
    void * _data_ptr; 
}; 

void DuckOrSoup :: ChangeType(int newType) 
{ 
    if (newType != _type) 
    { 
     switch(_type) 
     { 
     case TYPE_DUCK: (reinterpret_cast<Duck*>(_data_ptr))->~Duck(); break; 
     case TYPE_SOUP: (reinterpret_cast<Soup*>(_data_ptr))->~Soup(); break; 
     } 
     _type = newType; 
     switch(_type) 
     { 
     case TYPE_DUCK: (void) new (_data) Duck(); break; 
     case TYPE_SOUP: (void) new (_data) Soup(); break; 
     } 
    } 
} 

int main(int argc, char ** argv) 
{ 
    Duck sample_duck; sample_duck._speed = 23.23; 
    Soup sample_soup; sample_soup._temperature = 98.6; 
    std::cout << "Just saw sample constructors" << std::endl; 
    { 
    DuckOrSoup dos; 
    std::cout << "Setting to Duck" << std::endl; 
    dos.SetValueDuck(sample_duck); 
    std::cout << "Setting to Soup" << std::endl; 
    dos.SetValueSoup(sample_soup); 
    std::cout << "Should see DuckOrSoup destruct which will dtor a Soup" 
     << std::endl; 
    } 
    { 
    std::cout << "Do it again with the templates" << std::endl; 
    DuckOrSoup dos; 
    std::cout << "Setting to Duck" << std::endl; 
    dos.set(sample_duck); 
    std::cout << "duck speed: " << dos.get_const<Duck>()._speed << std::endl; 
    std::cout << "Setting to Soup" << std::endl; 
    dos.set(sample_soup); 
    std::cout << "soup temp: " << dos.get_const<Soup>()._temperature << std::endl; 
    std::cout << "Should see DuckOrSoup destruct which will dtor a Soup" 
     << std::endl; 
    } 
    { 
    std::cout << "Do it again with only template get" << std::endl; 
    DuckOrSoup dos; 
    std::cout << "Setting to Duck" << std::endl; 
    dos.get<Duck>() = Duck(42.42); 
    std::cout << "duck speed: " << dos.get_const<Duck>()._speed << std::endl; 
    std::cout << "Setting to Soup" << std::endl; 
    dos.get<Soup>() = Soup(0, 32); 
    std::cout << "soup temp: " << dos.get_const<Soup>()._temperature << std::endl; 
    std::cout << "Should see DuckOrSoup destruct which will dtor a Soup" 
     << std::endl; 
    } 
    std::cout << "Get ready to see sample destructors" << std::endl; 
    return 0; 
} 
+0

BTW, können Sie die Standard-Ctor loswerden, wenn Sie Typen durch Überlastung (und noch mehr mit mehr Vorlagen) ändern.Auf diese Weise können Sie direkt Konstrukt kopieren oder konstruiere mit Parametern und speichere den initialen Standard-Ctor-Overhead, wenn du den Typ wechselst –

+0

Das funktioniert, obwohl es nicht klar ist, warum ich den extra void-Zeiger speichern sollte, wenn ich den void-Zeiger bei Bedarf neu erstellen kann. Es scheint, dass der einzige Grund für das Speichern des void-Zeigers als eine Mitgliedsvariable darin besteht, den Warnungsgenerator des Compilers zu überlisten, was kein sehr befriedigender Grund dafür ist, dass pro Element ein Fehler auftritt Laufzeitstrafe –

0

ich habe es geschafft, GCC (4.2.4, laufen mit -Wstrict-aliasing=2) unter Verwendung eines void * temporär, dh zu beklagen nicht zu überzeugen.

void SetValueDuck(const Duck & duck) {ChangeType(TYPE_DUCK); void *t=&_data; reinterpret_cast<Duck*>(t)[0] = duck;} 
+0

Kein Glück, gcc 4.4.3 warnt immer noch mit der obigen Änderung (außer jetzt warnt es vor einem anonymen Zeiger, der die strengen Aliasing-Regeln bricht) –

0

Ich kann immer noch nicht verstehen, die Notwendigkeit oder Nutzung für diese aber g ++ 4.4.3 mit O3 -Wall arbeitet mit dem folgenden Patch. Wenn es funktioniert, können Sie den Anwendungsfall teilen, warum brauchen Sie das?

class DuckOrSoup 
{ 
public: 
    DuckOrSoup() : _type(TYPE_UNSET) {_duck = NULL; _soup = NULL;/* empty*/} 
    ~DuckOrSoup() {Unset();} 

    void Unset() {ChangeType(TYPE_UNSET);} 
    void SetValueDuck(const Duck & duck) {ChangeType(TYPE_DUCK); _duck = new (&_data[0])Duck (duck); } 
    void SetValueSoup(const Soup & soup) { ChangeType(TYPE_SOUP); _soup = new (&_data[0])Soup (soup); } 

private: 
    void ChangeType(int newType); 

    template <int S1, int S2> struct _maxx {enum {sz = (S1>S2)?S1:S2};}; 
    #define compile_time_max(a,b) (_maxx< (a), (b) >::sz) 
    enum {STORAGE_SIZE = compile_time_max(sizeof(Duck), sizeof(Soup))}; 

    char _data[STORAGE_SIZE]; 
    int _type; // a TYPE_* indicating what type of data we currently hold 
    Duck* _duck; 
    Soup* _soup; 
}; 

void DuckOrSoup :: ChangeType(int newType) 
{ 
    if (newType != _type) 
    { 
     switch(_type) 
     { 
     case TYPE_DUCK: 
      _duck->~Duck(); 
      _duck = NULL; 
      break; 
     case TYPE_SOUP: 
      _soup->~Soup(); 
      _soup = NULL; 
      break; 
     } 
     _type = newType; 
     switch(_type) 
     { 
     case TYPE_DUCK: _duck = new (&_data[0]) Duck(); break; 
     case TYPE_SOUP: _soup = new (&_data[0]) Soup(); break; 
     } 
    } 
} 
+0

Warum muss ich verschiedene Arten von Daten in einem einzigen Objekt speichern? Aus den gleichen Gründen verwenden andere Leute boost :: variant ... außer dass ich mein Programm nicht von Boost abhängig machen möchte. –

+0

Der obige Code kompiliert ohne Warnungen, aber er ruft die Objektkonstruktoren zweimal auf (einmal in ChangeType() und erneut in SetValue *.) Außerdem fügt er jedem einzelnen DuckOrSoup-Objekt N zusätzliche Zeigerfelder hinzu, die mehr Speicher verbrauchen (viel mehr) in meinem Nicht-Spielzeug-Code, der mehr als zwei mögliche Datentypen unterstützt) –

1

Ich hätte geschrieben Sie den Code wie folgt:

typedef boost::variant<Duck, Soup> DuckOrSoup; 

aber ich denke, jeder seinen eigenen Geschmack bekam.

Übrigens, Ihr Code ist fehlerhaft, Sie haben sich nicht um mögliche Ausrichtungsprobleme gekümmert, Sie können nicht einfach ein Objekt an irgendeiner Stelle im Speicher platzieren, es gibt eine Einschränkung zu respektieren, die sich mit jedem Typ ändert. In C++ 0x gibt es das alignof Schlüsselwort, um es zu erhalten, und einige andere Dienstprogramme, um ausgerichteten Speicher zu erhalten.