Действительно ли законно использовать инкрементный оператор в вызове функции C++?

__malloc_hook вызывается сразу после ввода __libc_malloc в здесь :

void *
__libc_malloc (size_t bytes)
{
  mstate ar_ptr;
  void *victim;

  void *(*hook) (size_t, const void *)
    = atomic_forced_read (__malloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0));

, в то время как блокировка потока реализована в __libc_malloc 20 строками ниже, в здесь :

  if (SINGLE_THREAD_P)
    {
      victim = _int_malloc (&main_arena, bytes);
      assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
          &main_arena == arena_for_chunk (mem2chunk (victim)));
      return victim;
    }

  arena_get (ar_ptr, bytes);

arena_get строит арену для нитки или выбирает арену и блокирует арену, объявленную здесь .

Таким образом, при разработке собственного __malloc_hook разработчик несет ответственность за все, включая блокировку потоков (при необходимости).

43
задан Community 23 May 2017 в 10:31
поделиться

8 ответов

Quoth стандарт C++ 1.9.16:

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

, Таким образом, мне что этот код казалось бы:

foo(i++);

совершенно законно. Это будет увеличивать i и затем звонить foo с предыдущим значением i. Однако этот код:

foo(i++, i++);

урожаи неопределенное поведение, потому что в абзаце 1.9.16 также говорится:

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

59
ответ дан Michael Kristofik 26 November 2019 в 22:51
поделиться

Для построения ответ Kristo ,

foo(i++, i++);

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

int i = 1;
foo(i++, i++);

мог бы привести к вызову функции [1 110]

foo(2, 1);

или

foo(1, 2);

или даже

foo(1, 1);

Выполнение следующее для наблюдения то, что происходит на платформе:

#include <iostream>

using namespace std;

void foo(int a, int b)
{
    cout << "a: " << a << endl;
    cout << "b: " << b << endl;
}

int main()
{
    int i = 1;
    foo(i++, i++);
}

На моей машине я добираюсь

$ ./a.out
a: 2
b: 1

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

13
ответ дан Bill the Lizard 26 November 2019 в 22:51
поделиться

В стандарте говорится, что побочный эффект происходит перед вызовом, таким образом, код совпадает с:

std::list<item*>::iterator i_before = i;

i = i_before + 1;

items.erase(i_before);

вместо того, чтобы быть:

std::list<item*>::iterator i_before = i;

items.erase(i);

i = i_before + 1;

, Таким образом, это безопасно в этом случае, потому что list.erase () конкретно не делает недействительным итераторов кроме стертого того.

Тем не менее это - плохой стиль - функция стирания для всех контейнеров возвращает следующий итератор конкретно, таким образом, Вы не должны волноваться о лишении законной силы итераторов из-за перераспределения, таким образом, идиоматический код:

i = items.erase(i);

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

Вы также не заставили бы исходный код компилировать без предупреждений - необходимо будет записать

(void)items.erase(i++);

для предотвращения предупреждения о неиспользованном возврате, который был бы большой подсказкой, что Вы делаете что-то нечетное.

5
ответ дан Pete Kirkham 26 November 2019 в 22:51
поделиться

Это совершенно в порядке. Значение передало, будет значение "i" перед инкрементом.

3
ответ дан Himadri Choudhury 26 November 2019 в 22:51
поделиться

Основываться на ответе MarkusQ:;)

Или скорее комментарий счета к нему:

( Редактирование: Ай, комментария не стало снова... О, хорошо)

Они , позволил быть оцененным параллельно. Происходит ли это, на практике с технической точки зрения не важно.

Вы не должны распараллеливать параллелизм для этого для появления, хотя, просто оцените первый шаг обоих (примите значение i) перед вторым (увеличивают i). Совершенно законный, и некоторые компиляторы может считать это более эффективным, чем полностью оценка той я ++ прежде, чем запуститься на втором.

На самом деле, я ожидал бы, что это будет общей оптимизацией. Посмотрите на него из инструкции, планируя точку зрения. У Вас есть следующий, который необходимо оценить:

  1. Принимают значение меня для правильного аргумента
  2. , Инкремент i в правильном аргументе
  3. Принимает значение меня для левого аргумента
  4. Инкремент i в левом аргументе

, Но между левым и правым аргументом нет действительно никакой зависимости. Оценка аргумента происходит в неуказанном порядке и не должна быть сделана последовательно ни один (который является, почему новый () в аргументах функции обычно утечка памяти, даже когда обернуто в интеллектуальный указатель), Это также не определено, что происходит, когда Вы изменяете ту же переменную дважды в том же выражении. У нас действительно есть зависимость между 1 и 2, однако, и между 3 и 4. Итак, почему компилятор ожидал бы 2 для завершения прежде, чем вычислить 3? Это представляет добавленную задержку, и она возьмет еще дольше, чем необходимый, прежде чем 4 станет доступным. Принятие там является 1 задержкой цикла между каждым, потребуется 3 цикла от 1, завершено, пока результат 4 не готов, и мы можем вызвать функцию.

, Но если мы переупорядочиваем их и оцениваем в порядке 1, 3, 2, 4, мы можем сделать это в 2 циклах. 1 и 3 может быть запущен в том же цикле (или даже объединен в одну инструкцию, так как это - то же выражение), и в следующем, 2 и 4 может быть оценен. Весь современный ЦП может выполнить 3-4 инструкции на цикл, и хороший компилятор должен попытаться использовать это.

1
ответ дан jalf 26 November 2019 в 22:51
поделиться

Для построения тарифицируют ответ Ящерицы:

int i = 1;
foo(i++, i++);

мог бы также привести к вызову функции

foo(1, 1);

(подразумевать, что фактические данные оценены параллельно, и затем postops применяются).

- MarkusQ

-1
ответ дан MarkusQ 26 November 2019 в 22:51
поделиться

Sutter Гуру Недели № 55 (и соответствующая часть в "Более исключительном C++") обсуждает этот точный случай как пример.

, По его словам, это - совершенно допустимый код и на самом деле случай где, пытаясь преобразовать оператор в две строки:

items.erase(i);
i++;

делает не , производят код, который семантически эквивалентен исходному оператору.

0
ответ дан Boojum 26 November 2019 в 22:51
поделиться

++ Kristo!

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

Хорошо видеть, что стандарты улучшаются!


Однако я отчетливо не забываю использовать более старый (предварительный ANSI) компиляторы C где:

foo -> bar(i++) -> charlie(i++);

Не сделал то, что Вы думаете! Вместо этого это скомпилировало эквивалентный:

foo -> bar(i) -> charlie(i); ++i; ++i;

И это поведение было зависящим от реализации компилятором. (Делающий портирование забавы.)


Достаточно легко протестировать и проверить, что современные компиляторы теперь ведут себя правильно:

#define SHOW(S,X)  cout << S << ":  " # X " = " << (X) << endl

struct Foo
{
  Foo & bar(const char * theString, int theI)
    { SHOW(theString, theI);   return *this; }
};

int
main()
{
  Foo f;
  int i = 0;
  f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i);
  SHOW("END ",i);
}


Ответ на комментарий в потоке...

... И основываясь в значительной степени на ОБЩИХ ответах... (Спасибо парни!)


Я думаю, что мы должны обстоятельно объяснить это немного лучше:

Данный:

baz(g(),h());

Затем мы не знаем, будет ли g () вызван прежде или после h (). Это является "неуказанным".

Но мы действительно знаем, что оба g () и h () будут вызваны прежде baz ().

Данный:

bar(i++,i++);

Снова, мы не знаем, который я ++ буду оценен сначала, и возможно даже, буду ли я увеличен несколько раз, прежде чем панель () называют. Результаты не определены! (Данный i=0, это могло быть панелью (0,0) или панелью (1,0) или панелью (0,1) или что-то действительно странное!)


Данный:

foo(i++);

Мы теперь знаем, что я буду увеличен, прежде чем нечто () вызывается. Поскольку Kristo, на которые указывают из стандарта C++, разделяют 1.9.16:

Когда вызывание функции (является ли функция подставляемой), каждое вычисление значения и побочный эффект, связанный с любым выражением аргумента, или с постфиксным выражением, определяющим вызванную функцию, упорядочивается перед выполнением каждого выражения или оператора в теле вызванной функции. [Отметьте: вычисления Значения и побочные эффекты, связанные с различными выражениями аргумента, не упорядочиваются. - заканчивают примечание]

Хотя я думаю, разделяют 5.2.6, говорит это лучше:

Значение постфикса ++ выражение является значением своего операнда. [Отметьте: полученное значение является копией исходного значения - примечание конца], операнд должен быть модифицируемым lvalue. Тип операнда должен быть арифметическим типом или указателем на полный эффективный тип объекта. Значение объекта операнда изменяется путем добавления 1 к нему, если объект не имеет типа bool, в этом случае это имеет значение true. [Отметьте: это использование удерживается от использования, см. Приложение D. - примечание конца] вычисление значения ++ выражение упорядочивается перед модификацией объекта операнда. Относительно неопределенно упорядоченного вызова функции операция постфикса ++ является единственной оценкой. [Отметьте: Поэтому вызов функции не должен вмешиваться между lvalue-to-rvalue преобразованием и побочным эффектом, связанным ни с каким единственным постфиксом ++ оператор. - примечание конца] результатом является rvalue. Тип результата является дисквалифицированной условной ценой версией типа операнда. См. также 5.7 и 5.17.

Стандарт, в разделе 1.9.16, также перечисляет (как часть своих примеров):

i = 7, i++, i++;    // i becomes 9 (valid)
f(i = -1, i = -1);  // the behavior is undefined

И мы можем тривиально продемонстрировать это с:

#define SHOW(X)  cout << # X " = " << (X) << endl
int i = 0;  /* Yes, it's global! */
void foo(int theI) { SHOW(theI);  SHOW(i); }
int main() { foo(i++); }

Так, да, я увеличен, прежде чем нечто () вызывается.


Все это имеет большой смысл с точки зрения:

class Foo
{
public:
  Foo operator++(int) {...}  /* Postfix variant */
}

int main() {  Foo f;  delta( f++ ); }

Здесь Нечто:: оператор ++ (интервал) должен быть вызван до дельты (). И инкрементная операция должна быть завершена во время того вызова.


В моем (возможно, чрезмерно сложный) пример:

f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i);

f.bar ("A", i) должен быть выполнен для получения объекта, используемого для object.bar ("B", я ++), и так далее для "C" и "D".

Таким образом, мы знаем, что я ++ увеличиваю i до вызова панели ("B", я ++) (даже при том, что панель ("B"...) вызывается со старым значением i), и поэтому я увеличен до панели ("C", i) и панели ("D", i).


Возвращение к комментарию j_random_hacker:

записи j_random_hacker:
+1, но я должен был прочитать стандарт тщательно, чтобы убедить меня, что это было в порядке. Действительно ли я прав в размышлении, что, если панель () была вместо этого глобальным функциональным возвратом, говорят, что интервал, f был интервалом, и те вызовы были соединены, говорят "^" вместо".", то какой-либо из A, C и D мог сообщить "0"?

Этот вопрос намного более сложен, чем Вы могли бы думать...

Перезапись вопроса как код...

int bar(const char * theString, int theI) { SHOW(...);  return i; }

bar("A",i)   ^   bar("B",i++)   ^   bar("C",i)   ^   bar("D",i);

Теперь у нас есть ТОЛЬКО ОДНО выражение. Согласно стандарту (разделяют 1.9, страница 8, страница 20 PDF):

Примечание: операторы могут быть перегруппированы согласно обычным математическим правилам только там, где операторы действительно являются ассоциативными или коммутативными. (7), Например, в следующем фрагменте: a=a+32760+b+5; оператор выражения ведет себя точно то же как: = (((a+32760) +b) +5); из-за ассоциативности и приоритета этих операторов. Таким образом результат суммы (a+32760) затем добавляется к b, и тот результат затем добавляется к 5, который приводит к значению, присвоенному a. На машине, в которой переполнение производит исключение и в котором диапазон значений, представимых интервалом, [-32768, +32767], реализация не может переписать это выражение как = (a+b) +32765); с тех пор, если бы значения для a и b были, соответственно,-32754 и-15, сумма a+b произвела бы исключение, в то время как исходное выражение не будет; и при этом выражение не может быть переписано ни один как = (a+32765) +b); или = (+ (b+32765)); так как значения для a и b, возможно, были, соответственно, 4 и-8 или-17 и 12. Однако на машине, в которой переполнение не производит исключение и в котором результаты переполнения обратимы, вышеупомянутый оператор выражения может быть переписан реализацией любым из вышеупомянутых способов, потому что тот же результат произойдет. - заканчивают примечание]

Таким образом, мы могли бы думать, что, из-за приоритета, что наше выражение совпадет с:

(
       (
              ( bar("A",i) ^ bar("B",i++)
              )
          ^  bar("C",i)
       )
    ^ bar("D",i)
);

Но, потому что (a^b) ^c == a^ (b^c) без любых возможных водосливных ситуаций, это могло быть переписано в любом порядке...

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

Который приятно определяет порядок оценки панели ().

Теперь, когда делает это, я + = 1 происхожу? Хорошо это все еще должно произойти, прежде чем панель ("B"...) будет вызвана. (Даже при том, что панель ("B"....) вызывается со старым значением.)

Таким образом, это детерминировано происходит перед панелью (C) и панелью (D), и после панели (A).

Ответ: НЕТ. У нас всегда будет "A=0, B=0, C=1, D=1", если компилятор будет совместим стандартами.


Но рассмотрите другую проблему:

i = 0;
int & j = i;
R = i ^ i++ ^ j;

Каково значение R?

Если бы я + = 1 произошел, то прежде j, у нас был бы 0^0^1=1. Но если бы я + = 1 произошел то после целого выражения у нас было бы 0^0^0=0.

На самом деле R является нулем. Я + = 1 не происходит, пока выражение не было оценено.


То, которое я считаю, то, почему:

i = 7, я ++, я ++;//я становлюсь 9 (допустимым)

Законно... Это имеет три выражения:

  • i = 7
  • я ++
  • я ++

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


PS: рассмотрите:

int foo(int theI) { SHOW(theI);  SHOW(i);  return theI; }
i = 0;
int & j = i;
R = i ^ i++ ^ foo(j);

В этом случае я + = 1 должен быть оценен перед нечто (j). theI равняется 1. И R является 0^0^1=1.

3
ответ дан Community 26 November 2019 в 22:51
поделиться
Другие вопросы по тегам:

Похожие вопросы: