Почему мы помещаем закрытые функции-члены в заголовки?

16

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

Почему мы должны помещать частных членов в заголовки?

Но есть ли причина объявлять частные функции в определении класса?

Альтернативой может быть, по сути, идиома прыщей, но без лишней косвенности.

Эта языковая особенность больше, чем историческая ошибка?

Praxeolitic
источник

Ответы:

11

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

Может показаться, что это похоже на «ставить корзину перед лошадью», потому что выбор реализации компилятора не должен влиять на спецификацию языка. Однако в действительности сам язык C ++ был разработан одновременно с рабочей реализацией ( Cfront ), в которой использовались vtables.

Грег Хьюгилл
источник
4
«Выбор реализации не должен влиять на язык» - почти полная противоположность мантре C ++. Спецификация не требует какой-либо конкретной реализации, но есть много правил, специально разработанных для удовлетворения требований особенно эффективного выбора реализации.
Бен Фойгт
Это звучит очень правдоподобно. Есть ли источник по этому поводу?
Праксеолит
@Praxeolitic: Вы можете прочитать о таблице виртуальных методов .
Грег Хьюгилл
1
@Praxeolitic - найдите старую копию «Справочного руководства по аннотированным C ++» ( stroustrup.com/arm.html ) - авторы рассказывают о различных деталях реализации и о том, как они формировали язык.
Майкл Кохне
Я не уверен, что это действительно отвечает на вопрос. Для меня это просто адрес одного конкретного аспекта - хотя и хорошо - и оставляет позади все остальное: почему мы должны помещать не виртуальные частные функции-члены в заголовки? Конечно, чисто для последовательности / простоты это один из аргументов - но в идеале у нас тоже должно быть механистическое и / или философское обоснование. Итак, я добавил ответ, который, по моему мнению, объясняет это как очень осознанный выбор дизайна с очень полезными эффектами.
underscore_d
13

Если вы разрешаете добавлять методы в класс вне его определения, они могут быть добавлены кем угодно , в любом файле.

Это сразу же предоставит всем клиентским кодам простой доступ к закрытым и защищенным элементам данных.

После того, как вы закончили определение класса, нет никакого способа пометить некоторые файлы как специально благословленные автором, чтобы расширить его - есть только плоские единицы перевода. Таким образом, единственный разумный способ сообщить компилятору, что определенный набор методов является официальным или благословенным автором класса, - это объявить их внутри класса.


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

Эти спецификаторы доступа не пытаются предотвратить эти атаки, потому что это невозможно. Они только указывают, как должен использоваться класс.

Бесполезный
источник
1
Определение класса может ссылаться на сущность контейнера функции, скрытую от клиентского кода. Альтернативно, это может быть инвертировано, и полное традиционное определение класса, скрытое от клиентского кода, может обозначить видимый интерфейс, который описывает только открытые члены и может раскрыть его размер.
Праксеолит
1
Конечно, но модель компиляции не обеспечивает разумного способа остановить клиентский код, вставляя его собственную версию скрытого контейнера или другой видимый интерфейс с дополнительными средствами доступа.
бесполезно
Это хороший момент, но разве это не так для определений всех функций-членов, которых нет в заголовке?
Праксеолит
1
Все функции-члены объявлены внутри класса. Дело в том, что вы не можете добавлять новые функции-члены, которые по крайней мере не были объявлены в определении класса (а одно правило определения предотвращает множественные определения).
бесполезно
Тогда как насчет правила одного контейнера частных функций?
праксеолит
8

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

Другой ответ вызывает тот факт, что классы должны быть объявлены в одном блоке - после чего они запечатаны и не могут быть добавлены. Это то, что вы будете делать, опуская объявление частного метода в заголовке, а затем пытаясь определить его в другом месте. Хороший вопрос. Почему некоторые пользователи этого класса должны иметь возможность расширять его так, чтобы другие пользователи не могли наблюдать? Частные методы являются его частью и не исключаются из этого. Но затем вы спрашиваете, почему они включены, и это кажется немного тавтологическим. Почему классные пользователи должны знать о них? Если бы они не были видны, пользователи не могли бы добавить ни одного, и эй Presto.

Итак, я хотел бы дать ответ, который вместо того, чтобы просто включать частные методы по умолчанию, дает определенные моменты в пользу того, чтобы они были видны пользователям. Механистическая причина для не-виртуальных частных функций, требующих публичного объявления, дана в GotW # 100 Херба Саттера об идиоме Pimpl в качестве части ее обоснования. Я не буду рассказывать о Пимпле здесь, так как уверен, что мы все знаем об этом. Но вот соответствующий бит:

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

  • Размер и макет : [членов и виртуальных функций - не требует пояснений и отлично подходит для производительности, но не потому, что мы здесь]
  • Функции : вызывающий код должен иметь возможность разрешать вызовы функций-членов класса, включая недоступные частные функции, которые перегружаются несобственными функциями - если частная функция лучше подходит, вызывающий код не сможет скомпилироваться. (C ++ принял преднамеренное проектное решение, чтобы выполнить разрешение перегрузки перед проверкой доступности по соображениям безопасности. Например, считалось, что изменение доступности функции с частного на общедоступное не должно изменить значение легального вызывающего кода.)

Саттер, конечно, является чрезвычайно надежным источником в качестве члена Комитета, поэтому он знает «обдуманное дизайнерское решение», когда видит его. И идея требовать публичного объявления частных методов как способа избежать измененной семантики или случайно нарушенной доступности позже, вероятно, является наиболее убедительным обоснованием. К счастью, все это казалось довольно бессмысленным до сих пор!

underscore_d
источник
« Но тогда вы спрашиваете, почему, и этот ответ кажется тавтологическим. Опять же, почему пользователи класса должны знать о них? » Нет, это не тавтологический. Пользователям не обязательно «знать о них». Что должно произойти, это то, что вы должны быть в состоянии запретить людям расширять классы без согласия автора класса. Таким образом, все такие члены должны быть объявлены заранее. То, что это заставляет пользователей знать о частных членах, является просто необходимым следствием.
Никол Болас
1
@NicolBolas Конечно. Это, вероятно, не очень хорошая формулировка с моей стороны. Я имел в виду, что ответ объясняет только видимость приватных методов как следствие (очень правильного) правила, которое охватывает многие вещи, а не дает обоснование для видимости приватных методов конкретно. Конечно, реальный ответ - это комбинация бесполезных, скрежетных и моих. Я просто хочу представить другую перспективу, которой еще не было.
underscore_d
4

Есть две причины для этого.

Во-первых, следует понимать, что спецификатор доступа предназначен для компилятора и не имеет отношения во время выполнения. Доступ к закрытому члену вне области видимости - ошибка компиляции .

сжатость

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

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

Есть еще одно важное преимущество:

Встроенные функции

Закрытая функция может быть встроенной, и это обязательно требует, чтобы она была в заголовке. Учти это:

class A {
  private:
    inline void myPrivateFunction() {
      ...
    }

  public:
    inline void somePublicFunction() {
      myPrivateFunction();
      ...
    }
};

Закрытая функция может быть встроена вместе с публичной функцией. Это делается на усмотрение компилятора, так как inlineключевое слово технически является предложением , а не требованием.

Сообщество
источник
Все функции, определенные внутри тела класса, автоматически помечаются inline, поэтому нет смысла использовать ключевое слово там.
Бен Фойгт
@ BenVoigt так и есть. Я не осознавал этого, и я уже довольно давно использую C ++. Этот язык не перестает удивлять меня такими маленькими самородками.
Я не думаю, что метод должен быть в заголовке, чтобы быть встроенным. Это просто должно быть в том же модуле компиляции. Есть ли случай, когда имеет смысл иметь отдельные модули компиляции для класса?
Самуэль Даниэльсон
@SamuelDanielson правильно, но в определении класса должна быть частная функция. Само понятие «частный» подразумевает, что оно является частью класса. Можно было бы иметь в .cppфайле функцию , не являющуюся членом, которая указывается функциями-членами, которые определены вне определения класса, но такая функция не будет закрытой.
Суть вопроса заключалась в том, «есть ли какая-либо причина для объявления частных функций в определении класса [sic, под которым контекст показывает, что OP действительно означает объявление класса]». Вы говорите об определении частных функций. Это не отвечает на вопрос. @SamuelDanielson В настоящее время LTO означает, что функции могут быть в любом месте проекта и имеют равные шансы быть встроенными. Что касается разделения класса на несколько единиц перевода, простейший случай состоит в том, что класс просто большой, и вы хотите семантически разделить его на несколько исходных файлов. Я уверен, что такие большие классы не рекомендуется, но в любом случае
underscore_d
2

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

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

gnasher729
источник
Отличное замечание о встраивании! Я полагаю, что это особенно актуально для stdlib и других библиотек с методами, встроенными. (если не так много для внутреннего кода, где LTO может делать практически все что угодно за пределами TU)
underscore_d
0

Это позволяет этим функциям получать доступ к закрытым членам. В противном случае вам все friendравно понадобится их в шапке.

Если какая-либо функция может получить доступ к закрытым членам класса, тогда private будет бесполезен.

чокнутый урод
источник
1
Может быть, я должен был быть более ясным в вопросе - я имел в виду, почему язык разработан таким образом? Почему это должно быть или почему этот дизайн хорош?
Праксеолит