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

Доступ к защищенному члену базового класса в другом подклассе

Почему это компилируется:

class FooBase
{
protected:
    void fooBase(void);
};

class Foo : public FooBase
{
public:
    void foo(Foo& fooBar)
    {
        fooBar.fooBase();
    }
};

но это не так?

class FooBase
{
protected:
    void fooBase(void);
};

class Foo : public FooBase
{
public:
    void foo(FooBase& fooBar)
    {
        fooBar.fooBase();
    }
};

С одной стороны, С++ предоставляет доступ к частным/защищенным членам для всех экземпляров этого класса, но, с другой стороны, он не предоставляет доступ к защищенным членам базового класса для всех экземпляров подкласса. Это выглядит довольно непоследовательно для меня.

Я тестировал компиляцию с VС++ и с ideone.com и компилировал первый, но не второй фрагмент кода.

4b9b3361

Ответ 1

Когда foo получает ссылку FooBase, компилятор не знает, является ли аргумент потомком foo, поэтому он должен это считать. foo имеет доступ к унаследованным защищенным членам других объектов foo, а не ко всем другим классам sibling.

Рассмотрим этот код:

class FooSibling: public FooBase { };

FooSibling sib;
Foo f;
f.foo(sib); // calls sib.fooBase()!?

Если Foo::foo может вызывать защищенные члены произвольных потомков FooBase, тогда он может вызывать защищенный метод FooSibling, который не имеет прямого отношения к foo. Это не то, как должен работать защищенный доступ.

Если foo нужен доступ к защищенным членам всех объектов FooBase, а не только те, которые, как известно, являются потомками foo, то foo должен быть другом FooBase:

class FooBase
{
protected:
  void fooBase(void);
  friend class Foo;
};

Ответ 2

С++ часто задает вопрос:

[Вам] разрешено выбирать собственные карманы, но вам не разрешается выбирать карманы отцов и карманы брата.

Ответ 3

Ключевым моментом является то, что protected предоставляет вам доступ к вашей собственной копии элемента, а не к тем членам любого другого объекта. Это распространенное заблуждение, так как чаще всего мы обобщаем и указываем, что protected предоставляет доступ к члену производному типу (без явного указания этого только на свои собственные базы...)

Теперь это по какой-то причине, и в общем случае вы не должны обращаться к члену в другой ветки иерархии, так как вы можете разбить инварианты, от которых зависят другие объекты. Рассмотрим тип, который выполняет дорогостоящий расчет для некоторого большого члена данных (защищенного) и двух производных типов, который кэширует результат, следующий за различными стратегиями:

class base {
protected:
   LargeData data;
// ...
public:
   virtual int result() const;      // expensive calculation
   virtual void modify();           // modifies data
};
class cache_on_read : base {
private:
   mutable bool cached;
   mutable int cache_value;
// ...
   virtual int result() const {
       if (cached) return cache_value;
       cache_value = base::result();
       cached = true;
   }
   virtual void modify() {
       cached = false;
       base::modify();
   }
};
class cache_on_write : base {
   int result_value;
   virtual int result() const {
      return result_value;
   }
   virtual void modify() {
      base::modify();
      result_value = base::result(); 
   }
};

Тип cache_on_read фиксирует изменения данных и помещает результат как недопустимый, так что следующее считывание значения пересчитывается. Это хороший подход, если количество записей относительно велико, так как мы выполняем только расчет по требованию (т.е. Множественные модификации не будут вызывать пересчеты). cache_on_write предварительно вычисляет исходный результат, который может быть хорошей стратегией, если количество записей невелико, и вы хотите детерминированные затраты на чтение (считайте, что низкая латентность при чтении).

Теперь вернемся к исходной проблеме. Обе стратегии кэша поддерживают более строгий набор инвариантов, чем база. В первом случае дополнительный инвариант состоит в том, что cached является true, только если data не был изменен после последнего чтения. Во втором случае дополнительный инвариант состоит в том, что result_value - значение операции во все времена.

Если третий производный тип взял ссылку на base и получил доступ к data для записи (если protected разрешил это), то он сломался бы с инвариантами производных типов.

Таким образом, спецификация языка нарушается (личное мнение), поскольку он оставляет бэкдор для достижения этого конкретного результата. В частности, если вы создаете указатель на член элемента из базы в производном типе, доступ проверяется в derived, но возвращаемый указатель является указателем на член base, который может быть применен к любому base объект:

class base {
protected:
   int x;
};
struct derived : base {
   static void modify( base& b ) {
      // b.x = 5;                        // error!
      b.*(&derived::x) = 5;              // allowed ?!?!?!
   }
}

Ответ 4

В обоих примерах Foo наследуется защищенный метод fooBase. Однако в первом примере вы пытаетесь получить доступ к данному защищенному методу из того же класса (Foo::foo calls Foo::fooBase), а во втором примере вы пытаетесь получить доступ к защищенному методу из другого класса, который не объявлен как friend class (Foo::foo пытается вызвать FooBase::fooBase, который не работает, позже защищен).

Ответ 5

В первом примере вы передаете объект типа Foo, который, очевидно, наследует метод fooBase() и поэтому может его вызвать. Во втором примере вы пытаетесь вызвать защищенную функцию, просто так, независимо от того, в каком контексте вы не можете вызвать защищенную функцию из экземпляра класса, где это было объявлено. В первом примере вы наследуете защищенный метод fooBase, и поэтому имеете право называть его WITHIN Foo context

Ответ 6

Я имею тенденцию видеть вещи с точки зрения понятий и сообщений. Если ваш метод FooBase был на самом деле назван "SendMessage", а Foo был "EnglishSpeakingPerson", а FooBase - SpeakingPerson, ваша защищенная декларация предназначена для ограничения SendMessage между EnglishSpeakingPersons (и подклассами, например: AmericanEnglishSpeakingPerson, AustralianEnglishSpeakingPerson). Другой тип FrenchSpeakingPerson, полученный от SpeakingPerson, не сможет получить SendMessage, если вы не объявили FrenchSpeakingPerson как друга, где "друг" означает, что у FrenchSpeakingPerson есть особая способность получать SendMessage от EnglishSpeakingPerson (т.е. может понимать английский).

Ответ 7

В дополнение к hobo answer вы можете найти обходной путь.

Если вы хотите, чтобы подклассы хотели вызвать метод fooBase, вы можете сделать его static. статические защищенные методы доступны подклассами со всеми аргументами.