Продолжая что-то узнал в Ошибка 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
является членом базового класса. Но я не вижу в Стандарте ничего такого, что могло бы это сделать, и есть очень конкретный пример, демонстрирующий это.)
Я понимаю, что некоторые люди могут быть удивлены тем, что третий комментируемый подход (второй идеальный тестовый пример) на самом деле не работает. Это связано с тем, что правильный подход к защищенному состоит не в том, что «мои производные классы имеют доступ и никто другой», а «если вы наследуете от меня, у вас будет доступ к этим унаследованным переменным, содержащимся в ваших экземплярах, и никто другой не получит, если вы не Даруй это «. Например, если 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 в котором приводятся дополнительные примеры, мотивирующие изменения, и содержится призыв к решению этой проблемы Рабочей группой Evolution.
Также актуально: ПОЛУЧИЛО 76: Использование и злоупотребление правами доступа
Я видел эту технику, которую я называю «защищенным взломом», упоминавшейся довольно много раз здесь и в других местах. Да, это правильное поведение, и это действительно законный способ обойти защищенный доступ, не прибегая к каким-либо «грязным» взломам.
когда 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
что не хорошо.
Постскриптум Я согласен, это не очень убедительный пример («просто используйте &Base:m
с самого начала и проблема решена «). Однако http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#203 имеет больше информации, которая проливает свет на то, почему такое решение было принято изначально. Они заявляют
Заметки от 04/00 встречи:
Обоснование текущего лечения заключается в том, чтобы
возможно использование данного выражения адреса члена. поскольку
указатель на базовый член может быть неявно преобразован в
указатель на производный член, делающий тип выражения
указатель на базовый элемент позволяет результату инициализироваться или быть назначенным
либо указатель на базовый член или указатель на производный член.
Принятие этого предложения позволит только последнее использование.
Главное, что нужно иметь в виду о спецификаторах доступа в 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 p1.
То, что можно создать доступное имя или иным образом предоставить доступ к иным неименованным объектам, является предполагаемым поведением, даже если оно отличается от некоторых других языков и может удивлять.