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

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

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

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

person Daniel Langr    schedule 24.10.2018    source источник
comment
в моем невежестве я скорее задаюсь вопросом, для чего хорош destroy_at ;)   -  person 463035818_is_not_a_number    schedule 24.10.2018
comment
@ user463035818 - Как можно вызвать псевдодеструктор для объекта std::string? Не поймите меня неправильно, я не говорю, что это невозможно, я просто хочу, чтобы вы ярко представили требуемое выражение лица.   -  person StoryTeller - Unslander Monica    schedule 24.10.2018
comment
@user463035818 user463035818 это полезно при реализации сложных вещей низкого уровня, таких как, например, std::any или std::Optional. В повседневном коде вы не будете использовать это.   -  person Marek R    schedule 24.10.2018
comment
@StoryTeller Позвольте мне быть еще одним наивным, но что не так с auto s = new string{"test"}; s->~string();? Я пропустил предпосылку вопроса? Я предполагаю, что вызов псевдодеструкторов для типов составных шаблонов был бы синтаксическим кошмаром, но typedef достаточно. Решает ли destroy_at те же проблемы, что и make_* обертки?   -  person luk32    schedule 24.10.2018
comment
@ luk32 - Попробуйте без объявления использования. Полностью соответствует требованиям std::string.   -  person StoryTeller - Unslander Monica    schedule 24.10.2018
comment
Хорошо, я думаю, я понял. Я отредактировал комментарий. Это то же самое, что make_Stuff обертки вокруг c'tors, да?   -  person luk32    schedule 24.10.2018
comment
@ luk32 Не существует деструктора с именем ~string(). Вам нужно будет вызвать s->~basic_string();. std::destroy_at(s); в порядке.   -  person Daniel Langr    schedule 24.10.2018
comment
@ luk32 - Псевдоним может быть решением, да. Но в этом случае псевдоним также является проблемой. Итак, вы знаете, это менее чем звездная ситуация.   -  person StoryTeller - Unslander Monica    schedule 24.10.2018
comment
@DanielLangr Использование псевдонима типа для dtor работает так же хорошо, как и для ctor, по крайней мере здесь: godbolt.org/z/V_Hf7Y< /а>   -  person luk32    schedule 24.10.2018
comment
Рефлекторы комитета только вчера обсудили добавление construct_at :) (Это для усилий по контекстуализации контейнера.)   -  person T.C.    schedule 24.10.2018
comment
Я думаю, что это хорошая идея, потому что, как только эта функция станет стандартной, вы можете объявить ее другом класса с защищенным конструктором копирования, для которого вы хотите разрешить копирование-размещение-новым. (см. мой ответ).   -  person alfC    schedule 01.11.2018


Ответы (6)


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)(); так как анализатор С++ так не работает. Мало того, 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 не может позволить вам выбрать тот, который вы хотите. Он может иметь код, который выполняет инициализацию по умолчанию, если вы не укажете никаких параметров, но тогда он не сможет предоставить версию для инициализации значения. И он может инициализировать значение без параметров, но тогда вы не сможете инициализировать объект по умолчанию.


Обратите внимание, что C++20 добавил std::construct_at. Но это было сделано по причинам, отличным от согласованности. Они предназначены для поддержки выделения и построения памяти во время компиляции.

Вы можете вызвать заменяемые глобальные операторы new в постоянном выражении (если вы фактически не заменили его). Но Placement-new не является заменяемой функцией, поэтому вы не можете вызвать ее там.

Предыдущие версии предложения о распределении constexpr полагались на std::allocator_traits<std::allocator<T>>::construct/destruct. Позже они перешли к std::construct_at в качестве функции построения constexpr, на которую будет ссылаться construct.

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

person Nicol Bolas    schedule 24.10.2018
comment
Жаль, что комитет по стандартам не просто добавил поддержку синтаксиса ptr->~auto();... но, возможно, здесь есть самая неприятная проблема с анализом. - person Sneftel; 24.10.2018
comment
@Sneftel Сомневаюсь, что это проблема MVP. Я предполагаю, что это комбинация 1) консервативного подхода комитета к новому синтаксису и 2) вопроса о том, относится ли auto к типу указателя или к типу, с которым он был создан (или к типу итератора, если ptr на самом деле является итератором! ). - person anonymous; 24.10.2018
comment
@Sneftel: Проблема на самом деле в том, что и выделение, и построение, и построение в памяти используют одно и то же ключевое слово и общий синтаксис: new. Напротив, уничтожение и освобождение и уничтожение используют другой синтаксис и разные ключевые слова: delete против вызова деструктора. Таким образом, хотя ptr->auto() и решит эту проблему, основная проблема заключается в том, что C++ несовместим с этим. Для согласованности было бы лучше иметь вещь типа размещения-delete. - person Nicol Bolas; 25.10.2018
comment
Можно ли сделать размещение новым другом класса? Если нет, то может быть законное использование a для стандартного std::construct, потому что тогда вы можете разрешить выборочное копирование-размещение-новое для класса только перемещения. std::construct может быть аналогично уже существующему _Construct в libc++. (см. мой ответ ниже). - person alfC; 01.11.2018
comment
@alfC: Placement-new никоим образом не взаимодействует с вашим классом. Он не создает ваш T; это неоперативная функция, которая возвращает указанный указатель. Что строит ваше T, так это тот факт, что вы предоставили его new-выражению. Кроме того, использование подобных средств управления доступом, как правило, не лучший вариант. Вы даже не можете подружиться с make_shared/unique, потому что нет гарантии, что функции make_* не делегируют обязанности по конструированию какой-либо внутренней функции. Для управления доступом такого рода лучше использовать типы закрытых ключей. - person Nicol Bolas; 01.11.2018
comment
@alfC: тип закрытого ключа — это пустой класс, который может быть создан только друзьями класса, которым вы хотите управлять. Не друзья могут скопировать его, но не могут создать. Это позволяет коду, которому предоставлен доступ, делегировать этот доступ другим. - person Nicol Bolas; 01.11.2018
comment
Я согласен с ограничениями, возникающими при делегировании некоторых внутренних функций. По крайней мере наличие std::construct дает конкретное имя очень вероятной делегированной функции. Вот почему я предлагаю, чтобы _Construct было стандартным. Вопрос: так являются ли выбранные функции друзьями, объявленными в приватном ключе? Можете ли вы привести пример, использующий эту технику? - person alfC; 01.11.2018
comment
@alfC: Писать не сложно. Вы создаете пустой класс с закрытым конструктором по умолчанию, но public копируете/перемещаете конструкторы/операторы (по умолчанию). У вас есть основной класс, который принимает один из этих объектов в качестве параметра для любого частного конструктора. И предположительно ваш основной класс является другом ключевого класса, так что он может вызывать свои собственные конструкторы, но он также может передавать ключевой объект другим. Я не вижу причин для стандартизации _Construct, когда вы можете просто назвать его std::construct с теми же ограничениями, которые вы наложили бы на _Construct (какими бы они ни были). - person Nicol Bolas; 01.11.2018
comment
@NicolBolas, извините, я еще не понимаю вашу точку зрения. Не стесняйтесь комментировать в моем ответе ниже. - person alfC; 06.11.2018
comment
Размещение new создает ваш объект. В этом весь смысл! Это не для создания объекта в памяти, для которой вы вызвали new (поскольку new уже создает объект), это для создания объекта в подходящем хранилище. Например, такие контейнеры, как std::vector, вызывают размещение new (через std::allocator::construct) для создания вашего объекта в их хранилище. - person David Stone; 21.03.2019
comment
@DavidStone: Placement new создает ваш объект. Placement new syntax создает ваш объект; фактическая перегрузка operator new, вызываемая этим синтаксисом, не работает. Таким образом, превращение operator new для размещения new в друга ничего не даст. - person Nicol Bolas; 21.03.2019
comment
Есть ли причина, по которой они просто не включили новое размещение для константных выражений? Потому что помимо этой части, palcement-new еще более эффективен, верно? - person Deduplicator; 02.01.2021

Такая штука есть, но названа не так, как можно было бы ожидать:

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

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

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

  • uninitialized_fill_n копирует объект в неинициализированную область памяти, определяемую началом и количеством (шаблон функции)
  • uninitialized_move (C++17) перемещает диапазон объектов в неинициализированную область памяти (шаблон функции).
  • uninitialized_move_n (C++17) перемещает ряд объектов в неинициализированную область памяти (шаблон функции).
  • uninitialized_default_construct (C++17) создает объекты с инициализацией по умолчанию в неинициализированной области памяти, определяемой диапазоном (шаблон функции).
  • uninitialized_default_construct_n (C++17) создает объекты с помощью инициализации по умолчанию в неинициализированной области памяти, определяемой началом и количеством (шаблон функции)
  • uninitialized_value_construct (C++17) создает объекты путем инициализации значения в неинициализированной области памяти, определяемой диапазоном (шаблон функции).
  • uninitialized_value_construct_n (C++17) создает объекты путем инициализации значения в неинициализированной области памяти, определяемой началом и количеством
person Marek R    schedule 24.10.2018
comment
Однако нет функции пересылки параметров в стиле emplace. - person Quentin; 24.10.2018
comment
Может ли какая-либо из этих функций передавать произвольные аргументы конструктору? - person Daniel Langr; 24.10.2018
comment
@Quentin, все они работают на диапазонах. Если у вас есть один набор параметров и одно местоположение, вы можете просто установить новое размещение. - person Caleth; 24.10.2018
comment
@MarekR Как бы вы использовали std::uninitialized_move для создания одного объекта при пересылке аргументов? Что бы вы использовали в качестве третьего аргумента std::uninitialized_move, который представляет итератор назначения? - person Daniel Langr; 24.10.2018
comment
@DanielLangr: Зачем тебе это нужно? Почему вы не можете просто использовать размещение-new? - person Nicol Bolas; 24.10.2018
comment
@NicolBolas Могу, конечно. Простое использование std::construct_at в качестве аналога std::destroy_at кажется мне более симметричным и красивым. Это просто вопрос читабельности кода. - person Daniel Langr; 24.10.2018
comment
Это не имеет смысла, -1. Это все диапазонные алгоритмы. Точно так же, как у нас есть std::destroy(), который является алгоритмом уничтожения диапазона. Мы говорим об одной конструкции - N вариантов конструкции не существует. Вернее, есть варианты (list-init, paren-init, default-init), но алгоритм их не зафиксирует. Смотрите ответ Николя. - person Barry; 24.10.2018
comment
@ Барри, я согласен с тобой, что это не точные заменители std::construct(_at). Возможно, ответ означает, что если вам нужно, вы можете сделать вид, что вызов std::uninitialized_copy_n(&other, 1, place_ptr) ведет себя как гипотетический std::construct_at(place_ptr, other). - person alfC; 01.11.2018

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

Целью предложения, которое добавило эту функцию, является поддержка выделения памяти constexpr, включая std::vector. Для этого требуется возможность создавать объекты в выделенном хранилище. Однако простое размещение новых сделок с точки зрения void *, а не T *. 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...).

person David Stone    schedule 21.03.2019

Существует std::allocator_traits::construct. Раньше в std::allocator был еще один, но он был удален, обоснование в документ комитета по стандартам D0174R0.

person Community    schedule 24.10.2018
comment
Есть еще std::allocator_traits::destroy и еще есть std::destroy_at. - person Daniel Langr; 24.10.2018
comment
@DanielLangr ИМХО, лучше спросить, кто существует эта избыточность. Тот, кто предложил std::destroy_at для включения, вероятно, имел в виду конкретный вариант использования, но я не уверен, почему комитет согласился. - person Konrad Rudolph; 24.10.2018
comment
@KonradRudolph: allocator_traits::destroy предназначен для уничтожения объекта, выделенного с помощью распределителя. Это означает, что вы должны передать ему объект-распределитель, который может его уничтожить. destroy_at предназначен для уничтожения объекта вызовом его деструктора. - person Nicol Bolas; 24.10.2018
comment
@NicolBolas Просто передайте соответствующий распределитель. Я согласен, однако, что это приводит к чрезмерному синтаксическому шуму. Честно говоря, я вообще не понимаю смысла дизайна измененного интерфейса <memory>. Конечно, старый API аллокатора был несовершенен, но новый не кажется мне лучше. - person Konrad Rudolph; 24.10.2018
comment
@KonradRudolph: Просто передайте соответствующий распределитель. Но в данном случае его нет, потому что объект не был создан с помощью allocator_traits::construct распределителя. вызов. - person Nicol Bolas; 24.10.2018
comment
@NicolBolas Вы можете создать такой распределитель (но std::allocator по умолчанию также будет работать). Проверьте документацию. На самом деле все, что ему нужно сделать, это не объявлять перегрузку destroy, потому что в этом случае allocator_traits::destroy вызовет p->~T(). - person Konrad Rudolph; 24.10.2018
comment
@KonradRudolph: Или вы можете просто позвонить std::destroy_at. - person Nicol Bolas; 24.10.2018
comment
@NicolBolas Конечно. См. мой предыдущий комментарий относительно этого варианта. Вопрос в том, почему существует эта избыточность — но только для освобождения, а не для выделения? - person Konrad Rudolph; 24.10.2018
comment
@KonradRudolph: Я хочу сказать, что избыточности нет. Они делают две разные вещи, используя два разных механизма, по двум разным причинам... которые оказались одним и тем же в этом конкретном случае. - person Nicol Bolas; 24.10.2018
comment
@NicolBolas Это неправда. std::destroy_at — это строго частный случай и оболочка для частного случая более общего решения std::allocator_traits (другой API, но тот же механизм). Я думаю, что это, возможно, правильное дополнение, но только потому, что API распределителя настолько запутан. - person Konrad Rudolph; 24.10.2018
comment
Давайте продолжим обсуждение в чате. - person Nicol Bolas; 24.10.2018

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

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

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;
    ...
};
person alfC    schedule 01.11.2018
comment
это позволяет сделать место размещения новым другом. Зачем вам нужно делать место размещения новым другом? Цель сделать конструкторы частными состоит в том, чтобы помешать людям каким-либо образом создать класс. Если вы сделаете такую ​​низкоуровневую конструкцию другом, вы на самом деле не помешаете людям что-либо делать. Любой может скопировать ваш некопируемый шрифт; они просто должны написать это немного по-другому. Это отличается от того, чтобы сделать make_shared/unique другом, поскольку они на самом деле добавляют ценность помимо создания своего объекта. - person Nicol Bolas; 06.11.2018
comment
Следует также отметить, что это стало бы возможным сделать std::construct другом: optional<my_move_only_class> t(some_instance);, в зависимости от реализации optional. Как именно это тип только для перемещения, если вы можете скопировать его так прямо и так легко? - person Nicol Bolas; 06.11.2018

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

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 в библиотеке может дополнять словарь стандартной библиотеки.

person Oliv    schedule 24.10.2018
comment
Это также можно легко включить в construct_at, вернув указатель T* из нового размещения. Это не причина, по которой такая функция не может существовать. - person Daniel Langr; 24.10.2018
comment
Кстати, как бы вы реализовали std::vector? Когда вы push_back/emplace_back, вам нужно использовать новое размещение для создания объекта в векторном буфере. И затем вы не возвращаете этот конкретный указатель (возвращенный новым размещением) в operator[](). Вы просто возвращаете указатель из некоторой переменной-члена, увеличенной на некоторый индекс. Это то же самое, что и ptr в приведенном выше коде. - person Daniel Langr; 24.10.2018
comment
@DanielLangr Это хорошо известная проблема, std::vector невозможно реализовать (или легко, я думаю, у меня есть решение), если попытаться использовать только основной язык. - person Oliv; 24.10.2018
comment
@DanielLangr Таким образом, конструкция была бы объявлена ​​template[...] T* construct_at(void*,Args&&...), она была бы очень похожа на размещение new, и мы потеряли бы некоторое исключение конструкции копирования из-за Args&&... аргументов. Эти привязки вызовут временную материализацию. - person Oliv; 24.10.2018
comment
@DanielLangr Поскольку ваше редактирование только что сделало мой предыдущий ответ устаревшим, я стер его. - person Oliv; 24.10.2018
comment
Спасибо, что указали на проблему в моем коде. Однако я спросил об общем вопросе construct_at, который можно использовать, например, со статическим буфером (std::aligned_storage). Так что мой вопрос не привязан к этому конкретному сценарию. - person Daniel Langr; 24.10.2018
comment
@DanielLangr Я думаю, что мы близки к вопросу / ответу, основанному на мнениях. Потому что я думаю, что construct_at мог бы дополнить словарный запас стандартной библиотеки. - person Oliv; 24.10.2018