Переосмысление структуры с элементами того же типа, что и массив, стандартным образом

В различных математических кодах 3d я иногда сталкиваюсь с чем-то вроде этого:

struct vec {
    float x, y, z;

    float& operator[](std::size_t i)
    {
        assert(i < 3);
        return (&x)[i];
    }
};

Что, AFAIK, является недопустимым, потому что реализациям разрешено произвольно добавлять отступы между членами, даже если они одного типа, хотя на практике никто этого не сделает.

Можно ли сделать это законным путем наложения ограничений через static_asserts?

static_assert(sizeof(vec) == sizeof(float) * 3);

Т.е. делает static_assert не будучи вызванным подразумевает operator[] делает то, что ожидается и не вызывает UB во время выполнения?

5 ответов

Решение

Нет, это недопустимо, поскольку при добавлении целого числа к указателю применяется следующее ([expr.add]/5):

Если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или один после последнего элемента объекта массива, оценка не должна вызывать переполнение; в противном случае поведение не определено.

y занимает место в памяти один конец x (рассматривается как массив с одним элементом), поэтому добавление 1 к &x определяется, но добавляя 2 к &x не определено

Вы никогда не можете быть уверены, что это будет работать

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

Нет никакого способа сделать это законным, используя static_assert ни alignas ограничения. Все, что вы можете сделать, это предотвратить компиляцию, когда элементы не являются смежными, используя свойство уникальности адреса каждого объекта:

    static_assert (&y==&x+1 && &z==&y+1, "PADDING in vector"); 

Но вы можете переопределить оператор, чтобы сделать его стандартным

Безопасной альтернативой было бы переопределение operator[] чтобы избавиться от требования смежности для трех членов:

struct vec {
    float x,y,z; 

    float& operator[](size_t i)
    {
        assert(i<3); 
        if (i==0)     // optimizing compiler will make this as efficient as your original code
            return x; 
        else if (i==1) 
            return y; 
        else return z;
    }
};

Обратите внимание, что оптимизирующий компилятор сгенерирует очень похожий код как для повторной реализации, так и для вашей исходной версии (см. Пример здесь). Поэтому лучше выбрать совместимую версию.

Псевдонимы типов (использование более одного типа для практически одинаковых данных) - огромная проблема в C++. Если вы храните функции-члены вне структур и сохраняете их как POD, все должно работать. Но

  static_assert(sizeof(vec) == sizeof(float) * 3);

не может сделать доступ к одному типу как к другому технически законным. На практике, конечно, дополнения не будет, но C++ недостаточно умен, чтобы понять, что vec - это массив чисел с плавающей точкой, а массив vecs - это массив чисел с плавающей точкой, кратный трем, и приведение &vecasarray[0] vec * законно, но кастинг и vecasarray[1] запрещены.

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

Проблема в том, что код math3D может использоваться интенсивно, и оптимизация низкого уровня имеет смысл. C++ совместимый способ состоял бы в том, чтобы непосредственно хранить массив и использовать методы доступа или ссылки на отдельные члены массива. И ни один из этих двух вариантов совершенно не подходит:

  • аксессоры:

    struct vec {
    private:
        float arr[3];
    public:
        float& operator[](std::size_t i)
        {
            assert(i < 3);
            return arr[i];
        }
        float& x() { return arr[0];}
        float& y() { return arr[0];}
        float& z() { return arr[0];}
    };
    

    Проблема в том, что использование функции в качестве lvalue не является естественным для старых программистов на Си: v.x() = 1.0; это действительно правильно, но я бы предпочел избежать библиотеки, которая заставила бы меня написать это. Конечно, мы могли бы использовать сеттеры, но если возможно, я бы предпочел написать v.x = 1.0; чем v.setx(1.0);из-за общей идиомы v.x = v.z = 1.0; v.y = 2.0;, Это только моё мнение, но я нахожу это аккуратнее, чем v.x() = v.z() = 1.0; v.y() = 2.0; или же v.setx(v.sety(1.0))); v.setz(2.0);,

  • Рекомендации

    struct vec {
    private:
        float arr[3];
    public:
        float& operator[](std::size_t i)
        {
            assert(i < 3);
            return arr[i];
        }
        float& x;
        float& y;
        float& z;
        vec(): x(arr[0]), y(arr[1]), z(arr[2]) {}
    };
    

    Ницца! Мы можем написать v.x а также v[0]оба представляют одну и ту же память... к сожалению, компиляторы все еще недостаточно умны, чтобы видеть, что ссылки - это просто псевдонимы для массива структуры, а размер структуры в два раза больше размера массива!

По этим причинам некорректно используется псевдонимы...

Как насчет хранения члена данных в виде массива и доступа к ним по именам?

struct vec {
    float p[3];

    float& x() { return p[0]; }
    float& y() { return p[1]; }
    float& z() { return p[2]; }

    float& operator[](std::size_t i)
    {
        assert(i < 3);
        return p[i];
    }
};

РЕДАКТИРОВАТЬ: Для первоначального подхода, если x, y и z - все переменные-члены, которые у вас есть, то структура всегда будет иметь размер 3 float, поэтому static_assert может быть использован для проверки того, что operator[] будет доступ в пределах ограниченного размера.

Смотрите также: Распределение памяти членов структуры C++

РЕДАКТИРОВАТЬ 2: Как Брайан сказал в другом ответе, (&x)[i] само по себе неопределенное поведение в стандарте. Однако, учитывая, что 3 числа с плавающей точкой являются единственными членами данных, код в этом контексте должен быть безопасным.

Быть педантичным по поводу правильности синтаксиса:

struct vec {
  float x, y, z;
  float* const p = &x;

  float& operator[](std::size_t i) {
    assert(i < 3);
    return p[i];
  }
};

Хотя это увеличит каждый vec на размер указателя.

Другие вопросы по тегам