Стоимость параметров по умолчанию в С++

Я наткнулся на пример из "Эффективного C++ во встроенной среде" Скотта Мейерса, где были описаны два способа использования параметров по умолчанию: один из них был описан как дорогостоящий, а другой как лучший вариант.

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

void doThat(const std::string& name = "Unnamed"); // Bad

const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better

Ответ 1

В первом случае временная std::string инициализируется из литерала "Unnamed" каждый раз, когда функция вызывается без аргумента.

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

Ответ 2

void doThat(const std::string& name = "Unnamed"); // Bad

Это "плохо" в том, что при каждом doThat() создается новая std::string с содержимым "Unnamed".

Я говорю "плохо" и не плохо, потому что небольшая оптимизация строк в каждом компиляторе C++, который я использовал, поместит "Unnamed" данные во временную std::string созданную на сайте вызова, и не выделяет для нее никакого хранилища. Поэтому в этом конкретном случае для временного аргумента мало затрат. Стандарт не требует оптимизации небольших строк, но он явно предназначен для его разрешения, и вся используемая в настоящее время стандартная библиотека реализует его.

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

const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better

Здесь мы создаем глобальное имя по defaultName с содержимым "Unnamed". Это создается при статическом времени инициализации. Здесь есть некоторые риски; if doThat вызывается при статической инициализации или времени уничтожения (до или после main запусков), он может быть вызван с defaultName или тем, которое уже было уничтожено.

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


Теперь правильным решением в современном является:

void doThat(std::string_view name = "Unnamed"); // Best

который не будет выделяться, даже если строка длинна; он даже не скопирует строку! Кроме того, в 999/1000 случаях это замена на замену старого doThat API и даже повышение производительности при передаче данных в doThat и не полагаться на аргумент по умолчанию.

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

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

Ответ 3

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

void foo(int x = 0);
void bar(int x = 0) { foo(x); }

Это становится ошибкой, подверженной кошмару, когда вы добавляете больше гнездования, потому что значение по умолчанию должно повторяться в нескольких местах (т.е. дорогостоящее в том смысле, что одно крошечное изменение требует изменения разных мест в коде). Лучший способ избежать этого - как в вашем примере:

const int foo_default = 0;
void foo(int x = foo_default);
void bar(int x = foo_default) { foo(x); } // no need to repeat the value here