Как запретить производному классу сделать частную / защищенную виртуальную функцию общедоступной?

Есть веские причины для создания интерфейса базового класса со всеми виртуальными функциями как частными или защищенными (см. это). Но как тогда предотвратить превращение производных классов (которые могут находиться в руках внешних клиентов) в частную виртуальную функцию как открытую? В Virtually Yours авторы говорят об этой проблеме, но ее решение не обсуждается.

Изменить. Судя по вашим ответам, как я думал ранее, кажется, что это невозможно предотвратить. Но поскольку в этой ситуации легко ошибиться (клиент обязательно касается защищенной виртуальной функции), было бы разумно, чтобы компилятор предупредил о таком использовании. Пробовал протестировать с g ++. Сначала я написал:

class A {
        protected:
        virtual void none() { return; }
};

class B: public A {
        public:
        void none() { return; }
};

g++ -c -Wall -pedantic file.cpp скомпилировано без ошибок. Добавление -Weffc++ дало предупреждение: warning: ‘class A’ has virtual functions and accessible non-virtual destructor, что имеет смысл. После добавления виртуального деструктора предупреждения нет. Так что в этом легком случае ошибки нет никаких предупреждений.


person amit    schedule 27.01.2010    source источник
comment
Что может помешать им создать общедоступный метод с той же сигнатурой, которая просто вызывает частный метод, тем самым раскрывая функциональность частного метода?   -  person Chris Lutz    schedule 27.01.2010
comment
Есть разница между явным и непреднамеренным открытием. Клиент касается защищенного элемента при попытке переопределить виртуальный член, и легко ошибиться.   -  person amit    schedule 28.01.2010


Ответы (6)


Как сказал Бьярн, контроль доступа в C ++ предназначен для защиты от Мерфи, а не от Макиавелли. То же самое и в целом - его функции предназначены для защиты от несчастных случаев, а не для того, чтобы люди намеренно делали что-то не так.

В некоторой степени использование C ++ означает хотя бы некоторую степень доверия к другим людям, которые будут иметь доступ к вашему исходному коду. Если они захотят достаточно сильно, они могут облажаться всеми способами, и вы ничего не сможете сделать, чтобы их остановить. Если вы хотите наложить реальные ограничения на использование вашего кода, C ++ - неподходящий язык для ваших целей.

Изменить: это вообще не «аргумент» - это просто указание на основу, на которой были приняты решения. Поскольку у меня есть копия D&E после ответа на предыдущий вопрос, я напечатаю немного больше, если она здесь 1:

Гораздо важнее разрешить полезную функцию, чем предотвратить любое неправильное использование: Вы можете писать плохие программы на любом языке. Важно свести к минимуму вероятность случайного неправильного использования функций, и было потрачено много усилий на то, чтобы гарантировать, что поведение конструкций C ++ по умолчанию является разумным или приводит к ошибкам во время компиляции. Например, по умолчанию проверяются все типы аргументов функции - даже через отдельные границы компиляции - и по умолчанию все члены класса являются закрытыми. Однако язык системного программирования не может помешать решительному программисту сломать систему, поэтому усилия по проектированию лучше направить на создание возможностей для написания хороших программ, чем на предотвращение неизбежных плохих. В долгосрочной перспективе программисты, кажется, учатся. Это вариант старого слогана C «Доверься программисту». Существуют различные правила проверки типов и контроля доступа, позволяющие провайдеру класса четко указывать, что ожидается от пользователей, для защиты от несчастных случаев. Эти правила не предназначены для защиты от преднамеренного нарушения (§2.10).

В §2.10 он, среди прочего, говорит:

Задача системы защиты - убедиться, что любое такое нарушение системы типов является явным, и минимизировать необходимость в таких нарушениях.

Эти цели здесь, похоже, были достигнуты - публикация защищенного члена базового класса определенно требует явных действий в производном классе, и за более чем 20 лет написания C ++ я не могу припомнить, чтобы когда-либо нуждался (или даже хотел) в этом.

1 §4.3, стр. 115, 116.

person Jerry Coffin    schedule 27.01.2010
comment
Я часто слышу этот аргумент, и мне он действительно начинает не нравиться. Да, вы не можете защититься от злого умысла или крайнего незнания, но этот аргумент не должен освобождать вас от ответственности за рассмотрение разумных механизмов / альтернативных решений, которые сделают ваш код более умным и безопасным, а также менее уязвимым для случайного неправильного использования. - person ; 27.01.2010
comment
@STingRaySC: я думаю, что невозможно формально отличить преднамеренное раскрытие частных членов (например, когда интерфейс имеет геттеры и сеттеры, поддерживаемые частным элементом данных), и случайное (когда кто-то некомпетентно замораживает вашу реализацию, добавляя общедоступные геттеры и сеттеры для элемента данных, который даже не имеет смысла в вашей предполагаемой перезаписи, но теперь должен поддерживаться бессрочно как часть наблюдаемого состояния объекта). Так что я не думаю, что язык может многое сделать, чтобы здесь помочь. - person Steve Jessop; 27.01.2010
comment
В этом случае имеет смысл предупредить компилятор (см. Правку к моему вопросу). Интересно, есть ли в стандарте что-нибудь сказать о ситуациях, о которых компилятор должен предупреждать. Кроме того, может быть механизм, похожий на аннотацию java, чтобы переопределить предупреждение в коде (да, прагма есть, но большинство прагм зависят от компилятора). - person amit; 28.01.2010
comment
Я не буду классифицировать клиента как Макиавелли в этом случае. Очень легко ошибиться. Не уверен, что публикация члена защищенного базового класса определенно требует явных действий в производном классе. Это тонкий случай. - person amit; 28.01.2010

Вы не можете. «виртуальность» функции и типа доступа - это два разных, не связанных между собой понятия.

person Terry Mahaffey    schedule 27.01.2010
comment
Кажется, ваш ответ ничего не говорит. Либо это? - person amit; 28.01.2010

Повышение уровня частного / защищенного виртуального метода до общедоступного в производном классе не раскрывает метод базового класса. Его по-прежнему нельзя вызвать через указатель базового класса. Он не становится частью интерфейса базового класса.

person Community    schedule 27.01.2010

Контроль доступа в C ++, возможно, не делает того, что вы хотите. Он не предназначен для применения ограничений в стиле DRM, чтобы помешать вам делиться своим доступом. Если у A есть доступ к B, то A может вызвать B и использовать результат для любых целей, в том числе вернуть его другому вызывающему абоненту, у которого нет доступа к B.

Проблема, которая обсуждается в статье, на которую вы ссылаетесь, не связана с намеренным или злонамеренным обменом B. Шаблоны шаблонных методов, включая частные виртуальные функции. Дочерние классы написали общедоступные переопределения виртуальной функции, поэтому вы больше не можете разделять две проблемы (доступ и виртуальность) без изменения всех дочерних классов. Насколько я понимаю, статья действительно предлагает решение проблемы, которую она представляет, и это решение состоит в том, чтобы «вообще никогда не делать виртуальные функции общедоступными».

К виртуальным функциям следует относиться во многом как к элементам данных - делайте их частными до тех пор, пока потребности проектирования не укажут на менее ограниченный подход. Их гораздо легче повысить до более доступного уровня, чем понизить до более закрытого уровня.

Причина, по которой это не решает вашу проблему, заключается в том, что они не учли вашу проблему.

person Steve Jessop    schedule 27.01.2010
comment
Я хотел бы обратить внимание на отрывок из статьи, в котором говорится: Кроме того, производные классы, скорее всего, оставят f () в общедоступном интерфейсе, а клиенты, которые используют эти производные классы напрямую. Да, согласен, это не основная тема статьи, но это вопрос, ИМО, тем не менее, большой. Большой, потому что здесь легко ошибиться. - person amit; 28.01.2010

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

С чего бы это? Конструктор каждого класса должен решить, какой внешний интерфейс он имеет.

В C ++ базовый класс не имеет специальных полномочий для принудительного применения свойств интерфейса производного класса. Производный класс может решить сделать некоторую переопределяющую функцию частной, если базовая функция является общедоступной, или наоборот. Интерфейс производного класса - это контракт со своими клиентами, а не с базовым классом (если только базовый класс не является клиентом производного класса, как в случае с любопытно повторяющимся базовым классом шаблона).

person curiousguy    schedule 26.12.2011

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

protected:
class ProtectedToken { virtual ~ProtectedToken() { } };
virtual void my_tough_cookie(int arg,
  ProtectedToken const &tok = ProtectedToken() ) {
    assert ( typeid( tok ) == typeid( ProtectedToken ) );
    …
}

Конечно, это нехорошо ни с кем, в том числе с самим собой.

Изменить: Ба, это не работает. Даже если бы это было так, вы могли бы сделать public: using Base::ProtectedToken и таким образом обойти защиту. Еще 15 минут моей жизни потрачены впустую ...

person Potatoswatter    schedule 27.01.2010
comment
Две вещи: в качестве аргумента по умолчанию он не обеспечивает никакой защиты, а выбор токена по значению делает его подверженным нарезке. - person Georg Fritzsche; 27.01.2010
comment
@gf: аргумент по умолчанию по-прежнему создается вызывающим. Что значит нарезка? Хотя я забыл, что он на самом деле не пустой, есть указатель на vtable, поэтому он должен быть ссылкой… шесть из одного, полдюжины другого, вы попадете в ад, если сделаете это. - person Potatoswatter; 27.01.2010
comment
@gf: Понятно, он создаст ProtectedToken с помощью копирования, и утверждение всегда будет успешным. - person Potatoswatter; 27.01.2010
comment
Хм, почему у меня создалось впечатление, что вызывающему абоненту требуется разрешение для аргумента по умолчанию… - person Potatoswatter; 27.01.2010
comment
Выражения аргументов по умолчанию в объявлениях функций-членов находятся в области класса, соответственно применяется проверка доступа. См. Примечание в конце §8.3.6 / 5, если вам интересно. - person Georg Fritzsche; 27.01.2010
comment
@gf: Да, я посмотрел: vP. Я, конечно, делаю это навязчиво ... Вероятно, я думал о некотором опыте работы с аргументами по умолчанию в шаблоне, которые вряд ли получат специальный доступ. (§14.7.1 §11) - person Potatoswatter; 27.01.2010