В C++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 является членом базового класса. Но я не вижу в Стандарте ничего такого, что могло бы сделать это, и есть очень конкретный пример, демонстрирующий это.)


Я понимаю, что некоторые люди могут быть удивлены тем, что третий комментируемый подход (второй тестовый пример идеона) на самом деле терпит неудачу. Это связано с тем, что правильный подход к защищенному состоит не в том, что "мои производные классы имеют доступ и никто другой", а "если вы наследуете от меня, у вас будет доступ к этим унаследованным переменным, содержащимся в ваших экземплярах, и никто другой не получит, если вы не Даруй это ". Например, если 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 которая предоставляет дополнительные примеры, мотивирующие изменения, и призывает к тому, чтобы проблема поднималась Evolution Рабочая группа.


Также актуально: ПОЛУЧЕНИЕ 76: Использование и злоупотребление правами доступа

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

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

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

Заметки от 04/00 встречи:

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

Главное, что нужно иметь в виду о спецификаторах доступа в 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.

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

Другие вопросы по тегам