Ordem de execução C ++ no encadeamento de métodos

108

A saída deste programa:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

É:

method 1
method 2:0

Por que nunão é 1 quando meth2()começa?

Moises Viñas
fonte
41
@MartinBonner: Embora eu saiba a resposta, eu não a chamaria de "óbvia" em nenhum sentido da palavra e, mesmo se fosse, não seria um motivo decente para um voto negativo. Decepcionante!
Lightness Races in Orbit de
4
Isso é o que você obtém quando modifica seus argumentos. Funções que modificam seus argumentos são mais difíceis de ler, seus efeitos são inesperados para o próximo programador trabalhar no código e eles levam a surpresas como esta. Eu sugiro fortemente evitar a modificação de quaisquer parâmetros, exceto o invocante. Modificar o invocante não seria um problema aqui, porque o segundo método é chamado no resultado do primeiro, então os efeitos são ordenados nele. Ainda existem alguns casos em que eles não seriam.
Jan Hudec
@JanHudec É exatamente por isso que a programação funcional dá tanta ênfase à pureza das funções.
Pharap de
2
Por exemplo, uma convenção de chamada baseada em pilha provavelmente prefere empurrar nu, &nue cpara a pilha nessa ordem, então invocar meth1, empurrar o resultado para a pilha e então invocar meth2, enquanto uma convenção de chamada baseada em registrador gostaria de carregar ce &nuem registradores, invocar meth1, carregar nuem um registrador e então invocar meth2.
Neil

Respostas:

66

Porque a ordem de avaliação não é especificada.

Você está vendo nuem mainser avaliado 0antes mesmo de meth1ser chamado. Este é o problema do encadeamento. Eu aconselho não fazer isso.

Basta fazer um programa agradável, simples, claro, fácil de ler e entender:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
Lightness Races in Orbit
fonte
14
Existe a possibilidade de que uma proposta para esclarecer a ordem de avaliação em alguns casos , que corrige este problema, chegue para C ++ 17
Revolver_Ocelot
7
Eu gosto de encadeamento de métodos (por exemplo, <<para saída e "construtores de objeto" para objetos complexos com muitos argumentos para os construtores - mas combina muito mal com argumentos de saída.
Martin Bonner apoia Monica
34
Eu entendo isso direito? a ordem de avaliação de meth1e meth2é definida, mas a avaliação do parâmetro para meth2pode acontecer antes de meth1ser chamada ...?
Roddy
7
O encadeamento de métodos é adequado, desde que os métodos sejam sensíveis e modifiquem apenas o invocante (para o qual os efeitos são bem ordenados, porque o segundo método é chamado no resultado do primeiro).
Jan Hudec
4
É lógico, quando você pensa sobre isso. Funciona comometh2(meth1(c, &nu), nu)
BartekChom
29

Acho que esta parte do projeto de norma em relação à ordem de avaliação é relevante:

1.9 Execução do Programa

...

  1. Exceto onde indicado, as avaliações de operandos de operadores individuais e de subexpressões de expressões individuais não são sequenciadas. Os cálculos do valor dos operandos de um operador são sequenciados antes do cálculo do valor do resultado do operador. Se um efeito colateral em um objeto escalar não for sequenciado em relação a outro efeito colateral no mesmo objeto escalar ou um cálculo de valor usando o valor do mesmo objeto escalar, e eles não forem potencialmente simultâneos, o comportamento é indefinido

e também:

5.2.2 Chamada de função

...

  1. [Nota: As avaliações da expressão pós-fixada e dos argumentos são todas sem seqüência em relação umas às outras. Todos os efeitos colaterais das avaliações de argumento são sequenciados antes que a função seja inserida - nota final]

Portanto, para sua linha c.meth1(&nu).meth2(nu);, considere o que está acontecendo no operador em termos do operador de chamada de função para a chamada final para meth2, para que possamos ver claramente a divisão na expressão pós-fixada e no argumento nu:

operator()(c.meth1(&nu).meth2, nu);

As avaliações da expressão pós-fixada e do argumento para a chamada de função final (ou seja, a expressão pós-fixada c.meth1(&nu).meth2e nu) não são sequenciadas entre si de acordo com a regra de chamada de função acima. Portanto, o efeito colateral do cálculo da expressão pós-fixada no objeto escalar não aré sequenciado em relação à avaliação do argumento nuanterior à meth2chamada da função. Pela regra de execução do programa acima, este é um comportamento indefinido.

Em outras palavras, não há nenhum requisito para o compilador avaliar o nuargumento da meth2chamada após a meth1chamada - ele é livre para assumir que nenhum efeito colateral meth1afeta a nuavaliação.

O código de montagem produzido acima contém a seguinte sequência na mainfunção:

  1. A variável nué alocada na pilha e inicializada com 0.
  2. Um cadastro ( ebxno meu caso) recebe uma cópia do valor denu
  3. Os endereços de nue csão carregados nos registros de parâmetros
  4. meth1 é chamado
  5. O registo valor de retorno eo valor previamente armazenado em cache de nuno ebxregisto são carregados em registradores de parâmetros
  6. meth2 é chamado

De forma crítica, na etapa 5 acima, o compilador permite que o valor em cache da nuetapa 2 seja reutilizado na chamada de função para meth2. Aqui, ele desconsidera a possibilidade de que nupode ter sido alterado pela chamada para meth1- 'comportamento indefinido' em ação.

NOTA: Esta resposta mudou substancialmente de sua forma original. Minha explicação inicial em termos de efeitos colaterais do cálculo do operando não sendo sequenciado antes da chamada da função final estava incorreta, porque eles estão. O problema é o fato de que o cálculo dos próprios operandos é sequenciado de forma indeterminada.

Smeeheey
fonte
2
Isto está errado. As chamadas de função são sequenciadas indeterminadamente com outras avaliações na função de chamada (a menos que uma restrição sequenciada antes seja imposta); eles não se intercalam.
TC
1
@TC - Eu nunca disse nada sobre as chamadas de função serem intercaladas. Eu apenas me referi aos efeitos colaterais dos operadores. Se você olhar para o código assembly produzido por acima, verá que meth1é executado antes meth2, mas o parâmetro para meth2é um valor nuarmazenado em cache em um registro antes da chamada para meth1- ou seja, o compilador ignorou os efeitos colaterais potenciais, que é consistente com minha resposta.
Smeeheey
1
Você está afirmando exatamente que - "seu efeito colateral (isto é, definir o valor de ar) não tem garantia de ser sequenciado antes da chamada". A avaliação da expressão pós-fixada em uma chamada de função (que é c.meth1(&nu).meth2) e a avaliação do argumento para essa chamada ( nu) geralmente não são sequenciados, mas 1) seus efeitos colaterais são todos sequenciados antes da entrada em meth2e 2) já que c.meth1(&nu)é uma chamada de função , é sequenciado indeterminadamente com a avaliação de nu. Por dentro meth2, se de alguma forma obtivesse um ponteiro para a variável em main, sempre veria 1.
TC
2
"No entanto, o efeito colateral do cálculo dos operandos (isto é, definir o valor de ar) não tem garantia de ser sequenciado antes de qualquer coisa (conforme 2) acima)." É absolutamente garantido que será sequenciado antes da chamada para meth2, conforme observado no item 3 da página cppreference que você está citando (que você também deixou de citar corretamente).
TC
1
Você entendeu algo errado e piorou. Não há absolutamente nenhum comportamento indefinido aqui. Continue lendo [intro.execution] / 15, após o exemplo.
TC
9

No padrão C ++ de 1998, Seção 5, parágrafo 4

Exceto onde indicado, a ordem de avaliação dos operandos de operadores individuais e subexpressões de expressões individuais, e a ordem em que os efeitos colaterais ocorrem, não é especificada. Entre o ponto de sequência anterior e o próximo, um objeto escalar deve obrigatoriamente ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão. Além disso, o valor anterior deve ser acessado apenas para determinar o valor a ser armazenado. Os requisitos deste parágrafo devem ser atendidos para cada ordenação permitida das subexpressões de uma expressão completa; caso contrário, o comportamento é indefinido.

(Omiti uma referência à nota de rodapé # 53 que não é relevante para esta questão).

Essencialmente, &nudeve ser avaliado antes da chamada c1::meth1()e nudeve ser avaliado antes da chamada c1::meth2(). Não há, entretanto, nenhum requisito que deve nuser avaliado antes &nu(por exemplo, é permitido que nuseja avaliado primeiro, então &nu, e então c1::meth1()seja chamado - o que pode ser o que seu compilador está fazendo). A expressão *ar = 1in, c1::meth1()portanto, não tem garantia de ser avaliada antes de nuin main()ser avaliada, a fim de ser passada para c1::meth2().

Os padrões C ++ posteriores (que não tenho no PC que estou usando hoje à noite) têm essencialmente a mesma cláusula.

Peter
fonte
7

Eu acho que ao compilar, antes que as funções meth1 e meth2 sejam realmente chamadas, os parâmetros foram passados ​​para eles. Quero dizer, quando você usa "c.meth1 (& nu) .meth2 (nu);" o valor nu = 0 foi passado para meth2, então não importa se "nu" foi alterado posteriormente.

você pode tentar isto:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

obterá a resposta que você deseja

Saintor de camiseta
fonte