web-dev-qa-db-fra.com

Réutilisation d'un tampon flottant pour les doublons sans comportement indéfini

Dans une fonction C++ particulière, il m'arrive d'avoir un pointeur sur un gros tampon de flotteurs que je veux utiliser temporairement pour stocker la moitié du nombre de doubles. Existe-t-il une méthode permettant d’utiliser ce tampon comme espace de travail pour stocker les doublons, ce qui est également autorisé (c’est-à-dire que le comportement n’est pas indéfini) par la norme?

En résumé, j'aimerais bien ceci:

void f(float* buffer)
{
  double* d = reinterpret_cast<double*>(buffer);
  // make use of d
  d[i] = 1.;
  // done using d as scratch, start filling the buffer
  buffer[j] = 1.;
}

Autant que je sache, il n’ya pas de moyen facile de faire cela: si je comprends bien, un reinterpret_cast<double*> comme celui-ci provoque un comportement indéfini à cause du repliement de type, et utiliser memcpy ou un float/double union n’est pas possible sans copier les données et allouer de l’espace , ce qui va à l'encontre de l'objectif et s'avère coûteux dans mon cas (et l'utilisation d'une union pour le type punning n'est pas autorisée en C++).

On peut supposer que le tampon flottant est correctement aligné pour pouvoir être utilisé en double.

24
André Offringa

Je pense que le code suivant est un moyen valable de le faire (c'est juste un petit exemple de l'idée):

#include <memory>

void f(float* buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];

    // we have started the lifetime of the doubles.
    // "d" is a new pointer pointing to the first double object in the array.        
    // now you can use "d" as a double buffer for your calculations
    // you are not allowed to access any object through the "buffer" pointer anymore since the floats are "destroyed"       
    d[0] = 1.;
    // do some work here on/with the doubles...


    // conceptually we need to destory the doubles here... but they are trivially destructable

    // now we need to start the lifetime of the floats again
    new (buffer) float[10];  


    // here we are unsure about wether we need to update the "buffer" pointer to 
    // the one returned by the placement new of the floats
    // if it is nessessary, we could return the new float pointer or take the input pointer
    // by reference and update it directly in the function
}

int main()
{
    float* floats = new float[10];
    f(floats, sizeof(float) * 10);
    return 0;
}

Il est important que vous n'utilisiez que le pointeur que vous recevez de placement new. Et il est important de replacer les flotteurs. Même s'il s'agit d'une construction sans opération, vous devez redémarrer la durée de vie des flotteurs.

Oubliez std::launder et reinterpret_cast dans les commentaires. Le placement nouveau fera le travail pour vous.

edit: Assurez-vous d’avoir un alignement correct lors de la création du tampon dans main.

Mettre à jour:

Je voulais juste faire le point sur les points abordés dans les commentaires.

  1. La première chose mentionnée est que nous devrons peut-être mettre à jour le pointeur float créé initialement sur le pointeur renvoyé par les flottants repositionnés à nouveau (la question est de savoir si le pointeur initialement flottant peut toujours être utilisé pour accéder aux flottants, car le les flottants sont maintenant des "nouveaux" flotteurs obtenus par une nouvelle expression supplémentaire).

Pour ce faire, nous pouvons soit a) passer le pointeur float par référence et le mettre à jour, soit b) renvoyer le nouveau pointeur float obtenu à partir de la fonction:

une)

void f(float*& buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];    
    // do some work here on/with the doubles...
    buffer = new (buffer) float[10];  
}

b)

float* f(float* buffer, std::size_t buffer_size_in_bytes)
{
    /* same as inital example... */
    return new (buffer) float[10];  
}

int main()
{
    float* floats = new float[10];
    floats = f(floats, sizeof(float) * 10);
    return 0;
}
  1. La prochaine chose plus importante à mentionner est que placement-new est autorisé à avoir une surcharge de mémoire. Ainsi, l’implémentation est autorisée à placer des métadonnées en face du tableau retourné. Si cela se produit, le calcul naïf du nombre de doublons qui entreraient dans notre mémoire sera évidemment erroné. Le problème est que nous ne savons pas combien d'octets l'implémentation va acquérir à l'avance pour l'appel spécifique. Mais il serait nécessaire d’ajuster les quantités de doubles dont nous savons qu’elles s’inséreront dans le stockage restant . Ici ( https://stackoverflow.com/a/8721932/3783662 ) est un autre SO poste où Howard Hinnant a fourni un extrait de test. J'ai testé cela en utilisant un compilateur en ligne et j'ai constaté que, pour les types trivialement destructibles (par exemple, les doublons), la surcharge était de 0. Pour les types plus complexes (par exemple, std :: string), il y avait une surcharge de 8 octets. Mais cela peut varier pour votre plateforme/compilateur. Testez-le préalablement avec l'extrait de code de Howard.

  2. Pour ce qui est de la question de savoir pourquoi nous devons utiliser un type de placement nouveau (soit par nouveau [], soit par élément unique nouveau): nous avons le droit de lancer des pointeurs de toutes les manières que nous voulons. Mais en fin de compte, lorsque nous accédons à la valeur, nous devons utiliser le bon type pour éviter d’annuler les règles de crénelage strictes. Facile à parler: il est uniquement autorisé à accéder à un objet lorsqu'il y a vraiment un objet du type pointeur vivant à l'emplacement indiqué par le pointeur. Alors, comment donnez-vous vie à des objets? Le standard dit:

https://timsong-cpp.github.io/cppwp/intro.object#1 :

"Un objet est créé par une définition, par une nouvelle expression, lors de la modification implicite du membre actif d'une union ou lors de la création d'un objet temporaire."

Il y a un secteur supplémentaire qui peut sembler intéressant:

https://timsong-cpp.github.io/cppwp/basic.life#1 :

"Un objet est dit avoir une initialisation non vide s'il s'agit d'un type de classe ou d'agrégat et que lui-même ou l'un de ses sous-objets est initialisé par un constructeur autre qu'un constructeur par défaut trivial. La durée de vie d'un objet de type T commence lorsque:

  • un stockage avec le bon alignement et la bonne taille pour le type T est obtenu, et
  • si l'objet a une initialisation non vide, son initialisation est terminée "

Alors maintenant, nous pouvons dire que, parce que les doubles sont triviaux, devons-nous prendre des mesures pour donner vie aux objets triviaux et changer les objets vivants? Je dis oui, car nous avons initialement obtenu un stockage pour les flottants et son accès via un double pointeur violerait le crénelage strict. Nous devons donc indiquer au compilateur que le type actuel a changé. Tout ce dernier point 3 était assez controversé. Vous pouvez vous faire votre propre opinion. Vous avez toutes les informations sous la main maintenant.

10
phön

Vous pouvez y parvenir de deux manières.

Premier:

void set(float *buffer, size_t index, double value) {
    memcpy(reinterpret_cast<char*>(buffer)+sizeof(double)*index, &value, sizeof(double));
}
double get(const float *buffer, size_t index) {
    double v;
    memcpy(&v, reinterpret_cast<const char*>(buffer)+sizeof(double)*index, sizeof(double));
    return v;
}
void f(float *buffer) {
    // here, use set and get functions
}

Deuxièmement: au lieu de float *, vous devez allouer un tampon char[] "sans type" et utiliser le placement new pour placer des flottants ou des doubles dans:

template <typename T>
void setType(char *buffer, size_t size) {
    for (size_t i=0; i<size/sizeof(T); i++) {
        new(buffer+i*sizeof(T)) T;
    }
}
// use it like this: setType<float>(buffer, sizeOfBuffer);

Puis utilisez cet accesseur:

template <typename T>
T &get(char *buffer, size_t index) {
    return *std::launder(reinterpret_cast<T *>(buffer+index*sizeof(T)));
}
// use it like this: get<float>(buffer, index) = 33.3f;

Une troisième façon pourrait être quelque chose comme la réponse de phön (voir mes commentaires sous cette réponse). Malheureusement, je ne peux pas trouver de solution adéquate, à cause de ce problème .

7
geza

Voici une approche alternative moins effrayante.

Vous dites,

... une union flottante/double n'est pas possible sans ... allouer de l'espace supplémentaire, ce qui va à l'encontre du but recherché et s'avère coûteux dans mon cas ...

Il suffit donc que chaque objet d'union contienne deux flottants au lieu d'un.

static_assert(sizeof(double) == sizeof(float)*2, "Assuming exactly two floats fit in a double.");
union double_or_floats
{
    double d;
    float f[2];
};

void f(double_or_floats* buffer)
{
    // Use buffer of doubles as scratch space.
    buffer[0].d = 1.0;
    // Done with the scratch space.  Start filling the buffer with floats.
    buffer[0].f[0] = 1.0f;
    buffer[0].f[1] = 2.0f;
}

Bien sûr, cela complique l'indexation et le code d'appel devra être modifié. Mais il n'y a pas de frais généraux et c'est plus évidemment correct.

1
Maxpm

tl; dr Ne faites pas de pseudonymes - du tout, à moins que vous ne disiez au compilateur que vous allez aller sur la ligne de commande.


Pour ce faire, le moyen le plus simple consiste peut-être à déterminer quel commutateur du compilateur désactive l'alias strict et à l'utiliser pour le ou les fichiers source en question.

Les besoins doivent, hein?


Pensée à ce sujet un peu plus. En dépit de tous ces trucs sur le placement, c'est le seul moyen sûr.

Pourquoi?

Eh bien, si vous avez deux pointeurs de types différents pointant vers la même adresse, vous avez un alias de cette adresse et vous avez une bonne chance de duper le compilateur. Et peu importe la façon dont vous avez attribué des valeurs à ces pointeurs. Le compilateur ne s'en souviendra pas.

C'est donc le seul moyen sûr et c'est pourquoi nous avons besoin de std::pun.

1
Paul Sanders

Ce problème ne peut pas être résolu en C++ portable.

C++ est strict en ce qui concerne le crénelage de pointeur. De manière quelque peu paradoxale, cela lui permet de compiler sur de très nombreuses plates-formes (par exemple, les numéros double peuvent être stockés à des emplacements différents des numéros float).

Inutile de dire que si vous recherchez le code portable, vous devrez recoder ce que vous avez. La deuxième meilleure chose à faire est d’être pragmatique, d’accepter que cela fonctionne sur n’importe quel système de bureau que j’ai rencontré; peut-être même static_assert sur le nom/l'architecture du compilateur.

0
Bathsheba