Скопировать произвольный тип в C без динамического выделения памяти
Вопрос:
Я думаю, что нашел способ, который, насколько я могу судить, позволяет вам писать полностью независимый от типа код, который делает копию переменной произвольного типа в "стеке" (в кавычках, потому что стандарт C на самом деле не требует должен быть стек, поэтому я имею в виду, что он скопирован с классом автоматического хранения в локальной области видимости). Вот:
/* Save/duplicate thingToCopy */
char copyPtr[sizeof(thingToCopy)];
memcpy(copyPtr, &thingToCopy, sizeof(thingToCopy));
/* modify the thingToCopy variable to do some work; do NOT do operations directly on the data in copyPtr, that's just a "storage bin". */
/* Restore old value of thingToCopy */
memcpy(&thingToCopy, copyPtr, sizeof(thingToCopy));
Из моего ограниченного тестирования это работает и, насколько я могу судить, должно работать на всех реализациях C, соответствующих стандартам, но на случай, если я что-то пропустил, я хотел бы знать:
- Это полностью соответствует стандарту C (я считаю, что это должно быть хорошо на всем пути от C89 до современных вещей), и если нет, то можно ли это исправить и как?
- Какие ограничения на использование этот метод накладывает на себя, чтобы оставаться совместимым со стандартами?
- Например, насколько я понимаю, я в безопасности от проблем с выравниванием, поскольку я никогда не использую временные копии массива char напрямую - просто в качестве корзин для сохранения и загрузки с помощью memcpy. Но я не мог передать эти адреса другим функциям, ожидающим указатели того типа, с которым я работаю, не рискуя проблемами с выравниванием (очевидно, синтаксически я мог бы сделать это извращенно, сначала получив
void *
отchar *
даже без указания точного типа, с которым я работаю, но дело в том, что я думаю, что при этом я буду вызывать неопределенное поведение).
- Например, насколько я понимаю, я в безопасности от проблем с выравниванием, поскольку я никогда не использую временные копии массива char напрямую - просто в качестве корзин для сохранения и загрузки с помощью memcpy. Но я не мог передать эти адреса другим функциям, ожидающим указатели того типа, с которым я работаю, не рискуя проблемами с выравниванием (очевидно, синтаксически я мог бы сделать это извращенно, сначала получив
- Есть ли более чистый и / или эффективный способ достижения того же самого?
* GCC 4.6.1 на моем тестовом устройстве armel v7 с оптимизацией -O3 создавал код, идентичный обычному коду, используя обычные присвоения временных переменных, но, возможно, мои тестовые примеры были достаточно просты, чтобы его можно было выяснить и что было бы запутаться, если бы эта техника использовалась более широко.
В качестве бонуса при прохождении интереса, мне любопытно, будет ли это нарушаться в основном в C-совместимых языках (из тех, что мне известны, это C++, Objective-C, D и, возможно, C#, хотя упоминания о других тоже приветствуются).
Обоснование:
Вот почему я думаю, что вышесказанное работает, если вам будет полезно узнать, откуда я, чтобы объяснить любые ошибки, которые я, возможно, сделал:
"Байт" стандарта C (в традиционном смысле "наименьшая адресуемая единица памяти", а не в модернизированном значении "8 бит") является char
введите sizeof
Оператор производит числа в единицах char
, Таким образом, мы можем получить точно наименьший размер хранилища (с которым мы можем работать в C), необходимый для типа произвольной переменной, используя sizeof
оператор этой переменной.
Стандарт C гарантирует, что практически все типы указателей могут быть неявно преобразованы в void *
(но с изменением представления, если их представление отличается (но, между прочим, стандарт C гарантирует, что void *
а также char *
имеют одинаковые представления)).
"Имя" массива данного типа и указатель на этот же тип могут в основном обрабатываться идентично с точки зрения синтаксиса.
sizeof
оператор вычисляется во время компиляции, поэтому мы можем сделать char foo[sizeof(bar)]
без зависимости от фактически непереносимых VLA.
Следовательно, мы должны иметь возможность объявить массив "символов", который является минимальным размером, необходимым для хранения данного типа.
Таким образом, мы должны быть в состоянии передать адрес переменной, которая будет скопирована, и имя массива, в memcpy
(насколько я понимаю, имя массива неявно используется как char *
к первому элементу массива). Поскольку любой указатель может быть неявно преобразован в void *
(с изменением представления необходимо), это работает.
Memcpy должен сделать побитовую копию переменной, которую мы копируем в массив. Независимо от того, какой тип, какие-либо биты заполнения и т. Д., sizeof
гарантирует, что мы возьмем все биты, которые составляют тип, включая заполнение.
Поскольку мы не можем явно использовать / объявлять тип переменной, которую мы только что скопировали, и поскольку некоторые архитектуры могут иметь требования к выравниванию для различных типов, которые этот хак может иногда нарушать, мы не можем использовать эту копию напрямую - мы ' должен был memcpy
обратно в переменную, из которой мы ее получили, или в один и тот же тип, чтобы использовать ее. Но как только мы копируем это обратно, у нас есть точная копия того, что мы поместили туда в первую очередь. По сути, мы освобождаем саму переменную для использования в качестве пустого пространства.
Мотивация (или "Дорогой Бог, Почему!?!"):
Мне нравится писать код, независимый от типа, когда это полезно, и все же я также наслаждаюсь кодированием на C, и объединение этих двух функций в значительной степени сводится к написанию универсального кода в функционально-подобных макросах (затем вы можете повторно запросить проверку типов, создав оболочку определения функций, которые вызывают подобный функции макрос). Думайте об этом как о действительно сырых шаблонах в C.
Сделав это, я столкнулся с ситуациями, когда мне понадобилась дополнительная переменная пустого пространства, но, учитывая отсутствие переносимого оператора typeof(), я не могу объявить какие-либо временные переменные соответствующего типа в таком "универсальном" макрос "фрагменты кода. Это самое близкое к действительно портативному решению, которое я нашел.
Так как мы можем делать этот трюк несколько раз (достаточно большой массив символов, чтобы мы могли вместить несколько копий, или несколько массивов символов, достаточно больших, чтобы соответствовать одному), до тех пор, пока мы можем сохранить наш memcpy
вызовы и копирование имен указателей прямо, это функционально, как иметь произвольное количество временных переменных копируемого типа, в то же время сохраняя способность независимого от типа кода.
PS Чтобы немного отклонить вероятный неизбежный дождь суждений, я хотел бы сказать, что я признаю, что это серьезно запутано, и я бы оставил это на практике только для очень хорошо протестированного библиотечного кода, где он значительно добавил полезность, а не что-то, что я бы регулярно использовал.
2 ответа
Да, это работает. Да, это стандарт C89. Да, это запутанно.
Незначительное улучшение
Таблица байтов char[]
может начать в любой позиции в памяти. В зависимости от содержания вашего thingToCopy
и, в зависимости от процессора, это может привести к неоптимальной производительности копирования.
Если скорость имеет значение (поскольку это может быть не так, если эта операция встречается редко), вы можете предпочесть выравнивание таблицы, используя int
, long long
или же size_t
единицы вместо.
Основное ограничение
Ваше предложение работает, только если вы знаете размер thingToCopy
, Это серьезная проблема: это означает, что ваш компилятор должен знать, что thingToCopy
находится в типе компиляции (следовательно, это не может быть неполный тип).
Следовательно, следующее предложение вызывает беспокойство:
Поскольку мы не можем явно использовать / объявлять тип переменной, которую мы только что скопировали
Ни за что. Для того, чтобы скомпилировать char copyPtr[sizeof(thingToCopy)];
, компилятор должен знать, что thingToCopy
есть, следовательно, он должен иметь доступ к своему типу!
Если вы знаете это, вы можете просто сделать:
thingToCopy_t save;
save = thingToCopy;
/* do some stuff with thingToCopy */
thingToCopy = save;
который яснее читать, а еще лучше с точки зрения выравнивания.
Было бы плохо использовать ваш код на объекте, содержащем указатель (кроме указателя const на const). Кто-то может изменить указанные данные или сам указатель (например, realloc). Это оставит вашу копию объекта в неожиданном или даже недействительном состоянии.
Универсальное программирование является одной из основных движущих сил C++. Другие пытались сделать общее программирование на C, используя макросы и приведение типов. Это нормально для небольших примеров, но плохо масштабируется. Компилятор не может поймать ошибки за вас, когда вы используете эти методы.