Wie kann ein Programm mit einer globalen Variablen namens main anstelle einer Hauptfunktion funktionieren?

97

Betrachten Sie folgendes Programm:

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

Unter Verwendung von g ++ 4.8.1 (mingw64) unter Windows 7 kompiliert das Programm und läuft einwandfrei. Es druckt:

C ++ ist ausgezeichnet!

zur Konsole. mainscheint eher eine globale Variable als eine Funktion zu sein; Wie kann dieses Programm ohne die Funktion ausgeführt werden main()? Entspricht dieser Code dem C ++ - Standard? Ist das Verhalten des Programms gut definiert? Ich habe die -pedantic-errorsOption auch verwendet, aber das Programm wird immer noch kompiliert und ausgeführt.

Zerstörer
quelle
11
@ πάνταῥεῖ: Warum ist ein Tag für einen Sprachanwalt erforderlich?
Zerstörer
14
Beachten Sie, dass dies 195der Opcode für die RETAnweisung ist und dass der Aufrufer in der C-Aufrufkonvention den Stapel löscht.
Brian
2
@PravasiMeet "dann, wie dieses Programm ausgeführt wird" - denken Sie nicht, dass der Initialisierungscode für eine Variable ausgeführt werden sollte (auch ohne die main()Funktion? Tatsächlich sind sie völlig unabhängig.)
The Paramagnetic Croissant
4
Ich gehöre zu denen, die festgestellt haben, dass das Programm unverändert bleibt (64-Bit-Linux, g ++ 5.1 / clang 3.6). Ich kann dies jedoch korrigieren, indem ich es ändere int main = ( std::cout << "C++ is excellent!\n", exit(0),1 );(und einbeziehe <cstdlib>), obwohl das Programm rechtlich schlecht gestaltet ist.
Mike Kinghan
11
@Brian Sie sollten die Architektur erwähnen, wenn Sie solche Aussagen machen. Die ganze Welt ist keine VAX. Oder x86. Oder Wasauchimmer.
dmckee --- Ex-Moderator Kätzchen

Antworten:

84

Bevor wir auf die Frage eingehen, was los ist, ist es wichtig darauf hinzuweisen, dass das Programm gemäß Fehlerbericht 1886: Sprachverknüpfung für main () schlecht geformt ist :

[...] Ein Programm, das eine Variable main im globalen Bereich deklariert oder den Namen main mit C-Sprachverknüpfung (in einem beliebigen Namespace) deklariert, ist fehlerhaft. [...]

Die neuesten Versionen von clang und gcc machen dies zu einem Fehler und das Programm wird nicht kompiliert ( siehe gcc live-Beispiel ):

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

Warum gab es in älteren Versionen von gcc und clang keine Diagnose? Dieser Fehlerbericht hatte erst Ende 2014 einen Lösungsvorschlag, so dass dieser Fall erst vor kurzem explizit falsch formuliert wurde, was eine Diagnose erfordert.

Vor dieser Zeit scheint es so nicht definiertes Verhalten wäre , da wir ein verstoßen wird Anforderung des Entwurfs C ++ Standard aus dem Abschnitt 3.6.1 [basic.start.main] :

Ein Programm muss eine globale Funktion namens main enthalten, die den festgelegten Start des Programms darstellt. [...]

Undefiniertes Verhalten ist unvorhersehbar und erfordert keine Diagnose. Die Inkonsistenz, die wir bei der Reproduktion des Verhaltens sehen, ist ein typisches undefiniertes Verhalten.

Was macht der Code eigentlich und warum führt er in einigen Fällen zu Ergebnissen? Mal sehen, was wir haben:

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

Wir haben mainein int , das im globalen Namespace deklariert ist und initialisiert wird. Die Variable hat eine statische Speicherdauer. Es ist die Implementierung definiert, ob die Initialisierung stattfinden soll, bevor ein Aufruf mainversucht wird, aber es scheint, dass gcc dies vor dem Aufruf tut main.

Der Code verwendet den Komma-Operator , der linke Operand ist ein Ausdruck für verworfene Werte und wird hier ausschließlich für den Nebeneffekt des Aufrufs verwendet std::cout. Das Ergebnis des Kommaoperators ist der richtige Operand, in diesem Fall der Wert, 195der der Variablen zugewiesen ist main.

Wir können sehen, dass sergej auf die generierten Assemblyshows hinweist, die coutwährend der statischen Initialisierung aufgerufen werden. Obwohl der interessantere Diskussionspunkt in der Live-Godbolt-Sitzung folgender wäre:

main:
.zero   4

und die folgenden:

movl    $195, main(%rip)

Das wahrscheinliche Szenario ist, dass das Programm zu dem Symbol springt und mainerwartet, dass gültiger Code vorhanden ist, und in einigen Fällen einen Seg-Fehler aufweist . Wenn dies der Fall ist, würden wir erwarten, dass das Speichern von gültigem Maschinencode in der Variablen mainzu einem funktionsfähigen Programm führen könnte , vorausgesetzt, wir befinden uns in einem Segment, das die Codeausführung ermöglicht. Wir können sehen, dass dieser IOCCC-Eintrag von 1984 genau das tut .

Es scheint, dass wir gcc dazu bringen können, dies in C zu tun, indem wir ( live sehen ):

const int main = 195 ;

Es gibt Fehler, wenn die Variable mainvermutlich nicht const ist, weil sie sich nicht an einem ausführbaren Speicherort befindet. Hat Tipp zu diesem Kommentar hier, der mir diese Idee gegeben hat.

Siehe auch FUZxxl Antwort hier auf eine C-spezifische Version dieser Frage.

Shafik Yaghmour
quelle
Warum die Implementierung auch keine Warnungen gibt. (Wenn ich -Wall & -Wextra verwende, gibt es immer noch keine einzige Warnung). Warum? Was denkst du über die Antwort von @Mark B auf diese Frage?
Zerstörer
IMHO sollte der Compiler keine Warnung geben, da maines sich nicht um eine reservierte Kennung handelt (3.6.1 / 3). In diesem Fall denke ich, dass die Behandlung dieses Falls durch VS2013 (siehe Francis Cuglers Antwort) in der Behandlung korrekter ist als bei gcc & clang.
CDMH
@PravasiMeet Ich habe meine Antwort dahingehend aktualisiert, warum frühere Versionen von gcc keine Diagnose gaben.
Shafik Yaghmour
2
... und tatsächlich, wenn ich das OP-Programm unter Linux / x86-64 mit g ++ 5.2 teste (das das Programm akzeptiert - ich glaube, Sie haben sich nicht über die "neueste Version" lustig gemacht), stürzt es genau dort ab, wo ich es erwartet hatte würde.
zwol
1
@Walter Ich glaube nicht, dass dies Duplikate sind, die erstere stellen viel engere Fragen. Es gibt eindeutig eine Gruppe von SO-Benutzern, die eine reduktionistischere Sicht auf Duplikate haben, was für mich nicht sehr sinnvoll ist, da wir die meisten SO-Fragen auf eine Version älterer Fragen reduzieren könnten, aber SO wäre SO nicht sehr nützlich.
Shafik Yaghmour
20

Ab 3.6.1 / 1:

Ein Programm muss eine globale Funktion namens main enthalten, die den festgelegten Start des Programms darstellt. In der Implementierung wird definiert, ob ein Programm in einer freistehenden Umgebung erforderlich ist, um eine Hauptfunktion zu definieren.

Aus diesem Grund sieht es so aus, als würde g ++ ein Programm (vermutlich als "freistehende" Klausel) ohne Hauptfunktion zulassen.

Dann ab 3.6.1 / 3:

Die Funktion main darf innerhalb eines Programms nicht verwendet werden (3.2). Die Verknüpfung (3.5) von main ist implementierungsdefiniert. Ein Programm, das main als inline oder statisch deklariert, ist fehlerhaft. Der Name main ist nicht anderweitig reserviert.

Hier erfahren wir also, dass es vollkommen in Ordnung ist, eine ganzzahlige Variable mit dem Namen zu haben main.

Wenn Sie sich schließlich fragen, warum die Ausgabe gedruckt wird, verwendet die Initialisierung von den int mainKommaoperator, um coutbei statischer Init auszuführen, und gibt dann einen tatsächlichen Integralwert für die Initialisierung an.

Mark B.
quelle
7
Es ist interessant festzustellen, dass die Verknüpfung fehlschlägt, wenn Sie mainin etwas anderes umbenennen : (.text+0x20): undefined reference to main '`
Fred Larson
1
Müssen Sie gcc nicht angeben, dass Ihr Programm freistehend ist?
Shafik Yaghmour
9

gcc 4.8.1 generiert die folgende x86-Assembly:

.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

Beachten Sie, dass dies coutwährend der Initialisierung aufgerufen wird, nicht in der mainFunktion!

.zero 4deklariert 4 (0-initialisierte) Bytes ab Position main, wobei mainder Name der Variablen [!] ist .

Das mainSymbol wird als Programmstart interpretiert. Das Verhalten hängt von der Plattform ab.

sergej
quelle
1
Beachten Sie, wie Brian betont, dass dies 195 der Opcode für reteinige Architekturen ist. Das Sagen von Null-Anweisungen ist daher möglicherweise nicht korrekt.
Shafik Yaghmour
@ShafikYaghmour Danke für deinen Kommentar, du hast recht. Ich habe die Assembler-Anweisungen durcheinander gebracht.
Sergej
8

Das ist ein schlecht geformtes Programm. Es stürzt in meiner Testumgebung cygwin64 / g ++ 4.9.3 ab.

Aus dem Standard:

3.6.1 Hauptfunktion [basic.start.main]

1 Ein Programm muss eine globale Funktion namens main enthalten, die den festgelegten Start des Programms darstellt.

R Sahu
quelle
Ich denke, vor dem von mir zitierten Fehlerbericht war dies einfach nur undefiniertes Verhalten.
Shafik Yaghmour
@ShafikYaghmour, Ist das das allgemeine Prinzip, das an allen Stellen anzuwenden ist, an denen der Standard verwendet werden soll ?
R Sahu
Ich möchte ja sagen, aber ich habe keine gute Beschreibung des Unterschieds gesehen. Nach dem, was ich aus dieser Diskussion entnehmen kann , sind schlecht geformter NDR und undefiniertes Verhalten wahrscheinlich synonym, da für beide keine Diagnose erforderlich ist. Dies scheint schlecht geformt zu sein und UB sind verschieden, aber nicht sicher.
Shafik Yaghmour
3
C99 Abschnitt 4 ("Konformität") macht dies eindeutig: "Wenn eine Anforderung" soll "oder" soll nicht ", die außerhalb einer Einschränkung erscheint, verletzt wird, ist das Verhalten undefiniert." Ich kann in C ++ 98 oder C ++ 11 keine äquivalente Formulierung finden, aber ich vermute sehr, dass das Komitee es so gemeint hat. (Die C- und C ++ - Komitees müssen sich wirklich hinsetzen und alle terminologischen Unterschiede zwischen den beiden Standards
ausbügeln
7

Der Grund, warum ich glaube, dass dies funktioniert, ist, dass der Compiler nicht weiß, dass er die main()Funktion kompiliert, sodass er eine globale Ganzzahl mit Zuweisungsnebeneffekten kompiliert.

Das Objektformat, in das diese Übersetzungseinheit kompiliert wird, kann nicht zwischen einem Funktionssymbol und einem Variablensymbol unterscheiden .

So ist der Linker verbindet glücklich mit dem (variabel) Hauptsymbol und behandelt es wie ein Funktionsaufruf. Aber erst, wenn das Laufzeitsystem den globalen Variableninitialisierungscode ausgeführt hat.

Als ich das Beispiel laufen ließ, wurde es ausgedruckt, aber dann verursachte es einen Seg-Fehler . Ich gehe davon aus, dass das Laufzeitsystem versucht hat, eine int-Variable so auszuführen , als wäre es eine Funktion .

Galik
quelle
4

Ich habe dies auf einem Win7 64-Bit-Betriebssystem mit VS2013 versucht und es wird korrekt kompiliert, aber wenn ich versuche, die Anwendung zu erstellen, erhalte ich diese Meldung aus dem Ausgabefenster.

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
quelle
2
FWIW, das ist ein Linkerfehler, keine Nachricht vom Debugger. Die Zusammenstellung gelungen, aber die Linke konnte keine Funktion finden , main()da es sich um eine Variable vom Typ istint
CDMH
Vielen Dank für die Antwort. Ich werde meine erste Antwort umformulieren, um dies widerzuspiegeln.
Francis Cugler
-1

Sie machen hier knifflige Arbeit. Als Haupt (irgendwie) könnte als Ganzzahl deklariert werden. Sie haben den Listenoperator verwendet, um die Nachricht zu drucken und ihr dann 195 zuzuweisen. Wie von jemandem unten gesagt, ist es wahr, dass es mit C ++ nicht tröstet. Da der Compiler jedoch keinen benutzerdefinierten Namen gefunden hat, hat er sich nicht beschwert. Denken Sie daran, dass main keine systemdefinierte Funktion ist. Die benutzerdefinierte Funktion und das Element, von dem aus das Programm gestartet wird, ist das Hauptmodul, nicht main (). Wieder wird main () von der Startfunktion aufgerufen, die vom Loader absichtlich ausgeführt wird. Dann werden alle Ihre Variablen initialisiert und während der Initialisierung wird die Ausgabe so ausgeführt. Das ist es. Programm ohne main () ist in Ordnung, aber nicht Standard.

Vikas.Ghode
quelle