Подтвердить что ты не робот

Почему С++ разрешает частным членам изменять этот подход?

После просмотра этого вопроса несколько минут назад я задавался вопросом, почему дизайнеры языка допускают это, поскольку это позволяет косвенную модификацию частных данных. В качестве примера

 class TestClass {
   private:
    int cc;
   public:
     TestClass(int i) : cc(i) {};
 };

 TestClass cc(5);
 int* pp = (int*)&cc;
 *pp = 70;             // private member has been modified

Я протестировал вышеуказанный код, и действительно личные данные были изменены. Есть ли объяснение, почему это разрешено, или это просто надзор на языке? Кажется, это прямо подрывает использование частных данных.

4b9b3361

Ответ 1

Потому что, как выразился Бьярне, С++ предназначен для защиты от Мерфи, а не Макиавелли.

Другими словами, он должен защищать вас от несчастных случаев, но если вы идете на какую-либо работу вообще, чтобы подорвать ее (например, используя приведение), она даже не попытается остановить вас.

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

Изменить: что касается вопроса @Xeo, о том, почему в стандарте говорится, что "имеет тот же контроль доступа", а не "имеет общий контроль доступа", ответ длинный и немного извилистый.

Вернемся к началу и рассмотрим структуру типа:

struct X {
    int a;
    int b;
};

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

Для С++ было намерение поддерживать это, особенно для существующих C-структур. В то же время было очевидное намерение, что если компилятор захочет принудительно выполнить privateprotected) во время выполнения, это должно быть легко сделать (разумно эффективно).

Поэтому, учитывая что-то вроде:

struct Y { 
    int a;
    int b;
private:
    int c;
    int d;
public:
    int e;

    // code to use `c` and `d` goes here.
};

Компилятор должен быть обязан поддерживать те же правила, что и C, в отношении Y.a и Y.b. В то же время, если он собирается обеспечить доступ во время выполнения, он может захотеть переместить все общедоступные переменные вместе в памяти, поэтому макет будет больше похож:

struct Z { 
    int a;
    int b;
    int e;
private:
    int c;
    int d;
    // code to use `c` and `d` goes here.
};

Затем, когда он выполняет функции во время выполнения, он может в основном сделать что-то вроде if (offset > 3 * sizeof(int)) access_violation();

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

Чтобы обеспечить соблюдение обоих из них, С++ 98 сказал, что Y::a и Y::b должны быть в этом порядке в памяти, а Y::a должен был быть в начале структуры (т.е. C-like правила). Но из-за промежуточных спецификаторов доступа Y::c и Y::e больше не должны были быть относительно друг друга. Другими словами, все последовательные переменные, определенные без спецификатора доступа между ними, были сгруппированы вместе, компилятор был свободен для изменения этих групп (но все же должен был сохранить первый в начале).

Это было хорошо, пока какой-то рывок (т.е. я) не указал, что способ написания правил имел еще одну небольшую проблему. Если я написал код вроде:

struct A { 
    int a;
public:
    int b;
public:
    int c;
public:
    int d;
};

... у вас получилось немного самоуничтожения. С одной стороны, это была официально структура POD, поэтому правила C-like должны были применяться, но поскольку у вас были (по общему признанию, бессмысленные) спецификаторы доступа между членами, это также давало компилятору разрешение на переупорядочение членов, таким образом нарушая правила C, подобные им.

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

Ответ 2

Из-за обратной совместимости с C, где вы можете сделать то же самое.


Для всех людей интересно, вот почему это не UB и фактически разрешено стандартом:

Во-первых, TestClass - это класс стандартного макета (§9 [class] p7):

Класс стандартного макета - это класс, который:

  • не содержит нестатических элементов данных типа нестандартного макета (или массива таких типов) или ссылки, //ОК: нестатический элемент данных имеет тип "int"
  • не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1), //OK
  • имеет тот же контроль доступа (раздел 11) для всех нестатических членов данных, //ОК, все нестатические элементы данных (1) 'private'
  • не имеет базовых классов нестандартной компоновки, //ОК, нет базовых классов
  • либо не имеет нестатических членов данных в самом производном классе и не более одного базового класса с нестатическими членами данных, либо не имеет базовых классов с нестатическими членами данных, а //OK, no базовые классы снова
  • не имеет базовых классов того же типа, что и первый нестатический член данных. //ОК, новые базовые классы

И с этим вам может быть разрешен reinterpret_cast класс к типу его первого члена (§9.2 [class.mem] p20):

Указатель на объект структуры стандартного макета, соответствующим образом преобразованный с помощью reinterpret_cast, указывает на его начальный член (или если этот элемент является битовым полем, а затем блоку, в котором он находится) и наоборот.

В вашем случае листинг C-style (int*) разрешает a reinterpret_cast (§5.4 [expr.cast] p4).

Ответ 3

Хорошей причиной является совместимость с C, но дополнительная безопасность доступа на уровне С++.

Рассмотрим:

struct S {
#ifdef __cplusplus
private:
#endif // __cplusplus
    int i, j;
#ifdef __cplusplus
public:
    int get_i() const { return i; }
    int get_j() const { return j; }
#endif // __cplusplus
};

Если требуется, чтобы C-visible S и С++ - видимый S были совместимы с макетами, S можно использовать на границе языка, а сторона С++ обладает большей безопасностью доступа. Подрывная операция безопасности reinterpret_cast является неудачным, но необходимым следствием.

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

Ответ 4

Компилятор дал бы вам ошибку, если бы вы попробовали int *pp = &cc.cc, компилятор сказал бы вам, что вы не можете получить доступ к частному члену.

В вашем коде вы переинтерпретируете адрес cc как указатель на int. Вы написали это стиль стиля C, стиль стиля С++ был бы int* pp = reinterpret_cast<int*>(&cc);. Reinterpret_cast всегда является предупреждением о том, что вы выполняете бросок между двумя указателями, которые не связаны. В таком случае вы должны убедиться, что вы поступаете правильно. Вы должны знать базовую память (макет). Компилятор не мешает вам делать это, потому что это часто требуется.

При выполнении броска вы бросаете все знания о классе. Теперь компилятор видит только указатель int. Конечно, вы можете получить доступ к памяти, на которую указывает указатель. В вашем случае на вашей платформе компилятор случайно поставил cc в первые n байтов объекта TestClass, поэтому указатель TestClass также указывает на член cc.

Ответ 5

Вся цель reinterpret_cast (и приведение стиля C еще более мощная, чем reinterpret_cast), - это обеспечить путь выхода из-за мер безопасности.

Ответ 6

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