4

Following from this question, I want to use an unitialised_allocator with, say, std::vector to avoid default initialisation of elements upon construction (or resize() of the std::vector (see also here for a use case). My current design looks like this:

// based on a design by Jared Hoberock
template<typename T, typename base_allocator >
struct uninitialised_allocator : base_allocator::template rebind<T>::other
{
  // added by Walter   Q: IS THIS THE CORRECT CONDITION?
  static_assert(std::is_trivially_default_constructible<T>::value,
                "value type must be default constructible");
  // added by Walter   Q: IS THIS THE CORRECT CONDITION?
  static_assert(std::is_trivially_destructible<T>::value,
                "value type must be default destructible");
  using base_t = typename base_allocator::template rebind<T>::other;
  template<typename U>
  struct rebind
  {
    typedef uninitialised_allocator<U, base_allocator> other;
  };
  typename base_t::pointer allocate(typename base_t::size_type n)
  {
    return base_t::allocate(n);
  }
  // catch default construction
  void construct(T*)
  {
    // 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)default_
  {
    base_t::construct(p, std::forward<Arg1>(arg1), std::forward<Args>(args)...);
  }
};

Then an unitialised_vector<> template could be defined like this:

template<typename T, typename base_allocator = std::allocator<T>>
using uninitialised_vector =
  std::vector<T,uninitialised_allocator<T,base_allocator>>;

However, as indicated by my comments, I'm not 100% certain as to what are the appropriate conditions in the static_assert()? (Btw, one may consider SFINAE instead -- any useful comments on this are welcome)

Obviously, one has to avoid the disaster that would ensue from the attempted non-trivial destruction of an uninitialised object. Consider

unitialised_vector< std::vector<int> > x(10); // dangerous.

It was suggested (comment by Evgeny Panasyuk) that I assert trivial constructibility, but this does not seem to catch the above disaster scenario. I just tried to check what clang says about std::is_trivially_default_constructible<std::vector<int>> (or std::is_trivially_destructible<std::vector<int>>) but all I got was a crash of clang 3.2 ...

Another, more advanced, option would be to design an allocator which only elides the default construction for objects for which this would be safe to do so.

Community
  • 1
  • 1
Walter
  • 44,150
  • 20
  • 113
  • 196
  • You'll likely also want to completely elide any `destruct` call, as long as the type is trivial. – Xeo Apr 12 '13 at 09:20
  • @Xeo good point! will add that in similar fashion. Is there actually any deep reason why the standard doesn't support this type of optimisation for trivial types? – Walter Apr 12 '13 at 09:43
  • @Xeo I would have thought, though, that the destruction of trivial types is optimised away in most critical cases. – Walter Apr 12 '13 at 09:52
  • The description of `resize` reads: "If size() < sz, appends sz - size() value-initialized elements to the sequence." I don't think your allocator can do much about it. – R. Martinho Fernandes Apr 12 '13 at 11:49

1 Answers1

4

Fwiw, I think the design can be simplified, assuming a C++11 conforming container:

template <class T>
class no_init_allocator
{
public:
    typedef T value_type;

    no_init_allocator() noexcept {}
    template <class U>
        no_init_allocator(const no_init_allocator<U>&) noexcept {}
    T* allocate(std::size_t n)
        {return static_cast<T*>(::operator new(n * sizeof(T)));}
    void deallocate(T* p, std::size_t) noexcept
        {::operator delete(static_cast<void*>(p));}
    template <class U>
        void construct(U*) noexcept
        {
            static_assert(std::is_trivially_default_constructible<U>::value,
            "This allocator can only be used with trivally default constructible types");
        }
    template <class U, class A0, class... Args>
        void construct(U* up, A0&& a0, Args&&... args) noexcept
        {
            ::new(up) U(std::forward<A0>(a0), std::forward<Args>(args)...);
        }
};
  1. I see little advantage to deriving from another allocator.

  2. Now you can let allocator_traits handle rebind.

  3. Template the construct members on U. This helps if you want to use this allocator with some container that needs to allocate something other than a T (e.g. std::list).

  4. Move the static_assert test into the single construct member where it is important.

You can still create a using:

template <class T>
using uninitialised_vector = std::vector<T, no_init_allocator<T>>;

And this still fails to compile:

unitialised_vector< std::vector<int> > x(10);


test.cpp:447:17: error: static_assert failed "This allocator can only be used with trivally default constructible types"
                static_assert(std::is_trivially_default_constructible<U>::value,
                ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I think the test for is_trivially_destructible is overkill, unless you also optimize destroy to do nothing. But I see no motivation in doing that since I believe it should get optimized anyway whenever appropriate. Without such a restriction you can:

class A
{
    int data_;
public:
    A() = default;
    A(int d) : data_(d) {}
};

int main()
{
    uninitialised_vector<A> v(10);
}

And it just works. But if you make ~A() non trivial:

    ~A() {std::cout << "~A(" << data_ << ")\n";}

Then, at least on my system, you get an error on construction:

test.cpp:447:17: error: static_assert failed "This allocator can only be used with trivally default constructible types"
                static_assert(std::is_trivially_default_constructible<U>::value,
                ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I.e. A is no longer trivially constructible if it has a non-trivial destructor.

However even with the non-trivial destructor you can still:

    uninitialised_vector<A> v;
    v.push_back(A());

This works, only because I didn't overreach with requiring a trivial destructor. And when executing this I get ~A() to run as expected:

~A(0)
~A(0)
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • *I see little advantage to deriving from another allocator* this is important, for example, to ensure desired alignment (by using an allocator that guarantees that). There is no harm in deriving from another allocator, so not doing that seem sort of stupid (if it can be helpful). – Walter Apr 12 '13 at 16:45
  • Hmm. Is there a good example of a type with trivial default constructor but non-trivial destructor? If so, would it be safe to use in the above design (without overridden `destroy()`)? This would be crucial to really answer my question. – Walter Apr 12 '13 at 16:47
  • Points 2-4 are good, but I don't think you need to declare the constructors. After all, this is a trivial type. – Walter Apr 12 '13 at 16:52
  • On the derivation: Certainly it is not incorrect to derive from another allocator. However if all you want to do is allocate/deallocate with new/delete, it does seem more complicated to me to do so. Consider this a stylistic comment. I tend to avoid public derivation except when I'm wanting a run-time is-a relationship. – Howard Hinnant Apr 12 '13 at 17:12
  • My current understanding is that a non-trivial destructor automatically means that `std::is_trivially_destructible` will answer false for that type. The `A` example with the non-trivial destructor might be handy for debugging. – Howard Hinnant Apr 12 '13 at 18:01
  • The default and converting constructors are required for allocators. The latter is required if you initialize a container with an allocator, and that container needs to "rebind" the user-supplied allocator to another type to allocate an internal structure such as a node. These requirements are laid out in 17.6.3.5 [allocator.requirements]. With `vector` you can get away with not meeting these requirements only because `vector` has no need to rebind the allocator. – Howard Hinnant Apr 12 '13 at 18:08
  • 1) I do want to derive from another allocator; 2) this design has no disadvantage over directly using new/delete when not using anything different from `std::allocator`; 3) derivation is *the* method to avoid code redundancy. I therefore don't understand the motivation behind your design choice *I tend to avoid public derivation* -- just for the sake of it? – Walter Apr 13 '13 at 09:50
  • Okay, the copy ctor from another allocator type needs to be declared, but the default ctor will be auto generated, no need to declare that. – Walter Apr 13 '13 at 09:51
  • @Walter: When you declare *any* constructor other than the default constructor, including the templated converting copy constructor, this suppresses the default constructor. Therefore if you want your allocator to be default constructible, you must declare a default constructor. If desired you can implement it with `= default`. – Howard Hinnant Apr 13 '13 at 16:20
  • I don't think that `is_trivially_default_constructible` should depend on the destructor. Which part of the standard would demand that? – Walter Apr 14 '13 at 16:23
  • 1
    It is implied by 20.9.4.3 Type properties [meta.unary.prop], pargraph 6. When the invented t is constructed, the expression is ill-formed if T does not have an accessible destructor. All of the other is_*_contructible traits derive from is_constructible. This issue is currently under debate and tracked by LWG issue 2116: http://cplusplus.github.io/LWG/lwg-active.html#2116 – Howard Hinnant Apr 14 '13 at 18:08