Может ли компилятор оптимизироваться от кучи к распределению стека?

Что касается оптимизации компилятора, является ли законным и/или возможно изменить распределение кучи на распределение стека? Или это нарушит правило as-if?

Например, скажем, что это оригинальная версия кода

{
    Foo* f = new Foo();
    f->do_something();
    delete f;
}

Может ли компилятор изменить это на следующий

{
    Foo f{};
    f.do_something();
}

Я бы так не подумал, потому что это имело бы последствия, если бы исходная версия полагалась на такие вещи, как пользовательские распределители. Означает ли стандарт что-то конкретно об этом?

Ответ 1

Да, это законно. expr.new/10 для С++ 14:

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

expr.delete/7:

Если значение операнда выражения-удаления не является нулевым значение указателя, затем:

- Если вызов выделения для нового выражения для объекта исключение не было опущено и распределение не было расширено (5.3.4), выражение-выражение должно вызывать функцию освобождения (3.7.4.2). Значение, возвращаемое из вызова выделения нового выражения должен быть передан в качестве первого аргумента функции освобождения.

- В противном случае, если распределение было расширено или было предоставлено расширяя выделение другого нового выражения, и delete-expression для любого другого значения указателя, созданного новое выражение, у которого было хранилище, предоставленное расширенным новое выражение было оценено, выражение-выражение должно вызвать функция освобождения. Значение, возвращаемое из вызова распределения расширенное новое выражение должно быть передано в качестве первого аргумента в функция освобождения.

- В противном случае выражение delete не будет вызывать освобождение функция (3.7.4.2).

Итак, в заключение, законно заменить new и delete на какую-то реализацию, определенную, например, используя стек вместо кучи.

Примечание. Как комментирует Массимилиано Джанс, компилятор не мог точно придерживаться этого преобразования для вашего образца, если do_something throws: компилятор должен опустить деструкторный вызов f в этом случае (в то время как ваш преобразованный образец вызывает деструктор в этом случае). Но кроме этого, можно положить f в стек.

Ответ 2

Они не эквивалентны. f.do_something() может быть брошен, и в этом случае первый объект остается в памяти, второй будет разрушен.

Ответ 3

Я хотел бы указать на то, что ИМО недостаточно подчеркивал в других ответах:

struct Foo {
    static void * operator new(std::size_t count) {
        std::cout << "Hey ho!" << std::endl;
        return ::operator new(count);
    }
};

Распределение new Foo() обычно не может быть заменено, потому что:

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

Таким образом, как и в приведенном выше примере Foo, необходимо вызвать Foo::operator new. Опущение этого вызова изменит наблюдаемое поведение программы.

Пример реального мира: Foo может потребоваться для правильной работы в какой-либо специальной области памяти (например, с отображением памяти IO).