У меня есть следующий шаблон функции:
template <class MostDerived, class HeldAs>
HeldAs* duplicate(MostDerived *original, HeldAs *held)
{
// error checking omitted for brevity
MostDerived *copy = new MostDerived(*original);
std::uintptr_t distance = reinterpret_cast<std::uintptr_t>(held) - reinterpret_cast<std::uintptr_t>(original);
HeldAs *copyHeld = reinterpret_cast<HeldAs*>(reinterpret_cast<std::uintptr_t>(copy) + distance);
return copyHeld;
}
Цель состоит в том, чтобы продублировать объект определенного типа и вернуть его, «удерживаемый» тем же подобъектом, что и входные данные. Обратите внимание, что в принципе HeldAs
может быть неоднозначным или недоступным базовым классом MostDerived
так что никакой актерский состав не может помочь здесь.
Это мой код, но он может быть использован с типами вне моего контроля (т.е. я не могу изменить MostDerived
или же HeldAs
). Функция имеет следующие предварительные условия:
*original
имеет динамический тип MostDerived
HeldAs
является MostDerived
или прямой или косвенный базовый класс MostDerived
(игнорируя cv-квалификацию)*held
относится к *original
или один из его подобъектов базового класса.Давайте предположим, что предварительные условия выполнены. Есть ли duplicate
определили поведение в таком случае?
C ++ 11 [expr.reinterpret.cast] говорит (выделение жирным шрифтом мое):
4 Указатель может быть явно преобразован в любой целочисленный тип, достаточно большой для его хранения. Функция отображения
реализации. [ Замечания: Это должно быть неудивительно для тех, кто знает структуру адресации
базовой машины. —Конечная записка ] …5 Значение целочисленного типа или типа перечисления может быть явно преобразовано в указатель. Указатель преобразован
к целому числу достаточного размера (если таковое существует в реализации) и обратно к тому же типу указателя
будет иметь свое первоначальное значение; Отображения между указателями и целыми числами определяются реализацией.
[ Замечания: За исключением случаев, описанных в 3.7.4.3, результатом такого преобразования не будет безопасный указатель
значение. —Конечная записка ]
Хорошо, допустим, мой компилятор — GCC (или Clang, поскольку он использует определения поведения, определенного GCC). квотирование GCC документы глава 5 на C ++ определяемое реализацией поведение:
… Некоторые варианты описаны в соответствующем документе для языка Си. Увидеть Реализация С. …
На глава 4.7 (Реализация C, массивы и указатели):
Результат преобразования указателя в целое число или наоборот (C90 6.3.4, C99 и C11 6.3.2.3).
Преобразование из указателя в целое число отбрасывает наиболее значимые биты, если представление указателя больше целочисленного типа, расширяет знак, если представление указателя меньше целочисленного типа, в противном случае биты не изменяются.
Преобразование из целого числа в указатель отбрасывает наиболее значимые биты, если представление указателя меньше целочисленного типа, расширяется в соответствии со знаком целочисленного типа, если представление указателя больше целочисленного типа, в противном случае биты не изменяются.
Все идет нормально. Казалось бы, так как я использую std::uintptr_t
который гарантированно будет достаточно большим для любого указателя, а так как я имею дело с одними и теми же типами, copyHeld
следует указать на то же HeldAs
подобъект *copy
как held
указывал на внутри *original
,
К сожалению, есть еще один абзац в документах GCC:
При приведении от указателя к целому и обратно, результирующий указатель должен ссылаться на тот же объект, что и исходный указатель, в противном случае поведение не определено. То есть нельзя использовать целочисленную арифметику, чтобы избежать неопределенного поведения арифметики с указателями, как это запрещено в C99 и C11 6.5.6 / 8.
Бух. Так что теперь кажется, что хотя значение copyHeld
вычисляется в соответствии с правилами первых двух параграфов, третий все еще отправляет это в землю неопределенного поведения.
У меня в основном три вопроса:
Является ли мое чтение правильным и поведение duplicate
не определено?
Что это за неопределенное поведение? Формально «неопределенный, но все равно будет делать то, что вы хотите» или «ожидать случайных аварий и / или спонтанного самосожжения»?
Если это действительно не определено, есть ли способ сделать это хорошо определенным (возможно, зависящим от компилятора) способом?
Хотя мой вопрос ограничен поведением GCC (и Clang) в том, что касается компиляторов, я бы приветствовал ответ, который рассматривает все виды платформ HW, от обычных настольных компьютеров до экзотических.
Обычная схема для этого — поставить clone()
в базовом классе.
Затем каждый производный класс может реализовать свою собственную версию клона.
class Base
{
public:
virtual Base* clone() = 0;
};
class D: public Base
{
virtual Base* clone(){ return new D(*this);}
};