Управление объектами C ++ в буфере с учетом предположений о выравнивании и расположении памяти

StackOverflow https://stackoverflow.com/questions/417446

Вопрос

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

Если я знаю общий размер объекта, допустимо ли создать указатель на эту память и вызывать функции на ней?

например ,допустим, у меня есть следующий класс:

[int,int,int,int,char,padding*3bytes,unsigned short int*]

1) если я знаю, что этот класс имеет размер 24, и я знаю адрес, с которого он начинается в памяти хотя небезопасно предполагать расположение памяти, допустимо ли приводить это к указателю и вызывать функции на этом объекте, которые обращаются к этим элементам?(Знает ли c ++ каким-то волшебством правильное положение элемента?)

2) Если это небезопасно / ok, есть ли какой-либо другой способ, кроме использования конструктора, который принимает все аргументы и извлекает каждый аргумент из буфера по одному за раз?

Редактировать:Изменил название, чтобы сделать его более соответствующим тому, о чем я спрашиваю.

Это было полезно?

Решение

Вы можете создать конструктор, который принимает все элементы и назначает их, затем использовать placement new .

class Foo
{
    int a;int b;int c;int d;char e;unsigned short int*f;
public:
    Foo(int A,int B,int C,int D,char E,unsigned short int*F) : a(A), b(B), c(C), d(D), e(E), f(F) {}
};

...
char *buf  = new char[sizeof(Foo)];   //pre-allocated buffer
Foo *f = new (buf) Foo(a,b,c,d,e,f);

Это имеет то преимущество, что даже v-таблица будет сгенерирована правильно.Обратите внимание, однако, что если вы используете это для сериализации, указатель unsigned short int не будет указывать ни на что полезное при его десериализации, если только вы не будете очень осторожны и не воспользуетесь каким-либо методом для преобразования указателей в смещения, а затем обратно.

Индивидуальные методы на this указатель статически связан и представляет собой просто прямой вызов функции с this является первым параметром перед явными параметрами.

Ссылки на переменные-члены выполняются с использованием смещения от this указатель.Если объект расположен вот так:

0: vtable
4: a
8: b
12: c
etc...

a будет доступен путем разыменования this + 4 bytes.

Другие советы

По сути, то, что вы предлагаете сделать, - это прочитать кучу (надеюсь, не случайных) байтов, привести их к известному объекту, а затем вызвать метод класса для этого объекта.Это действительно может сработать, потому что эти байты окажутся в указателе "this" в этом методе класса.Но вы рискуете тем, что все окажется не там, где ожидает скомпилированный код.И в отличие от Java или C #, здесь нет реальной "среды выполнения" для устранения подобных проблем, так что в лучшем случае вы получите дамп ядра, а в худшем - поврежденную память.

Похоже, вам нужна версия сериализации / десериализации Java на C ++.Вероятно, для этого существует библиотека.

Вызовы невиртуальных функций связаны напрямую точно так же, как функция C.Указатель на объект (this) передается в качестве первого аргумента.Для вызова функции не требуется никаких знаний о расположении объекта.

Похоже, вы храните в буфере не сами объекты, а скорее данные, из которых они состоят.

Если эти данные находятся в памяти в том порядке, в котором поля определены в вашем классе (с соответствующим заполнением для платформы) и ваш типаж - это СТРУЧОК, тогда вы можете memcpy преобразуйте данные из буфера в указатель на ваш тип (или, возможно, приведите его, но будьте осторожны, есть некоторые ошибки, зависящие от конкретной платформы, с приведением к указателям разных типов).

Если ваш класс не является POD, то расположение полей в памяти не гарантируется, и вам не следует полагаться на какой-либо наблюдаемый порядок, поскольку ему разрешено меняться при каждой перекомпиляции.

Однако вы можете инициализировать не-МОДУЛЬ данными из модуля.

Что касается адресов, на которых расположены невиртуальные функции:они статически связаны во время компиляции с некоторым местоположением в вашем сегменте кода, которое одинаково для каждого экземпляра вашего типа.Обратите внимание, что здесь не задействовано "время выполнения".Когда вы пишете такой код, как этот:

class Foo{
   int a;
   int b;

public:
   void DoSomething(int x);
};

void Foo::DoSomething(int x){a = x * 2; b = x + a;}

int main(){
    Foo f;
    f.DoSomething(42);
    return 0;
}

компилятор генерирует код, который делает что-то вроде этого:

  1. функция main:
    1. выделить 8 байт в стеке для объекта "f"
    2. вызовите инициализатор по умолчанию для класса "Foo" (в данном случае ничего не делает)
    3. значение параметра push 42 на стопку
    4. подвести указатель к объекту "f" на стек
    5. вызвать функцию Foo_i_DoSomething@4 (фактическое имя обычно более сложное)
    6. загрузить возвращаемое значение 0 в регистр накопителя
    7. возврат к вызывающему абоненту
  2. функция Foo_i_DoSomething@4 (находится в другом месте сегмента кода)
    1. нагрузка "x" значение из стека (вводится вызывающим абонентом)
    2. умножьте на 2
    3. нагрузка "this" указатель из стека (нажимается вызывающим)
    4. вычислить смещение поля "a" в течение Foo объект
    5. добавьте вычисленное смещение к this указатель, загруженный на шаге 3
    6. сохраните продукт, рассчитанный на шаге 2, для смещения, рассчитанного на шаге 5
    7. нагрузка "x" значение из стека, снова
    8. нагрузка "this" указатель из стека, снова
    9. вычислить смещение поля "a" в течение Foo объект, снова
    10. добавьте вычисленное смещение к this указатель, загруженный на шаге 8
    11. нагрузка "a" значение, сохраненное со смещением,
    12. добавить "a" значение, загруженное на шаге 12, для "x" значение загружено на шаге 7
    13. нагрузка "this" указатель из стека, снова
    14. вычислить смещение поля "b" в течение Foo объект
    15. добавьте вычисленное смещение к this указатель, загруженный на шаге 14
    16. сохраните сумму, вычисленную на шаге 13, для смещения, вычисленного на шаге 16
    17. возврат к вызывающему абоненту

Другими словами, это был бы более или менее тот же код, как если бы вы написали это (особенности, такие как имя функции doSomething и метод передачи this указатель зависит от компилятора):

class Foo{
    int a;
    int b;

    friend void Foo_DoSomething(Foo *f, int x);
};

void Foo_DoSomething(Foo *f, int x){
    f->a = x * 2;
    f->b = x + f->a;
}

int main(){
    Foo f;
    Foo_DoSomething(&f, 42);
    return 0;
}
  1. Объект, имеющий тип POD, в этом случае уже создан (независимо от того, вызываете вы new или нет.Выделения требуемого хранилища уже достаточно), и вы можете получить доступ к его элементам, включая вызов функции для этого объекта.Но это сработает только в том случае, если вы точно знаете требуемое выравнивание T и размер T (буфер не может быть меньше его), а также выравнивание всех элементов T.Даже для типа pod компилятору разрешено помещать байты заполнения между элементами, если он хочет.Для типов, отличных от POD, вам может повезти так же, если в вашем типе нет виртуальных функций или базовых классов, нет пользовательского конструктора (конечно), и это относится также к base и всем его нестатическим членам.

  2. Для всех остальных типов все ставки отменены.Вы должны сначала считывать значения с помощью POD, а затем инициализировать тип, отличный от POD, с помощью этих данных.

Я храню объекты в буфере....Если я знаю общий размер объекта, допустимо ли создать указатель на эту память и вызывать функции на ней?

Это приемлемо в той мере, в какой допустимо использование приведений:

#include <iostream>

namespace {
    class A {
        int i;
        int j;
    public:
        int value()
        {
            return i + j;
        }
    };
}

int main()
{
    char buffer[] = { 1, 2 };
    std::cout << reinterpret_cast<A*>(buffer)->value() << '\n';
}

Преобразование объекта во что-то вроде необработанной памяти и обратно на самом деле довольно распространено, особенно в мире C.Однако, если вы используете иерархию классов, было бы более разумно использовать указатель на функции-члены.

допустим, у меня есть следующий класс:...

если я знаю , что этот класс имеет размер 24 , и я знаю адрес , с которого он начинается в памяти ...

Вот тут-то все и становится сложнее.Размер объекта включает в себя размер его элементов данных (и любых элементов данных из любых базовых классов) плюс любое заполнение плюс любые указатели на функции или информацию, зависящую от реализации, минус все, что сохранено в результате оптимизации определенного размера (оптимизация пустого базового класса).Если результирующее число равно 0 байтам, то объект должен занимать в памяти по крайней мере один байт.Все это представляет собой комбинацию языковых проблем и общих требований, предъявляемых большинством процессоров к доступу к памяти. Попытка заставить все работать должным образом может быть настоящей болью.

Если вы просто выделяете объект и выполняете приведение в необработанную память и из нее, вы можете игнорировать эти проблемы.Но если вы копируете внутренние компоненты объекта в какой-нибудь буфер, то они довольно быстро поднимают голову.Приведенный выше код опирается на несколько общих правил о выравнивании (т. Е. я случайно знаю, что класс A будет иметь те же ограничения на выравнивание, что и целые числа, и, таким образом, массив может быть безопасно приведен к A;но я не обязательно мог бы гарантировать то же самое, если бы я приводил части массива к A, а части к другим классам с другими элементами данных).

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

Вас также могут заинтересовать такие вещи, как Буферы протокола Google или Бережливость Facebook.


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

В JVM от Sun HotSpot хранилище объектов выровнено по ближайшей 64-разрядной границе.Вдобавок ко всему, каждый объект имеет заголовок из 2 слов в памяти.Размер слова JVM обычно равен размеру собственного указателя платформы.(Для объекта, состоящего только из 32-разрядного int и 64-разрядного double - 96 бит данных - потребуется) два слова для заголовка объекта, одно слово для int, два слова для double.Это 5 слов:160 бит.Из-за выравнивания этот объект будет занимать 192 бита памяти.

Это связано с тем, что Sun полагается на относительно простую тактику при решении проблем с выравниванием памяти (на воображаемом процессоре char может существовать в любой ячейке памяти, int - в любой ячейке, кратной 4, а double может потребоваться выделить только в ячейках памяти, кратных 32, - но наиболее строгое требование к выравниванию также удовлетворяет всем другим требованиям к выравниванию, поэтому Sun выравнивает все в соответствии с наиболее строгим расположением).

Другая тактика выравнивания памяти может освободить часть этого пространства.

  1. Если класс не содержит виртуальных функций (и, следовательно, экземпляры класса не имеют vptr), и если вы создаете правильный предположения о том, каким образом данные-члены класса размещаются в памяти, затем выполнение того, что вы предлагаете, может сработать (но может быть непереносимым).
  2. Да, другой способ (более идиоматичный, но не намного безопаснее ...вам все еще нужно знать, как класс размещает свои данные) было бы использовать так называемый "оператор размещения new" и конструктор по умолчанию.

Это зависит от того, что вы подразумеваете под "безопасным".Каждый раз, когда вы таким образом преобразуете адрес памяти в точку, вы обходите функции безопасности типов, предоставляемые компилятором, и берете ответственность на себя.Если, как подразумевает Крис, вы сделаете неверное предположение о расположении памяти или деталях реализации компилятора, то вы получите неожиданные результаты и потеряете переносимость.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top