Законно ли индексировать структуру?

104

Независимо от того, насколько «плохой» код и если предположить, что выравнивание и т. Д. Не является проблемой для компилятора / платформы, является ли это неопределенным или неправильным поведением?

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

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

struct data thing;

Является ли это законным для доступа a, bа cтакже (&thing.a)[0], (&thing.a)[1]и (&thing.a)[2]?

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

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

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

jcoder
источник
51
Нет, это не «законно». Это неопределенное поведение.
Сэм Варшавчик
10
Это работает для вас в этом очень простом случае, потому что компилятор не добавляет никаких отступов между членами. Попробуйте использовать конструкции разных размеров, и они рухнут.
Какой-то чувак-программист
7
Копаем прошлое - UB называли носовыми демонами .
Адриан Коломитчи
21
Отлично, здесь я спотыкаюсь, потому что следую тегу C, читаю вопрос, а затем пишу ответ, который применим только к C, потому что я не видел тег C ++. C и C ++ здесь очень разные! C позволяет набирать текст с объединениями, а C ++ - нет.
Lundin
7
Если вам нужно получить доступ к элементам как к массиву, определите их как массив. Если им нужно иметь разные имена, используйте их. Попытка съесть пирог в конечном итоге приведет к несварению желудка - вероятно, в самый неудобный момент, который только можно себе представить. (Я думаю, что индекс 0 допустим в C; индекс 1 или 2 - нет. Существуют контексты, в которых один элемент рассматривается как массив размера 1.)
Джонатан Леффлер,

Ответы:

73

Это незаконно 1 . Это неопределенное поведение в C ++.

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

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

Но для участников нет такого непрерывного требования:

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

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

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

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

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

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


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

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



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

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

WhiZTiM
источник
1
@jcoder Он определен в class.mem . См. Последний абзац для фактического текста.
NathanOliver
4
Строгий ализинг здесь не актуален. Тип int содержится в агрегатном типе, и этот тип может быть псевдонимом int. - an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
2501,
1
@ The downvoters, хотите прокомментировать? - и улучшить или указать, где этот ответ неверен?
WhiZTiM
4
Строгий псевдоним здесь не имеет значения. Заполнение не является частью сохраненного значения объекта. Также в этом ответе не рассматривается наиболее распространенный случай: что происходит, когда нет заполнения. На самом деле рекомендую удалить этот ответ.
MM
1
Готово! Я удалил абзац о строгом алиасинге.
WhiZTiM
48

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

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

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

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)
Для целей этих операторов указатель на объект, который не является элементом массива, ведет себя так же, как указатель на первый элемент массива. массив длины один с типом объекта в качестве типа его элемента.

2501
источник
3
Обратите внимание, это работает, только если класс является стандартным типом макета. Если нет, это все еще UB.
NathanOliver
@NathanOliver Я должен упомянуть, что мой ответ относится только к C. Отредактировано. Это одна из проблем, связанных с вопросами о двойном языке тегов.
2501,
Спасибо, и поэтому я попросил отдельно C ++ и C, так как интересно узнать различия
jcoder
@NathanOliver Адрес первого члена гарантированно совпадает с адресом класса C ++, если это стандартный макет. Однако это не гарантирует, что доступ четко определен, и не подразумевает, что такие обращения к другим классам не определены.
Potatoswatter
Вы бы сказали, что это char* p = ( char* )&thing.a + offsetof( thing , b );ведет к неопределенному поведению?
MM
43

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

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]; }
};
Слава
источник
1
Я бы хотел увидеть разборку этого, а не разборку программы на C с использованием каламбура. Но, но ... C ++ так же быстр, как C ... не так ли? Правильно?
Lundin
6
@Lundin, если вас волнует скорость построения, то данные должны быть организованы в первую очередь как массив, а не как отдельные поля.
Слава
2
@Lundin в обоих случаях вы имеете в виду нечитаемое и неопределенное поведение? Нет, спасибо.
Слава
1
@Lundin Перегрузка оператора - это синтаксическая функция времени компиляции, которая не вызывает никаких накладных расходов по сравнению с обычными функциями. Взгляните на godbolt.org/g/vqhREz, чтобы узнать, что на самом деле делает компилятор при компиляции кода C ++ и C. Удивительно, что они делают и чего от них ждут. Лично я предпочитаю в миллион раз лучшую безопасность типов и выразительность C ++. И он работает все время, не полагаясь на предположения о заполнении.
Jens
2
Эти ссылки, по крайней мере, увеличат размер вещи вдвое. Просто сделай thing.a().
TC
14

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

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;
Рассказчик - Незеленка Моника
источник
1
При этом используются языковые средства, и в результате он четко определен и, как я полагаю, эффективен. Лучший ответ.
Питер - Восстановите Монику
2
Считать эффективным? Я предполагаю обратное. Посмотрите на сгенерированный код.
JDługosz
1
@ JDługosz, вы совершенно правы. Взглянув на сгенерированную сборку, кажется, что gcc 6.2 создает код, эквивалентный использованию offsetoffв C.
StoryTeller - Unslander Monica
3
вы также можете улучшить ситуацию, сделав arr constexpr. Это создаст единую фиксированную таблицу поиска в разделе данных, а не на лету.
Тим
10

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

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

В текущих версиях gcc и clang написание функции-члена C ++ с использованием a 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 . Это полные функции 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 с использованием a switch()для C ++ делает asm подобным этому для индекса переменной времени выполнения. (Код в предыдущей ссылке 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

Это явно ужасно по сравнению с версией каламбура типа на основе объединения C (или GNU C ++):

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]
Питер Кордес
источник
@MM: хороший момент. Это скорее ответ на различные комментарии и альтернатива ответу Славы. Я переформулировал вводную часть, чтобы она, по крайней мере, начиналась как ответ на исходный вопрос. Спасибо что подметил это.
Питер Кордес
Хотя каламбур типов на основе объединения, похоже, работает в gcc и clang при использовании []оператора непосредственно на члене объединения, стандарт определяет array[index]как эквивалентный *((array)+(index)), и ни gcc, ни clang не будут надежно распознавать, что доступ к *((someUnion.array)+(index))является доступом к someUnion. Единственное объяснение, которое я вижу, это то, что someUnion.array[index]ни *((someUnion.array)+(index))не определены стандартом, а являются просто популярными расширениями, и gcc / clang решили не поддерживать второе, но, похоже, поддерживают первое, по крайней мере, на данный момент.
supercat 08
9

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

Из [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]if 0 <= i + j <= n; в противном случае поведение не определено.

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

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

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

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

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

Барри
источник
Вычисление (& thing.a) +1 почти законно, потому что указатель за концом массива допустим; чтение или запись хранимых там данных - неопределенное поведение, по сравнению с & thing.b с <,>, <=,> = - неопределенное поведение. (& thing.a) + 2 абсолютно незаконно.
gnasher729
@ gnasher729 Да, стоит еще немного уточнить ответ.
Barry
Это (&thing.a + 1)интересный случай, который мне не удалось раскрыть. +1! ... Просто любопытно, вы из комитета ISO C ++?
WhiZTiM
Это также очень важный случай, потому что в противном случае каждый цикл, использующий указатели в качестве полуоткрытого интервала, был бы UB.
Jens
По поводу последней стандартной цитаты. Здесь C ++ должен быть указан лучше, чем C.
2501,
8

Это неопределенное поведение.

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

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

Когда у вас есть переменная 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;

иметь и то и другое полезно.

Якк - Адам Неврамонт
источник
«тот факт, что он не является членом массива, означает, что компилятор может предположить, что никакой доступ к массиву на основе [] не может его изменить». - не соответствует действительности, например (&thing.a)[0]может изменить это
MM
Я не понимаю, какое отношение имеет пример const к вопросу. Это не удается только потому, что существует конкретное правило, запрещающее изменение константного объекта, а не по какой-либо другой причине.
MM
1
@MM, это не пример индексации в структуру, но это очень хорошая иллюстрация того, как использование неопределенного поведения для ссылки на что-то по его очевидному расположению в памяти может привести к другому результату, чем ожидалось, потому что компилятор может делать что-то еще с UB, чем вы хотели.
Wildcard
@MM К сожалению, нет доступа к массиву, кроме тривиального, через указатель на сам объект. А второй - просто пример легко заметных побочных эффектов неопределенного поведения; компилятор оптимизирует чтение, xпотому что знает, что вы не можете изменить его определенным образом. Аналогичная оптимизация может произойти, когда вы изменяете bчерез, (&blah.a)[1]если компилятор может доказать, что не было определенного доступа, bкоторый мог бы его изменить; такое изменение могло произойти из-за, казалось бы, безобидных изменений в компиляторе, окружающем коде и т. д. Так что даже проверки того, что он работает, недостаточно.
Yakk - Adam Nevraumont
6

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

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

Когда Data«S ->используется для доступа к элементу по имени (например: my_data->b = 5;), А Proxyобъект возвращается. Затем, поскольку это Proxyrvalue не является указателем, ->автоматически вызывается его собственный оператор, который возвращает указатель на себя. Таким образом 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
Кристофер Оиклес
источник
Отлично. Проголосовали в основном потому, что вы доказали, что это оптимизирует прочь. Кстати, вы можете сделать это намного проще, написав очень простую функцию, а не целиком main()с функциями синхронизации! например, int getb(Data *d) { return (*d)->b; }компилируется в просто mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ). (Да, Data &dэто упростило бы синтаксис, но я использовал указатель вместо ref, чтобы подчеркнуть странность ->такой перегрузки .)
Питер Кордес
Во всяком случае, это круто. Другие идеи, такие как int tmp[] = { a, b, c}; return tmp[idx];не оптимизировать, так что это здорово, что это делает.
Питер Кордес
Еще одна причина, по которой мне не хватает operator.C ++ 17.
Jens
2

Если чтения значений достаточно, и эффективность не вызывает беспокойства, или если вы доверяете своему компилятору хорошо оптимизировать вещи, или если структура составляет всего 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];
}

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

Гайд
источник
1

Это незаконно, но есть обходной путь:

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

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

Свен Нильссон
источник
6
Многие проекты на C ++ думают, что повсеместное отрицание - это нормально. Мы по-прежнему не должны проповедовать плохие практики.
StoryTeller - Unslander Моника
2
Объединение решает проблему строгого псевдонима на обоих языках. Но прокалывание типов через объединение нормально только в C, а не в C ++.
Lundin
1
тем не менее, я не удивлюсь, если это будет работать на 100% всех компиляторов C ++. Когда-либо.
Свен Нильссон,
1
Вы можете попробовать это в gcc с самыми агрессивными настройками оптимизатора.
Lundin
1
@Lundin: использование типов объединения разрешено в GNU C ++ как расширение по сравнению с ISO C ++. Кажется, это не очень четко указано в руководстве , но я почти уверен в этом. Тем не менее, этот ответ должен объяснить, где он действителен, а где нет.
Питер Кордес