В С++ 11 защищенный означает общедоступный?

Продолжая что-то, изученное в ошибке C++: базовая функция защищена...

Правила указателя на член C++11 эффективно лишают ключевое слово protected любого значения, потому что защищенные члены могут быть доступны в несвязанных классах без каких-либо вредоносных/небезопасных приведений.

А именно:

class Encapsulator
{
  protected:
    int i;
  public:
    Encapsulator(int v) : i(v) {}
};

Encapsulator f(int x) { return x + 2; }

#include <iostream>
int main(void)
{
    Encapsulator e = f(7);
    // forbidden: std::cout << e.i << std::endl; because i is protected
    // forbidden: int Encapsulator::*pi = &Encapsulator::i; because i is protected
    // forbidden: struct Gimme : Encapsulator { static int read(Encapsulator& o) { return o.i; } };

    // loophole:
    struct Gimme : Encapsulator { static int Encapsulator::* it() { return &Gimme::i; } };
    int Encapsulator::*pi = Gimme::it();
    std::cout << e.*pi << std::endl;
}

Это действительно поведение соответствует стандарту?

(Я считаю это недостатком и утверждаю, что тип &Gimme::i на самом деле должен быть int Gimme::*, хотя i является членом базового класса. Но я не вижу в Стандарте ничего, что делало бы это таковым, и есть очень конкретный пример, показывающий это.)


Я понимаю, что некоторые люди могут быть удивлены тем, что третий прокомментированный подход (второй тестовый пример ideone) на самом деле терпит неудачу. Это потому, что правильный способ думать о защищенном — это не «мои производные классы имеют доступ и никто другой», а «если вы наследуете от меня, вы будете иметь доступ к этим унаследованным переменным, содержащимся в ваших экземплярах, и никто другой не будет, если вы не даруй». Например, если Button наследует Control, то защищенные члены Control в экземпляре Button доступны только для Control и Button и (при условии, что Button не запрещает это) фактический динамический тип экземпляра и любые промежуточные базы.

Эта лазейка подрывает этот договор и полностью противоречит духу правила 11.4p1:

Дополнительная проверка доступа помимо описанных ранее в разделе 11 применяется, когда нестатический элемент данных или нестатическая функция-член является защищенным членом своего класса именования. Как описано ранее, доступ к защищенному элементу предоставляется, поскольку ссылка встречается у друга или члена некоторого класса C. Если доступ должен формировать указатель на элемент (5.3.1), описатель вложенного имени должен обозначать C или класс, производный от C. Все другие обращения включают (возможно, неявное) выражение объекта. В этом случае класс выражения объекта должен быть C или классом, производным от C.


Спасибо AndreyT за ссылку на http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203, где приведены дополнительные примеры, мотивирующие изменение, и содержится призыв к рассмотрению вопроса Рабочей группой по развитию.


Также актуально: GotW 76: использование и злоупотребление правами доступа


person Ben Voigt    schedule 06.06.2013    source источник
comment
Да, я не вижу, какую защиту сняли в C++11.   -  person CB Bailey    schedule 07.06.2013
comment
@Charles: я не хотел предполагать, что в C++98 нет такой же лазейки. Только то, что это существует в текущей версии.   -  person Ben Voigt    schedule 07.06.2013
comment
@BenVoigt: Также можно ограбить private участников без кастинга   -  person Andy Prowl    schedule 07.06.2013
comment
@Andy: Можете ли вы привести пример (который не вызывает неопределенное поведение)?   -  person Ben Voigt    schedule 07.06.2013
comment
@Ben: Йоханнес Шауб придумал этот трюк. Вот ссылка   -  person Andy Prowl    schedule 07.06.2013
comment
@Энди: Это поразительно ... и мне интересно, почему в этом контексте нет проверки доступа к &A::a.   -  person Ben Voigt    schedule 07.06.2013
comment
@BenVoigt: Понятия не имею, но я, честно говоря, не думаю, что это так уж плохо (я имел в виду тот факт, что правила доступности можно обойти, приложив большие усилия). Я думаю, что важно то, что язык сам по себе делает многое, не позволяя вам нарушать эти правила по ошибке или не осознавая, что вы делаете.   -  person Andy Prowl    schedule 07.06.2013
comment
@BenVoigt Это из-за 14.7.2 p12 Обычные правила проверки доступа не применяются к именам, используемым для указания явных экземпляров.   -  person bames53    schedule 07.06.2013
comment
@bames53: Да, я видел это правило, но мне интересно, почему оно там.   -  person Ben Voigt    schedule 07.06.2013
comment
@AndyProwl: мне не нравится обходить правила прилагая большие усилия. Мне нравится, как работает reinterpret_cast — он позволяет нарушать правила простым в использовании, но очень очевидным способом. Я думаю, что если разработчики языка хотят оставить дверь открытой для обхода проверок доступа, например, для сериализации объектов класса, синтаксис private_access( member-access-expression ) был бы намного лучше. Но вариант использования сериализации на самом деле меня не впечатляет, поскольку объекты POD могут быть тривиально сериализованы, а объекты, не относящиеся к POD, требуют помощи дизайнера объектов.   -  person Ben Voigt    schedule 07.06.2013


Ответы (2)


Я видел эту технику, которую я называю «защищенным взломом», упоминавшуюся довольно много раз здесь и в других местах. Да, такое поведение правильное, и это действительно легальный способ обойти защищенный доступ, не прибегая к каким-либо «грязным» хакам.

Когда m является членом класса Base, проблема с созданием выражения &Derived::m для создания указателя типа Derived::* заключается в том, что указатели членов класса являются контравариантными, а не ковариантными. Это сделало бы результирующие указатели непригодными для использования с Base объектами. Например, этот код компилируется

struct Base { int m; };
struct Derived : Base {};

int main() {
  int Base::*p = &Derived::m; // <- 1
  Base b;
  b.*p = 42;                  // <- 2
}

потому что &Derived::m производит значение int Base::*. Если бы он выдал значение int Derived::*, код не смог бы скомпилироваться в строке 1. И если бы мы попытались исправить это с помощью

  int Derived::*p = &Derived::m; // <- 1

он не сможет скомпилироваться в строке 2. Единственный способ заставить его скомпилироваться - выполнить принудительное приведение

  b.*static_cast<int Base::*>(p) = 42; // <- 2

что не хорошо.

P.S. Согласен, это не очень убедительный пример ("просто используйте &Base:m с самого начала и проблема решена"). Однако http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203 содержит дополнительную информацию, которая проливает свет на то, почему изначально было принято такое решение. Они заявляют

Заметки со встречи 04.00:

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

person AnT    schedule 06.06.2013
comment
Но он позволяет работать с объектами любого подкласса Encapsulated, даже с теми, которые блокируют дальнейшее наследование! Я не могу представить, что это правильно, даже если это совместимо. - person Ben Voigt; 07.06.2013
comment
@Ben Voigt: Никаких аргументов, это действительно похоже на огромный компромисс. - person AnT; 07.06.2013
comment
WRT ваше редактирование, весь смысл в том, чтобы сделать указатель на член непригодным для использования с объектами Base. Если вам нужен указатель на член Base, используйте &Base::m. - person Ben Voigt; 07.06.2013
comment
Я понимаю вашу точку зрения. Также см. здесь open-std.org/jtc1/ sc22/wg21/docs/cwg_closed.html#203 . У них есть более убедительные мотивирующие примеры. - person AnT; 07.06.2013
comment
Спасибо за ссылку. Насколько я могу судить, все эти примеры поддерживают изменение (они не работают в соответствии с текущим правилом). Так что я не согласен с вашим предпоследним предложением. Они являются лишь дополнительным доказательством того, что первоначальное решение было непродуманным. - person Ben Voigt; 07.06.2013
comment
Я бы привел еще один аргумент с точки зрения связи: код, который присваивает int Base::*p = &Derived::i;, уже тесно связан с Base и уже сломается, если значение Derived::i изменится (путем введения объявления, которое скрывает член Base), и поэтому &Base::i не ввести любую дополнительную нагрузку на техническое обслуживание. - person Ben Voigt; 07.06.2013

Главное, что нужно помнить о спецификаторах доступа в C++, это то, что они определяют, где можно использовать имя. На самом деле он ничего не делает для управления доступом к объектам. «доступ к члену» в контексте C++ означает «возможность использовать имя».

Наблюдать:

class Encapsulator {
  protected:
    int i;
};

struct Gimme : Encapsulator {
    using Encapsulator::i;
};

int main() {
  Encapsulator e;
  std::cout << e.*&Gimme::i << '\n';
}

Это, e.*&Gimme::i, разрешено, поскольку вообще не имеет доступа к защищенному участнику. Мы обращаемся к элементу, созданному внутри Gimme с помощью объявления using. То есть, хотя объявление using не подразумевает никаких дополнительных подобъектов в экземплярах Gimme, оно все равно создает дополнительный член. Члены и подобъекты — это не одно и то же, а Gimmie::i — это отдельный общедоступный член, который можно использовать для доступа к тем же подобъектам, что и защищенный член Encapsulator::i.


Как только различие между «членом класса» и «подобъектом» будет понято, должно быть ясно, что на самом деле это не является лазейкой или непреднамеренным нарушением контракта, указанным в 11.4 п.1.

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

person bames53    schedule 06.06.2013
comment
Нет, спецификаторы доступа не контролируют, где можно использовать имя. Разрешение перегрузки происходит до проверки доступа. А во-вторых, если бы &Gimme::i имел тип int Gimme::*, то он все равно имел бы доступ к Encapsulator::i, но только внутри объектов типа Gimme (или какого-то подкласса). Если бы ваша интерпретация была правильной, то текста из 14.4p1, который я выделил жирным шрифтом в вопросе, не существовало бы. - person Ben Voigt; 07.06.2013
comment
@BenVoigt Да, разрешение перегрузки на первом месте; Под «спецификаторы доступа контролируют, где можно использовать имя» я имею в виду, что как только разрешение перегрузки определяет, какое имя вы пытаетесь использовать, спецификаторы доступа определяют, разрешено ли вам использовать это имя. Во-вторых, прочитайте текст, который вы выделили жирным шрифтом, еще раз, но с пониманием того, что «член» не означает то же самое, что и «подобъект»; Фраза доступ к защищенному элементу применяется к имени Encapsulator::i, но одновременно не применяется к имени Gimmie::i, поскольку это разные элементы. Тип &Gimmie::i не влияет на это - person bames53; 07.06.2013
comment
Но тип &Gimme::i влияет на то, с какими объектами впоследствии можно будет использовать указатель. Я полностью согласен с тем, что если Encapsulator возвращает указатель на свой подобъект-член, любой код может затем разыменовать этот указатель без дополнительной проверки. Но Gimme не должен иметь возможности получить указатель на член, который в целом применим ко всем объектам Encapsulator... чтобы соответствовать выделенному полужирным шрифтом тексту, &Gimme::i должен возвращать указатель, который можно применять только к объектам Gimme. - person Ben Voigt; 07.06.2013
comment
@BenVoigt Тот факт, что можно использовать открытый член Gimmie::i, не противоречит выделенному полужирным шрифтом тексту; выделенный жирным шрифтом текст применяется только к защищенным членам. Должен ли результат использования этого открытого члена использоваться с базовым классом или другим классом, производным от того же базового класса, является полностью независимым проектным решением, не связанным с текстом, выделенным полужирным шрифтом. - person bames53; 07.06.2013
comment
Выделенный жирным шрифтом текст не соответствует проверке доступа к Gimme::i, а затем доступу к объекту, отличному от типа Gimme. - person Ben Voigt; 07.06.2013
comment
Утверждение вашего ответа, что Gimme::i является другим членом (но тем же подобъектом), что и Encapsulator::i, просто не работает. &Gimme::i является указателем на член (а не указателем на подобъект), и это указатель на Encapsulator-член, поскольку нет отдельного члена Gimme::i. Он не отличается, он наследуется с измененным спецификатором доступа. - person Ben Voigt; 07.06.2013
comment
Я признаю, что язык спецификации запутан из-за использования термина «член». Но ясно, что существуют разные сущности Gimmie::i и Encapsulator::i, которые одновременно имеют разные спецификации доступа. Это не тот случай, когда использование-объявления изменяет доступ какой-то третьей сущности, общей между двумя именами. Если это так, то это должно быть законным: Gimmie g; g.*&Encapsulator::i. - person bames53; 07.06.2013
comment
@BenVoigt Несовместимо с выделенным жирным шрифтом текстом, чтобы выполнить проверку доступа к Gimme::i, а затем получить доступ к объекту не типа Gimmi. Нигде в спецификации, не говоря уже о цитируемом разделе, не говорится, что имена, объявленные в классе, могут обращаться только к подобъектам экземпляров этого класса. Спецификация говорит, что проверка доступа выполняется по имени, а также что выражения, использующие имя, могут создавать значения, которые можно использовать для доступа к подобъектам в экземплярах совершенно разных типов. - person bames53; 07.06.2013
comment
Вы говорите, что было бы законно написать struct UseIt : Encapsulator { static void use( Encapsulator& e ) { e.UseIt::i = 1; } };? Но это запрещено той частью цитаты, которую я выделил жирным шрифтом. И для согласованности e.*&UseIt::i = 1; также не должно быть разрешено. - person Ben Voigt; 08.06.2013
comment
Когда вы говорите, что существуют разные объекты Gimmie::i и Encapsulator::i, которые одновременно имеют разные спецификации доступа, разве вы не сказали бы, что &Gimmie::i должен быть указателем на член, который обращается к первому? И поэтому может использоваться только с объектом, содержащим член Gimmie::i? Проблема в том, что они не являются отдельными объектами. &Gimmie::i создает указатель на Encapsulator::i. - person Ben Voigt; 08.06.2013
comment
@BenVoigt Вы говорите, что было бы законно писать [...]? Нет, и не из-за чего-либо в процитированном утверждении. Это незаконно, потому что . нужно, чтобы правая сторона была именем члена левой стороны, а UseIt::i не было именем члена инкапсулятора. .*, с другой стороны, позволяет правой стороне быть любым выражением, которое оказывается соответствующего типа. - person bames53; 08.06.2013
comment
Проблема в том, что они не являются отдельными объектами. &Gimmie::i создает указатель на Encapsulator::i Выражение &Gimmie::i имеет тот же тип и значение, что и выражение &Encapsulator::i, так же как выражение f() может иметь тот же тип и значение, что и выражение g(). Это не означает, что id-выражение Gimmie::i совпадает с id-выражением Encapsulator::i, равно как и g и f должны быть одинаковыми. - person bames53; 08.06.2013
comment
В C++, если два выражения имеют одинаковый тип и адрес, это означает, что они одинаковы. Мы не говорим о ценности вообще, мы говорим об адресе. - person Ben Voigt; 09.06.2013
comment
@BenVoigt Если два объекта имеют одинаковый тип и адрес, они являются одним и тем же объектом. Нельзя применить одну и ту же логику, чтобы сделать вывод, что Gimmie::i и Encapsulator::i одинаковы только потому, что применение так называемого оператора «адрес» дает один и тот же адрес для обоих; Они не являются объектами, а указатели на члены на самом деле не являются адресами, как указатели на объекты. В любом случае, мы уже определили несколько способов, которыми Gimmie::i и Encapsulator::i ведут себя по-разному, и поэтому они не должны быть одинаковыми. - person bames53; 09.06.2013