2010-11-23 45 views
6

J'ai du code que j'utilise avec succès depuis quelques années pour implémenter un "objet de type variant"; c'est-à-dire, un objet C++ qui peut contenir des valeurs de différents types, mais utilise seulement (approximativement) autant de mémoire que le plus grand des types possibles. Le code est similaire dans l'esprit à une union étiquetée, sauf qu'il prend également en charge les types de données non-POD. Il accomplit cette magie en utilisant un tampon de caractère, placement new/delete, et reinterpret_cast <>.Placement-nouveau vs gcc 4.4.3 règles d'aliasing strict

J'ai récemment essayé de compiler ce code sous gcc 4.4.3 (avec et -O3 -Wall), et a obtenu beaucoup d'avertissements comme celui-ci:

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

D'après ce que j'ai lu, cela est une indication que le nouvel optimiseur de la gcc pourrait générer du code «bogué», ce que je souhaite évidemment éviter.

J'ai collé une «version jouet» de mon code ci-dessous; Est-ce que je peux faire quelque chose à mon code pour le rendre plus sûr sous gcc 4.4.3, tout en prenant en charge les types de données non-POD? Je sais qu'en dernier recours je pourrais toujours compiler le code avec -fno-strict-aliasing, mais ce serait bien d'avoir du code qui ne se casse pas sous l'optimisation, donc je préfère ne pas le faire. (Notez que je voudrais éviter d'introduire une amélioration ou une dépendance C++ 0X dans la base de code, alors même si les solutions boost/C++ 0X sont intéressantes, je préférerais quelque chose d'un peu plus démodé)

#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

C'est le code capricieuse ... –

+0

Avez-vous soumis ce code à l'équipe GCC, peut-être comme un rapport de bogue? – curiousguy

Répondre

1

OK, vous pouvez le faire si vous souhaitez stocker un vide supplémentaire *. J'ai reformaté votre échantillon un peu, donc c'était plus facile pour moi de travailler avec. Regardez ceci et voyez si cela correspond à vos besoins. De plus, notez que j'ai fourni quelques exemples afin que vous puissiez y ajouter des modèles qui faciliteront l'utilisation. Ils peuvent être prolongés beaucoup plus, mais cela devrait vous donner une bonne idée.

Il existe également des éléments de sortie pour vous aider à voir ce qui se passe. Une autre chose, je suppose que vous savez que vous devez fournir un copier-ctor et un opérateur d'assignation appropriés, mais ce n'est pas le cœur de ce problème.

Mon g ++ version info:

g ++ --version g ++ (SUSE Linux) 4.5.0 20100604 [gcc-4_5-branche révision 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, vous pouvez vous débarrasser de l'cteur par défaut lors de la modification des types par une surcharge (et encore plus avec plus de modèles). de cette façon, vous pouvez copier directement construire ou construire avec des paramètres et enregistrer les frais généraux de cteur par défaut initial lors du passage des types. –

+0

Cela fonctionne, même si on ne sait pas pourquoi je devrais besoin de stocker le pointeur vide supplémentaire, quand je peux recréer ce pointeur vide « à la volée » si nécessaire. il semble que la seule raison pour stocker le pointeur vide comme une variable de membre est de duper avertissement générateur du compilateur, ce qui est une raison très satisfaisante pour engager un par élément pénalité d'exécution. –

0

J'ai réussi à convaincre GCC (4.2.4, courir avec -Wstrict-aliasing=2) ne pas se plaindre en utilisant un void * temporaire, à savoir.

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

Pas de chance, 4.4.3 gcc avertit encore avec le changement ci-dessus (sauf que maintenant il met en garde contre un pointeur anonyme enfreint les règles strictes d'aliasing) –

0

Je ne comprends toujours pas la nécessité ou l'utilisation pour cela, mais g ++ 4.4.3 avec -O3 -Wall travaille avec le patch suivant. Si cela fonctionne, pouvez-vous partager le cas d'utilisation, pourquoi avez-vous besoin de cela?

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

Pourquoi je besoin de stocker différents types de données dans un seul objet? Pour les mêmes raisons, d'autres personnes utilisent boost :: variant ... sauf que je ne veux pas que mon programme dépende de boost. –

+0

Le code ci-dessus compile sans avertissement, mais il appelle les constructeurs d'objets deux fois (une fois dans ChangeType() et à nouveau en SetValue *()., Il ajoute également N champs de pointeur supplémentaire pour chaque objet DuckOrSoup, qui utilise plus de mémoire (beaucoup plus dans mon code non-jouet, qui prend en charge plus de deux types de données possibles) –

1

j'aurais écrit le code comme si:

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

mais je suppose que tout le monde a son propre goût.D'ailleurs, votre code est bogué, vous n'avez pas réglé les problèmes d'alignement possibles, vous ne pouvez pas mettre un objet à n'importe quel point de la mémoire, il y a une contrainte à respecter, qui change avec chaque type. En C++ 0x, il y a le mot-clé alignof pour l'obtenir, et quelques autres utilitaires pour obtenir le stockage aligné.