Подтвердить что ты не робот

В С++ 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: Использование и злоупотребления правами доступа

4b9b3361

Ответ 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.

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