web-dev-qa-db-fra.com

Pourquoi n'y a-t-il pas std :: construct_at en C++ 17?

C++ 17 ajoute std::destroy_at, mais il n'y a pas de contrepartie std::construct_at. Pourquoi donc? Ne pourrait-il pas être mis en œuvre aussi simplement que ce qui suit?

template <typename T, typename... Args>
T* construct_at(void* addr, Args&&... args) {
  return new (addr) T(std::forward<Args>(args)...);
}

Ce qui permettrait d'éviter ce placement tout à fait naturel nouvelle syntaxe:

auto ptr = construct_at<int>(buf, 1);  // instead of 'auto ptr = new (buf) int(1);'
std::cout << *ptr;
std::destroy_at(ptr);
55
Daniel Langr

std::destroy_at fournit deux améliorations objectives par rapport à un appel de destructeur direct:

  1. Cela réduit la redondance:

    T *ptr = new T;
    //Insert 1000 lines of code here.
    ptr->~T(); //What type was that again?
    

    Bien sûr, nous préférerions tous l’envelopper dans un unique_ptr et en finir, mais si cela ne peut pas arriver pour une raison quelconque, en ajoutant T, il y a un élément de redondance. Si nous changeons le type en U, nous devons maintenant changer l'appel du destructeur ou la rupture des choses. Utiliser std::destroy_at(ptr) élimine le besoin de changer la même chose à deux endroits.

    SEC est bon.

  2. Cela rend cela facile:

    auto ptr = allocates_an_object(...);
    //Insert code here
    ptr->~???; //What type is that again?
    

    Si nous déduisons le type du pointeur, sa suppression devient un peu difficile. Vous ne pouvez pas faire ptr->~decltype(ptr)(); puisque l'analyseur C++ ne fonctionne pas de cette façon. De plus, decltype déduit le type en tant que pointeur , de sorte que vous devez supprimer un pointeur indirectionnel du type déduit. Vous conduisant à:

    auto ptr = allocates_an_object(...);
    //Insert code here
    using delete_type = std::remove_pointer_t<decltype(ptr)>;
    ptr->~delete_type();
    

    Et qui veut taper ça ?

En revanche, votre hypothétique std::construct_at ne fournit aucun objectif améliorations par rapport à l'emplacement new. Vous devez indiquer le type que vous créez dans les deux cas. Les paramètres du constructeur doivent être fournis dans les deux cas. Le pointeur sur la mémoire doit être fourni dans les deux cas.

Donc, votre hypothétique std::construct_at ne résout pas le problème.

Et il est objectivement moins capable que le placement nouveau. Tu peux le faire:

auto ptr1 = new(mem1) T;
auto ptr2 = new(mem2) T{};

Ce sont différents . Dans le premier cas, l'objet est initialisé par défaut, ce qui peut le laisser non initialisé. Dans le second cas, l'objet est initialisé en valeur.

Votre hypothétique std::construct_at ne peut pas vous permet de choisir lequel vous voulez. Si vous ne fournissez aucun paramètre, le code qui effectue l'initialisation par défaut peut ne pas être en mesure de fournir une version pour l'initialisation de la valeur. Et il pourrait initialiser une valeur sans paramètre, mais vous ne pourriez alors pas initialiser l'objet par défaut.

40
Nicol Bolas

Il y a une telle chose, mais pas nommé comme vous vous en doutez :

  • uninitialized_copy copie une plage d'objets dans une zone non initialisée de la mémoire

  • uninitialized_copy_n (C++ 11) copie un certain nombre d'objets dans une zone non initialisée de la mémoire (modèle de fonction)

  • uninitialized_fill copie un objet dans une zone non initialisée de la mémoire, définie par une plage (modèle de fonction)

  • uninitialized_fill_n copie un objet dans une zone non initialisée de la mémoire, définie par un début et un nombre (modèle de fonction)
  • uninitialized_move (C++ 17) déplace une plage d'objets dans une zone non initialisée de la mémoire (modèle de fonction)
  • uninitialized_move_n (C++ 17) déplace un certain nombre d'objets dans une zone non initialisée de la mémoire (modèle de fonction)
  • uninitialized_default_construct (C++ 17) construit des objets par initialisation par défaut dans une zone non initialisée de la mémoire, définie par une plage .__ (modèle de fonction)
  • uninitialized_default_construct_n (C++ 17) construit des objets par initialisation par défaut dans une zone non initialisée de la mémoire, définie par un début et un nombre .__ (modèle de fonction)
  • uninitialized_value_construct (C++ 17) construit des objets par initialisation de valeur dans une zone non initialisée de la mémoire, définie par une plage .__ (modèle de fonction)
  • uninitialized_value_construct_n (C++ 17) construit des objets par initialisation de valeurs dans une zone non initialisée de la mémoire, définie par un début et un nombre 
13
Marek R

Il y a std::allocator_traits::construct . Il y en avait un de plus dans std::allocator, mais cela a été supprimé, raison dans document de comité des normes D0174R0 .

8
jakub_d

std::construct_at a été ajouté à C++ 20. Le papier qui l’a été est Plus de conteneurs constexpr . Vraisemblablement, cela n’a pas été jugé suffisamment avantageux par rapport au nouveau placement en C++ 17, mais le C++ 20 a changé les choses.

La proposition qui a ajouté cette fonctionnalité a pour but de prendre en charge les allocations de mémoire constexpr, y compris std::vector. Cela nécessite la possibilité de construire des objets dans le stockage alloué. Cependant, il suffit de placer de nouvelles offres en termes de void * et non de T *. L’évaluation constexpr n’a actuellement aucune possibilité d’accéder au stockage brut et le comité souhaite que cela reste ainsi. La fonction de bibliothèque std::construct_at ajoute une interface typée constexpr T * construct_at(T *, Args && ...).

Cela présente également l'avantage de ne pas obliger l'utilisateur à spécifier le type en cours de construction; il est déduit du type du pointeur. La syntaxe pour appeler correctement le nouveau placement est assez horrible et contre-intuitive. Comparez std::construct_at(ptr, args...) avec ::new(static_cast<void *>(ptr)) std::decay_t<decltype(*ptr)>(args...).

4
David Stone

Je pense qu'il devrait y avoir une fonction de construction standard. En fait, libc ++ en a un comme détail d'implémentation dans le fichier stl_construct.h.

namespace std{
...
  template<typename _T1, typename... _Args>
    inline void
    _Construct(_T1* __p, _Args&&... __args)
    { ::new(static_cast<void*>(__p)) _T1(std::forward<_Args>(__args)...); }
...
}

Je pense que c'est quelque chose d'utile à avoir car cela permet de faire du "placement nouveau" un ami. C'est un excellent point de personnalisation pour un type de déplacement uniquement qui nécessite uninitialized_copy dans le tas par défaut (à partir d'un élément std::initializer_list par exemple.)


J'ai ma propre bibliothèque de conteneurs qui réimplémente un detail::uninitialized_copy (d'une plage) pour utiliser un detail::construct personnalisé:

namespace detail{
    template<typename T, typename... As>
    inline void construct(T* p, As&&... as){
        ::new(static_cast<void*>(p)) T(std::forward<As>(as)...);
    }
}

Qui est déclaré un ami d'une classe de déménagement seulement pour autoriser la copie uniquement dans le contexte du placement nouveau.

template<class T>
class my_move_only_class{
    my_move_only_class(my_move_only_class const&) = default;
    friend template<class TT, class...As> friend void detail::construct(TT*, As&&...);
public:
    my_move_only_class(my_move_only_class&&) = default;
    ...
};
0
alfC

construct ne semble pas fournir de sucre syntaxique. De plus, il est moins efficace qu'un placement nouveau. La liaison à des arguments de référence entraîne une matérialisation temporaire et une construction supplémentaire du déplacement/de la copie:

struct heavy{
   unsigned char[4096];
   heavy(const heavy&);
};
heavy make_heavy(); // Return a pr-value
auto loc = ::operator new(sizeof(heavy));
// Equivalently: unsigned char loc[sizeof(heavy)];

auto p = construct<heavy>(loc,make_heavy()); // The pr-value returned by
         // make_heavy is bound to the second argument,
         // and then this arugment is copied in the body of construct.

auto p2 = new(loc) auto(make_heavy()); // Heavy is directly constructed at loc
       //... and this is simpler to write!

Malheureusement, il n’ya aucun moyen d’éviter ces constructions supplémentaires de copie/déplacement lors de l’appel d’une fonction. Le transfert est presque parfait.

Par ailleurs, construct_at dans la bibliothèque pourrait compléter le vocabulaire standard de la bibliothèque.

0
Oliv