Стоимость параметров по умолчанию в С++
Я наткнулся на пример из "Эффективного 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
или тем, которое уже было уничтожено.
С другой стороны, нет риска, что распределение памяти для каждого вызова будет происходить здесь.
Теперь правильным решением в современном c++17 является:
void doThat(std::string_view name = "Unnamed"); // Best
который не будет выделяться, даже если строка длинна; он даже не скопирует строку! Кроме того, в 999/1000 случаях это замена на замену старого doThat
API и даже повышение производительности при передаче данных в doThat
и не полагаться на аргумент по умолчанию.
На данный момент поддержка c++17 во встроенном не может быть, но в некоторых случаях это может произойти в ближайшее время. И представление строк - это достаточно большое увеличение производительности, что в мире существует множество подобных типов, которые делают то же самое.
Но урок все еще остается; не делайте дорогих операций в аргументах по умолчанию. И распределение может быть дорогостоящим в некоторых контекстах (особенно во встроенном мире).
Ответ 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