Как может работать программа с глобальной переменной main вместо основной функции?

97

Рассмотрим следующую программу:

#include <iostream>
int main = ( std::cout << "C++ is excellent!\n", 195 ); 

Используя g ++ 4.8.1 (mingw64) в ОС Windows 7, программа компилируется и работает нормально, печатая:

C ++ отличный!

к консоли. mainвыглядит как глобальная переменная, а не функция; как эта программа может работать без функции main()? Соответствует ли этот код стандарту C ++? Хорошо ли определено поведение программы? Я тоже использовал эту -pedantic-errorsопцию, но программа все еще компилируется и запускается.

Деструктор
источник
11
@ πάνταῥεῖ: зачем нужен языковой тег адвоката?
Деструктор
14
Обратите внимание, что 195это код операции для RETинструкции и что в соглашении о вызовах C вызывающий объект очищает стек.
Брайан
2
@PravasiMeet «тогда как эта программа выполняется» - не думаете ли вы, что код инициализации для переменной должен выполняться (даже без main()функции? На самом деле, они совершенно не связаны.)
Парамагнитный круассан
4
Я среди тех, кто обнаружил, что программа работает без ошибок (64-битный linux, g ++ 5.1 / clang 3.6). Однако я могу исправить это, внося поправки в int main = ( std::cout << "C++ is excellent!\n", exit(0),1 );(и в том числе <cstdlib>), хотя программа остается юридически плохо оформленной.
Майк Кинган,
11
@Brian Вы должны упоминать архитектуру, когда делаете подобные заявления. Весь мир - это не VAX. Или x86. Или как там.
dmckee --- котенок экс-модератора

Ответы:

85

Прежде чем перейти к сути вопроса о том, что происходит, важно указать, что программа плохо сформирована в соответствии с отчетом о дефекте 1886: Языковая привязка для main () :

[...] Программа, которая объявляет переменную main в глобальной области или объявляет имя main со связью на языке C (в любом пространстве имен), имеет неправильный формат. [...]

В самых последних версиях clang и gcc это вызывает ошибку, и программа не компилируется ( см. Пример gcc live ):

error: cannot declare '::main' to be a global variable
int main = ( std::cout << "C++ is excellent!\n", 195 ); 
    ^

Так почему же в старых версиях gcc и clang не было диагностики? В этом отчете о дефектах даже не было предложенного решения до конца 2014 года, поэтому этот случай совсем недавно был явно неправильно оформлен, что требует диагностики.

До этого, похоже, это было бы неопределенным поведением, поскольку мы нарушаем обязательное требование чернового варианта стандарта C ++ из раздела 3.6.1 [basic.start.main] :

Программа должна содержать глобальную функцию с именем main, которая является назначенным запуском программы. [...]

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

Итак, что на самом деле делает код и почему в некоторых случаях он дает результаты? Посмотрим, что у нас есть:

declarator  
|        initializer----------------------------------
|        |                                           |
v        v                                           v
int main = ( std::cout << "C++ is excellent!\n", 195 ); 
    ^      ^                                   ^
    |      |                                   |
    |      |                                   comma operator
    |      primary expression
global variable of type int

У нас mainесть int, объявленный в глобальном пространстве имен и инициализируемый, переменная имеет статический срок хранения. Реализация определяет, будет ли инициализация выполняться до попытки вызова, mainно похоже, что gcc делает это до вызова main.

В коде используется оператор запятой , левый операнд представляет собой выражение отброшенного значения и используется здесь исключительно для побочного эффекта вызова std::cout. Результатом оператора запятой является правый операнд, который в данном случае является значением prvalue, 195присвоенным переменной main.

Мы видим, что сергей указывает на сгенерированную сборку, которая coutвызывается во время статической инициализации. Хотя более интересным моментом для обсуждения см. Живую сессию Godbolt будет следующее:

main:
.zero   4

и последующие:

movl    $195, main(%rip)

Вероятный сценарий состоит в том, что программа переходит к символу, mainожидая присутствия действительного кода, и в некоторых случаях выполняет сегментарный сбой . Так что, если это так, мы ожидаем, что сохранение действительного машинного кода в переменной mainможет привести к работоспособной программе , если мы находимся в сегменте, который позволяет выполнение кода. Мы видим, что эта запись IOCCC 1984 г. делает именно это .

Похоже, мы можем заставить gcc сделать это на C, используя ( см. Это вживую ):

const int main = 195 ;

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

Также см. Ответ FUZxxl здесь на версию этого вопроса для C.

Шафик Ягмур
источник
Почему реализация также не дает никаких предупреждений. (Когда я использую -Wall & -Wextra, он все равно не дает ни одного предупреждения). Зачем? Что вы думаете об ответе @Mark B на этот вопрос?
Деструктор
IMHO, компилятор не должен выдавать предупреждение, потому что mainэто не зарезервированный идентификатор (3.6.1 / 3). В этом случае я думаю, что обработка этого случая VS2013 (см. Ответ Фрэнсиса Куглера) более правильна в этом отношении, чем gcc & clang.
cdmh
@PravasiMeet Я обновил свой ответ относительно того, почему более ранние версии gcc не давали диагностику.
Шафик Ягмур,
2
... и действительно, когда я тестирую программу OP на Linux / x86-64, с g ++ 5.2 (которая принимает программу - я думаю, вы не шутили насчет «самой последней версии»), она вылетает именно там, где я ожидал бы.
zwol
1
@Walter Я не верю, что это дубликаты, первый задает гораздо более узкий вопрос. Очевидно, что существует группа пользователей SO, которые имеют более редукционистское представление о дубликатах, что для меня не имеет большого смысла, поскольку мы могли бы свести большинство вопросов SO к какой-либо версии старых вопросов, но тогда SO не будет очень полезен.
Шафик Ягмур 01
20

Начиная с 3.6.1 / 1:

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

Отсюда похоже, что g ++ позволяет программе (предположительно, как "автономное" предложение) без основной функции.

Тогда из 3.6.1 / 3:

Функцию main нельзя использовать (3.2) в программе. Связь (3.5) main определяется реализацией. Программа, объявляющая main как встроенную или статическую, плохо сформирована. Имя main иначе не зарезервировано.

Итак, здесь мы узнаем, что иметь целочисленную переменную совершенно нормально main.

Наконец, если вам интересно, почему вывод выводится на печать, при инициализации int mainиспользуется оператор запятой для выполнения coutв статической инициализации, а затем предоставляется фактическое целое значение для инициализации.

Марк Б
источник
7
Интересно отметить, что связывание не удается, если вы переименовываете mainчто-то другое: (.text+0x20): undefined reference to main ''
Фред Ларсон
1
Разве вам не нужно указывать gcc, что ваша программа является автономной?
Шафик Ягмур
9

gcc 4.8.1 генерирует следующую сборку x86:

.LC0:
    .string "C++ is excellent!\n"
    subq    $8, %rsp    #,
    movl    std::__ioinit, %edi #,
    call    std::ios_base::Init::Init() #
    movl    $__dso_handle, %edx #,
    movl    std::__ioinit, %esi #,
    movl    std::ios_base::Init::~Init(), %edi  #,
    call    __cxa_atexit    #
    movl    $.LC0, %esi #,
    movl    std::cout, %edi #,
    call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)   #
    movl    $195, main(%rip)    #, main
    addq    $8, %rsp    #,
    ret
main:
    .zero   4

Обратите внимание: coutвызывается во время инициализации, а не в mainфункции!

.zero 4объявляет 4 (инициализированных 0) байта, начиная с местоположения main, где main- имя переменной [!] .

mainСимвол интерпретируется как начало программы. Поведение зависит от платформы.

сергей
источник
1
Обратите внимание, как указывает Брайан, 195 это код операции для retнекоторых архитектур. Таким образом, утверждение нулевых инструкций может быть неточным.
Шафик Ягмур
@ShafikYaghmour Спасибо за ваш комментарий, вы правы. Я испортил директивы ассемблера.
sergej
8

Это плохо сформированная программа. Он вылетает в моей тестовой среде cygwin64 / g ++ 4.9.3.

Из стандарта:

3.6.1 Основная функция [basic.start.main]

1 Программа должна содержать глобальную функцию с именем main, которая является назначенным запуском программы.

Р Саху
источник
Я думаю, что до отчета о дефектах, который я цитировал, это было просто неопределенное поведение.
Шафик Ягмур
@ShafikYaghmour, это общий принцип, который должен применяться во всех местах, где должен использоваться стандарт ?
Р Саху,
Я хочу сказать «да», но не вижу хорошего описания разницы. Из того, что я могу сказать из этого обсуждения , плохо сформированный отчет о недоставке и неопределенное поведение, вероятно, являются синонимами, поскольку ни то, ни другое не требует диагностики. Это может означать, что они плохо сформированы, и UB различны, но не уверены.
Шафик Ягмур
3
Раздел 4 C99 («Соответствие») делает это недвусмысленным: «Если нарушается требование« должен »или« не должен », которое появляется вне ограничения, поведение не определено». Я не могу найти эквивалентную формулировку в C ++ 98 или C ++ 11, но я сильно подозреваю, что комитет имел в виду, что это было там. (Комитеты C и C ++ действительно должны сесть и сгладить все терминологические различия между двумя стандартами.)
zwol
7

Я считаю, что это работает, потому что компилятор не знает, что он компилирует main()функцию, поэтому он компилирует глобальное целое число с побочными эффектами присваивания.

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

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

Когда я запустил образец, он распечатал, но затем это вызвало ошибку сегмента . Я предполагаю, что это когда система времени выполнения пыталась выполнить переменную типа int, как если бы это была функция .

Галик
источник
4

Я пробовал это на 64-битной ОС Win7 с использованием VS2013, и он компилируется правильно, но когда я пытаюсь создать приложение, я получаю это сообщение из окна вывода.

1>------ Build started: Project: tempTest, Configuration: Debug Win32 ------
1>LINK : fatal error LNK1561: entry point must be defined
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========
Фрэнсис Куглер
источник
2
FWIW, это ошибка компоновщика, а не сообщение от отладчика. Компиляция прошла успешно, но компоновщик не смог найти функцию, main()потому что это переменная типаint
cdmh
Спасибо за ответ. Я переформулирую свой первоначальный ответ, чтобы отразить это.
Фрэнсис Куглер,
-1

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

Vikas.Ghode
источник