Почему изменение поля, на которое ссылается другая переменная, приводит к неожиданному поведению? - PullRequest
26 голосов
/ 11 июля 2020

Я написал этот код, который на первый взгляд показался мне очень простым. Он изменяет переменную, на которую ссылается ссылочная переменная, а затем возвращает значение ссылки. Упрощенная версия, которая воспроизводит странное поведение, выглядит так:

#include <iostream>
using std::cout;

struct A {
    int a;
    int& b;

    A(int x) : a(x), b(a) {}
    A(const A& other) : a(other.a), b(a) {}
    A() : a(0), b(a) {}
};

int foo(A a) {
    a.a *= a.b;
    return a.b;
}


int main() {
    A a(3);

    cout << foo(a) << '\n';
    return 0;
}

Однако, когда она скомпилирована с включенной оптимизацией (g ++ 7.5), она выдает вывод, отличный от неоптимизированного кода (т.е. 9 без оптимизаций - как ожидалось и 3 с включенной оптимизацией).

Мне известно ключевое слово volatile, которое не позволяет компилятору переупорядочивать и другие оптимизации при наличии определенных побочных эффектов ( например, asyn c выполнение и аппаратно-зависимые c вещи), и это помогает и в этом случае.

Однако я не понимаю, почему мне нужно объявлять ссылку b как изменчивую в этом конкретном случае ? Где источник ошибки в этом коде?

1 Ответ

13 голосов
/ 12 июля 2020

Исходник UB по стандарту найти не смог. Мне это кажется ошибкой оптимизатора, который не заметил бы, что a.b и a.a оба относятся к одному и тому же объекту:

  • Прежде всего, foo() работает на копии. Я изменил foo() на передачу по ссылке, и ожидаемый результат стабильно получился. Заподозрил проблему в инициализации ссылки. Но предоставленный конструктор копирования правильно работает с a.b.

  • Тогда я подозревал, что некоторые UB связаны с побочными эффектами неопределенно упорядоченных операций в том же выражении. Но побочный эффект в левой части *= упорядочен после правой, так что здесь тоже нет UB.

  • Добавление некоторого протоколирования после того, как оператор *= сделал это неожиданно работать как положено. Это выглядело очень странно: похоже, что обычные проблемы возникают, когда не соблюдается строгое ограничение псевдонима, то есть когда компилятор не понимает, что заостренный объект был изменен, и сокращает код, как если бы значение не изменилось. В таком случае нет ничего необычного в том, что дополнительный код приведет к перезагрузке правильного значения и получению другого результата.

  • Однако здесь нет проблемы с псевдонимом, поскольку исходный член и ссылка на него оба основаны на одном и том же типе.
    - Сэр Артур Конан Дойл

    После устранения ошибок и UB в коде OP единственная оставшаяся возможность - это ошибка в оптимизаторе. Кажется, что оптимизатор не замечает, что aa и ab являются одним и тем же объектом, и что он просто повторно использует последнее известное значение ab, которое уже находится в регистре.

...