Почему в С++ 17 нет std :: construct_at?

С++ 17 добавляет std::destroy_at, но нет аналога std::construct_at. Это почему? Разве это не может быть реализовано так просто, как показано ниже?

template <typename T, typename... Args>
T* construct_at(void* addr, Args&&... args) {
  return new (addr) T(std::forward<Args>(args)...);
}

Что позволило бы избежать этого не совсем естественного размещения нового синтаксиса:

auto ptr = construct_at<int>(buf, 1);  // instead of 'auto ptr = new (buf) int(1);'
std::cout << *ptr;
std::destroy_at(ptr);

Ответ 1

std::destroy_at обеспечивает два объективных улучшения по сравнению с прямым вызовом деструктора:

  1. Это уменьшает избыточность:

    T *ptr = new T;
    //Insert 1000 lines of code here.
    ptr->~T(); //What type was that again?
    

    Конечно, мы бы все предпочли просто обернуть его в unique_ptr и покончить с этим, но если этого не произойдет по какой-то причине, добавление T означает элемент избыточности. Если мы изменим тип на U, теперь мы должны изменить вызов деструктора или все сломается. Использование std::destroy_at(ptr) устраняет необходимость менять одно и то же в двух местах.

    СУХОЙ это хорошо.

  2. Это делает это легко:

    auto ptr = allocates_an_object(...);
    //Insert code here
    ptr->~???; //What type is that again?
    

    Если мы вывели тип указателя, то удалить его становится довольно сложно. Вы не можете сделать ptr->~decltype(ptr)(); так как парсер C++ не работает таким образом. Кроме того, decltype выводит тип в качестве указателя, поэтому вам нужно будет удалить косвенность указателя из выведенного типа. Ведущий к вам:

    auto ptr = allocates_an_object(...);
    //Insert code here
    using delete_type = std::remove_pointer_t<decltype(ptr)>;
    ptr->~delete_type();
    

    И кто хочет это напечатать?

Напротив, ваш гипотетический std::construct_at предоставляет никаких объективных улучшений по сравнению с new. Вы должны указать тип, который вы создаете в обоих случаях. Параметры для конструктора должны быть предоставлены в обоих случаях. Указатель на память должен быть предоставлен в обоих случаях.

Так что нет необходимости решать ваши гипотетические std::construct_at.

И это объективно менее способно, чем размещение нового. Вы можете сделать это:

auto ptr1 = new(mem1) T;
auto ptr2 = new(mem2) T{};

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

Ваш гипотетический std::construct_at не может позволить вам выбрать тот, который вы хотите. Он может иметь код, который выполняет инициализацию по умолчанию, если вы не укажете параметры, но тогда он не сможет предоставить версию для инициализации значения. И это может иметь значение initialize без параметров, но тогда вы не можете по умолчанию инициализировать объект.

Ответ 2

Есть такая вещь, но не названная так, как вы могли бы ожидать:

  • uninitialized_copy копирует диапазон объектов в неинициализированную область памяти

  • uninitialized_copy_n (С++ 11) копирует несколько объектов в неинициализированную область памяти (шаблон функции)

  • uninitialized_fill копирует объект в неинициализированную область памяти, определяемую диапазоном (шаблон функции)

  • uninitialized_fill_n копирует объект в неинициализированную область памяти, определяемую началом и количеством (шаблон функции)
  • uninitialized_move (С++ 17) перемещает диапазон объектов в неинициализированную область памяти (шаблон функции)
  • uninitialized_move_n (С++ 17) перемещает несколько объектов в неинициализированную область памяти (шаблон функции)
  • uninitialized_default_construct (С++ 17) создает объекты по умолчанию при инициализации в неинициализированной области памяти, определенной диапазоном (шаблоном функции)
  • uninitialized_default_construct_n (С++ 17) создает объекты путем инициализации по умолчанию в неинициализированной области памяти, определяемой началом и количеством (шаблон функции)
  • uninitialized_value_construct (С++ 17) создает объекты путем инициализации значения в неинициализированной области памяти, определенной диапазоном (шаблон функции)
  • uninitialized_value_construct_n (С++ 17) создает объекты путем инициализации значения в неинициализированной области памяти, определяемой началом и счетчиком

Ответ 4

std::construct_at был добавлен в С++ 20. Бумага, которая сделала это, - Больше контейнеров constexpr. Предположительно, это не было замечено, чтобы иметь достаточно преимуществ по сравнению с размещением новых в С++ 17, но С++ 20 меняет вещи.

Целью предложения, добавившего эту функцию, является поддержка выделения памяти constexpr, включая std::vector. Для этого требуется способность создавать объекты в выделенном хранилище. Однако, просто простое размещение новых сделок с точки зрения void *, а не T *. constexpr настоящее время оценка constexpr не имеет доступа к исходному хранилищу, и комитет хочет сохранить его таким образом. Библиотечная функция std::construct_at добавляет типизированный интерфейс constexpr T * construct_at(T *, Args &&...).

Это также имеет то преимущество, что пользователь не должен указывать тип создаваемого объекта; это выводится из типа указателя. Синтаксис для правильного вызова размещения нового является своего рода ужасным и нелогичным. Сравните std::construct_at(ptr, args...) с ::new(static_cast<void *>(ptr)) std::decay_t<decltype(*ptr)>(args...).

Ответ 5

Кажется, что construct не обеспечивает какой-либо синтаксический сахар. Более того, это менее эффективно, чем размещение новых. Привязка к ссылочным аргументам вызывает временную материализацию и дополнительную конструкцию перемещения/копирования:

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!

К сожалению, нет никакого способа избежать этой дополнительной конструкции копирования/перемещения при вызове функции. Пересылка практически идеальная.

С другой стороны, construct_at в библиотеке может дополнить стандартный словарь библиотеки.

Ответ 6

Я думаю, что должна быть стандартная функция конструкции. Фактически libc++ содержит один из них в качестве детали реализации в файле 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)...); }
...
}

Я думаю, что это что-то полезное, потому что это позволяет сделать "размещение нового" другом. Это отличная точка настройки для типа "только для перемещения", который требует uninitialized_copy в кучу по умолчанию (например, из элемента std::initializer_list.)


У меня есть собственная библиотека контейнеров, которая переопределяет detail::uninitialized_copy (из диапазона) для использования пользовательской detail::construct:

namespace detail{
    template<typename T, typename... As>
    inline void construct(T* p, As&&... as){
        ::new(static_cast<void*>(p)) T(std::forward<As>(as)...);
    }
}

Который объявлен другом класса только для перемещения, чтобы разрешить копирование только в контексте размещения new.

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