Этот код хорошо определен? - PullRequest
27 голосов
/ 17 января 2011

Этот код взят из обсуждения, продолжающегося здесь .

someInstance.Fun(++k).Gun(10).Sun(k).Tun();

Правильно ли определен этот код? ++k в Fun () оценивается до k в Sun ()?

Что если k это пользовательский тип, а не встроенный? И чем отличается порядок вышеупомянутых функций от этого:

eat(++k);drink(10);sleep(k);

Насколько мне известно, в обеих ситуациях существует точка последовательности после каждого вызова функции . Если так, то почему первый случай также не может быть четко определен, как второй?

Раздел 1.9.17 стандарта C ++ ISO говорит это о точках последовательности и оценке функций:

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

Ответы [ 6 ]

22 голосов
/ 17 января 2011

Я думаю, что если вы прочитаете точно , что говорит эта стандартная цитата, первый случай не будет четко определен:

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

Это говорит нам не о том, что «единственное, что может произойти после того, как аргументы для функции были оценены, это фактический вызов функции», а просто о том, что в какой-то момент после завершения вычисления аргументов существует точка последовательности и перед вызовом функции.

Но если представить себе такой случай:

foo(X).bar(Y)

единственная гарантия, которую это дает нам, заключается в том, что:

  • X оценивается до вызова foo, а
  • Y оценивается до вызова bar.

Но такой порядок все еще возможен:

  1. оценить X
  2. evalute Y
  3. (точка последовательности, отделяющая X от foo вызова)
  4. звонок foo
  5. (точка последовательности, отделяющая Y от bar вызова)
  6. вызов bar

и, конечно, мы также можем поменять местами первые два элемента, оценивая Y до X. Почему бы и нет? Стандарт требует только того, чтобы аргументы функции были полностью оценены перед первым оператором тела функции, и приведенные выше последовательности удовлетворяют этому требованию.

Это моя интерпретация, по крайней мере. Кажется, не говорится, что между оценкой аргумента и телом функции может произойти ничего - просто эти два разделены точкой последовательности.

12 голосов
/ 17 января 2011

Это зависит от того, как определяется Sun. Следующее четко определено

struct A {
  A &Fun(int);
  A &Gun(int);
  A &Sun(int&);
  A &Tun();
};

void g() {
  A someInstance;
  int k = 0;
  someInstance.Fun(++k).Gun(10).Sun(k).Tun();
}

Если вы измените тип параметра с Sun на int, он станет неопределенным. Давайте нарисуем дерево версии, взяв int.

                     <eval body of Fun>
                             |
                             % // pre-call sequence point
                             | 
 { S(increment, k) }  <-  E(++x) 
                             |     
                      E(Fun(++k).Gun(10))
                             |
                      .------+-----.       .-- V(k)--%--<eval body of Sun>
                     /              \     /
                   E(Fun(++k).Gun(10).Sun(k))
                              |
                    .---------+---------. 
                   /                     \ 
                 E(Fun(++k).Gun(10).Sun(k).Tun())
                              |
                              % // full-expression sequence point

Как видно, у нас есть чтение k (обозначено V(k)) и побочный эффект для k (в самом верху), которые не разделены точкой последовательности: в этом выражении По отношению друг к другу подвыражение отсутствует точка последовательности. Самый нижний % обозначает точку последовательности полного выражения.

10 голосов
/ 17 января 2011

Это неопределенное поведение, потому что значение k изменяется и читается в одном и том же выражении, без промежуточной точки последовательности. Смотрите отличный длинный ответ на этот вопрос .

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

eat(++k);drink(10);sleep(k);

отличается тем, что ; является точкой последовательности, поэтому порядок оценки четко определен.

8 голосов
/ 17 января 2011

В качестве небольшого теста рассмотрим:

#include <iostream>

struct X
{
    const X& f(int n) const
    {
        std::cout << n << '\n';
        return *this;
    }
};

int main()
{
    int n = 1;

    X x;

    x.f(++n).f(++n).f(++n).f(++n);
}

Я запускаю это с gcc 3.4.6 и без оптимизации и получаю:

5
4
3
2

... с -O3 ...

2
3
4
5

Таким образом, либо в версии 3.4.6 была серьезная ошибка (в которую трудно поверить), либо последовательность не определена, как предложил Филипп Поттер.(GCC 4.1.1 с / без -O3 произведено 5, 5, 5, 5.)

РЕДАКТИРОВАТЬ - мое резюме обсуждения в комментариях ниже:

  • 3.4.6 действительновозможно, была ошибка (ну да)
  • много новых компиляторов производят 5/5/5/5 ... это определенное поведение?
    • , вероятно, нет, так как он соответствует всем побочным эффектам приращения, "действующим" перед выполнением любого из вызовов функций, что не является тем поведением, которое, как предполагалось здесь, может быть гарантировано Стандартом
  • это не очень хороший подход к исследованию требований Стандарта (особенно с более старым компилятором, таким как 3.4.6): согласен, но это полезная проверка работоспособности
1 голос
/ 17 января 2011

Я знаю, что поведение компиляторов не может ничего доказать, но я подумал, что было бы интересно проверить, что даст внутреннее представление компилятора (все же немного более высокий уровень, чем проверка сборки).

Я использовал онлайн-демонстрацию Clang / LLVM с этим кодом:

#include <stdio.h>
#include <stdlib.h>

struct X
{
  X const& f(int i) const
  {
    printf("%d\n", i);
    return *this;
  }
};

int main(int argc, char **argv) {
  int i = 0;
  X x;
  x.f(++i).f(++i).f(++i);         // line 16
}

И скомпилированный со стандартными оптимизациями (в режиме C ++), он дал:

/ tmp / webcompile / _13371_0.cc: в функции 'int main (int, char **)':
/tmp/webcompile/_13371_0.cc:16: предупреждение: операция над 'i' может быть неопределенной

что мне показалось интересным (предупредил ли об этом какой-либо другой компилятор? Comeau онлайн не сделал)


Помимо этого, он также создал следующее промежуточное представление (прокрутка вправо):

@.str = private constant [4 x i8] c"%d\0A\00", align 1 ; <[4 x i8]*> [#uses=1]

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind {
entry:
  %0 = tail call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([4 x i8]* @.str, i64 0, i64 0), i32 3) nounwind ; <i32> [#uses=0]
                                                                                                             ^^^^^
  %1 = tail call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([4 x i8]* @.str, i64 0, i64 0), i32 3) nounwind ; <i32> [#uses=0]
                                                                                                             ^^^^^
  %2 = tail call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([4 x i8]* @.str, i64 0, i64 0), i32 3) nounwind ; <i32> [#uses=0]
                                                                                                             ^^^^^
  ret i32 0
}

Очевидно, Clang ведет себя так же, как gcc 4.x.x, и сначала оценивает все аргументы перед выполнением любого вызова функции.

0 голосов
/ 17 января 2011

Второй случай, безусловно, четко определен. Строка токенов, заканчивающаяся точкой с запятой, является атомарным оператором в C ++. Каждый оператор анализируется, обрабатывается и завершается до начала следующего оператора.

...