Доступ к защищенному члену через указатель-член: это взлом?

Мы все знаем, что члены, указанные 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

Очень полезно для "агрессивных" внешних файлов заголовков. Хорошая практика? Как вы думаете? Но иногда ваши варианты ограничены.

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