Указатель / целочисленное арифметическое (не) определенное поведение

У меня есть следующий шаблон функции:

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 docs глава 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 вычисляется в соответствии с правилами первых двух параграфов, третий все еще отправляет это в землю неопределенного поведения.

У меня в основном три вопроса:

  1. Является ли мое чтение правильным и поведение duplicate не определено?

  2. Что это за неопределенное поведение? Формально "неопределенный, но все равно будет делать то, что вы хотите" или "ожидать случайных аварий и / или спонтанного самосожжения"?

  3. Если это действительно не определено, есть ли способ сделать это хорошо определенным (возможно, зависящим от компилятора) способом?

Хотя мой вопрос ограничен поведением GCC (и Clang) в том, что касается компиляторов, я бы приветствовал ответ, который рассматривает все виды платформ HW, от обычных настольных компьютеров до экзотических.

1 ответ

Обычная схема для этого - поставить clone() в базовом классе.
Затем каждый производный класс может реализовать свою собственную версию клона.

class Base
{
     public:
        virtual Base*  clone() = 0;
};

class D: public Base
{
        virtual Base*  clone(){  return new D(*this);}
};
Другие вопросы по тегам