2009-09-28 3 views
10

Я пишу динамически типизированный язык. В настоящее время мои объекты представлены следующим образом:Представление динамической типизации в C

struct Class { struct Class* class; struct Object* (*get)(struct Object*,struct Object*); }; 
struct Integer { struct Class* class; int value; }; 
struct Object { struct Class* class; }; 
struct String { struct Class* class; size_t length; char* characters; }; 

Цель состоит в том, что я должен быть в состоянии передать все вокруг как struct Object*, а затем обнаружить тип объекта, сравнивая атрибут class. Например, насыпать целое число для использования я бы просто сделать следующее (предположим, что integer имеет тип struct Class*):

struct Object* foo = bar(); 

// increment foo 
if(foo->class == integer) 
    ((struct Integer*)foo)->value++; 
else 
    handleTypeError(); 

Проблема заключается в том, что, насколько я знаю, стандарт C не дает никаких обещаний по поводу как хранятся структуры. На моей платформе это работает. Но на другой платформе struct String может хранить value до class, и когда я обратился к foo->class в вышеуказанное, я бы действительно получил доступ к foo->value, что явно плохо. Переносимость - большая цель.

Есть альтернативы такому подходу:

struct Object 
{ 
    struct Class* class; 
    union Value 
    { 
     struct Class c; 
     int i; 
     struct String s; 
    } value; 
}; 

Проблема здесь состоит в том, что объединение использует столько же места, как и размер самой большой вещи, которые могут храниться в союзе. Учитывая, что некоторые из моих типов во много раз больше моих других типов, это будет означать, что мои маленькие типы (int) занимают столько места, сколько мои большие типы (map), что является неприемлемым компромиссом.

struct Object 
{ 
    struct Class* class; 
    void* value; 
}; 

Это создает уровень перенаправления, который замедляет работу. Скорость здесь - цель.

Конечная альтернатива заключается в том, чтобы пройти около void* s и самостоятельно управлять внутренними элементами конструкции. Например, для выполнения испытания указанного выше типа:

void* foo = bar(); 

// increment foo 
if(*((struct Class*) foo) == integer) 
    (*((int*)(foo + sizeof(struct Class*))))++; 
else 
    handleTypeError(); 

Это дает мне все, что я хочу (портативность, различные размеры для различных типов и т.д.), но имеет по крайней мере две отрицательные стороны:

  1. Hideous , подверженный ошибкам C. В приведенном выше коде только вычисляется смещение с одним членом; он будет намного хуже с типами, более сложными, чем целые. Я мог бы немного облегчить это с помощью макросов, но это будет болезненно, несмотря ни на что.
  2. Поскольку нет struct, который представляет объект, у меня нет возможности распределения стека (по крайней мере, без реализации моего собственного стека в куче).

В принципе, мой вопрос в том, как я могу получить то, что хочу, не заплатив за это? Есть ли способ быть переносимым, иметь разницу в размере для разных типов, не использовать перенаправление и сохранять мой код довольно?

EDIT: Это лучший ответ, который я когда-либо получал по запросу. Выбор ответа был трудным. SO только позволяет мне выбрать один ответ, поэтому я выбрал тот, который приведет меня к моему решению, но вы все получили upvotes.

ответ

5

См. Python PEP 3123 (http://www.python.org/dev/peps/pep-3123/), как Python решает эту проблему, используя стандарт C. Решение Python может быть непосредственно применено к вашей проблеме.По существу, вы хотите сделать это:

struct Object { struct Class* class; }; 
struct Integer { struct Object object; int value; }; 
struct String { struct Object object; size_t length; char* characters; }; 

Вы можете смело бросить Integer* к Object* и Object* к Integer*, если вы знаете, что ваш объект представляет собой целое число.

+0

Спасибо за эту ссылку; Я многому научился у него. – Imagist

+0

Согласно вашей ссылке, похоже, что это можно сделать с меньшей косвенностью, чем ваш код; в частности: «[I] f a' struct' начинается с 'int',' struct * 'также может быть передан в' int * ', что позволяет записывать значения int в первое поле." Это означает, что в этом случае «struct Integer *» может быть передано в 'struct Class **', что означает, что мне не нужно менять свои объявления; Мне нужно только быть уверенным, чтобы ссылаться на класс через указатели (так я все равно передаю их). – Imagist

7

C дает вам достаточные гарантии, что ваш первый подход будет работать.Единственное изменение, которое вам нужно сделать, что для того, чтобы сделать указатель сглаживанием OK, вы должны иметь union в области, которая содержит все struct с, что вы литейные между:

union allow_aliasing { 
    struct Class class; 
    struct Object object; 
    struct Integer integer; 
    struct String string; 
}; 

(Вы не должны когда-либо использования Союз ни за что - он просто должен находиться в области видимости)

Я считаю, что соответствующая часть стандарта заключается в следующем:

[# 5], за одним исключением, если значение члена объекта объединения используется , когда последний магазин для объекта принадлежал другому члену, поведение определено реализацией. Особая гарантия производятся в порядке упростить использование союзов: Если объединения содержит несколько структур, которые имеют общую исходную последовательность (см ниже), и если объект объединения содержит в настоящее время один из этих структур, разрешается проверять общую начальную часть любого из них в любом месте, где декларация завершенного типа соединения видна. Две структуры имеют общую начальную последовательность , если соответствующие члены имеют совместимые типы (и для бит-полей, одинаковые ширины) для последовательности одного или нескольких начальных членов.

(Это не непосредственно говорят, что это нормально, но я считаю, что это не гарантирует, что если два struct s имеют общую intial последовательность и помещают в союз вместе, они будут изложены в память одинаково - это, безусловно, было идиоматическим C в течение длительного времени, чтобы предположить это, так или иначе).

+0

Требование к союзу довольно близко к чисто теоретическому. Причина довольно проста: если вы создаете одну из этих структур и передаете ее в код в другой единицы перевода, и что TU определяет объединение, структура должна быть совместимой. Поскольку компилятор не знает о каких-либо других ТУ, у него остается только один выбор: убедитесь, что структуры совместимы, если вы можете ... –

+2

Джерри: Конечно, вы знаете, что они будут выложены одинаково в памяти, но в отсутствие союза компилятор может свободно оптимизировать в предположении, что если вы измените объект типа 'struct String', no объекты типа 'struct Object' будут изменены. Это называется «строгий псевдоним». – caf

+1

@caf: это возможно только в случае, если переменные имеют тип объединения - он не может применяться к отдельным структурам. По крайней мере, код должен был бы использовать объединение для получения гарантии, предоставляемой цитируемой секцией (где она появляется в стандарте C99, BTW?). –

2

Проблема в том, что, насколько мне известно, стандарт C не дает никаких обещаний о том, как хранятся структуры. На моей платформе это работает. Но на другой платформе struct String может хранить value до class, и когда я обратился к foo->class в вышеуказанное, я бы действительно получил доступ к foo->value, что явно плохо. Переносимость - большая цель.

Я считаю, что вы здесь не правы. Во-первых, потому что ваш struct String не имеет члена value. Во-вторых, потому что я считаю, что C делает гарантией макета в памяти членов вашей структуры. Именно поэтому следующие различные размеры:

struct { 
    short a; 
    char b; 
    char c; 
} 

struct { 
    char a; 
    short b; 
    char c; 
} 

Если C не сделал никаких гарантий, то составители, вероятно, оптимизировать оба эти быть одинакового размера. Но это гарантирует внутреннюю компоновку ваших структур, поэтому правила естественного выравнивания пинают и делают второй больше, чем первый.

+0

Нужно исправить то, что вы считаете фактически неточным? Или вы просто хотите понизить? –

+0

Я не спускал вниз, но C определенно не гарантирует макет в памяти переменных-членов, однако вы гарантированно, что всегда можете указывать указатель на структуру на указатель на первый член структуры. – Falaina

+1

+1: Мне тоже хорошо. Я полагаю, что самый педантичный может утверждать, что на машине, где недостаточно штрафа за неправильный доступ к короткому члену, структуры могут быть одного размера; Я не знаю такой машины. И некоторые компиляторы поддерживают прагму для достижения этого эффекта. Тем не менее, когда мобильность является целью (как указано в вопросе), единственное безопасное предположение состоит в том, что две структуры будут иметь разные размеры. –

2

Я ценю педантичные проблемы, поднятые этим вопросом и ответами, но я просто хотел упомянуть, что CPython использовал подобные трюки «более или менее навсегда», и он десятилетиями работал с огромным разнообразием компиляторов C.В частности, см. object.h, макросы, такие как PyObject_HEAD, такие структуры, как PyObject: все виды объектов Python (вплоть до уровня API C) получают на них указатели навсегда, отбрасываемые взад и вперед от/PyObject* без ущерба. Прошло некоторое время с тех пор, как я в последний раз играл в морского юриста со стандартом ISO C, до такой степени, что у меня нет копии (!), Но я верю, что есть некоторые ограничения там, что должно заставить это работать так как он имеет почти 20 лет ...

+2

Alex: Вам может быть интересна эта статья о строгом псевдониме: http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html – caf

+2

С другой стороны, см. PEP 3123 (http://www.python.org/dev/peps/pep-3123/), почему Python изменил определение PyObject_HEAD в Py3k, чтобы соответствовать стандарту C. –

3

Раздел 6.2.5 ИСО 9899: 1999 (стандарт С99) говорит:

тип структура описывает последовательно выделено непустое множество объектов-членов (и , при определенных обстоятельствах - неполный массив), каждый из которых имеет опционально указанное имя и, возможно, отдельный тип.

Раздел 6.7.2.1 также говорит:

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

[...]

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

Это гарантирует, что вам нужно.

В вопросе вы говорите:

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

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

Но на другой платформе STRUCT Струнный Integer может хранить значение перед классом и когда доступ foo-> класс выше, я бы на самом деле будет доступ к foo-> значение, которое, очевидно, плохо. Переносимость - большая цель.

Никакой совместимый компилятор не может этого сделать. [Я заменил String на Integer, предполагая, что вы ссылаетесь на первый набор объявлений. При ближайшем рассмотрении вы, возможно, ссылались на структуру со встроенным объединением. Компилятору до сих пор не разрешено изменять порядок class и value.]

+1

Разделы, которые вы цитируете, гарантируют компоновку структуры, однако в стандарте также говорится: Объект должен иметь свое сохраненное значение, доступное только выражением lvalue, которое имеет один из следующих типов: », за которым следует список условий (6.5 bullet 7). Доступ к 'Integer *' через 'Object *' является неопределенным AFAIK и может привести к неправильной оптимизации. Вот почему Python прекратил использовать этот стиль, см. Http://www.python.org/dev/peps/pep-3123/. –

+0

Это хорошая новость; по крайней мере для компиляторов, которые соответствуют этой части стандарта C99, мой код будет работать. – Imagist

+0

@Josh Haberman: Мне нужно будет прочитать PEP более тщательно, чем я готов в это время ночи. Однако, внешне, похоже, что исправление очень похоже на код выше.Я предполагаю, что у меня что-то не хватает. –

2

Существует 3 основных подхода к реализации динамических типов и которые лучше всего зависят от ситуации.

1) Наследование стиля C: Первый показан в ответе Джоша Хабермана. Мы создаем тип-иерархию с использованием классического наследования C-стиля:

struct Object { struct Class* class; }; 
struct Integer { struct Object object; int value; }; 
struct String { struct Object object; size_t length; char* characters; }; 

Функции с динамически типизированных аргументов получить их как Object*, осмотрите class элемент, и поверг в зависимости от обстоятельств. Стоимость проверки типа - это два прыжка с указателем. Стоимость получения базового значения - это один указательный прыжок. В подходах, подобных этому, объекты обычно выделяются в куче, поскольку размер объектов неизвестен во время компиляции. Поскольку в большинстве реализаций malloc выделяется минимум 32 байта за раз, небольшие объекты могут тратить значительную часть памяти на этот подход.

2) Tagged союз: Мы можем удалить уровень косвенности для доступа малых объектов с помощью «короткой оптимизации строки»/«небольшая оптимизация объекта»:

struct Object { 
    struct Class* class; 
    union { 
     // fundamental C types or other small types of interest 
     bool as_bool; 
     int as_int; 
     // [...] 
     // object pointer for large types (or actual pointer values) 
     void* as_ptr; 
    }; 
}; 

Функции с динамически типизированными аргументами получать их как Object, проверьте член class и прочитайте, если необходимо, союз. Стоимость проверки типа - это один указательный прыжок. Если тип является одним из специальных небольших типов, он хранится непосредственно в объединении, и нет никакой возможности для получения значения. В противном случае для извлечения значения требуется один указательный прыжок. Такой подход иногда может не выделять объекты в куче. Хотя точный размер объекта пока неизвестен во время компиляции, теперь мы знаем размер и выравнивание (наш union), необходимый для размещения небольших объектов.

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

3) Nan-boxing: Наконец-то есть нано-бокс, где каждый дескриптор объекта всего 64 бит.

double object; 

Любое значение, соответствующее не-NaN double понимается быть просто double. Все остальные дескрипторы объектов - это NaN. На самом деле существуют большие ряды битовых значений плавающих чисел с двойной точностью, которые соответствуют NaN в обычно используемом стандарте с плавающей точкой IEEE-754. В пространстве NaN мы используем несколько бит для типов тегов и оставшихся битов для данных. Воспользовавшись тем, что большинство 64-разрядных машин фактически имеют только 48-битное адресное пространство, мы можем даже указывать указатели в NaN. Этот метод не требует косвенного или дополнительного использования памяти, но ограничивает наши маленькие типы объектов, неудобен и теоретически не переносится.