Я написал код, который накапливает ненужное пространство памяти и возвращает его при необходимости, организуя в виде стека (требования клиентов странные).
void* stack = nullptr;
template <typename P>
void push(P* p) {
// reinterpret memory space of *p as a storage for the address of the top element of the stack;
*reinterpret_cast<void**>(p) = stack;
// now *p is the top element, so update stack to point to *p;
stack = p;
}
template <typename P>
P* pop() {
if (stack == nullptr) throw;
// get the address of the top element
P* p = static_cast<P*>(stack);
// now stack should point to the next element after *p, which address is stored in *p's memory space
stack = *reinterpret_cast<void**>(stack);
return p;
}
stack — указатель на верхний элемент. Когда создается новое пространство памяти pushed, его первые 8 байтов используются для хранения адреса предыдущего элемента, stack обновляется. Простая цепочка стека.
Пока я пытался добиться такого поведения, компилятор часто говорил следующее (я знаю, что некоторые манипуляции с памятью незаконны):
присвоение приведения является незаконным, приведение lvalue не поддерживается
Я хочу знать, законна ли моя реализация сейчас или нет?
#include <iostream>
#include <bitset>
constexpr size_t bytes_8 = 8 * 8;
constexpr size_t bytes_20 = 8 * 20;
constexpr size_t bytes_50 = 8 * 50;
// we assume that std::bitset holds its data contigiously in the beginning
struct A {
std::bitset<bytes_8> bits;
};
struct B {
std::bitset<bytes_20> bits;
};
struct C {
std::bitset<bytes_50> bits;
};
int main() {
// Imagine they were allocated on heap:
A a; B b; C c;
std::cout << "Addresses" << '\n'
<< "a: " << &a << "\t"
<< "b: " << &b << "\t"
<< "c: " << &c
<< std::endl << std::endl << std::endl;
push<A>(&a);
std::cout << "---------- push<A>(&a) ----------\n"
<< "'a' is the top element (and the only one), so 'stack' points to it,\n"
<< "meanwhile 'a' stores the address of the previous element (none)\n\n"
<< "stack: " << stack << "\ta.bits: " << std::hex << a.bits.to_ullong()
<< std::endl << std::endl;
push<B>(&b);
std::cout << "---------- push<B>(&b) ----------\n"
<< "now 'b' is the top element, so 'stack' updates to point to it,\n"
<< "'b' stores the address of the previous element (a)\n\n"
<< "stack: " << stack << "\tb.bits: " << std::hex << b.bits.to_ullong()
<< std::endl << std::endl;
push<C>(&c);
std::cout << "---------- push<C>(&c) ----------\n"
<< "finally 'c' is the top element and 'stack' points to it,\n"
<< "'c' stores the address of the previous element (b)\n\n"
<< "stack: " << stack << "\tc.bits: " << std::hex << c.bits.to_ullong()
<< std::endl << std::endl << std::endl;
auto p1 = pop<C>();
std::cout << "---------- pop<C>() ----------\n"
<< "'b' is the top element and the next one after it is 'a'\n\n"
<< "ret: " << p1 << "\nstack: " << stack << "\tb.bits: " << std::hex << b.bits.to_ullong()
<< std::endl << std::endl;
auto p2 = pop<B>();
std::cout << "---------- pop<B>() ----------\n"
<< "'a' is the top element and the last one in the stack\n\n"
<< "ret: " << p2 << "\nstack: " << stack << "\ta.bits: " << std::hex << a.bits.to_ullong()
<< std::endl << std::endl;
auto p3 = pop<A>();
std::cout << "---------- pop<A>() ----------\n"
<< "the stack is empty\n\n"
<< "ret: " << p3 << "\nstack: " << "0x" << stack
<< std::endl << std::endl;
return 0;
}
Addresses
a: 0x7fffffffdc20 b: 0x7fffffffdc40 c: 0x7fffffffdc60
---------- push<A>(&a) ----------
'a' is the top element (and the only one), so 'stack' points to it,
meanwhile 'a' stores the address of the previous element (none)
stack: 0x7fffffffdc20 a.bits: 0
---------- push<B>(&b) ----------
now 'b' is the top element, so 'stack' updates to point to it,
'b' stores the address of the previous element (a)
stack: 0x7fffffffdc40 b.bits: 7fffffffdc20
---------- push<C>(&c) ----------
finally 'c' is the top element and 'stack' points to it,
'c' stores the address of the previous element (b)
stack: 0x7fffffffdc60 c.bits: 7fffffffdc40
---------- pop<C>() ----------
'b' is the top element and the next one after it is 'a'
ret: 0x7fffffffdc60
stack: 0x7fffffffdc40 b.bits: 7fffffffdc20
---------- pop<B>() ----------
'a' is the top element and the last one in the stack
ret: 0x7fffffffdc40
stack: 0x7fffffffdc20 a.bits: 0
---------- pop<A>() ----------
the stack is empty
ret: 0x7fffffffdc20
stack: 0x0
Вы читали это?
Я не уверен, что вы подразумеваете под «законным». За это тебя не посадят в тюрьму. В C++ есть спецификация, которая говорит вам, как интерпретировать вашу программу как C++ (и в вашем случае вывод таков: «это недопустимый C++, и из спецификации не могут быть выведены никакие дальнейшие последствия», т. е. вы сами по себе). Но ты, кажется, уже это знаешь. Возможно, вы спрашиваете, как можно написать переносимый код с четко определенным поведением?
Странно, что хоть здесь и не происходит ничего трагического: я просто использую память так, как хочу, но это не валидный C++... Почему?
@frippe в основном да
Я хочу использовать этот метод для распределителя свободных списков, и будет жаль, если это что-то сломается (непереносимо и т. д.).
Возможно, вы захотите «просто использовать память так, как я хочу», но если стандарт C++ говорит, что в данном случае это UB (и я признаю, что не читал всех подробностей в вашем вопросе, но вы упомянули, что знаете, это UB), тогда не стоит. Это так просто. Если вы это сделаете, нет никакой гарантии относительно результата вашей программы.
Вы просто временно уничтожаете базовую память своих объектов, записывая туда указатели. Почему? Недостаточно памяти? Используйте boost::inrusive::slist (или прочитайте его код для вдохновения), чтобы получить контейнер, который не владеет своими элементами.
У этого кода нет причин использовать void* (не говоря уже о void**) и бороться/мешать/подрывать систему типов, чтобы накопить ненужное пространство памяти и вернуть его при необходимости. Переосмыслите дизайн.
@Eljay Добавляет ли эта система типов какие-либо проблемы с производительностью, если она так сильно запутана void*?
Нет, система типов часто устраняет проблемы с производительностью. qsort из стандартной библиотеки C часто работает медленнее, чем std::sort, именно потому, что C++ сохраняет информацию о типе, что позволяет компилятору выполнять более агрессивную оптимизацию.
шаблон и указатели приведения к void* и обратно для чего-то похожего на стек, похоже, кто-то не понимает, как использовать шаблоны. К сожалению, код примеров настолько сильно урезан, что невозможно понять, что там хуже и как это улучшить.
@ÖöTiib: Я почти уверен, что OP кэширует память, чтобы обойти new и delete
Добавляет ли эта система типов какие-либо проблемы с производительностью, если она так сильно запутана void*? Использование системы типов (которая используется во время компиляции) улучшит производительность или, в худшем случае, не будет иметь никакого значения. Зависимость текущей реализации от неопределенного поведения делает производительность спорной.





Ваша реализация не может работать в ее нынешнем виде. Ваш пример с локальными объектами всегда имеет неопределенное поведение, как и любое использование объектов, которые не были уничтожены.
Помещение элемента в стек перезаписывает его часть. Это операция с потерями. Байты sizeof void* в начале представления объекта *p исчезли и никогда не будут восстановлены. Даже если вы больше не используете этот объект, его деструктор все равно должен запуститься и, как правило, ему нужны эти данные.
Я согласен, но push следует применять только к тем частям памяти, которые не содержат никакой информации (мусор)
@AlexanderS • показанная память не мусор, она используется. До тех пор, пока объект не будет уничтожен, после чего он станет доступен для повторного использования... но вы повторно используете его, когда он все еще является живым объектом. (И ваш пример кода использует живые объекты автоматического хранения (стека), но мы игнорируем это из-за комментария, чтобы игнорировать это и притворяться, что они находятся в динамическом свободном хранилище (куче).)
Вы можете повторно использовать хранилище объектов после окончания их жизни, но вам действительно следует отслеживать размеры.
struct frame {
frame * ptr;
std::size_t size;
};
frame* stack = nullptr;
template <typename P>
requires sizeof(P) >= sizeof frame
void push(P* obj) {
p->~P();
stack = new(p) frame{ stack, sizeof(P) };
}
template <typename P, typename... Args>
P* pop(Args&&... args) {
if (stack == nullptr || stack->size != sizeof(P)) throw std::bad_alloc{};
frame * top = stack;
stack = stack->ptr;
top->~frame();
return new(top) P(std::forward<Args>(args)...);
}
Но даже если вы это сделаете, я бы хотел рассказать, есть ли у этой схемы какие-либо улучшения по сравнению с обычным распределением. Скорее всего, вы добавляете целую кучу ограничений на способ распределения памяти ради незначительного или, возможно, отрицательного выигрыша в производительности.
Насколько я понимаю, весь этот странный код у меня был вызван потому, что я совсем об этом забыл new :)
Основное отличие состоит в том, что новое размещение в этом ответе просто использует объект *p для своего хранения, тогда как ваша переинтерпретация делает вид, что объект типа P является объектом типа void*, то есть UB.
Я знаю, что это УБ