Могут ли неиспользуемые аргументы функции стать оптимизированными?

#c

#c

Вопрос:

Я пытаюсь гарантировать, что объект, обернутый shared_ptr , работает до тех пор, пока выполняется функция, передавая ее как значение. Однако внутри функции объект вообще не используется, поэтому я просто хочу использовать его для «закрепления»:

 void doSomething(std::shared_ptr<Foo>) {
    // Perform some operations unrelated to the passed shared_ptr.
}

int main() {
    auto myFoo{std::make_shared<Foo>()};
    doSomething(std::move(myFoo)); // Is 'myFoo' kept alive until doSomething returns?
    return 0;
}
  

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

Комментарии:

1. Компилятору всегда разрешено изменять порядок всех действий, пока код работает «как если бы» написанный. Таким образом, у вашего кода есть хорошие шансы получить только «return 0» из main здесь!

2. Вы правы — это очень продуманный пример. Давайте предположим, что есть какой-то побочный эффект с точки зрения doSomething() , а также деструктор Foo выполняет некоторые cout действия. Тогда я ожидал бы, что печать функции появится раньше, чем та, которая поступает от деструктора.

Ответ №1:

Вам не нужно беспокоиться — время жизни аргумента функции на сайте вызова гарантированно сохраняется при вызове функции. (Вот почему такие вещи, как foo(s.c_str()) для std::string s работы.)

Компилятору не разрешается нарушать это правило при условии гибкости правила как если бы.

Комментарии:

1. Хорошо — и это также применимо, если параметр функции вообще не используется в самой функции, верно?

2. @Phlar: Абсолютно. Да, компилятору разрешено выполнять оптимизацию, но это не должно изменять поведение программы.

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

4. @Eljay: А также некоторые выделения в новом выражении начиная с C 14.

Ответ №2:

Это очень сильно зависит от того, как на самом деле будет выглядеть тело doSomething и Foo . Например, рассмотрим следующий пример:

 struct X 
{
    ~X() { std::cout << "2"; };
};

void f(std::shared_ptr<X>) { std::cout << "1"; }

int main()
{
    auto p = std::make_shared<X>();
    f(std::move(p));
}
  

Эта программа имеет тот же наблюдаемый эффект, что и:

 int main()
{
  std::cout << "12";
}
  

и порядок "12" гарантирован. Итак, в сгенерированной сборке может не использоваться общий указатель. Однако большинство компиляторов, скорее всего, не будут выполнять такую агрессивную оптимизацию, поскольку внутри задействованы динамические выделения памяти и вызовы виртуальных функций, что не так просто оптимизировать.

Ответ №3:

Компилятор мог бы оптимизировать копирование объекта в аргумент функции, если функция встроена и если копирование не имеет побочных эффектов.

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

Комментарии:

1. Но что, если количество ссылок не участвует в наблюдаемом эффекте программы? Это своего рода другая тема, но int main() { auto ptr = std::make_shared<int>(); } имеет тот же эффект, что и пустой main в соответствии с правилом «как если бы».

2. @DanielLangr возможно, я не уверен, что оптимизаторы могут удалять атомарные приращения / декременты?

3. std::atomic не наблюдается, но volatile из правила as_if .