Подтвердить что ты не робот

Является ли законным индексирование в структуру?

Независимо от того, как "плохо" используется код, и считая, что выравнивание и т.д. не является проблемой для компилятора/платформы, это undefined или нарушение поведения?

Если у меня есть такая структура: -

struct data
{
    int a, b, c;
};

struct data thing;

Является ли юридическим доступ к a, b и c как (&thing.a)[0], (&thing.a)[1] и (&thing.a)[2]?

В каждом случае, на каждом компиляторе и платформе я пробовал его, с каждой настройкой, которую я пробовал, он "работал". Я просто обеспокоен тем, что компилятор может не понимать, что b и вещь [1] - одно и то же, а магазины в "b" могут быть помещены в регистр и предмет [1] читает неправильное значение из памяти (например). В каждом случае я попробовал, но сделал все правильно. (Я понимаю, конечно, что не очень много)

Это не мой код; это код, с которым мне нужно работать, меня интересует, является ли этот код плохим или сломанным, поскольку разные влияют на мои приоритеты для его изменения:)

С тегами C и С++. Меня больше всего интересует С++, но и C, если он отличается, просто для интереса.

4b9b3361

Ответ 1

Это незаконно 1. Это поведение Undefined в С++.

Вы принимаете элементы в массиве, но вот что говорит стандарт С++ (выделение мое):

[dcl.array/1]:... Объект типа массива содержит смежно выделено непустое множество N подобъекты типа T...

Но для членов нет такого смежного требования:

[class.mem/17]:...; Требования к выравниванию реализации могут привести к два смежных члены не будут назначаться сразу после друг друга...

В то время как вышеупомянутых двух кавычек должно быть достаточно, чтобы намекнуть, почему индексирование в struct, как вы это делали, не является определенным поведением по стандарту С++, давайте выбрать один пример: посмотрите на выражение (&thing.a)[2] - Что касается индекса оператор:

[expr.post//expr.sub/1]:Постфиксное выражение, за которым следует выражение в квадратных скобках, является постфиксное выражение. Одним из выражений должно быть glvalue типа "массив из Т" или указатель типа "указатель на Т", а другой быть prvalue неперечисленного перечисления или интегрального типа. В результате типа "Т". Тип "T" должен быть полностью определенным типом объекта .66 Выражение E1[E2] идентично (по определению) на ((E1)+(E2))

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

[expr.add/4]: Когда выражение с интегральным типом добавляется или вычитается из указатель, результат имеет тип операнда указателя. Есливыражение P указывает на элемент x[i] объекта массив объект xс n элементами, выражения P + J и J + P (где J имеет значение J) указывают на (возможно, гипотетический) элемент x[i + j]если 0 ≤ i + j ≤ n; в противном случае, поведение undefined....

Обратите внимание на требование массива для предложения if; иначе в противном случае в приведенной выше цитате. Выражение (&thing.a)[2], очевидно, не подходит для предложения if; Следовательно, Undefined Поведение.


На стороне примечания: Хотя я интенсивно экспериментировал с кодом и его вариациями на разных компиляторах, и здесь они не вводят никаких дополнений (это работает); с точки зрения обслуживания, код чрезвычайно хрупкий. вы все равно должны утверждать, что реализация приступила к объединению участников, прежде чем делать это. И пребывание в границах:-). Но его по-прежнему Undefined поведение....

Некоторые жизнеспособные обходные пути (с определенным поведением) были предоставлены другими ответами.



Как справедливо указано в комментариях, [basic.lval/8], который был в моем предыдущее редактирование не применяется. Спасибо @2501 и @M.M.

1: см. @Barry ответ на этот вопрос только для одного юридического случая, когда вы можете получить доступ к thing.a через эту часть.

Ответ 2

Нет. В C это поведение undefined, даже если нет дополнения.

Вещь, которая вызывает поведение undefined, - это доступ за пределы доступа 1. Когда у вас есть скаляр (члены a, b, c в структуре) и пытайтесь использовать его как массив 2 для доступа к следующему гипотетическому элементу, вы вызываете поведение undefined, даже если это происходит быть другим объектом того же типа по этому адресу.

Однако вы можете использовать адрес объекта struct и вычислять смещение в конкретном члене:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

Это нужно сделать для каждого члена отдельно, но его можно поместить в функцию, которая похожа на доступ к массиву.


1 (Цитируется по: ISO/IEC 9899: 201x 6.5.6 Аддитивные операторы 8)
 Если результат указывает один за последний элемент объекта массива, он не должен использоваться в качестве операнда унарного * оператора, который оценивается.

2 (Цитируется по: ISO/IEC 9899: 201x 6.5.6 Аддитивные операторы 7)
Для целей этих операторов указатель на объект, который не является элементом массив ведет себя так же, как указатель на первый элемент массива длиной один с тип объекта как его тип элемента.

Ответ 3

В С++, если вам это действительно нужно - создайте оператор []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

он не только гарантированно работает, но и упрощается, вам не нужно писать нечитаемое выражение (&thing.a)[0]

Примечание: этот ответ дается в предположении, что у вас уже есть структура с полями, и вам нужно добавить доступ через индекс. Если скорость является проблемой, и вы можете изменить структуру, это может быть более эффективным:

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

Это решение изменит размер структуры, чтобы вы могли также использовать методы:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};

Ответ 4

Для С++: если вам нужно получить доступ к элементу без знания его имени, вы можете использовать указатель на переменную-член.

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;

Ответ 5

В стандарте ISO C99/C11 запрет на использование в профсоюзе является законным, поэтому вы можете использовать это вместо указателей указателей на не-массивы (см. различные другие ответы).

ISO С++ не допускает принудительного использования на основе профсоюзов. GNU С++ делает это как расширение, и я думаю, что некоторые другие компиляторы, которые не поддерживают расширения GNU в целом, поддерживают принцип объединения типов. Но это не поможет вам написать строго переносимый код.

С текущими версиями gcc и clang запись функции члена С++ с помощью switch(idx) для выбора члена будет оптимизирована для индексов постоянной времени компиляции, но создаст ужасные веткистые asm для индексов времени исполнения. Для этого нет ничего неправильного в switch(); это просто ошибка с пропущенной оптимизацией в текущих компиляторах. Они могут эффективно компилировать функцию Slava 'switch().


Решение/обходное решение для этого заключается в том, чтобы сделать это по-другому: дать вашему классу/структуре член массива и написать функции доступа для присоединения имен к определенным элементам.

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

Мы можем посмотреть выход asm для разных прецедентов, в Godbolt explorer. Это полные функции системы x86-64 System V, при этом конечная инструкция RET опущена, чтобы лучше показать, что вы получите, когда они будут встроены. ARM/MIPS/что бы ни было похоже.

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

Для сравнения, ответ @Slava с использованием switch() для С++ делает asm таким же, как для индекса переменной runtime. (Код в предыдущей ссылке Godbolt).

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

Это, очевидно, ужасно, по сравнению с версией Punisher, основанной на объединении на основе C (или GNU С++):

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]

Ответ 6

Это поведение undefined.

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

Существуют правила об aliasing (доступ к данным через два разных типа указателя), границы массивов и т.д.

Когда у вас есть переменная x, тот факт, что она не является членом массива, означает, что компилятор может предположить, что доступ к массиву без [] может изменить его. Поэтому он не должен постоянно перезагружать данные из памяти каждый раз, когда вы его используете; только если кто-то мог изменить его имя.

Таким образом, (&thing.a)[1] можно предположить, что компилятор не ссылается на thing.b. Он может использовать этот факт для изменения порядка чтения и записи на thing.b, что делает невозможным то, что вы хотите, чтобы не сделать то, что вы на самом деле сказали ему.

Классическим примером этого является отбрасывание const.

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

здесь вы обычно получаете компилятор, говорящий 7, затем 2!= 7, а затем два одинаковых указателя; несмотря на то, что ptr указывает на x. Компилятор принимает тот факт, что x является постоянным значением, чтобы не читать его, когда вы запрашиваете значение x.

Но когда вы берете адрес x, вы вынуждаете его существовать. Затем вы отбрасываете const и изменяете его. Таким образом, фактическое местоположение в памяти, где x было изменено, компилятор свободен на самом деле не читать его при чтении x!

Компилятор может получить достаточно умный способ выяснить, как избежать использования ptr для чтения *ptr, но часто это не так. Не стесняйтесь использовать и использовать ptr = ptr+argc-1 или некоторую путаницу, если оптимизатор становится умнее вас.

Вы можете предоставить пользовательский operator[], который получит правильный элемент.

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

которые оба полезны.

Ответ 7

В С++ это в основном undefined поведение (это зависит от того, какой индекс).

Из [expr.unary.op]:

Для указателя арифметика (5.7) и сравнение (5.9, 5.10), объект, который не является элементом массива, адрес которого берется в этот способ считается принадлежащим массиву с одним элементом типа T.

Таким образом, выражение &thing.a считается ссылкой на массив из одного int.

Из [expr.sub]:

Выражение E1[E2] идентично (по определению) до *((E1)+(E2))

И из [expr.add]:

Когда выражение, которое имеет интегральный тип, добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если выражение P указывает на элемент x[i] объекта массива x с элементами n, выражения P + J и J + P (где J имеет значение J) указывают на ( возможно-гипотетический) элемент x[i + j], если 0 <= i + j <= n; в противном случае поведение undefined.

(&thing.a)[0] отлично сформирован, потому что &thing.a считается массивом размера 1, и мы берем этот первый индекс. Это допустимый индекс.

(&thing.a)[2] нарушает предварительное условие, что 0 <= i + j <= n, так как мы имеем i == 0, j == 2, n == 1. Простое построение указателя &thing.a + 2 - это поведение undefined.

(&thing.a)[1] - интересный случай. Это фактически не нарушает ничего в [expr.add]. Нам разрешено взять указатель за концом массива - что бы это было. Здесь мы переходим к заметке в [basic.compound]:

Значение типа указателя, которое является указателем на конец объекта или мимо него, представляет собой адрес первый байт в памяти (1.7), занятый объектом53, или первый байт в памяти после окончания хранения занимаемых объектом, соответственно. [Примечание: указатель мимо конца объекта (5.7) не считается указывают на несвязанный объект типа объектов, который может быть расположен по этому адресу.

Следовательно, взятие указателя &thing.a + 1 определяется поведением, но разыменование его - undefined, потому что оно не указывает ни на что.

Ответ 8

Вот как использовать прокси-класс для доступа к элементам в массиве-члене по имени. Это очень С++ и не имеет преимуществ по сравнению с ref-возвращающими функциями доступа, кроме синтаксических предпочтений. Это перегружает оператор -> для доступа к элементам как к членам, поэтому, чтобы быть приемлемым, нужно как не нравится синтаксис accessors (d.a() = 5;), так и допускать использование -> с объектом без указателя. Я ожидаю, что это может также смутить читателей, не знакомых с кодом, поэтому это может быть скорее опрятный трюк, чем то, что вы хотите ввести в производство.

Структура Data в этом коде также включает в себя перегрузки для оператора индекса, для доступа к индексированным элементам внутри его элемента массива ar, а также к функциям begin и end для итерации. Кроме того, все они перегружены версиями non-const и const, которые, как мне казалось, необходимо включить для полноты.

Когда Data -> используется для доступа к элементу по имени (например: my_data->b = 5;), возвращается объект Proxy. Тогда, поскольку это Proxy rvalue не является указателем, его собственный оператор -> авто-цепной называется, который возвращает указатель на себя. Таким образом, объект Proxy создается и остается действительным во время оценки исходного выражения.

Конструирование объекта Proxy заполняет его 3 ссылочных элемента a, b и c в соответствии с указателем, переданным в конструкторе, который предполагается указывать на буфер, содержащий по меньшей мере 3 значения, тип которых задается как параметр шаблона T. Поэтому вместо использования названных ссылок, которые являются членами класса Data, это сохраняет память, заполняя ссылки в точке доступа (но, к сожалению, используя ->, а не оператор .).

Чтобы проверить, насколько оптимизирован оптимизатор компилятора, устраняет всю косвенность, введенную с помощью Proxy, приведенный ниже код содержит 2 версии main(). Версия #if 1 использует операторы -> и [], а версия #if 0 выполняет эквивалентный набор процедур, но только путем прямого доступа к Data::ar.

Функция Nci() генерирует целочисленные значения времени выполнения для инициализации элементов массива, что не позволяет оптимизатору просто подключать постоянные значения непосредственно к каждому вызову std::cout <<.

Для gcc 6.2, используя -O3, обе версии main() генерируют одну и ту же сборку (переключаются между #if 1 и #if 0 до сравнения первого main()): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif

Ответ 9

Если значений чтения достаточно, а эффективность не является проблемой, или если вы доверяете своему компилятору хорошо оптимизировать ситуацию или если структура равна 3 байтам, вы можете смело сделать следующее:

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

Только для версии С++ вы, вероятно, захотите использовать static_assert, чтобы убедиться, что struct data имеет стандартный макет и, возможно, вместо этого исключает исключение из индекса.

Ответ 10

Это незаконно, но есть обходное решение:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

Теперь вы можете индексировать v: