Как сделать мой 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()? (кстати, вместо этого можно рассмотреть 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 ...

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


person Walter    schedule 12.04.2013    source источник
comment
Вы, вероятно, также захотите полностью исключить любой вызов destruct, если тип тривиален.   -  person Xeo    schedule 12.04.2013
comment
@Xeo хорошая мысль! добавит это аналогичным образом. Есть ли на самом деле какая-то серьезная причина, по которой стандарт не поддерживает этот тип оптимизации для тривиальных типов?   -  person Walter    schedule 12.04.2013
comment
@Xeo Я бы подумал, однако, что уничтожение тривиальных типов оптимизировано в большинстве критических случаев.   -  person Walter    schedule 12.04.2013
comment
Описание resize гласит: Если size() ‹ sz, добавляет к последовательности элементы, инициализированные значением sz - size(). Я не думаю, что ваш распределитель может многое с этим поделать.   -  person R. Martinho Fernandes    schedule 12.04.2013


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

  2. Теперь вы можете позволить allocator_traits управлять rebind.

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

  4. Переместите тест 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)
person Howard Hinnant    schedule 12.04.2013
comment
Я не вижу большого преимущества в наследовании от другого распределителя это важно, например, для обеспечения желаемого выравнивания (путем использования распределителя, который это гарантирует). Нет никакого вреда в получении от другого распределителя, поэтому не делать этого кажется глупым (если это может быть полезно). - person Walter; 12.04.2013
comment
Хм. Есть ли хороший пример типа с тривиальным конструктором по умолчанию, но нетривиальным деструктором? Если да, то будет ли безопасно использовать его в приведенном выше дизайне (без переопределения destroy())? Это было бы важно, чтобы действительно ответить на мой вопрос. - person Walter; 12.04.2013
comment
Пункты 2-4 хороши, но я не думаю, что вам нужно объявлять конструкторы. Ведь это тривиальный тип. - person Walter; 12.04.2013
comment
О происхождении: ‹пожимает плечами› Конечно, нет ничего неправильного в происхождении от другого распределителя. Однако, если все, что вы хотите сделать, это выделить/освободить с помощью new/delete, мне это кажется более сложным. Считайте это стилистическим комментарием. Я склонен избегать публичных выводов, за исключением тех случаев, когда я хочу, чтобы время выполнения было отношением. - person Howard Hinnant; 12.04.2013
comment
В настоящее время я понимаю, что нетривиальный деструктор автоматически означает, что std::is_trivially_destructible ответит false для этого типа. Пример A с нетривиальным деструктором может быть удобен для отладки. - person Howard Hinnant; 12.04.2013
comment
Для распределителей требуются конструкторы по умолчанию и конструкторы преобразования. Последнее требуется, если вы инициализируете контейнер с помощью распределителя, и этому контейнеру необходимо повторно привязать предоставленный пользователем распределитель к другому типу, чтобы выделить внутреннюю структуру, такую ​​как узел. Эти требования изложены в 17.6.3.5 [allocator.requirements]. С vector вы можете избежать невыполнения этих требований только потому, что vector не нужно перепривязывать распределитель. - person Howard Hinnant; 12.04.2013
comment
1) я хочу получить от другого распределителя; 2) этот дизайн не имеет недостатков по сравнению с прямым использованием new/delete, когда не используется ничего отличного от std::allocator; 3) деривация — это метод, позволяющий избежать избыточности кода. Поэтому я не понимаю мотивации вашего выбора дизайна Я стараюсь избегать публичного происхождения — просто ради этого? - person Walter; 13.04.2013
comment
Хорошо, нужно объявить ctor копирования из другого типа распределителя, но ctor по умолчанию будет сгенерирован автоматически, его не нужно объявлять. - person Walter; 13.04.2013
comment
@Walter: когда вы объявляете любой конструктор, отличный от конструктора по умолчанию, включая шаблонный конструктор преобразования копии, это подавляет конструктор по умолчанию. Поэтому, если вы хотите, чтобы ваш распределитель был конструируемым по умолчанию, вы должны объявить конструктор по умолчанию. При желании вы можете реализовать это с помощью = default. - person Howard Hinnant; 13.04.2013
comment
Я не думаю, что is_trivially_default_constructible должен зависеть от деструктора. Какая часть стандарта требует этого? - person Walter; 14.04.2013
comment
Это подразумевается в 20.9.4.3 Свойства типа [meta.unary.prop], параграф 6. Когда придуманное t построено, выражение неправильно сформировано, если T не имеет доступного деструктора. Все остальные трейты is_*_constructible происходят от is_constructible. Эта проблема в настоящее время обсуждается и отслеживается LWG issue 2116: cplusplus.github.io /LWG/lwg-active.html#2116 - person Howard Hinnant; 14.04.2013