Доступ к массиву за пределами границ в C++ и reinterpret_cast

Скажем, у меня есть такой код

struct A {
  int header;
  unsigned char payload[1];
};

A* a = reinterpret_cast<A*>(new unsigned char[sizeof(A)+100]);

a->payload[50] = 42;

Это неопределенное поведение? Создание указателя, указывающего за пределы payload, должно быть неопределенным, AFAIK, но я не уверен, верно ли это и в случае, когда я выделил память после массива.

Стандарт говорит, что p[n] совпадает с *(p+ n) и "если выражение P указывает на i-й элемент объекта массива, выражения (P)+N указывают на i+n-е элементы массива". В примере payload указывает на элемент в массиве, выделенный с помощью new, так что это может быть нормально.

Если возможно, было бы неплохо, если бы ваши ответы содержали ссылки на стандарт C++.


person adrianN    schedule 01.07.2016    source источник
comment
Вне этой области, но из любопытства: unsigned char? Есть ли signed char?   -  person Khalil Khalaf    schedule 01.07.2016
comment
@FirstStep msdn.microsoft.com/nl-nl/library/s3f49ktz.aspx Да, есть.   -  person Hatted Rooster    schedule 01.07.2016
comment
@FirstStep char обычно (но не всегда) является знаковым символом, и есть signed char, который всегда подписан.   -  person alain    schedule 01.07.2016
comment
Конечно это UB, любой внешний доступ.   -  person Baum mit Augen    schedule 01.07.2016
comment
Неопределенная часть может заключаться в том, что макет объекта A зависит от реализации. Во многих реализациях полезная нагрузка начинается с четвертого байта объекта; в этом случае payload[50] будет 54-м байтом из выделенных 100 байтов. И, таким образом, это должно давать определенное поведение. Однако sizeof(int) не всегда равен 4 байтам; и полезную нагрузку не нужно выравнивать в начале A + sizeof(int).   -  person Klamer Schutte    schedule 01.07.2016
comment
@KlamerSchutte Я добавил sizeof (A) к своему выделению, чтобы убедиться, что выделенный массив достаточно велик для доступа.   -  person adrianN    schedule 01.07.2016
comment
Этого может быть недостаточно, вам нужно как минимум sizeof(int)+50+выравнивание полезной нагрузки.   -  person Jean-Baptiste Yunès    schedule 01.07.2016
comment
Стандарт заканчивается при условии, что они существуют, поэтому нужно учитывать выравнивание.   -  person Jean-Baptiste Yunès    schedule 01.07.2016
comment
Я чувствую, что вы в порядке, если A является POD, но есть гораздо более приятные способы справиться с этим.   -  person Richard Hodges    schedule 01.07.2016
comment
@BaummitAugen размер в типе может быть меньше размера массива, выход за границы относится к доступу за пределами хранилища, и это не связано с объявленным размером в типе, поскольку идентификатор массива распадается на указатель.   -  person Jean-Baptiste Yunès    schedule 01.07.2016
comment
Я полагаю, вы хотите static_cast здесь. Кроме того, чтобы получить четко определенное поведение, вам может понадобиться использовать новое размещение для создания фактического объекта, я не уверен. Лучше задайте вопрос, как это сделать правильно!   -  person    schedule 01.07.2016
comment
Я думаю, что это дубликат: stackoverflow.com/a/4413035/471160   -  person marcinj    schedule 01.07.2016
comment
@ Jean-BaptisteYunès, я в замешательстве. Люди все еще пытаются защитить этот старый хак? out of bounds относится к доступу за пределы хранилища, и это не связано с объявленным размером в типе — нет, не имеет, да и есть. Доступ к индексу за пределами размера, определенного для массива, является UB, простым и понятным. Независимо от того, является ли массив членом какого-либо содержащего его объекта, которому принадлежит дополнительное хранилище, к которому вы хотите относится доступ за пределами границ, не имеет значения. Тот факт, что имена массивов во многих случаях неявно преобразуются в указатели, также не имеет значения. Это всегда было UB в C и C++.   -  person underscore_d    schedule 03.07.2016
comment
@underscore_d Хорошо, но как насчет указателей и динамически выделяемой памяти? Нет размера на типе, но нет UB, пока вы не получите доступ к тому, что было выделено. Я думаю, что слишком многие люди преувеличивают, что такое UB в таких случаях. А как насчет гибкого члена массива?   -  person Jean-Baptiste Yunès    schedule 03.07.2016
comment
@Jean-BaptisteYunès Я думаю, что слишком многие люди преувеличивают интерпретацию того, что такое UB. Здесь нет места для интерпретации: reinterpret_cast имеет очень мало применений, которые не являются UB или в лучшем случае реализацией -определены, и это явно не один из них. И гибкие члены массива не являются частью C++, поэтому я не знаю, почему вы их упоминаете.   -  person underscore_d    schedule 03.07.2016
comment
@alain char - это отдельный тип, который не обязательно должен быть эквивалентен ни signed char, ни unsigned char, независимо от того, может ли он быть в практических реализациях. (наверное, академическое отличие, но оно описывает многие вещи на этом языке, которые все же стоит знать)   -  person underscore_d    schedule 03.07.2016
comment
@underscore_d да, это разные типы, но (3.9.1) В любой конкретной реализации простой объект char может принимать те же значения, что и знаковый символ или беззнаковый символ; какой из них определяется реализацией.   -  person alain    schedule 03.07.2016
comment
@alain Да, простой char будет отражать доступные значения одного из двух квалифицированных типов, в зависимости от реализации, но эти три отличаются тем, что они получают разные typeid и т. д. Просто хотел добавить этот нюанс.   -  person underscore_d    schedule 03.07.2016
comment
@underscore_d да, это действительно интересно, так как это отличается от int/signed int, которые одинаковы. Спасибо!   -  person alain    schedule 03.07.2016
comment
Когда к указателю добавляется выражение, имеющее целочисленный тип, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива, а массив достаточно велик, результат указывает на элемент, и std ссылается на new как на распределитель для объектов массива, а payload затухает на указатель. С актерским составом все в порядке... или?   -  person Jean-Baptiste Yunès    schedule 04.07.2016


Ответы (3)


Таким образом, поведение reinterpret_cast является неопределенным, мы может ли reinterpret_cast быть char или unsigned char, мы никогда не сможем преобразовать из char или unsigned char, если мы сделаем:

Доступ к объекту через новый указатель или ссылку вызывает неопределенное поведение. Это известно как строгое правило алиасинга.

Так что да, это нарушение строгого правила псевдонимов.

person Jonathan Mee    schedule 01.07.2016
comment
Я подозревал, что это нарушает строгое сглаживание. Не могли бы вы предоставить более подробную ссылку/более подробное объяснение того, что мы никогда не можем использовать char? - person adrianN; 01.07.2016
comment
@adrianN Нажмите на первый reinterpret_cast ;) - person Jonathan Mee; 01.07.2016
comment
Это для динамического типа, а не для POD. - person Jean-Baptiste Yunès; 04.07.2016
comment
@ Jean-BaptisteYunès Подождите, вы говорите, что эти правила не применяются к типам POD? Мне сообщили, что это относится и к типам POD: типы одинакового размера" title="почему не переосмысливает принудительное копирование n для приведения типов между типами одинакового размера"> stackoverflow.com/questions/28697626/ Если вы знаете по-другому, поделитесь! - person Jonathan Mee; 04.07.2016
comment
ваша ссылка просто относится к проблемам выравнивания и представлению ловушек, которые здесь не задействованы. - person Jean-Baptiste Yunès; 04.07.2016
comment
@ Jean-BaptisteYunès Можете ли вы просветить меня, что вы имеете в виду? Мне кажется, что вы выражаете это так: поскольку unsigned char*, для которого изначально была выделена память, не сохраняется, у нас не может быть проблемы с псевдонимами. Это то, что вы имеете в виду под проблемами с ловушками? - person Jonathan Mee; 06.07.2016

Рассмотрим код:

struct {char x[4]; char a; } foo;

int work_with_foo(int i)
{
  foo.a = 1;
  foo.x[i]++;
  return foo.a;
}

Несмотря на то, что программа будет «владеть» хранилищем в foo.x+4, тот факт, что доступ через тип массива определен только для первых четырех элементов, позволит компилятору, среди прочего, заменить приведенный выше код любым из следующее:

int work_with_foo(int i)  { foo.a = 1; foo.x[i]++; return 1; }

int work_with_foo(int i)  { foo.x[i]++; foo.a = 1; return 1; }

Вышеупомянутые замены явно разрешены Стандартом. Менее ясно, какие альтернативные способы записи приращения заставят компилятор вести себя так, как будто он перезагружает foo.a. Например, я думаю, что код *(i+(char*)&foo)+=1; определял бы поведение, когда i равняется смещению foo.a, и я думаю, что то же самое должно быть верно и для *(i+(char*)&foo.x)+=1;, но я не уверен насчет *(i+foo.x)+=1; или *(i+(char*)foo.x)+=1;.

person supercat    schedule 01.07.2016

Этот старый хак C никогда не понадобится в C++.

рассмотреть возможность:

#include <cstdint>
#include <utility>
#include <memory>

template<std::size_t Size>
struct A {
  int header;
  unsigned char payload[Size];
};

struct polyheader
{
  struct concept
  {
    virtual int& header() = 0;
    virtual unsigned char* payload() = 0;
    virtual std::size_t size() const = 0;
    virtual ~concept() = default;  // not strictly necessary, but a reasonable precaution
  };

  template<std::size_t Size>
  struct model : concept
  {
    using a_type = A<Size>;
    model(a_type a) : _a(std::move(a)) {}
    int& header() override {
      return _a.header;
    }

    unsigned char* payload() override {
      return _a.payload;
    }

    std::size_t size() const override {
      return Size;
    }

    A<Size> _a;
  };

  int& header() { return _impl->header(); }
  unsigned char* payload() { return _impl->payload(); }
  std::size_t size() const { return _impl->size(); }

  template<std::size_t Size>
  polyheader(A<Size> a) 
    : _impl(std::make_unique<model<Size>>(std::move(a)))
    {}

  std::unique_ptr<concept> _impl;
};


int main()
{
  auto p1 = polyheader(A<40>());
  auto p2 = polyheader(A<80>());

}
person Richard Hodges    schedule 01.07.2016
comment
Это не работает, если размер не известен во время компиляции, и вы можете мириться с потерями производительности из-за того, что ваши самые основные операции не являются встроенными и виртуальными диспетчерами! - person ; 01.07.2016
comment
@Hurkyl, если вы посмотрите на вопрос, вы увидите, что оператор действительно знает размер во время компиляции. Если вы хотите использовать шаблон A напрямую, конечно, вы можете. При необходимости можно легко расширить этот класс для работы с буферами переменного размера. Поли-обертка здесь просто для обеспечения полиморфизма, если этого потребует OP. - person Richard Hodges; 01.07.2016
comment
+1 исключительно за This old C hack is never necessary in C++. Есть штука под названием std::vector, которая вызывает много шума среди новаторов. - person underscore_d; 03.07.2016