Доступ к защищенному члену через указатель-член: это взлом?
Мы все знаем, что члены, указанные protected
из базового класса, могут быть доступны только из собственного экземпляра производного класса. Это функция из Стандарта, и это обсуждалось в Qaru несколько раз:
Но кажется, что можно обойти это ограничение с помощью указателей элементов, так как пользователь chtz показал мне:
struct Base { protected: int value; };
struct Derived : Base
{
void f(Base const& other)
{
//int n = other.value; // error: 'int Base::value' is protected within this context
int n = other.*(&Derived::value); // ok??? why?
(void) n;
}
};
Живая демонстрация на coliru
Почему это возможно, это желаемая функция или сбой в реализации или формулировка Стандарта?
Из комментариев возник еще один вопрос: если Derived::f
вызывается с фактическим Base
, это поведение undefined?
Ответы
Ответ 1
Тот факт, что член недоступен с использованием доступа к члену класса expr.ref (aclass.amember
) из-за контроля доступа [class.access] не делает этот элемент недоступным, используя другие выражения.
Выражение &Derived::value
(тип которого int Base::*
) является абсолютно стандартным, и оно обозначает член value
of Base
. Тогда выражение a_base.*p
, где p
является указателем на элемент Base
и a_base
, экземпляр Base
также стандартный совместимый.
Поэтому любой стандартный компилятор должен сделать выражение other.*(&Derived::value);
определенным поведением: получить доступ к элементу value
из other
.
Ответ 2
это взлом?
В том же ключе, что и при использовании reinterpret_cast
, это может быть опасно и потенциально может быть источником труднодоступных ошибок. Но он хорошо сформировался и не сомневается, что он должен работать.
Чтобы прояснить аналогию: поведение reinterpret_cast
также точно указано в стандарте и может использоваться без какого-либо UB. Но reinterpret_cast
обходит систему типов, а система типов существует по какой-то причине. Точно так же этот указатель на трюк участника хорошо формируется в соответствии со стандартом, но он обходит инкапсуляцию членов, и эта инкапсуляция (как правило) существует по определенной причине (я говорю, как правило, так как я предполагаю, что программист может использовать инкапсуляцию легкомысленно).
[Это ошибка] где-то в реализации или в формулировке Стандарта?
Нет, реализация верна. Вот как язык был указан для работы.
Функция-член Derived
может, очевидно, получить доступ к &Derived::value
, так как это защищенный элемент базы.
Результатом этой операции является указатель на элемент Base
. Это можно применить к ссылке на Base
. Права доступа к членству не применяются к указателям на участников: он применяется только к именам членов.
Из комментариев возник еще один вопрос: если Derived:: f вызывается с фактической базой, это поведение undefined?
Не UB. Base
имеет член.
Ответ 3
В основном то, что вы делаете, обманывает компилятор, и это должно работать. Я всегда вижу такие вопросы, и люди несколько раз получают плохие результаты, и иногда это работает, в зависимости от того, как это преобразуется в код ассемблера.
Я помню, что я видел случай с ключевым словом const
для целого числа, но затем с некоторой обманкой парень смог изменить значение и успешно обойти осведомленность компилятора. В результате получилось: неправильное значение для простой математической операции. Причина проста: сборка в x86 делает различие между константами и переменными, потому что некоторые инструкции содержат константы в их коде операции. Поэтому, поскольку компилятор считает это константой, он будет рассматривать ее как константу и обрабатывать ее оптимизированным образом с неправильной инструкцией процессора и baam, у вас есть ошибка в результирующем номере.
Другими словами: компилятор попытается обеспечить соблюдение всех правил, которые он может применять, но вы, вероятно, можете в конечном итоге обмануть его, и вы можете или не можете получить неправильные результаты на основе того, что вы пытаетесь сделать, поэтому вам лучше делать такие вещи, только если вы знаете, что делаете.
В вашем случае указатель &Derived::value
можно вычислить из объекта по количеству байтов из начала класса. Это в основном то, как компилятор обращается к нему, поэтому компилятор:
- Не видит проблем с разрешениями, потому что во время компиляции вы получаете доступ к
value
через derived
.
- Может это сделать, потому что вы принимаете смещение в байтах в объекте, который имеет ту же структуру, что и
derived
(ну, очевидно, base
).
Итак, вы не нарушаете никаких правил. Вы успешно обошли правила компиляции. Вы не должны этого делать, именно из-за причин, описанных в ссылках, которые вы подключили, так как он прерывает инкапсуляцию OOP, но, если вы знаете, что делаете...
Ответ 4
Просто добавьте ответы и немного увеличьте ужас, который я могу прочитать между вашими линиями. Если вы видите спецификаторы доступа как "закон", защищая вас, чтобы вы не делали "плохие вещи", я думаю, что вам не хватает смысла. public
, protected
, private
, const
... являются частью системы, которая является огромным плюсом для С++. Языки без него могут иметь много достоинств, но когда вы строите большие системы, такие вещи являются реальным активом.
Сказав это: я считаю, что хорошо, что можно обойти почти все защитные сети, предоставленные вам. Пока вы помните, что "возможный" не означает "хорошо". Вот почему он никогда не должен быть "легким". Но для остальных - это вам. Вы архитектор.
Несколько лет назад я мог просто сделать это (и он все еще может работать в определенных средах):
#define private public
Очень полезно для "агрессивных" внешних файлов заголовков. Хорошая практика? Как вы думаете? Но иногда ваши варианты ограничены.
Итак, да, то, что вы показываете, является своего рода нарушением в системе. Но эй, что мешает вам получать и распространять публичные ссылки на участника? Если ужасные проблемы в обслуживании повернут на вас - во что бы то ни стало, почему бы и нет?