Jak może działać program ze zmienną globalną o nazwie main zamiast funkcji głównej?

97

Rozważ następujący program:

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

Używając g ++ 4.8.1 (mingw64) w systemie operacyjnym Windows 7, program kompiluje się i działa dobrze, drukując:

C ++ jest doskonały!

do konsoli. mainwydaje się być raczej zmienną globalną niż funkcją; jak ten program może działać bez tej funkcji main()? Czy ten kod jest zgodny ze standardem C ++? Czy zachowanie programu jest dobrze zdefiniowane? Użyłem również tej -pedantic-errorsopcji, ale program nadal się kompiluje i działa.

Burzyciel
źródło
11
@ πάνταῥεῖ: dlaczego tag prawnika językowego jest konieczny?
Destructor,
14
Zauważ, że 195jest to kod RETinstrukcji i że w konwencji wywoływania C, wywołujący czyści stos.
Brian
2
@PravasiMeet "to jak ten program wykonuje" - czy nie sądzisz, że kod inicjujący dla zmiennej powinien zostać wykonany (nawet bez main()funkcji? W rzeczywistości są one całkowicie niezwiązane.)
The Paramagnetic Croissant
4
Jestem jednym z tych, którzy stwierdzili, że program segfaults taki, jaki jest (64-bitowy linux, g ++ 5.1 / clang 3.6). Mogę to jednak naprawić, zmieniając go int main = ( std::cout << "C++ is excellent!\n", exit(0),1 );(i włączając <cstdlib>), chociaż program pozostaje prawnie źle sformułowany.
Mike Kinghan
11
@Brian Pisząc takie stwierdzenia, należy wspomnieć o architekturze. Cały świat to nie VAX. Lub x86. Lub cokolwiek.
dmckee --- ex-moderator kitten

Odpowiedzi:

85

Zanim przejdziemy do sedna pytania o to, co się dzieje, ważne jest, aby zwrócić uwagę, że program jest źle sformułowany zgodnie z raportem defektu 1886: Powiązanie językowe dla main () :

[...] Program, który deklaruje zmienną main w zasięgu globalnym lub który deklaruje nazwę main z łączeniem języka C (w dowolnej przestrzeni nazw) jest źle sformułowany. […]

Najnowsze wersje clang i gcc powodują, że jest to błąd i program się nie skompiluje ( zobacz przykład na żywo z gcc ):

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

Dlaczego więc nie było diagnostyki w starszych wersjach gcc i clang? Ten raport o wadzie nie miał nawet proponowanego rozwiązania aż do końca 2014 roku, więc ten przypadek dopiero niedawno został wyraźnie źle sformułowany, co wymaga diagnostyki.

Przed tym, wydaje się, że byłoby to niezdefiniowane zachowanie ponieważ jesteśmy naruszono powinny wymogiem projektu C ++ standard od sekcji 3.6.1 [basic.start.main] :

Program powinien zawierać globalną funkcję zwaną main, która jest wyznaczonym początkiem programu. […]

Nieokreślone zachowanie jest nieprzewidywalne i nie wymaga diagnostyki. Niespójność, którą widzimy przy odtwarzaniu zachowania, jest typowym niezdefiniowanym zachowaniem.

Więc co właściwie robi kod i dlaczego w niektórych przypadkach daje wyniki? Zobaczmy, co mamy:

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

Mamy, mainktóra jest int zadeklarowaną w globalnej przestrzeni nazw i jest inicjalizowana, zmienna ma statyczny czas trwania. W implementacji zdefiniowano, czy inicjalizacja nastąpi przed podjęciem próby wywołania, mainale wygląda na to, że gcc robi to przed wywołaniem main.

Kod używa operatora przecinka , lewy operand jest odrzuconym wyrażeniem wartości i jest tutaj używany wyłącznie jako efekt uboczny wywołania std::cout. Wynikiem operatora przecinka jest prawy operand, którym w tym przypadku jest prvalue 195przypisana do zmiennej main.

Widzimy, że sergej wskazuje na wygenerowane zestawy, które coutsą wywoływane podczas statycznej inicjalizacji. Chociaż bardziej interesującym punktem do dyskusji jest sesja godbolt na żywo :

main:
.zero   4

i kolejne:

movl    $195, main(%rip)

Prawdopodobny scenariusz jest taki, że program przeskakuje do symbolu, mainoczekując, że będzie tam obecny prawidłowy kod, aw niektórych przypadkach wystąpi seg-fault . Więc jeśli tak jest, spodziewalibyśmy się, że przechowywanie prawidłowego kodu maszynowego w zmiennej mainmoże prowadzić do działającego programu , zakładając, że znajdujemy się w segmencie, który umożliwia wykonanie kodu. Widzimy, że wpis IOCCC z 1984 r. Właśnie to robi .

Wygląda na to, że możemy zmusić gcc do zrobienia tego w C używając ( zobacz na żywo ):

const int main = 195 ;

To seg-faults, jeśli zmienna mainnie jest przypuszczalnie const, ponieważ nie znajduje się w lokalizacji wykonywalnej, Hat Tip do tego komentarza, który dał mi ten pomysł.

Zobacz także odpowiedź FUZxxl na konkretną wersję tego pytania w języku C.

Shafik Yaghmour
źródło
Dlaczego implementacja również nie daje żadnych ostrzeżeń. (Kiedy używam -Wall & -Wextra nadal nie daje pojedynczego ostrzeżenia). Czemu? Co myślisz o odpowiedzi @Marka B na to pytanie?
Destructor
IMHO, kompilator nie powinien dawać ostrzeżenia, ponieważ mainnie jest zastrzeżonym identyfikatorem (3.6.1 / 3). W tym przypadku myślę, że obsługa tego przypadku przez VS2013 (patrz odpowiedź Francisa Cuglera) jest bardziej poprawna w obsłudze niż gcc & clang.
cdmh
@PravasiMeet Zaktualizowałem swoją odpowiedź w odpowiedzi na pytanie, dlaczego wcześniejsze wersje gcc nie dawały diagnostyki.
Shafik Yaghmour
2
... i rzeczywiście, kiedy testuję program OP na Linux / x86-64, z g ++ 5.2 (który akceptuje program - myślę, że nie żartowałeś z "najnowszej wersji"), wywala się dokładnie tam, gdzie się spodziewałem by.
zwol
1
@Walter Nie wierzę, że są to duplikaty, które zadaje dużo węższe pytanie. Jest wyraźnie grupa użytkowników SO, którzy mają bardziej redukcjonistyczny pogląd na duplikaty, co nie ma dla mnie większego sensu, ponieważ większość pytań SO można sprowadzić do jakiejś wersji starszych pytań, ale wtedy SO nie byłoby zbyt przydatne.
Shafik Yaghmour
20

Od 3.6.1 / 1:

Program powinien zawierać globalną funkcję zwaną main, która jest wyznaczonym początkiem programu. Jest to implementacja definiująca, czy program w środowisku wolnostojącym jest wymagany do zdefiniowania funkcji głównej.

Z tego wynika, że ​​g ++ pozwala programowi (prawdopodobnie jako klauzula „wolnostojąca”) bez funkcji głównej.

Następnie od 3.6.1 / 3:

W programie nie należy używać funkcji main (3.2). Powiązanie (3.5) main jest zdefiniowane. Program, który deklaruje main jako inline lub static, jest źle sformułowany. Nazwa main nie jest inaczej zastrzeżona.

Tutaj dowiadujemy się, że posiadanie zmiennej całkowitej o nazwie jest w porządku main.

Wreszcie, jeśli zastanawiasz się, dlaczego wyjście jest drukowane, inicjalizacja int mainużywa operatora przecinka do wykonania coutw statycznym init, a następnie dostarcza rzeczywistą wartość całkowitą do wykonania inicjalizacji.

Mark B.
źródło
7
Warto zauważyć, że linkowanie kończy się niepowodzeniem, jeśli zmienisz nazwę mainna coś innego: (.text+0x20): undefined reference to główna ''
Fred Larson
1
Czy nie musisz określać gcc, że twój program jest wolnostojący?
Shafik Yaghmour
9

gcc 4.8.1 generuje następujący zestaw 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

Zauważ, że coutjest wywoływane podczas inicjalizacji, a nie w mainfunkcji!

.zero 4deklaruje 4 (zainicjowane przez 0) bajty, zaczynając od lokalizacji main, gdzie mainjest nazwą zmiennej [!] .

mainSymbol jest interpretowana jako początek programu. Zachowanie zależy od platformy.

sergej
źródło
1
Uwaga, jak wskazuje Brian, 195 jest kodem operacyjnym dla retniektórych architektur. Więc mówienie o zerowej instrukcji może nie być dokładne.
Shafik Yaghmour
@ShafikYaghmour Dzięki za komentarz, masz rację. Mam problemy z dyrektywami asemblera.
sergej
8

To jest źle sformułowany program. Zawala się na moim środowisku testowym, cygwin64 / g ++ 4.9.3.

Ze standardu:

3.6.1 Główna funkcja [basic.start.main]

1 Program powinien zawierać globalną funkcję zwaną main, która jest wyznaczonym początkiem programu.

R Sahu
źródło
Myślę, że przed przytoczonym przeze mnie raportem o defekcie było to po prostu nieokreślone zachowanie.
Shafik Yaghmour
@ShafikYaghmour jest to, że ogólna zasada być stosowane na wszystkich miejscach, w których poziom zastosowania powinien ?
R Sahu
Chcę powiedzieć tak, ale nie widziałem dobrego opisu różnicy. Z tego, co mogę powiedzieć z tej dyskusji , źle sformułowane NDR i nieokreślone zachowanie są prawdopodobnie synonimami, ponieważ żadne z nich nie wymagają diagnostyki. Wydaje się, że oznacza to źle sformułowane, a UB są odrębne, ale nie są pewne.
Shafik Yaghmour
3
C99 sekcja 4 („Zgodność”) czyni to jednoznacznym: „Jeżeli wymóg„ powinien ”lub„ nie powinien ”, który pojawia się poza ograniczeniem, jest naruszony, zachowanie jest nieokreślone.” Nie mogę znaleźć równoważnego sformułowania w C ++ 98 lub C ++ 11, ale podejrzewam, że komitet chciał, żeby tam był. (Komitety C i C ++ naprawdę muszą usiąść i wyeliminować wszystkie różnice terminologiczne między tymi dwoma standardami.)
zwolnić
7

Uważam, że to działa, ponieważ kompilator nie wie, że kompiluje main()funkcję, więc kompiluje globalną liczbę całkowitą ze skutkami ubocznymi przypisania.

Format obiektu , który to tłumaczenie-jednostka jest kompilowany do nie jest zdolne do różnicowania pomiędzy symbolem funkcyjnym oraz symbol zmienny .

Zatem linker szczęśliwie łączy się z głównym symbolem (zmiennej) i traktuje go jak wywołanie funkcji. Ale dopiero wtedy, gdy system wykonawczy uruchomi kod inicjujący zmienną globalną.

Kiedy sprawdziłem próbkę, została wydrukowana, ale potem spowodowała błąd seg . Zakładam, że wtedy system wykonawczy próbował wykonać zmienną int, jakby to była funkcja .

Galik
źródło
4

Wypróbowałem to na 64-bitowym systemie operacyjnym Win7 z VS2013 i kompiluje się poprawnie, ale kiedy próbuję zbudować aplikację, otrzymuję ten komunikat z okna wyjściowego.

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 ==========
Francis Cugler
źródło
2
FWIW, to jest błąd konsolidatora, a nie komunikat z debuggera. Kompilacja powiodła się, ale konsolidator nie mógł znaleźć funkcji, main()ponieważ jest to zmienna typuint
cdmh
Dziękuję za odpowiedź. Przeformułuję moją początkową odpowiedź, aby to odzwierciedlić.
Francis Cugler
-1

Wykonujesz tutaj trudną pracę. Jako main (w jakiś sposób) można zadeklarować, że jest liczbą całkowitą. Użyłeś operatora listy do wydrukowania wiadomości, a następnie przypisałeś jej 195. Jak powiedział ktoś poniżej, że nie jest to wygodne w C ++, jest prawdą. Ale ponieważ kompilator nie znalazł żadnej nazwy użytkownika, main, nie skarżył się. Pamiętaj, że main nie jest funkcją zdefiniowaną przez system, jego funkcją zdefiniowaną przez użytkownika i rzeczą, od której program zaczyna się wykonywać, jest moduł główny, a nie main (). Ponownie main () jest wywoływana przez funkcję startową, która jest celowo wykonywana przez program ładujący. Następnie wszystkie twoje zmienne są inicjalizowane, a podczas inicjalizacji wyjście w ten sposób. Otóż ​​to. Program bez funkcji main () jest w porządku, ale nie jest standardowy.

Vikas.Ghode
źródło