Если у меня есть объединение с двумя членами данных одного типа, отличающееся только CV-квалификацией:
template<typename T>
union A
{
private:
T x_priv;
public:
const T x_publ;
public:
// Accept-all constructor
template<typename... Args>
A(Args&&... args) : x_priv(args...) {}
// Destructor
~A() { x_priv.~T(); }
};
И у меня есть функция f, которая объявляет объединение A, таким образом делая x_priv активнымчлен и затем читает x_publ из этого объединения:
int f()
{
A<int> a {7};
return a.x_publ;
}
В каждом протестированном мной компиляторе не было ошибок компиляции ни во время выполнения ни для типов int, ни для других, более сложных типов, таких как std :: string и std:: thread.
Я посмотрел на стандарте, было ли это законным поведением, и начал смотреть на разницу T
и const T
:
6.7.3.1 [basic.type.qualifier]
cv-квалифицированные или cv-неквалифицированные версии типа являются различными типами;однако они должны иметь одинаковые требования к представлению и выравниванию ([basic.align]).
Это означает, что при объявлении const T
он имеет точно такое же представление впамять как T
.Но потом я обнаружил, что стандарт на самом деле запрещает это для некоторых типов, что я нахожу странным, поскольку не вижу причин для этого.
Я начал поиск доступа к неактивным членам.
Доступ к общей начальной последовательности T
и const T
разрешен только в том случае, если оба типа стандартных макетов.
10.4.1 [class.union]
В любое время могут быть активны не более одного нестатических членов данных типа объекта объединения [...] [Примечание: одна специальная гарантиясделан для того, чтобы упростить использование объединений: Если объединение стандартного макета содержит несколько структур стандартного макета, которые имеют общую начальную последовательность ([class.mem]), и если нестатический член данных элементаобъект этого типа объединения стандартной компоновки является активным и является одной из структур стандартной компоновки, разрешено проверять общую начальную последовательность любого из элементов структуры стандартной компоновки ;см. [class.mem].- примечание к концу]
Начальная последовательность в основном соответствует порядку нестатических элементов данных с некоторыми исключениями, но поскольку T
и const T
имеют одинаковые элементы в одной и той же компоновкеэто означает, что общая начальная последовательность T
и const T
- это все члены T
.
10.3.22 [class.mem]
Общая начальная последовательность двух типов структуры стандартного макета ([class.prop]) - это самая длинная последовательность нестатических элементов данных и битовых полей в порядке объявления, начиная с первого такого объекта в каждой из структур, так что соответствующийсущности имеют совместимые с компоновкой типы, либо обе сущности объявлены с атрибутом no_unique_address ([dcl.attr.nouniqueaddr]), либо ни одна из них не существует, и либо обе сущности являются битовыми полями с одинаковой шириной, либо ни одна из них не является битовым полем[Пример:
И вот где ограничения вступают в силу, он ограничивает доступ к некоторым типам, даже если они имеют одинаковое представление в памяти:
10.1.3 [class.prop]
Класс S является классом стандартной компоновки, если он:
- (3.1) не имеет нестатических членов данных типа нестандартныйКласс макета (или массив таких типов) или ссылка,
- (3.2) не имеет виртуальных функций и виртуальных базовых классов,
- (3.3) имеет одинаковое управление доступом для всех нестатическихчлены данных,
- (3.4) не имеет базовых классов нестандартной компоновки,
- (3.5) имеет не более одного подобъекта базового класса любого данного типа,
- (3.6) имеет все нестатические члены-данные и битовые поля в классе, а его базовые классы впервые объявлены в одном и том же классе, а
- (3.7) не имеет элемента набора M (S) типов, какбазовый класс, где для любого типа X M (X) определяется следующим образом.108 [Примечание: M (X) - это набор типов всех nподобъекты базового класса, которые могут иметь нулевое смещение в X. - примечание конца]
- (3.7.1) Если Xявляется типом класса без объединения, у которого нет (возможно, унаследованных) нестатических членов-данных, набор M (X) пуст.
- (3.7.2) Если X является типом класса без объединения сэлемент нестатических данных типа X_0, который имеет нулевой размер или является первым элементом нестатических данных X (где указанный член может быть анонимным объединением), набор M (X) состоит из X_0 и элементов M(X_0).
- (3.7.3) Если X является типом объединения, набор M (X) является объединением всех M (U_i) и набора, содержащего все U_i, где каждый U_i является типомi-го элемента нестатических данных X.
- (3.7.4) Если X является типом массива с типом элемента X_e, множество M (X) состоит из X e и элементов M (X_e).).
- (3.7.5) Если X не является классом, не массивом, набор M (X) пуст.
У меня вопросы , есть ли причина, по которой это не будет корректным поведением? .
По сути, это:
Стандартпроизводители забыли объяснить этот конкретный сase?
Я не читал какую-то часть стандарта, которая допускает такое поведение?
Есть более конкретная причина этого не делатьбыть допустимым поведением?
Причиной правильности синтаксиса является, например, наличие в классе переменной «только для чтения», например:
struct B;
struct A
{
... // Everything that struct A had before
friend B;
}
struct B
{
A member;
void f() { member.x_priv = 100; }
}
int main()
{
B b;
b.f(); // Modifies the value of member.x_priv
//b.member.x_priv = 100; // Invalid, x_priv is private
int x = b.member.x_publ; // Fine, x_publ is public
}
Таким образом, вам не нужна функция получения, которая может вызвать снижение производительности, и хотя большинство компиляторов оптимизируют это, это все равно увеличивает ваш класс, и для получения переменной вам нужно написать int x = b.get_x()
.
Также вам не понадобится постоянная ссылка на эту переменную (как описано в в этом вопросе ), которая, хотя она прекрасно работает, добавляет размер вашему классу, что может быть плохо для достаточно больших классов или классов, которыедолжно быть как можно меньше.
И странно было бы писать b.member.x_priv
вместо b.x_priv
, но это было бы исправимо, если бы у нас были частные члены в анонимных объединениях, тогда мы могли бы переписать это так:
struct B
{
union
{
private:
int x_priv;
public:
int x_publ;
friend B;
};
void f() { x_priv = 100; }
}
int main()
{
B b;
b.f(); // Modifies the value of member.x_priv
//b.x_priv = 100; // Invalid, x_priv is private
int x = b.x_publ; // Fine, x_publ is public
}
Другим вариантом использования может быть присвоение разных имен одному и тому же элементу данных, например, в Shape, пользователь может ссылаться на позицию как shape.pos
, shape.position
, shape.cur_pos
или shape.shape_pos
.
Хотя это, вероятно, создаст больше проблем, чем оно того стоит, такой вариант использования может быть предпочтительным, когда, например, имя не рекомендуется.