union требует конструктор, зачем так сделано?
От: Sm0ke Россия ksi
Дата: 06.06.23 09:56
Оценка:
Собственно хочу создать через new массив элементов T без вызова их конструкторов, чтобы потом по требованию делать std::construct_at() только для отдельных элементов .

Если поместить этот T в union для отложенной инициализации, и у T есть конструктор по умолчанию не дефоолтный, то стандарт требует добавление в union тоже конструктора по умолчанию, даже если тот ничего не делает... Зачем так сделано?
Тобишь создам через new массив 50 элементов union и для каждого будет вызван конструктор в процессе.. Это же замедление!

Подскажите как лучше. Использовать aligned_storage нет желания, и reinterprete_cast тоже.
union
Re: union требует конструктор, зачем так сделано?
От: rg45 СССР  
Дата: 06.06.23 10:15
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>Собственно хочу создать через new массив элементов T без вызова их конструкторов, чтобы потом по требованию делать std::construct_at() только для отдельных элементов .


S>Подскажите как лучше. Использовать aligned_storage нет желания, и reinterprete_cast тоже.


Вот прямо так, как ты описал, не получится — инициализация массива предполагает инициализацию каждого его элемента. Но ты можешь получить очень близкую функциональность при помощи std::vector и двух его функций-членов — reserve и emplace_back. Эскизно так:

std::vector<MyItem> v;

v.reserve(50);                     // резервируем память под 50 элементов, без создания самих самих элементов.
assert(v.size() == 0);

v.emplace_back(arg1, arg2, arg3);  // конструируем элементы прямо по месту выделенной памяти, без промежуточных копирований и перемещений
v.emplace_back(arg1, arg2, arg3);  // фактически внутри вектора используется placement new, только скрыто от программиста
v.emplace_back(arg1, arg2, arg3);

assert(v.size() == 3);
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 06.06.2023 10:34 rg45 . Предыдущая версия .
Re: union требует конструктор, зачем так сделано?
От: Кодт Россия  
Дата: 06.06.23 16:43
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>Собственно хочу создать через new массив элементов T без вызова их конструкторов, чтобы потом по требованию делать std::construct_at() только для отдельных элементов .


Массив std::optional<T> не нравится? Хочется сэкономить на булевых флажках? Или нужна непрерывность адресов элементов?

S>Если поместить этот T в union для отложенной инициализации, и у T есть конструктор по умолчанию не дефоолтный, то стандарт требует добавление в union тоже конструктора по умолчанию, даже если тот ничего не делает... Зачем так сделано?


Это сделано для определённости. Чтобы было понятно, какие члены живут в юнионе "из коробки".

S>Тобишь создам через new массив 50 элементов union и для каждого будет вызван конструктор в процессе.. Это же замедление!


S>Подскажите как лучше. Использовать aligned_storage нет желания, и reinterprete_cast тоже.


Ну заведи юнион, у которого будет первый член с тривиальным конструктором — какой-нибудь std::monostate dummy

union TrulyUnion {
  std::monostate dummy = {};
  T data;
  .....
};
Перекуём баги на фичи!
Отредактировано 06.06.2023 16:58 Кодт . Предыдущая версия . Еще …
Отредактировано 06.06.2023 16:54 Кодт . Предыдущая версия .
Re: union требует конструктор, зачем так сделано?
От: sergii.p  
Дата: 06.06.23 19:58
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>Собственно хочу создать через new массив элементов T без вызова их конструкторов


а компилятор не соптимизирует? У меня в простейшем примере компилятор на О2 выкинул вызовы конструкторов. Может вам тоже повезёт.

https://godbolt.org/z/sh9vanPvo
Re: union требует конструктор, зачем так сделано?
От: Кодт Россия  
Дата: 07.06.23 09:32
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>Подскажите как лучше. Использовать aligned_storage нет желания, и reinterprete_cast тоже.


Кстати, зря.
Твой union ни при каких условиях не будет trivially constructible (даже если конструкторы там выродятся, даже без оптимизации — как в моём ответе с инициализированным monostate), это может пессимизировать код, который смотрит на эти признаки.

Ну и в любом случае, там будет какое-то опосредование, обращение к члену:
union U {
  .....
  T t;
  .....
};

U* arr = new U[50];

std::construct_at(&arr[0].t);
arr[0].t.foo();


так почему бы не сделать этот член как функцию
struct U {
  std::aligned_storage_t<sizeof(T), alignof(T)> buf;
  constexpr T& t() { return (T&)buf; }
  constexpr const T& t() const { return (const T&)buf; }

  // и даже перетащить сюда нужное из исходного типа
  auto foo() { return t().foo(); }
};
Перекуём баги на фичи!
Re: union требует конструктор, зачем так сделано?
От: Pzz Россия https://github.com/alexpevzner
Дата: 07.06.23 11:27
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>Тобишь создам через new массив 50 элементов union и для каждого будет вызван конструктор в процессе.. Это же замедление!


Наверное, если сделать его инлайновым и пустым, никакого вызова фактически генерироваться не будет, а значит не будет и замедления.
Re[2]: union требует конструктор, зачем так сделано?
От: so5team https://stiffstream.com
Дата: 07.06.23 14:35
Оценка: 8 (1)
Здравствуйте, Кодт, Вы писали:

К>так почему бы не сделать этот член как функцию

К>
К>struct U {
К>  std::aligned_storage_t<sizeof(T), alignof(T)> buf;
К>};
К>


Просто на всякий случай: std::aligned_storage задеприкейтили в C++23.
Re[2]: union требует конструктор, зачем так сделано?
От: Sm0ke Россия ksi
Дата: 07.06.23 16:33
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Массив std::optional<T> не нравится? Хочется сэкономить на булевых флажках? Или нужна непрерывность адресов элементов?


Непрерывность не важна. Флажки занимают память, плюс будут вызваны конструкторы у эдементов optional

К>Ну заведи юнион, у которого будет первый член с тривиальным конструктором — какой-нибудь std::monostate dummy


К>
К>union TrulyUnion {
К>  std::monostate dummy = {};
К>  T data;
К>  .....
К>};
К>


В TrulyUnion всё равно придётся вводить конструктор, ведь есть хотя бы один member с не тривиальным. И деструктор тоже, ведь T с ним.

union TrulyUnion {
  std::monostate dummy;
  T data;

  TrulyUnion() : dummy{} {}
  ~TrulyUnion() {}
};
::operator new() в самописном стэке
От: Sm0ke Россия ksi
Дата: 07.06.23 20:15
Оценка:
Здравствуйте, rg45, Вы писали:

R>Вот прямо так, как ты описал, не получится — инициализация массива предполагает инициализацию каждого его элемента. Но ты можешь получить очень близкую функциональность при помощи std::vector и двух его функций-членов — reserve и emplace_back. Эскизно так:


R>
R>std::vector<MyItem> v;

R>v.reserve(50);                     // резервируем память под 50 элементов, без создания самих самих элементов.
R>assert(v.size() == 0);

R>v.emplace_back(arg1, arg2, arg3);  // конструируем элементы прямо по месту выделенной памяти, без промежуточных копирований и перемещений
R>v.emplace_back(arg1, arg2, arg3);  // фактически внутри вектора используется placement new, только скрыто от программиста
R>v.emplace_back(arg1, arg2, arg3);

R>assert(v.size() == 3);
R>


Да, я думал об этом. Но было бы странно делая самописный вектор иметь стандартный под капотом)

Ещё думал про std::aligned_alloc() из <cstdlib> но в студии community его нет (на сайте написано, что микрософт его не поддерживает, тк он не дружит с их free).
Есть ещё конечно сишный aligned_alloc() без std:: ...

Пока остановился на вызове ::operator new(s_size * p_reserve, s_align, std::nothrow), пришлось писать reinterprete_cast<>()
Вот такая получилась заготовочка:

using t_index = std::ptrdiff_t;

    template <typename T>
    struct vector {
        using element_type = T;
        using pointer = element_type *;

        static constexpr std::align_val_t
            s_align{alignof(element_type)};
        static constexpr t_size
            s_size{sizeof(element_type)};

        struct mem {
            static pointer allocate(t_index p_reserve) {
                return reinterpret_cast<pointer>(
                    ::operator new(s_size * p_reserve, s_align, std::nothrow)
                );
            }

            static void free(pointer p_handle) {
                ::operator delete(p_handle, s_align);
            }

            using tfn_allocate = decltype(&allocate);
            using tfn_free = decltype(&free);

            // data
            tfn_allocate
                m_alloc = &allocate;
            tfn_free
                m_free = &free;

            pointer do_alloc(t_index p_reserve) const {
                pointer ret = m_alloc(p_reserve);
                if( ret == nullptr ) { throw std::bad_alloc{}; }
                return ret;
            }
        };

        static constexpr mem s_mem{};

        using mem_const_pointer = const mem *;

    private:

        struct t_data {
            pointer
                m_handle;
            t_index
                m_reserve,
                m_count = 0;
            mem_const_pointer
                m_mem = &s_mem;
        };

        // data
        t_data
            m_data;

    public:

        vector() = delete;
        vector(const vector &) = delete;
        vector(vector &&) = delete;
        vector & operator = (const vector &) = delete;

        vector & operator = (vector && p_other) {
            std::ranges::swap(m_data, p_other.m_data);
        }

        vector(t_index p_reserve) {
            m_data.m_handle = m_data.m_mem->do_alloc(p_reserve);
            m_data.m_reserve = p_reserve;
        }

        ~vector() {
            m_data.m_mem->m_free(m_data.m_handle);
        }
    };


Выделение и освобождение памяти тут через указатели на функцию, чтобы этот вектор можно было передавать в dll (надеюсь, что такое решение рабочее).

Чем не устраивает стандартный вектор? При перерезервировании он делает copy или move конструирование элементов (что нежелательно для типов, которые я собираюсь в нём хранить). Ещё при инсерте делает move assignment.

Планирую использовать memcpy() при перерезервировании, и memmove() при инсерте/удалении. Ещё добавлю полиси роста как шаблонный параметр, чтобы при добавлении — резерв увеличивался не как в std::vector<> в 2 раза, а на фиксированный шаг. Или задавать свои полиси роста.

Изначальная задача вообще в следующем — Нужен контейнер на подобии стэка, чтобы:
Может стОит взять std::forward_list<> и работать с началом списка? Или какие есть ещё варианты?
p.s: Число хранимых элементов в теории не превышает 20-30, но может быть что угодно, ведь это для интерпретатора. Каждый элемент хранит 2 указателя и итератор от std::list<>
Отредактировано 07.06.2023 22:18 Sm0ke . Предыдущая версия . Еще …
Отредактировано 07.06.2023 21:59 Sm0ke . Предыдущая версия .
Отредактировано 07.06.2023 20:37 Sm0ke . Предыдущая версия .
Отредактировано 07.06.2023 20:18 Sm0ke . Предыдущая версия .
Re: ::operator new() в самописном стэке
От: rg45 СССР  
Дата: 08.06.23 07:16
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>Да, я думал об этом. Но было бы странно делая самописный вектор иметь стандартный под капотом)

S> . . .
S>Пока остановился на вызове ::operator new(s_size * p_reserve, s_align, std::nothrow), пришлось писать reinterprete_cast<>()

По-моему, не стоит так уж избегать reinterpret_cast в данном случае, поскольку его использование инкапсулировано, локализовано и должным образом оттестировано, а не размазано по клиентскому коду.

P.S. И, возможно, даже имеет смысл заменить reinterpret_cast на двойной static_cast (с промежуточным преобразованием к void*) — чтобы следовать принципу использования преобразования наименьшей достаточной силы.
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 08.06.2023 7:50 rg45 . Предыдущая версия . Еще …
Отредактировано 08.06.2023 7:29 rg45 . Предыдущая версия .
Отредактировано 08.06.2023 7:17 rg45 . Предыдущая версия .
Re[2]: ::operator new() в самописном стэке
От: Sm0ke Россия ksi
Дата: 08.06.23 20:21
Оценка: +1
return reinterpret_cast<pointer>(
  ::operator new(s_size * p_reserve, s_align, std::nothrow)
);


Здравствуйте, rg45, Вы писали:

R>По-моему, не стоит так уж избегать reinterpret_cast в данном случае, поскольку его использование инкапсулировано, локализовано и должным образом оттестировано, а не размазано по клиентскому коду.


Спасибо

R>P.S. И, возможно, даже имеет смысл заменить reinterpret_cast на двойной static_cast (с промежуточным преобразованием к void*) — чтобы следовать принципу использования преобразования наименьшей достаточной силы.


Почему двойной? ::operator new() итак возвращает void *
И в этом фрагменте std::nothrow версия, следовательно new может вернуть null pointer. Можно ли кастить null pointer через static_cast из void *?
Отредактировано 08.06.2023 20:22 Sm0ke . Предыдущая версия .
Re[3]: ::operator new() в самописном стэке
От: rg45 СССР  
Дата: 09.06.23 09:04
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>Почему двойной? ::operator new() итак возвращает void *

S>И в этом фрагменте std::nothrow версия, следовательно new может вернуть null pointer. Можно ли кастить null pointer через static_cast из void *?

Видимо, я просто не совсем правильно представил сценарий, в котором используется преобразование. Сбило с толку упоминание о reinterpret_cast — оно, выходит, избыточно в данном случае, достаточно static_cast. Конечно, для нулевых указателей преобразование тоже будет работать.
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 09.06.2023 9:07 rg45 . Предыдущая версия .
Re[3]: ::operator new() в самописном стэке
От: rg45 СССР  
Дата: 10.06.23 13:12
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>
S>return reinterpret_cast<pointer>(
S>  ::operator new(s_size * p_reserve, s_align, std::nothrow)
S>);
S>


Вообще, этот фрагмент настораживает. Здесь ты выполняешь преобразование указаетля на просто кусок памяти к указателю на объект, что небезопасно в общем случае. Если Т — это нетривиально конструируемый тип, то время жизни объекта в этой точке еще не началось и любая попытка обращения к этому объекту порождает неопределенное поведение. Использовать данный кусок памяти как объект типа Т можно будет только после выполнения placement new. Но в таком случае и никаких преобразований не понадобится, ибо placement new сразу возвратит указатель нужного типа.
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[4]: ::operator new() в самописном стэке
От: Sm0ke Россия ksi
Дата: 10.06.23 18:53
Оценка:
Здравствуйте, rg45, Вы писали:

R>Здравствуйте, Sm0ke, Вы писали:


S>>
S>>return reinterpret_cast<pointer>(
S>>  ::operator new(s_size * p_reserve, s_align, std::nothrow)
S>>);
S>>


R>Вообще, этот фрагмент настораживает. Здесь ты выполняешь преобразование указаетля на просто кусок памяти к указателю на объект, что небезопасно в общем случае. Если Т — это нетривиально конструируемый тип, то время жизни объекта в этой точке еще не началось и любая попытка обращения к этому объекту порождает неопределенное поведение. Использовать данный кусок памяти как объект типа Т можно будет только после выполнения placement new. Но в таком случае и никаких преобразований не понадобится, ибо placement new сразу возвратит указатель нужного типа.


placement new будет в методе emplace_back(). А при m_count == 0 не должно быть обращений к непроинициализированной памяти.

Конструктор лишь резервирует память под массив. Создание объектов там будет по требованию.

Исходник этого вектора на гитхабе (в процессе написания):
https://github.com/deemetrius/ksi/blob/main/ksi/src_just/just.vector.ixx

Вот вопрос: как лучше написать метод insert(), emplace() чтобы было exception safety? Если конструктор элемента кинет исключение.

Вариант 1: Раздвигать последующие элементы, placement new, ловить исключение (сдвигать обратно и рефровать), обновлять count когда нет исключения.
Вариант 2: В отдельном месте placement new, потом memcpy в раздвинутое место массива.
Вариант 3: ...

В варианте 1 возможно изменение capacity даже при исключении. В варианте 2 при исключении capacity прежнее.
Отредактировано 10.06.2023 18:55 Sm0ke . Предыдущая версия .
Re[5]: ::operator new() в самописном стэке
От: reversecode google
Дата: 10.06.23 19:23
Оценка:
S>Вот вопрос: как лучше написать метод insert(), emplace() чтобы было exception safety? Если конструктор элемента кинет исключение.

забавно смотреть как народ изобретает vector еще раз
Re[6]: ::operator new() в самописном стэке
От: Sm0ke Россия ksi
Дата: 10.06.23 21:17
Оценка:
Здравствуйте, reversecode, Вы писали:


S>>Вот вопрос: как лучше написать метод insert(), emplace() чтобы было exception safety? Если конструктор элемента кинет исключение.


R>забавно смотреть как народ изобретает vector еще раз


Мне нужен вектор, который можно передать в dll и который не вызывает лишний раз операторы копирования/мува
Re: ::operator new() в самописном стэке
От: reversecode google
Дата: 11.06.23 18:59
Оценка:
S>Выделение и освобождение памяти тут через указатели на функцию, чтобы этот вектор можно было передавать в dll (надеюсь, что такое решение рабочее).

бред
уже придумали без вас аллокаторы

S>Чем не устраивает стандартный вектор? При перерезервировании он делает copy или move конструирование элементов (что нежелательно для типов, которые я собираюсь в нём хранить). Ещё при инсерте делает move assignment.


бред
разберитесь когда они не используются
хотя бы сорсы посмотрите

S>Планирую использовать memcpy() при перерезервировании, и memmove() при инсерте/удалении. Ещё добавлю полиси роста как шаблонный параметр, чтобы при добавлении — резерв увеличивался не как в std::vector<> в 2 раза, а на фиксированный шаг. Или задавать свои полиси роста.


это и обычный vector делает с кастом аллокатором
Re[2]: ::operator new() в самописном стэке
От: σ  
Дата: 11.06.23 20:12
Оценка:
S>>Планирую использовать memcpy() при перерезервировании, и memmove() при инсерте/удалении. Ещё добавлю полиси роста как шаблонный параметр, чтобы при добавлении — резерв увеличивался не как в std::vector<> в 2 раза, а на фиксированный шаг. Или задавать свои полиси роста.

R>это и обычный vector делает с кастом аллокатором


Можно подробней?
Re[3]: ::operator new() в самописном стэке
От: reversecode google
Дата: 11.06.23 20:25
Оценка:
а минусик от вас где ?

очевидно что подразумевалась вся конструкция целиком
в самом аллокаторе такого нет как и кастомайз в векторе

https://codereview.stackexchange.com/questions/124492/stdvector-with-custom-growth-factor
Re[2]: ::operator new() в самописном стэке
От: Sm0ke Россия ksi
Дата: 12.06.23 08:08
Оценка:
Здравствуйте, reversecode, Вы писали:

S>>Чем не устраивает стандартный вектор? При перерезервировании он делает copy или move конструирование элементов (что нежелательно для типов, которые я собираюсь в нём хранить). Ещё при инсерте делает move assignment.


R>бред

R>разберитесь когда они не используются
R>хотя бы сорсы посмотрите

https://godbolt.org/z/GGsdhqndT

При изменении capacity стандартный вектор вызывает copy конструкторы элементов в новом месте. Если нет copy конструктора, то вызывает move конструкторы.
При удалении не с конца будут вызваны операторы move присваивания для оставшихся элементов после удаляемого.

Разве нет?

Мне достаточно просто делать memmove() или memcpy() для элементов, которые я собираюсь в нём хранить.
Отредактировано 12.06.2023 8:20 Sm0ke . Предыдущая версия .
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.