12
votes

Éviter la construction par défaut d'éléments dans des conteneurs standard

Je suis intéressé par la construction d'un Uninitialized_vector Code>, qui sera sémantiquement identique à std :: vecteur code> avec la mise en garde que les nouveaux éléments seraient autrement créés avec un Le constructeur de non-arguments sera plutôt créé sans initialisation. Je suis principalement intéressé à éviter l'initialisation de la cossane à 0. autant que je puisse dire, il n'y a aucun moyen de l'accomplir en combinant std :: vecteur code> avec un type particulier d'allocateur. Strike>

Je voudrais construire mon conteneur dans la même veine que std :: pile code>, qui adapte un conteneur fourni par l'utilisateur (dans mon cas, std :: vecteur ). En d'autres termes, je voudrais éviter de réimplémenter l'intégralité de std :: vecteur code> et fournir une "façade" autour de lui. P>

existe un moyen simple de contrôler par défaut Construction de "extérieur" de std :: vecteur code>? p>


Voici la solution que je suis arrivée à, qui a inspiré la réponse de Kerrek: P>

#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <cassert>

// uninitialized_allocator adapts a given base allocator
// uninitialized_allocator's behavior is equivalent to the base
// except for its no-argument construct function, which is a no-op
template<typename T, typename BaseAllocator = std::allocator<T>>
  struct uninitialized_allocator
    : BaseAllocator::template rebind<T>::other
{
  typedef typename BaseAllocator::template rebind<T>::other super_t;

  template<typename U>
    struct rebind
  {
    typedef uninitialized_allocator<U, BaseAllocator> other;
  };

  // XXX for testing purposes
  typename super_t::pointer allocate(typename super_t::size_type n)
  {
    auto result = super_t::allocate(n);

    // fill result with 13 so we can check afterwards that
    // the result was not default-constructed
    std::fill(result, result + n, 13);
    return result;
  }

  // catch default-construction
  void construct(T *p)
  {
    // no-op
  }

  // forward everything else with at least one argument to the base
  template<typename Arg1, typename... Args>
    void construct(T* p, Arg1 &&arg1, Args&&... args)
  {
    super_t::construct(p, std::forward<Arg1>(arg1), std::forward<Args>(args)...);
  }
};

namespace std
{

// XXX specialize allocator_traits
//     this shouldn't be necessary, but clang++ 2.7 + libc++ has trouble
//     recognizing that uninitialized_allocator<T> has a well-formed
//     construct function
template<typename T>
  struct allocator_traits<uninitialized_allocator<T> >
    : std::allocator_traits<std::allocator<T>>
{
  typedef uninitialized_allocator<T> allocator_type;

  // for testing purposes, forward allocate through
  static typename allocator_type::pointer allocate(allocator_type &a, typename allocator_type::size_type n)
  {
    return a.allocate(n);
  }

  template<typename... Args>
    static void construct(allocator_type &a, T* ptr, Args&&... args)
  {
    a.construct(ptr, std::forward<Args>(args)...);
  };
};

}

// uninitialized_vector is implemented by adapting an allocator and
// inheriting from std::vector
// a template alias would be another possiblity

// XXX does not compile with clang++ 2.9
//template<typename T, typename BaseAllocator>
//using uninitialized_vector = std::vector<T, uninitialized_allocator<T,BaseAllocator>>;

template<typename T, typename BaseAllocator = std::allocator<T>>
  struct uninitialized_vector
    : std::vector<T, uninitialized_allocator<T,BaseAllocator>>
{};

int main()
{
  uninitialized_vector<int> vec;
  vec.resize(10);

  // everything should be 13
  assert(std::count(vec.begin(), vec.end(), 13) == vec.size());

  // copy construction should be preserved
  vec.push_back(7);
  assert(7 == vec.back());

  return 0;
}


3 commentaires

Pouvez-vous simplement définir un constructeur par défaut qui ne fait rien? Je parie que le compilateur peut en ligne ou omettez-le ESP avec optimisation allumé.


@Doug T. - qui peut résoudre le cas du constructeur par défaut, mais les éléments intéressants semble se produire dans redimensionner - il est difficile à appeler des fonctions telles que redimensionner sans invoquer constructeurs.


@Doug: Cela a la mise en garde que vous n'êtes plus pod avec un constructeur défini par l'utilisateur ...


3 Réponses :


1
votes

Je ne crois pas que cela soit possible en emballant un vecteur (qui fonctionne avec chaque type), sauf si vous redimensionnez le vecteur sur chaque opération ADD et Supprimer . . .

Si vous pouviez abandonner les conteneurs STL d'emballage, vous pouvez le faire en gardant une matrice de char sur le tas et à l'aide de placement Nouveau pour chacun des objets que vous voulez construire. De cette façon, vous pouvez contrôler exactement quand les constructeurs et les destructeurs d'objets ont été appelés, un par un.


0 commentaires

7
votes

Au lieu d'utiliser une enveloppe autour du conteneur, envisagez d'utiliser une enveloppe autour du type d'élément:

template <typename T>
struct uninitialized
{
    uninitialized() { }
    T value;
};


4 commentaires

Ce n'est-ce pas que la valeur de la valeur ?


Ah, n'a pas réalisé qu'il n'utilisait que des types de pod.


Je pense que quelque chose d'intelligent comme cela peut fonctionner, mais j'ai les exigences supplémentaires des traits de conservation comme std_type , std :: vecteur :: référence , etc . Ceux-ci ne devraient pas exposer ininténalisé .


Écrire un Uninitialized_vector en termes de vecteur > devrait être simple; Chaque fonction membre doit simplement reporter la fonction de membre correspondante du vecteur sous-jacente , encapsulant l'accès à la valeur si nécessaire.



5
votes

Je pense que le problème se résume au type d'initialisation que le conteneur effectue des éléments. Comparaison:

template <typename T>
void definit_construct(void * addr)
{
  new (addr) T;  // default-initialization
}


6 commentaires

Merci pour cette réponse, mais je ne comprends pas votre commentaire sur construction () prendre un argument qui devient initialisé de la valeur. Pourquoi ne peut-il pas être un non-op?


@Jared: Je veux dire que la façon dont les allocateurs standard sont utilisés dans des conteneurs standard - leur fonction construction () est généralement quelque chose comme construction (vide *, const t &) , Donc, il y a au moins une copie pendant la construction, mais j'imagine que cette fonction serait appelée d'un objet initialisé de valeur, comme construction (p, t ()) , vous ne pouvez donc pas échapper à la valeur. initialisation à l'intérieur des conteneurs standard. (Même les allocators Emplacement C ++ 11 ne peuvent pas vous aider car vous ne pouvez pas "déplacer" quelque chose de plus d'une fois ...)


Donc, fondamentalement, je dis que vous pouvez gratiser un conteneur qui se comporte beaucoup comme vecteur , mais au lieu d'appeler l'allocator standard, vous devez simplement dire nouveau (addr) t ; au lieu d'appeler construction () . Vous pouvez conserver les autres aspects de l'allocator (c'est-à-dire une allocation de mémoire).


Intéressant - il ressemble à votre redimensionnement (taille_t, t = t ()) exemple n'est en réalité pas standard. std :: vecteur est requis pour distinguer entre redimensionner (Taille_t) et redimensionner (taille_t, const t &) (De même pour ses constructeurs). Il semble donc qu'un allocator standard puisse fonctionner, en fonction de la qualité de référence std :: vecteur conforme.


Par "Standard" Je voulais dire bien sûr pris d'une source fiable :-) En effet, N3290 indique que deux signatures distinctes redimatures (taille_t) et redimensionnement (taille_t, const t & t &) existe. Il est donc possible que redimensionner (n) se comporte déjà comme vous le souhaitez.


Le problème est qu'il y a Trois types d'initialisation , mais STD :: Allocator n'a que deux: zéro et valeur. Il n'y a pas de méthode d'initialisation par défaut. S'il y en avait, les conteneurs pouvaient éventuellement l'utiliser à la place.