В С++ 11 защищенные средства являются общедоступными?
Продолжение чего-то в Ошибка С++: защищена базовая функция...
Правила с указателем на элемент С++ 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
является членом базового класса. Но я ничего не вижу в стандарте, который делает это поэтому, и там есть очень конкретный пример, показывающий это.)
Я понимаю, что некоторые люди могут быть удивлены тем, что третий прокомментированный подход (второй случай проверки идеона) действительно терпит неудачу. Это потому, что правильный способ думать о защите не "мои производные классы имеют доступ и никто другой", но "если вы проистекаете из меня, у вас будет доступ к этим унаследованным переменным, содержащимся в ваших экземплярах, и никто другой не будет, если только вы дайте его". Например, если 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: Использование и злоупотребления правами доступа
Ответы
Ответ 1
Я видел эту технику, которую я называю "защищенным взломом", упоминается здесь несколько раз здесь и в другом месте. Да, это правильное поведение, и это действительно законный способ обойти защищенный доступ, не прибегая к каким-либо "грязным" хакам.
Когда 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 заседания:
Обоснованием для текущего лечения является разрешение самого широкого возможно использование определенного выражения адреса-члена. поскольку элемент-указатель на базу может быть неявно преобразован в pointer-to-производный-член, делая тип выражения a указатель на базовый элемент позволяет инициализировать или назначать результат либо с указателем на базовый элемент, либо с указателем-получателем. Принятие этого предложения позволит использовать только последние.
Ответ 2
Главное, чтобы иметь в виду о спецификаторах доступа в С++, заключается в том, что они контролируют, где можно использовать имя. На самом деле это не делает ничего для контроля доступа к объектам. "доступ к члену" в контексте С++ означает "возможность использования имени".
Заметим:
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 p1.
Чтобы создать доступное имя или иным образом предоставить доступ к нему, иначе непознаваемый объект является предполагаемым поведением, даже если он отличается от некоторых других языков и может быть неожиданным.