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);
std::destroy_at
fournit deux améliorations objectives par rapport à un appel de destructeur direct:
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.
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.
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)
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 .
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...)
.
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;
...
};
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.