Como um programa com uma variável global chamada principal em vez de uma função principal pode funcionar?

97

Considere o seguinte programa:

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

Usando g ++ 4.8.1 (mingw64) no sistema operacional Windows 7, o programa é compilado e executado corretamente, imprimindo:

C ++ é excelente!

para o console. mainparece ser uma variável global em vez de uma função; como este programa pode ser executado sem a função main()? Este código está em conformidade com o padrão C ++? O comportamento do programa está bem definido? Também usei a -pedantic-errorsopção, mas o programa ainda compila e executa.

Destruidor
fonte
11
@ πάνταῥεῖ: por que a etiqueta do advogado da língua é necessária?
Destruidor de
14
Observe que 195é o opcode da RETinstrução e que, na convenção de chamada C, o chamador limpa a pilha.
Brian,
2
@PravasiMeet "então como este programa é executado" - você não acha que o código de inicialização para uma variável deve ser executado (mesmo sem a main()função? Na verdade, eles não estão relacionados.)
O Croissant Paramagnético
4
Estou entre aqueles que descobriram que o programa não funciona exatamente como está (Linux de 64 bits, g ++ 5.1 / clang 3.6). Posso retificar isso, no entanto, alterando-o para int main = ( std::cout << "C++ is excellent!\n", exit(0),1 );(e incluindo <cstdlib>), embora o programa permaneça legalmente mal formado.
Mike Kinghan,
11
@Brian Você deve mencionar a arquitetura ao fazer declarações como essa. Todo o mundo não é um VAX. Ou x86. Como queiras.
dmckee --- ex-moderador gatinho

Respostas:

84

Antes de entrar no cerne da questão sobre o que está acontecendo, é importante apontar que o programa está malformado de acordo com relatório de defeito 1886: Language linkage for main () :

[...] Um programa que declara uma variável main no escopo global ou que declara o nome main com a ligação da linguagem C (em qualquer namespace) está mal formado. [...]

As versões mais recentes do clang e do gcc tornam isso um erro e o programa não compila ( consulte o exemplo ao vivo do gcc ):

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

Então, por que não havia diagnóstico nas versões mais antigas do gcc e do clang? Esse relatório de defeito nem tinha uma proposta de resolução até o final de 2014 e, portanto, esse caso só foi explicitamente malformado muito recentemente, o que requer um diagnóstico.

Antes disso, parece que isso seria um comportamento indefinido, uma vez que estão a violar um deve exigência do projeto de C ++ padrão da seção 3.6.1 [basic.start.main] :

Um programa deve conter uma função global chamada main, que é o início designado do programa. [...]

O comportamento indefinido é imprevisível e não requer um diagnóstico. A inconsistência que vemos ao reproduzir o comportamento é um comportamento indefinido típico.

Então, o que o código está realmente fazendo e por que, em alguns casos, ele produz resultados? Vamos ver o que temos:

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

Temos mainque é um int declarado no namespace global e está sendo inicializado, a variável tem duração de armazenamento estático. É definido pela implementação se a inicialização ocorrerá antes que uma tentativa de chamada mainseja feita, mas parece que o gcc faz isso antes de chamar main.

O código usa o operador vírgula , o operando esquerdo é uma expressão de valor descartada e é usado aqui apenas para o efeito colateral da chamada std::cout. O resultado do operador vírgula é o operando correto que, neste caso, é o prvalue 195atribuído à variável main.

Podemos ver que sergej aponta a montagem gerada mostra que couté chamada durante a inicialização estática. Embora o ponto mais interessante para discussão, consulte a sessão do godbolt ao vivo seria este:

main:
.zero   4

e o subsequente:

movl    $195, main(%rip)

O cenário provável é que o programa salte para o símbolo mainesperando que o código válido esteja lá e, em alguns casos, apresentará falha de seg . Portanto, se for esse o caso, esperaríamos que armazenar código de máquina válido na variável mainpudesse levar a um programa viável , supondo que estejamos localizados em um segmento que permite a execução do código. Podemos ver que esta entrada do IOCCC de 1984 faz exatamente isso .

Parece que podemos fazer o gcc fazer isso em C usando ( veja ao vivo ):

const int main = 195 ;

Ele falha se a variável mainnão for const presumivelmente porque não está localizada em um local executável, Hat Dica para este comentário aqui que me deu essa idéia.

Veja também a resposta FUZxxl aqui para uma versão C específica desta pergunta.

Shafik Yaghmour
fonte
Por que a implementação também não está dando avisos. (Quando eu uso -Wall & -Wextra ainda não dá um único aviso). Por quê? O que você acha da resposta de @Mark B a essa pergunta?
Destrutor
IMHO, o compilador não deve dar um aviso porque mainnão é um identificador reservado (3.6.1 / 3). Neste caso, acho que o tratamento do VS2013 neste caso (veja a resposta de Francis Cugler) é mais correto no tratamento do que o gcc & clang.
cdmh
@PravasiMeet Eu atualizei minha resposta por que as versões anteriores do gcc não forneciam um diagnóstico.
Shafik Yaghmour
2
... e de fato, quando eu testo o programa do OP no Linux / x86-64, com g ++ 5.2 (que aceita o programa - acho que você não estava brincando sobre a "versão mais recente"), ele trava exatamente onde eu esperava seria.
zwol
1
@Walter Não acredito que sejam duplicatas, o primeiro está fazendo uma pergunta muito mais restrita. Há claramente um grupo de usuários do SO que tem uma visão mais reducionista das duplicatas, o que não faz muito sentido para mim, pois poderíamos resumir a maioria das perguntas do SO a alguma versão de perguntas mais antigas, mas o SO não seria muito útil.
Shafik Yaghmour
20

De 3.6.1 / 1:

Um programa deve conter uma função global chamada main, que é o início designado do programa. É a implementação definida se um programa em um ambiente independente é necessário para definir uma função principal.

A partir disso, parece que g ++ permite que um programa (presumivelmente como a cláusula "independente") sem uma função principal.

Então, de 3.6.1 / 3:

A função main não deve ser usada (3.2) dentro de um programa. A ligação (3.5) de principal é definida pela implementação. Um programa que declara main inline ou estático está malformado. O nome principal não é reservado de outra forma.

Então aqui nós aprendemos que é perfeitamente normal ter uma variável inteira chamada main .

Finalmente, se você está se perguntando por que a saída é impressa, a inicialização do int mainusa o operador vírgula para executar coutna inicialização estática e então fornece um valor integral real para fazer a inicialização.

Mark B
fonte
7
É interessante notar que a vinculação falha se você renomear mainpara outra coisa: (.text+0x20): undefined reference to principal '`
Fred Larson
1
Você não precisa especificar para o gcc que seu programa é independente?
Shafik Yaghmour
9

O gcc 4.8.1 gera o seguinte conjunto 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

Observe que couté chamado durante a inicialização, não na mainfunção!

.zero 4declara 4 bytes (inicializados em 0) começando no local main, onde mainé o nome da variável [!] .

O mainsímbolo é interpretado como o início do programa. O comportamento depende da plataforma.

Sergej
fonte
1
Observe, como Brian aponta, 195 é o opcode para retalgumas arquiteturas. Portanto, dizer zero instruções pode não ser preciso.
Shafik Yaghmour
@ShafikYaghmour Obrigado pelo seu comentário, você está certo. Eu me confundi com as diretivas do montador.
sergej
8

Esse é um programa malformado. Ele trava no meu ambiente de teste, cygwin64 / g ++ 4.9.3.

Do padrão:

3.6.1 Função principal [basic.start.main]

1 Um programa deve conter uma função global chamada main, que é o início designado do programa.

R Sahu
fonte
Acho que antes do relatório de defeito que citei, esse era um comportamento simplesmente indefinido.
Shafik Yaghmour
@ShafikYaghmour, é que o princípio geral a ser aplicado em todos os lugares onde os usos padrão deve ?
R Sahu
Eu quero dizer que sim, mas não vejo uma boa descrição da diferença. Pelo que posso dizer com essa discussão , NDR malformado e comportamento indefinido são provavelmente sinônimos, pois nenhum deles requer um diagnóstico. Isso pareceria implicar malformado e UB são distintos, mas não certos.
Shafik Yaghmour
3
A seção 4 do C99 ("Conformidade") torna isso inequívoco: "Se um requisito 'deve' ou 'não deve' que aparece fora de uma restrição for violado, o comportamento é indefinido." Não consigo encontrar palavras equivalentes em C ++ 98 ou C ++ 11, mas suspeito fortemente que o comitê pretendia que existisse. (Os comitês C e C ++ realmente precisam sentar e resolver todas as diferenças terminológicas entre os dois padrões.)
zwol
7

Acredito que isso funcione porque o compilador não sabe que está compilando a main()função, por isso compila um inteiro global com efeitos colaterais de atribuição.

O formato do objeto em que esta unidade de tradução é compilada não é capaz de diferenciar entre um símbolo de função e um símbolo de variável .

Assim, o vinculador felizmente se vincula ao símbolo principal (variável) e o trata como uma chamada de função. Mas não até o sistema de tempo de execução execute o código de inicialização da variável global.

Quando executei a amostra, ela imprimiu, mas causou uma falha de seg . Suponho que foi quando o sistema de tempo de execução tentou executar uma variável int como se fosse uma função .

Galik
fonte
4

Eu tentei isso em um sistema operacional Win7 de 64 bits usando VS2013 e ele compila corretamente, mas quando tento construir o aplicativo, recebo esta mensagem na janela de saída.

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
fonte
2
FWIW, isso é um erro do vinculador, não uma mensagem do depurador. A compilação foi bem-sucedida, mas o vinculador não conseguiu encontrar uma função main()porque é uma variável do tipoint
cdmh
Obrigado pela resposta, vou reformular minha resposta inicial para refletir isso.
Francis Cugler
-1

Você está fazendo um trabalho complicado aqui. Como principal (de alguma forma) pode ser declarado inteiro. Você usou o operador de lista para imprimir a mensagem e atribuir 195 a ela. Como disse alguém abaixo, que não é confortável com C ++, é verdade. Mas como o compilador não encontrou nenhum nome definido pelo usuário, main, ele não reclamou. Lembre-se de que main não é uma função definida pelo sistema, sua função definida pelo usuário e a coisa a partir da qual o programa começa a ser executado é o Módulo Principal, não main (), especificamente. Novamente, main () é chamado pela função de inicialização que é executada intencionalmente pelo carregador. Então, todas as suas variáveis ​​são inicializadas e, ao inicializar, a saída é assim. É isso aí. Programa sem main () está ok, mas não é padrão.

Vikas.Ghode
fonte