Чи законно індексувати в структуру?


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, якщо він інший, просто для інтересу.


51
Ні, це не "законно". Це невизначена поведінка.
Сем Варшавчик

10
Це працює для вас у цьому дуже простому випадку, оскільки компілятор не додає ніяких накладок між членами. Спробуйте зі структурами, що використовують різні типи, і вони будуть руйнуватися.
Якийсь програміст чувак

7
Копаючи минуле - UB раніше називали носові демони .
Адріан Коломітчі

21
Ну чудово, тут я натрапляю на те, що я слідую за тегом C, читаю питання, а потім пишу відповідь, яка стосується лише C, оскільки я не бачив тега C ++. C і C ++ тут ​​дуже різні! C дозволяє набирати покарання союзами, C ++ - ні.
Лундін

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", а інший повинен бути первинним значенням незаписаного числення чи цілісного типу. Результат - тип "T". Тип "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члена структури за допомогою цього партнера.


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
Суворе згладжування для цього не має значення. Прокладка не є частиною збереженого значення об'єкта. Також у цій відповіді не вдається вирішити найпоширеніший випадок: що відбувається, коли немає прокладки. Рекомендую фактично видалити цю відповідь.
ММ

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


3
Зверніть увагу, це працює лише в тому випадку, якщо клас є типовим типом компонування. Якщо ні, то все-таки UB.
NathanOliver

@NathanOliver Я мушу зазначити, що моя відповідь стосується лише C. Edited. Це одна з проблем таких питань з подвійною мовою тегів.
2501

Дякую, і тому я запитав окремо про C ++ та C, оскільки цікаво знати відмінності
jcoder

@NathanOliver Адреса першого члена гарантовано збігається з адресою класу C ++, якщо це стандартний макет. Однак це не гарантує, що доступ є чітко визначеним, а також не означає, що такі звернення для інших класів не визначені.
Potatoswatter

Ви б сказали, що це char* p = ( char* )&thing.a + offsetof( thing , b );призводить до невизначеної поведінки?
ММ

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 ... так? Правильно?
Лундін

6
@Lundin, якщо вам важлива швидкість цієї побудови, то дані повинні бути організовані в першу чергу як масив, а не як окремі поля.
Слава

2
@Lundin в обох ви маєте на увазі нечитабельну та не визначену поведінку? Ні, дякую.
Слава

1
Перевантаження @Lundin Operator - синтаксична функція часу компіляції, яка не викликає накладних витрат порівняно зі звичайними функціями. Подивіться на godbolt.org/g/vqhREz, щоб побачити, що насправді робить компілятор, коли компілює код C ++ та C. Дивно, що вони роблять і що очікують від них. Я особисто віддаю перевагу кращому типу безпеки та виразності C ++ понад C мільйон разів. І це працює весь час, не покладаючись на припущення про набивання.
Єнс

2
Ці посилання принаймні вдвічі перевищують розмір речі. Просто роби thing.a().
ТК

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)для вибору члена буде оптимізувати показник для постійних індексів компіляції, але створить жахливу гілку для індексів виконання. У цьому немає нічого суттєво поганого 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]; }
};

На досліднику компілятора Godbolt ми можемо подивитися вихід ASM для різних випадків використання . Це повноцінні функції системи 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()для C ++, робить такий, як цей, для індексу змінної часу виконання. (Код у попередньому посиланні на 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 / брязкіт вирішили не підтримувати другу , але , здається, підтримують найперше, принаймні зараз.
Supercat

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]якщо 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 Так, варто уточнити відповідь ще кілька.
Баррі

(&thing.a + 1)Цікавий випадок , я не в змозі покрити. +1! ... Просто цікаво, ви є членом комітету ISO C ++?
WhiZTiM

Це також дуже важливий випадок, оскільки в іншому випадку кожен цикл, що використовує покажчики як напіввідкритий інтервал, був би UB.
Єнс

Щодо останнього стандартного цитування. С ++ повинен бути кращим, ніж 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]може змінити його
ММ

Я не бачу, як приклад const має щось спільне з питанням. Це не вдається лише тому, що існує певне правило, згідно з яким об'єкт const не може бути змінений, не будь-яка інша причина.
ММ

1
@MM, це не приклад індексації в структури, але це дуже хороша ілюстрація того , як використання невизначеного поведінки еталонних то його удаваним місцем в пам'яті, може привести до різної потужності , ніж очікувався, тому що компілятор може зробити що - то ще з UB, ніж ви цього хотіли.
Wildcard

@MM Вибачте, жоден доступ до масиву, крім тривіального, через вказівник на сам об’єкт. А другий - лише приклад легко помітити побічні ефекти невизначеної поведінки; компілятор оптимізує прочитане, xоскільки знає, що ви не можете змінити його визначеним чином. Аналогічна оптимізація може статися , коли ви змінюєте bчерез (&blah.a)[1]якщо компілятор може довести , що не було ніякого певний доступу до , bщо може змінити його; така зміна може статися через начебто нешкідливі зміни компілятора, оточуючого коду чи будь-чого іншого. Тож навіть тестування, що це працює, недостатньо.
Якк - Адам Невраумон

6

Ось спосіб використовувати клас проксі для доступу до елементів у масиві членів на ім'я. Це дуже C ++ і не має користі проти функцій аксесуарів, що повертаються назад, за винятком синтаксичних уподобань. Це перевантажує ->оператора для доступу до елементів як членів, тому, щоб бути прийнятним, потрібно як не подобатися синтаксису аксесуарів ( d.a() = 5;), так і терпіти використання ->з об'єктом, що не вказує. Я думаю, це також може заплутати читачів, не знайомих з кодом, тому це може бути скоріше акуратним трюком, ніж чимось, що ви хочете поставити у виробництво.

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

Коли Data's ->використовується для доступу до елемента по імені (наприклад:) my_data->b = 5;, Proxyоб'єкт повертається. Потім, оскільки це Proxyзначення не є покажчиком, його власний ->оператор викликається автоматичним ланцюгом, який повертає вказівник до себе. Таким чином, 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; }компілює до just 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.
Єнс

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 - Невідповідна Моніка

2
Профспілка вирішує сувору проблему псевдонімів обома мовами. Але набирати покарання через союзи добре лише на C, а не на C ++.
Лундін

1
все-таки я не здивуюся, якщо це працює на 100% усіх компіляторів c ++. коли-небудь.
Свен Нільссон

1
Ви можете спробувати його в gcc із найагресивнішими налаштуваннями оптимізатора.
Лундін

1
@Lundin: покарання типу об'єднання є законним у GNU C ++, як розширення над ISO C ++. Здається, це не зазначено дуже чітко в посібнику , але я майже впевнений у цьому. Однак у цій відповіді потрібно пояснити, де вона дійсна, а де - ні.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.