Свойства с нулевой стоимостью с синтаксисом элемента данных

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

some_struct.some_member = var;
var = some_struct.some_member;

и эти обращения к членам перенаправляют на функции-члены с нулевыми издержками.

Хотя первоначальные тесты показывают, что этот подход работает на практике, я далеко не уверен, что он свободен от неопределенного поведения. Вот упрощенный код, который иллюстрирует подход:

template <class Owner, class Type, Type& (Owner::*accessor)()>
struct property {
    operator Type&() {
        Owner* optr = reinterpret_cast<Owner*>(this);
        return (optr->*accessor)();
    }
    Type& operator= (const Type& t) {
        Owner* optr = reinterpret_cast<Owner*>(this);
        return (optr->*accessor)() = t;
    }
};

union Point
{
    int& get_x() { return xy[0]; }
    int& get_y() { return xy[1]; }
    std::array<int, 2> xy;
    property<Point, int, &Point::get_x> x;
    property<Point, int, &Point::get_y> y;
};

Тестовый драйвер демонстрирует, что подход работает, и он действительно нулевой (свойства не занимают дополнительной памяти):

int main()
{
    Point m;
    m.x = 42;
    m.y = -1;

    std::cout << m.xy[0] << " " << m.xy[1] << "\n";
    std::cout << sizeof(m) << " " << sizeof(m.x) << "\n";
}

Реальный код немного сложнее, но суть подхода здесь. Он основан на использовании объединения реальных данных (в данном примере xy) и пустых объектов свойств. (Реальные данные должны быть стандартным классом макета, чтобы это работало).

Объединение необходимо, потому что в противном случае свойства без необходимости занимают память, несмотря на то, что они пусты.

Почему я думаю, что здесь нет UB? Стандарт разрешает доступ к общей начальной последовательности членов объединения стандартных макетов. Здесь общая начальная последовательность пуста. К элементам данных x и y вообще нет доступа, так как нет элементов данных. Мое чтение стандарта указывает на то, что это разрешено. reinterpret_cast должен быть в порядке, потому что мы приводим член объединения к содержащему его объединению, и они являются взаимозаменяемыми по указателю.

Это действительно разрешено стандартом, или я пропускаю некоторые UB здесь?

Ответ 1

TL; DR Это UB.

[basic.life]

Аналогично, до того, как началось время жизни объекта, но после того, как было выделено хранилище, которое будет занимать объект, или после того, как закончился срок жизни объекта, и до того, как хранилище, которое занимал объект, будет повторно использовано или освобождено, любое значение glvalue, которое относится к Оригинальный объект может быть использован, но только ограниченным образом. О строящемся или разрушаемом объекте см. [Class.cdtor]. В противном случае такое glvalue относится к выделенному хранилищу, и использование свойств glvalue, которые не зависят от его значения, является четко определенным. Программа имеет неопределенное поведение, если: [...]

  • glvalue используется для вызова нестатической функции-члена объекта, или

По определению, неактивный член союза не существует


Возможный обходной путь - использовать С++ 20 [[no_unique_address]]

struct Point
{
    int& get_x() { return xy[0]; }
    int& get_y() { return xy[1]; }
    [[no_unique_address]] property<Point, int, &Point::get_x> x;
    [[no_unique_address]] property<Point, int, &Point::get_y> y;
    std::array<int, 2> xy;
};

static_assert(offsetof(Point, x) == 0 && offsetof(Point, y) == 0);

Ответ 2

Вот что говорит правило об общей начальной последовательности об объединениях:

В объединении стандартной компоновки с активным членом типа структуры T1 разрешается считывать нестатический член данных m другого члена объединения типа структуры T2 если m является частью общей начальной последовательности T1 и T2; поведение такое, как если бы соответствующий член T1 был назначен.

Ваш код не соответствует. Зачем? Потому что вы не читаете "другого члена профсоюза". Вы делаете mx = 42; , Это не чтение; что вызов функции члена другого члена профсоюза.

Так что это не соответствует общему правилу начальной последовательности. А без общего правила начальной последовательности, которое вас защищает, доступ к неактивным членам объединения является UB.