Как сделать мой uninitialized_allocator безопасным?

Следуя этому вопросу, я хочу использовать unitialised_allocator с, скажем, std::vector, чтобы избежать инициализации элементов по умолчанию при построении (или resize() из std::vector (см. также здесь для использования). Мой текущий проект выглядит следующим образом:

// 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)...);
  }
};

Затем шаблон unitialised_vector<> можно определить следующим образом:

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

Однако, как указано в моих комментариях, я не уверен на 100% относительно каковы соответствующие условия в static_assert()? (Btw, вместо этого можно рассматривать SFINAE - любые полезные комментарии к этому приветствуются)

Очевидно, нужно избегать катастрофы, которая возникла бы при попытке нетривиального уничтожения неинициализированного объекта. Рассмотрим

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

Было высказано предположение (комментарий Евгения Панасюка), что я утверждаю тривиальную конструктивность, но это, похоже, не улавливает вышеупомянутый сценарий бедствия. Я просто попытался проверить, что говорит clang о std::is_trivially_default_constructible<std::vector<int>> (или std::is_trivially_destructible<std::vector<int>>), но все, что я получил, было сбой clang 3.2...

Другим, более продвинутым вариантом будет создание распределителя, который только лишает конструкцию по умолчанию для объектов, для которых это было бы безопасно.

Ответ 1

Fwiw, я думаю, что дизайн можно упростить, если предположить, что контейнер С++ 11 соответствует:

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)...);
        }
};
  • Я вижу мало преимуществ для получения от другого распределителя.

  • Теперь вы можете оставить allocator_traits дескриптор rebind.

  • Шаблон элементов construct на U. Это помогает, если вы хотите использовать этот распределитель с некоторым контейнером, которому нужно выделить что-то другое, кроме T (например, std::list).

  • Переместите тест static_assert в один элемент construct, где это важно.

Вы все равно можете создать using:

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

И это все еще не скомпилируется:

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,
                ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Я думаю, что тест для is_trivially_destructible является излишним, если вы также не оптимизируете destroy, чтобы ничего не делать. Но я не вижу никакой мотивации в этом, так как считаю, что в любом случае он должен быть оптимизирован. Без такого ограничения вы можете:

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

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

И это просто работает. Но если вы сделаете ~A() нетривиальным:

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

Тогда, по крайней мере, в моей системе вы получите ошибку при построении:

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,
                ^             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

т.е. A уже не является тривиально конструктивным, если он имеет нетривиальный деструктор.

Однако даже с нетривиальным деструктором вы все равно можете:

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

Это работает только, потому что я не справлялся с требованием тривиального деструктора. И при выполнении этого я получаю ~A() для запуска, как ожидалось:

~A(0)
~A(0)