Почему методы структур не должны быть объявлены в C++?

Возьмем, к примеру, следующий код:

#include <iostream>
#include <string>

int main()
{
    print("Hello!");
}

void print(std::string s) {
    std::cout << s << std::endl;
}

При попытке построить это я получаю следующее:

program.cpp: In function ‘int main()’:
program.cpp:6:16: error: ‘print’ was not declared in this scope

Что имеет смысл.

Так почему же я могу реализовать подобную концепцию в структуре, но при этом не получить за это крика?

struct Snake {
    ...

    Snake() {
        ...
        addBlock(Block(...));
    }

    void addBlock(Block block) {
        ...
    }

    void update() {
        ...
    }

} snake1;

Я не только не получаю предупреждений, но программа действительно компилируется! Без ошибки! Это просто природа структур? Что тут происходит? Ясно, что addBlock(Block) был вызван до объявления метода.


person Community    schedule 30.07.2014    source источник
comment
на самом деле это хороший вопрос, и есть люди, которые задаются вопросом, почему это так. в частности, некоторые ответы на это приходят в форме, это историческая причина, потому что двухфазная компиляция была дорогой еще в 1976 году. но C++ - это какой-то ублюдок с некоторыми старыми частями и некоторыми новыми частями, поэтому вы получаете это несоответствие.   -  person v.oddou    schedule 30.07.2014
comment
Краткий ответ: тела функций не компилируются до тех пор, пока не будет скомпилировано определение класса.   -  person M.M    schedule 30.07.2014
comment
С практической точки зрения это осуществимо, поскольку компилятору приходится выполнять возврат только к одному классу, а не ко всему файлу. Последнее было неоправданно дорого с использованием технологий эпохи 90-х.   -  person MSalters    schedule 30.07.2014
comment
Недавно был аналогичный вопрос: title="почему c никогда не позволял использовать функции до их объявления%23comment38731751_24925831"> stackoverflow.com/questions/24925831/   -  person Tony Delroy    schedule 30.07.2014
comment
Аналогично и с классами: ideone.com/Xw2dbF   -  person user3791372    schedule 30.07.2014
comment
В D&E Страуструп говорит, что эта функция была введена для решения проблем с поиском имен. Например, int i; struct s { void foo() { i = 42; } int i; }; Когда foo определено вне класса, i явно указывает на this->i, и это должно быть то же самое при перемещении определения внутри класса.   -  person dyp    schedule 30.07.2014


Ответы (3)


struct в C++ на самом деле является определением class, где его содержимое равно public, если не указано иное путем включения раздела protected: или private:.

Когда компилятор видит class или struct, он сначала переваривает все объявления в блоке ({}), прежде чем работать с ними.

В случае обычного метода компилятор еще не видел объявленный тип.

person NirMH    schedule 30.07.2014
comment
@juanchopanza: это полностью относится к внутренним типам. Когда компилятор видит класс или структуру, он сначала обрабатывает все объявления, что означает, что объявления должны быть удобочитаемыми. Если в объявлении используется внутренний тип, внутренний тип должен быть объявлен (возможно, определен в зависимости от объявления). - person Mooing Duck; 30.07.2014
comment
@juanchopanza: я думаю, у нас ошибка связи. Я говорю, что шаг, который сначала переваривает все объявления, подразумевает, что в противном случае объявления должны следовать обычным правилам для типов/сигнатур, объявляемых по порядку. Это отлично повторяется во внутренних типах. После этого определения анализируются. Я не рассматриваю это как исключение для внутренних типов, я рассматриваю это как внутренние типы, следующие тем же правилам, что и внешние типы. - person Mooing Duck; 30.07.2014
comment
@MooingDuck Ты прав, я недостаточно внимательно прочитал твой первый комментарий. Я думаю, вы имеете в виду следующее: ideone.com/K40DDE, что имеет смысл. - person juanchopanza; 30.07.2014

Стандарт С++ 3.4.1:

.4:

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

Вот почему глобальные переменные и функции нельзя использовать перед объявлением before.

.5:

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

то же самое, только что написанное снова, поскольку абзац .4 явно ограничил его использование словом «глобальный», этот абзац теперь говорит: «кстати, это верно и для имен, ребята ...»

.7:

Имя, используемое в определении класса X вне тела функции-члена или определения вложенного класса29, должно быть объявлено одним из следующих способов: - до его использования в классе X или быть членом базового класса X (10.2) , или - если X является вложенным классом класса Y (9.7), перед определением X в Y, или должен быть членом базового класса Y (этот поиск применяется, в свою очередь, к охватывающим Y классам, начиная с самый внутренний охватывающий класс)30, или — если X является локальным классом (9.8) или является вложенным классом локального класса, перед определением класса X в блоке, охватывающем определение класса X, или — если X является член пространства имен N, или является вложенным классом класса, который является членом N, или является локальным классом, или вложенным классом внутри локального класса функции, которая является членом N, до определения класса X в namespace N или в одном из N объемлющих пространств имен.

Я думаю, это говорит обо всем коде, который не стоит в исполняемом процессором коде (например, декларативный код).

и, наконец, самое интересное:

3.3.7 Область действия класса [basic.scope.class]

1 Следующие правила описывают область действия имен, объявленных в классах.

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

2) Имя N, используемое в классе S, должно ссылаться на одно и то же объявление в его контексте и при повторной оценке в завершенной области видимости S. Для нарушения этого правила диагностика не требуется.

3) Если переупорядочивание объявлений элементов в классе дает альтернативную допустимую программу в соответствии с (1) и (2), программа имеет неправильный формат, диагностика не требуется.

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

фактически говоря, в классе объявление просматривается в двухэтапной компиляции.

person v.oddou    schedule 30.07.2014

"почему я могу реализовать аналогичную концепцию в структуре, но при этом не получить за это крика?"

В определении struct или class вы представляете общедоступный интерфейс классу, и гораздо проще понять, найти и поддерживать/обновлять этот API, если он представлен в:

  • предсказуемый порядок, с
  • минимальный беспорядок.

Для предсказуемого порядка у людей есть свои собственные стили, и здесь задействовано немного «искусства», но, например, я использую каждый спецификатор доступа не более одного раза и всегда public перед protected перед private, затем в них я обычно помещаю typedefs, const данные, конструкторы , деструкторы, изменяющие/неконстантные функции, const функции, statics, friends....

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

Это отличается от функций, которые не являются членами класса — где люди, которым нравится программирование сверху вниз, используют объявления функций и прячут определения позже в файле — тем, что:

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

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

  • Лучшая практика заключается в том, чтобы классы не впихивались в дико обширный интерфейс... обычно вам нужно функциональное ядро, а затем некоторые дополнительные удобные функции, после чего стоит подумать о том, что можно добавить в качестве функций, не являющихся членами. Часто говорят, что у std::string слишком много функций-членов, хотя лично я считаю это вполне разумным. Тем не менее, это также отличается от заголовочного файла, объявляющего интерфейс библиотеки, где можно ожидать, что исчерпывающая функциональность будет переполнена, что делает более желательным разделение даже inline реализации.

person Tony Delroy    schedule 30.07.2014