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

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

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

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

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

3 ответа

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

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

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

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

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

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

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

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

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


Теперь правильное решение в современном C++17:

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

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

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

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

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

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
Другие вопросы по тегам