Является ли законным индексирование в структуру?
Независимо от того, как "плохо" используется код, и считая, что выравнивание и т.д. не является проблемой для компилятора/платформы, это 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, если он отличается, просто для интереса.
Ответы
Ответ 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: