web-dev-qa-db-fra.com

Est-il correct de faire un nouvel emplacement sur la mémoire géré par un pointeur intelligent?

Le contexte

À des fins de test, j'ai besoin de construire un objet sur une mémoire non nulle. Cela pourrait être fait avec:

{
    struct Type { /* IRL not empty */};
    std::array<unsigned char, sizeof(Type)> non_zero_memory;
    non_zero_memory.fill(0xC5);
    auto const& t = *new(non_zero_memory.data()) Type;
    // t refers to a valid Type whose initialization has completed.
    t.~Type();
}

Étant donné que cela est fastidieux et fait plusieurs fois, je voudrais fournir une fonction renvoyant un pointeur intelligent vers une telle instance Type. Je suis venu avec ce qui suit, mais je crains qu'un comportement indéfini ne se cache quelque part.

Question

Le programme suivant est-il bien défini? Surtout, est le fait qu'un std::byte[] a été alloué mais un Type de taille équivalente est libéré un problème?

#include <cstddef>
#include <memory>
#include <algorithm>

auto non_zero_memory(std::size_t size)
{
    constexpr std::byte non_zero = static_cast<std::byte>(0xC5);

    auto memory = std::make_unique<std::byte[]>(size);
    std::fill(memory.get(), memory.get()+size, non_zero);
    return memory;
}

template <class T>
auto on_non_zero_memory()
{
    auto memory = non_zero_memory(sizeof(T));
    return std::shared_ptr<T>(new (memory.release()) T());
}    

int main()
{
    struct Type { unsigned value = 0; ~Type() {} }; // could be something else
    auto t = on_non_zero_memory<Type>();
    return t->value;
}

Démo en direct

22
YSC

Ce programme n'est pas bien défini.

La règle est que si un type a un destructeur trivial (Voir this ), vous n'avez pas besoin de l'appeler. Donc ça:

return std::shared_ptr<T>(new (memory.release()) T());

est presque correct. Il omet le destructeur du sizeof(T)std::byte s, ce qui est bien, construit un nouveau T dans la mémoire, ce qui est bien, puis lorsque le shared_ptr est prêt à être supprimé, il appelle delete this->get();, ce qui est faux. Cela déconstruit d'abord un T, mais ensuite il désalloue un T au lieu d'un std::byte[], ce qui probablement (non défini) ne fonctionnera pas .

Norme C++ §8.5.2.4p8 [expr.new]

Une nouvelle expression peut obtenir un stockage pour l'objet en appelant une fonction d'allocation. [...] Si le type alloué est un type tableau, le nom de la fonction d'allocation est operator new[].

(Tous ces "peut" sont dus au fait que les implémentations sont autorisées à fusionner de nouvelles expressions adjacentes et à appeler uniquement operator new[] pour l'une d'entre elles, mais ce n'est pas le cas car new ne se produit qu'une seule fois ( Dans make_unique))

Et la partie 11 de la même section:

Lorsqu'une nouvelle expression appelle une fonction d'allocation et que cette allocation n'a pas été étendue, la nouvelle expression transmet la quantité d'espace demandée à la fonction d'allocation comme premier argument de type std::size_t. Cet argument ne doit pas être inférieur à la taille de l'objet créé; elle peut être supérieure à la taille de l'objet en cours de création uniquement si l'objet est un tableau. Pour les tableaux de char, unsigned char et std::byte, la différence entre le résultat de la nouvelle expression et l'adresse renvoyée par la fonction d'allocation doit être un multiple entier de l'exigence d'alignement fondamental la plus stricte (6.6.5) de tout type d'objet dont la taille n'est pas supérieure à la taille du tableau en cours de création. [Remarque: Étant donné que les fonctions d'allocation sont supposées renvoyer des pointeurs vers un stockage correctement aligné pour les objets de tout type avec alignement fondamental, cette contrainte sur la surcharge d'allocation de tableau permet l'idiome commun d'allouer des tableaux de caractères dans lesquels des objets d'autres types seront ultérieurement placés. . - note de fin]

Si vous lisez le §21.6.2 [new.delete.array], vous voyez que les paramètres par défaut operator new[] et operator delete[] font exactement les mêmes choses que operator new et operator delete, le problème est que nous ne connaissons pas la taille qui lui est transmise, et c'est probablement plus que ce que delete ((T*) object) appelle (pour stocker la taille) .

En regardant ce que font les expressions de suppression:

§8.5.2.5p8 [expr.delete]

[...] delete-expression invoquera le destructeur (le cas échéant) pour les [...] éléments du tableau en cours de suppression

p7.1

Si l'appel d'allocation de la nouvelle expression pour l'objet à supprimer n'a pas été omis [...], l'expression de suppression doit appeler une fonction de désallocation (6.6.4.4.2). La valeur renvoyée par l'appel d'allocation de la nouvelle expression doit être transmise comme premier argument à la fonction de désallocation.

Puisque std::byte n'a pas de destructeur, nous pouvons appeler en toute sécurité delete[], car il ne fera rien d'autre que d'appeler la fonction de désallocation (operator delete[]). Il nous suffit de le réinterpréter dans std::byte*, et nous récupérerons ce que new[] a renvoyé.

Un autre problème est qu'il y a une fuite de mémoire si le constructeur de T lance. Une solution simple consiste à placer new alors que la mémoire appartient toujours au std::unique_ptr, donc même s'il le lance, il appellera correctement delete[].

T* ptr = new (memory.get()) T();
memory.release();
return std::shared_ptr<T>(ptr, [](T* ptr) {
    ptr->~T();
    delete[] reinterpret_cast<std::byte*>(ptr);
});

Le premier placement new met fin à la durée de vie des sizeof(T)std::byte s et démarre la durée de vie d'un nouvel objet T à la même adresse, conformément au § 6.6.3p5 [basic.life]

Un programme peut mettre fin à la durée de vie de n'importe quel objet en réutilisant le stockage qu'il occupe ou en appelant explicitement le destructeur pour un objet d'un type de classe avec un destructeur non trivial. [...]

Ensuite, lors de sa suppression, la durée de vie de T se termine par un appel explicite du destructeur, puis selon ce qui précède, l'expression de suppression désalloue le stockage.


Cela conduit à la question de:

Que faire si la classe de stockage n'était pas std::byte et n'était pas trivialement destructible? Comme, par exemple, nous utilisions une union non triviale comme stockage.

L'appel de delete[] reinterpret_cast<T*>(ptr) appellerait le destructeur sur quelque chose qui n'est pas un objet. Il s'agit d'un comportement clairement indéfini et conforme au §6.6.3p6 [basic.life]

Avant le début de la durée de vie d'un objet, mais après que le stockage qu'il occupera a été alloué [...], tout pointeur qui représente l'adresse de l'emplacement de stockage où l'objet sera ou était situé peut être utilisé, mais uniquement dans des moyens limités. [...] Le programme a un comportement indéfini si: l'objet sera ou était d'un type classe avec un destructeur non trivial et le pointeur est utilisé comme l'opérande d'une expression de suppression

Donc, pour l'utiliser comme ci-dessus, nous devons le construire juste pour le détruire à nouveau.

Le constructeur par défaut fonctionne probablement bien. La sémantique habituelle est "créer un objet qui peut être détruit", c'est exactement ce que nous voulons. Utilisez std::uninitialized_default_construct_n pour les construire tous puis les détruire immédiatement:

    // Assuming we called `new StorageClass[n]` to allocate
    ptr->~T();
    auto* as_storage = reinterpret_cast<StorageClass*>(ptr);
    std::uninitialized_default_construct_n(as_storage, n);
    delete[] as_storage;

Nous pouvons également appeler operator new et operator delete nous-mêmes:

static void byte_deleter(std::byte* ptr) {
    return ::operator delete(reinterpret_cast<void*>(ptr));
}

auto non_zero_memory(std::size_t size)
{
    constexpr std::byte non_zero = static_cast<std::byte>(0xC5);

    auto memory = std::unique_ptr<std::byte, void(*)(std::byte*)>(
        reinterpret_cast<std::byte*>(::operator new(size)),
        &::byte_deleter
    );
    std::fill(memory.get(), memory.get()+size, non_zero);
    return memory;
}

template <class T>
auto on_non_zero_memory()
{
    auto memory = non_zero_memory(sizeof(T));
    T* ptr = new (memory.get()) T();
    memory.release();
    return std::shared_ptr<T>(ptr, [](T* ptr) {
        ptr->~T();
        ::operator delete(ptr, sizeof(T));
                            // ^~~~~~~~~ optional
    });
}

Mais cela ressemble beaucoup à std::malloc et std::free.

Une troisième solution pourrait consister à utiliser std::aligned_storage comme type donné à new, et à faire fonctionner le suppresseur comme avec std::byte car l'alignement le stockage est un agrégat trivial.

23
Artyer
std::shared_ptr<T>(new (memory.release()) T())

Est un comportement indéfini. La mémoire acquise par memory était pour un std::byte[] Mais le suppresseur de shared_ptr Fait pour appeler delete sur un pointeur vers T. Puisque le pointeur n'a plus le même type, vous ne pouvez pas appeler delete dessus [expr.delete]/2

Dans une expression de suppression d'un seul objet, la valeur de l'opérande de suppression peut être une valeur de pointeur nulle, un pointeur vers un objet non tableau créé par une nouvelle expression précédente, ou un pointeur vers un sous-objet représentant une classe de base de ces un objet. Sinon, le comportement n'est pas défini.

Vous devez fournir au shared_ptr Un suppresseur personnalisé qui détruit T, puis replacer le pointeur sur son type de source et appeler delete[] À ce sujet.


Il convient également de noter que new (memory.release()) T() lui-même ne sera pas défini si memory a alloué un type dont la destruction n'est pas triviale. Vous devez appeler le destructeur sur le pointeur à partir de memory.release() avant de réutiliser sa mémoire.

15
NathanOliver