Почему C++ позволяет изменять закрытые члены, используя этот подход?

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

 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

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


person mathematician1975    schedule 23.08.2012    source источник
comment
В C++ в тот момент, когда вы используете подобные приведения, вы говорите компилятору, что вас не волнует его безопасность типов и проверки. Это не сетевая безопасность; компилятор позволит вам испортить ваши собственные вещи, если вы настаиваете, как это делает ваш код.   -  person tenfour    schedule 23.08.2012
comment
@tenfour Да, я понимаю, что был просто немного удивлен, что это возможно. Я изменил приведение C на static_cast, после чего компилятор начал жаловаться.   -  person mathematician1975    schedule 23.08.2012
comment
вам действительно не следует использовать приведения C в C++. По сути, они говорят компилятору: «Уходи, я знаю, что делаю, и беру на себя всю ответственность». Они даже хуже, чем reinterpret_cast (который, кстати, тоже сработал бы. static_cast не сработает, потому что нет четко определенного способа сделать то, что вы просите)   -  person Tom Tanner    schedule 23.08.2012
comment
@TomTanner Полностью согласен, и я бы никогда не сделал этого сам. Я только что видел подобное в другом вопросе, который заставил меня задуматься, почему это возможно. Хотя я вижу твою точку зрения.   -  person mathematician1975    schedule 23.08.2012
comment
Обратите внимание, что в общем случае это не гарантирует работу: class A { int a; public: int b; }; относительный порядок a и b не предписывается стандартом (у них разные спецификаторы доступа), поэтому *reinterpret_cast<int*>(&a) = 5; может изменить либо a, либо b.   -  person David Rodríguez - dribeas    schedule 23.08.2012
comment
@JerryCoffin: Насколько я понял, гарантии на reinterpret_cast применяются только к классам стандартного макета, и стандарт явно требует, чтобы класс был стандартным макетом для всех не- статические элементы данных должны иметь одинаковый контроль доступа, глава 9/7 (отсутствие виртуальных функций является еще одним требованием для стандартной компоновки, но достаточно нескольких спецификаторов контроля доступа, чтобы нарушить гарантии)   -  person David Rodríguez - dribeas    schedule 23.08.2012
comment
@DavidRodríguez-dribeas: Извините, вы правы. Я знаю, что правила изменились, но каким-то образом мой мозг все еще обычно придумывает правила C++ 98/03 для POD.   -  person Jerry Coffin    schedule 23.08.2012
comment
@JerryCoffin: гарантия в C++03 распространяется только на типы POD, которые являются строгим подмножеством типов стандартного макета. В частности, тип POD не может иметь частные или защищенные элементы данных (C++03 8.5.1/1 накладывает это ограничение на агрегаты, а типы POD являются агрегатами). В С++ 03 код в вопросе не гарантируется (хотя на практике работает)   -  person David Rodríguez - dribeas    schedule 23.08.2012


Ответы (6)


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

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

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

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

Давайте вернемся к началу и рассмотрим такую ​​структуру, как:

struct X {
    int a;
    int b;
};

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

Для C++ было намерение сохранить это, особенно для существующих структур 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();

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

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

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

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

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

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

person Jerry Coffin    schedule 23.08.2012
comment
На самом деле это не совсем так. Стандарт полностью четко определяет этот конкретный состав. - person Puppy; 23.08.2012
comment
@DeadMG: и как, по-твоему, это противоречит всему, что я сказал? Я очень конкретно не сказал, что результат был неопределенным или чем-то похожим — только то, что вы должны использовать приведение типов. - person Jerry Coffin; 23.08.2012
comment
Что ж, немного неискренне предполагать, что Стандарт намеревается защитить от Мерфи в этом случае, когда его намерения явно не являются чем-то подобным, и это не подрыв юридических правил Стандарта. Здесь нет правила Мерфи/Макиавелли — это простой, четко определенный актерский состав и все. - person Puppy; 23.08.2012
comment
@DeadMG: Еще раз, все вместе, на счет три: здесь защита заключается в том, что вы должны использовать приведение. Да, как только вы используете приведение, результат четко определен. Несчастный случай, от которого вы защищены, — это написание задания без приведения. - person Jerry Coffin; 23.08.2012

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


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

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

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

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

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

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

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

person Xeo    schedule 23.08.2012
comment
Это не отвечает на вопрос... почему стандарт позволяет вам это делать, если первый член является закрытым? Если подумать, почему стандарт вообще активно поддерживает эту функцию? :П - person tenfour; 23.08.2012
comment
@tenfour: обратная совместимость с C, как всегда. Не спрашивайте меня, однако, почему они сделали его с таким же контролем доступа и не имеют общего контроля доступа. : s Вы были правы, я добавил крошечный ответ вверху. :) - person Xeo; 23.08.2012
comment
@Xeo, потому что это менее ограничительно? - person R. Martinho Fernandes; 23.08.2012

Хорошая причина — разрешить совместимость с C, но обеспечить дополнительную безопасность доступа на уровне 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
};

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

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

person ecatmur    schedule 23.08.2012

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

person AProgrammer    schedule 23.08.2012

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

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

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

person Werner Henze    schedule 23.08.2012

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

person sam1589914    schedule 23.08.2012